YouTube → Coolhole Queue Buttons

CH queues to Coolhole (or Q+ if full). CP focuses the hole and clicks Work. Draggable Hist/Q+ pill + attached list on Coolhole.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube → Coolhole Queue Buttons
// @namespace    coolhole-queue-buttons
// @version      2.6.9
// @description  CH queues to Coolhole (or Q+ if full). CP focuses the hole and clicks Work. Draggable Hist/Q+ pill + attached list on Coolhole.
// @author       soapylerd
// @match        *://*.youtube.com/*
// @match        *://youtube.com/*
// @match        *://m.youtube.com/*
// @match        *://youtu.be/*
// @match        *://*.youtu.be/*
// @match        *://*.youtube.ca/*
// @match        *://youtube.ca/*
// @match        *://*.youtube.co.uk/*
// @match        *://youtube.co.uk/*
// @match        *://*.youtube.de/*
// @match        *://youtube.de/*
// @match        *://*.youtube.fr/*
// @match        *://youtube.fr/*
// @match        *://*.youtube.com.au/*
// @match        *://youtube.com.au/*
// @match        *://*.youtube.co.jp/*
// @match        *://youtube.co.jp/*
// @match        *://*.youtube.in/*
// @match        *://youtube.in/*
// @match        *://*.youtube.com.br/*
// @match        *://youtube.com.br/*
// @match        *://*.youtube.es/*
// @match        *://youtube.es/*
// @match        *://*.youtube.it/*
// @match        *://youtube.it/*
// @match        *://*.youtube.nl/*
// @match        *://youtube.nl/*
// @match        *://*.youtube.pl/*
// @match        *://youtube.pl/*
// @match        *://*.youtube.com.mx/*
// @match        *://youtube.com.mx/*
// @include      *://youtube.*/*
// @include      *://*.youtube.*/*
// @include      *://m.youtube.*/*
// @match        https://coolhole.org/*
// @match        https://new.coolhole.org/*
// @noframes
// @connect      www.youtube.com
// @connect      youtube.com
// @run-at       document-idle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @grant        GM_openInTab
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.openInTab
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const gm = {
    setValue(key, value) {
      try {
        if (typeof GM_setValue === 'function') return GM_setValue(key, value);
        if (typeof GM?.setValue === 'function') return GM.setValue(key, value);
      } catch (e) {
        console.warn('[CoolholeQueue] setValue failed', e);
      }
    },
    getValue(key, fallback) {
      try {
        if (typeof GM_getValue === 'function') return GM_getValue(key, fallback);
        if (typeof GM?.getValue === 'function') return fallback;
      } catch (e) {
        console.warn('[CoolholeQueue] getValue failed', e);
      }
      return fallback;
    },
    onValueChange(key, callback) {
      try {
        if (typeof GM_addValueChangeListener === 'function') {
          return GM_addValueChangeListener(key, callback);
        }
      } catch (e) {
        console.warn('[CoolholeQueue] value change listener unavailable', e);
      }
      return null;
    },
    openInTab(url, options = { active: true, insert: true, setParent: true }) {
      try {
        if (typeof GM_openInTab === 'function') return GM_openInTab(url, options);
        if (typeof GM?.openInTab === 'function') return GM.openInTab(url, options);
      } catch (e) {
        console.warn('[CoolholeQueue] openInTab failed, using window.open', e);
      }
      try {
        window.open(url, '_blank', 'noopener,noreferrer');
      } catch {
        try {
          location.assign(url);
        } catch (e2) {
          console.error('[CoolholeQueue] could not open URL', e2);
        }
      }
    },
  };

  const HOSTS = Object.freeze(['coolhole.org', 'new.coolhole.org']);
  const PRIMARY_HOST = 'coolhole.org';
  const queueKey = 'cq_queue';
  const workKey = 'cq_work';
  const cdKey = 'cq_work_cd';
  const settingsKey = 'cq_cp_settings';
  const aliveKey = (h) => `cq_alive_${h}`;
  const pendingKey = 'cq_pending';
  const historyKey = 'cq_history';
  const limitKey = 'cq_my_limit';
  const autoKey = 'cq_auto_queue';
  const collapsedKey = 'cq_float_collapsed';
  const ALIVE_MAX_MS = 15_000;
  const MAX_PENDING = 40;
  const MAX_HISTORY = 30;
  const HOLD_MS = 450;

  const defaultSettings = Object.freeze({
    work: true,
    unAfk: false,
    autoFocus: true,
    qPlus: true,
    ytOpacity: 0.38,
    holeOpacity: 0.35,
  });

  function loadSettings() {
    try {
      const raw = gm.getValue(settingsKey, null);
      if (raw && typeof raw === 'object') return { ...defaultSettings, ...raw };
    } catch {
      /* ignore */
    }
    return { ...defaultSettings };
  }

  function saveSettings(next) {
    try {
      gm.setValue(settingsKey, { ...defaultSettings, ...next });
    } catch (e) {
      console.warn('[CoolholeQueue] saveSettings failed', e);
    }
  }
  const BAD_TITLES = /^(shared\s*link|youtube|video|null|undefined|\s*)$/i;
  const CARD_SELECTORS = [
    'ytd-rich-item-renderer',
    'ytd-rich-grid-media',
    'ytd-grid-video-renderer',
    'ytd-video-renderer',
    'ytd-compact-video-renderer',
    'ytd-playlist-video-renderer',
    'ytd-reel-item-renderer',
    'yt-lockup-view-model',
    'ytd-lockup-view-model',
    'ytm-rich-item-renderer',
    'ytm-video-with-context-renderer',
    'ytm-compact-video-renderer',
  ].join(',');

  const host = location.hostname;
  const isYouTube = /(?:^|\.)youtube\./i.test(host) || /(^|\.)youtu\.be$/i.test(host);
  const isCoolhole = HOSTS.includes(host);

  try {
    if (isYouTube) initYouTubeSide();
    else if (isCoolhole) initCoolholeSide();
  } catch (e) {
    console.error('[CoolholeQueue] fatal init error', e);
  }

  function injectStyles(css) {
    try {
      const el = document.createElement('style');
      el.textContent = css;
      (document.head ?? document.documentElement).append(el);
    } catch (e) {
      console.error('[CoolholeQueue] injectStyles failed', e);
    }
  }

  function cleanTitle(value) {
    if (value == null) return null;
    const text = String(value).replace(/\s+/g, ' ').trim();
    if (!text || BAD_TITLES.test(text)) return null;
    return text.replace(/\s*-\s*YouTube\s*$/i, '').trim() || null;
  }

  function anyHostAlive() {
    return HOSTS.some((h) => {
      const ts = gm.getValue(aliveKey(h), 0);
      return typeof ts === 'number' && Date.now() - ts < ALIVE_MAX_MS;
    });
  }

  // ======================================================================
  // YOUTUBE
  // ======================================================================
  function initYouTubeSide() {
    injectStyles(`
      .cq-pill {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 2147483000;
        align-items: stretch;
        border-radius: 999px;
        overflow: hidden;
        box-shadow: 0 2px 8px rgba(0,0,0,.4), inset 0 0 0 1px rgba(255,255,255,.08);
        font-family: Roboto, Arial, sans-serif;
        line-height: 1;
        user-select: none;
        white-space: nowrap;
        pointer-events: auto;
        transition: opacity .2s ease;
      }
      .cq-pill .cq-seg {
        cursor: pointer;
        border: 0;
        margin: 0;
        padding: 5px 9px;
        font-size: 10px;
        font-weight: 700;
        letter-spacing: 0.06em;
        color: #fff;
        transition: background .15s ease, filter .15s ease, transform .1s ease;
        min-width: 2.4em;
        text-align: center;
      }
      .cq-pill .cq-seg:active { transform: scale(0.97); }
      .cq-pill .cq-seg.cq-sent { filter: brightness(1.25); }
      .cq-pill .cq-seg-ch {
        background: #4a4a4a;
        border-right: 1px solid rgba(0,0,0,.25);
      }
      .cq-pill .cq-seg-cp {
        background: #606060;
        position: relative;
        overflow: hidden;
        z-index: 0;
      }
      /* Fill grows left → right. Visible on both gray and blue hover. */
      .cq-pill .cq-seg-cp .cq-cp-fill {
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        width: 0%;
        background: #9e9e9e;
        z-index: 0;
        pointer-events: none;
        transition: width 0.3s linear;
      }
      .cq-pill .cq-seg-cp > * {
        position: relative;
        z-index: 1;
      }
      /* Ready = fully filled look (same tone as base so it reads as charged) */
      .cq-pill .cq-seg-cp.cq-ready .cq-cp-fill {
        width: 100% !important;
        background: #6e6e6e;
      }
      .cq-pill .cq-seg-cp.cq-ready:hover .cq-cp-fill {
        background: transparent;
      }
      .cq-pill .cq-seg-ch:hover {
        background: linear-gradient(180deg, #e53935, #b71c1c);
      }
      /* Original blue hover — fill stays fully visible underneath */
      .cq-pill .cq-seg-cp:hover {
        background: linear-gradient(180deg, #1184e8, #0a5fad);
      }
      .cq-pill .cq-seg-cp:hover .cq-cp-fill {
        background: rgba(255, 255, 255, 0.28);
      }
      /* Hold progress — grows right → left (opposite of charge) */
      .cq-pill .cq-seg-cp .cq-cp-hold {
        position: absolute;
        right: 0;
        top: 0;
        bottom: 0;
        width: 0%;
        background: rgba(255, 255, 255, 0.22);
        z-index: 0;
        pointer-events: none;
        transition: none;
      }
      .cq-pill .cq-seg-cp.cq-holding .cq-cp-hold {
        transition: width 0.52s linear;
        width: 100%;
      }
      /* CP settings menu — visibility driven by inline styles in openMenu */
      #cq-cp-menu {
        position: fixed;
        z-index: 2147483647;
        width: 260px;
        background: #1e1e1e;
        border: 2px solid #1184e8;
        border-radius: 8px;
        box-shadow: 0 12px 40px rgba(0,0,0,.75);
        font: 12px/1.35 system-ui, sans-serif;
        color: #eee;
        overflow: hidden;
      }
      #cq-cp-menu .cq-menu-head {
        pointer-events: none;
      }
      #cq-cp-menu .cq-menu-head {
        padding: 8px 10px;
        font-weight: 700;
        font-size: 11px;
        letter-spacing: 0.04em;
        color: #90caf9;
        border-bottom: 1px solid #333;
        background: #252525;
      }
      #cq-cp-menu .cq-menu-body {
        padding: 8px 10px 10px;
        display: flex;
        flex-direction: column;
        gap: 7px;
      }
      #cq-cp-menu label.cq-check {
        display: flex;
        align-items: center;
        gap: 7px;
        cursor: pointer;
        user-select: none;
        color: #ddd;
      }
      #cq-cp-menu label.cq-check input {
        margin: 0;
        accent-color: #1184e8;
      }
      #cq-cp-menu .cq-menu-sub {
        margin-left: 18px;
        display: flex;
        flex-direction: column;
        gap: 5px;
      }
      #cq-cp-menu .cq-menu-sub.hidden { display: none; }
      #cq-cp-menu select {
        background: #2a2a2a;
        color: #eee;
        border: 1px solid #444;
        border-radius: 4px;
        padding: 3px 6px;
        font-size: 11px;
      }
      #cq-cp-menu .cq-slider-row {
        display: flex;
        flex-direction: column;
        gap: 3px;
      }
      #cq-cp-menu .cq-slider-row span {
        display: flex;
        justify-content: space-between;
        color: #bbb;
        font-size: 11px;
      }
      #cq-cp-menu input[type="range"] {
        width: 100%;
        accent-color: #1184e8;
      }
      #cq-cp-menu .cq-menu-foot {
        padding: 6px 10px;
        border-top: 1px solid #333;
        background: #252525;
        display: flex;
        justify-content: flex-end;
        gap: 6px;
      }
      #cq-cp-menu .cq-menu-foot button {
        border: 0;
        border-radius: 4px;
        padding: 4px 10px;
        font-size: 11px;
        font-weight: 600;
        cursor: pointer;
        background: #424242;
        color: #ddd;
      }
      #cq-cp-menu .cq-menu-foot button.cq-primary {
        background: #1184e8;
        color: #fff;
      }
      #cq-diag {
        position: fixed;
        bottom: 16px;
        left: 16px;
        max-width: 360px;
        padding: 10px 12px;
        border-radius: 8px;
        background: #1e1e1e;
        color: #f5f5f5;
        font: 12px/1.4 system-ui, sans-serif;
        z-index: 2147483646;
        box-shadow: 0 4px 16px rgba(0,0,0,.45);
        border: 1px solid #c62828;
      }
      #cq-diag strong { color: #ef5350; }
      #cq-diag button {
        margin: 8px 6px 0 0;
        cursor: pointer;
        border: 0;
        border-radius: 4px;
        padding: 4px 10px;
        font-size: 11px;
        font-weight: 600;
      }
      #cq-diag .cq-diag-dismiss { background: #424242; color: #fff; }
      #cq-diag .cq-diag-retry { background: #1184e8; color: #fff; }
    `);

    let scanScheduled = false;
    let scanCount = 0;
    let diagShown = false;

    const scheduleScan = () => {
      if (scanScheduled) return;
      scanScheduled = true;
      setTimeout(() => {
        scanScheduled = false;
        scanCards();
        injectWatchPage();
        maybeShowDiagnostics();
      }, 250);
    };

    document.addEventListener('yt-navigate-finish', () => {
      diagShown = false;
      hidePillNow();
      setTimeout(() => {
        scanCards();
        injectWatchPage();
        maybeShowDiagnostics();
      }, 400);
    });

    setTimeout(() => {
      scanCards();
      injectWatchPage();
      maybeShowDiagnostics();
    }, 600);
    setTimeout(maybeShowDiagnostics, 4000);
    setTimeout(maybeShowDiagnostics, 8000);

    try {
      new MutationObserver(scheduleScan).observe(document.documentElement, {
        childList: true,
        subtree: true,
      });
    } catch (e) {
      console.error('[CoolholeQueue] MutationObserver failed', e);
      setInterval(() => {
        scanCards();
        injectWatchPage();
      }, 2000);
    }

    let lastHref = location.href;
    setInterval(() => {
      if (location.href !== lastHref) {
        lastHref = location.href;
        diagShown = false;
        hidePillNow();
        scheduleScan();
      }
      scanCards();
      injectWatchPage();
    }, 2000);

    function videoIdFromHref(href) {
      try {
        const url = new URL(href, location.href);
        if (url.hostname.includes('youtu.be')) {
          return url.pathname.slice(1).split('/')[0] || null;
        }
        if (url.pathname === '/watch') return url.searchParams.get('v');
        const shorts = url.pathname.match(/^\/shorts\/([^/?]+)/);
        if (shorts) return shorts[1];
        const embed = url.pathname.match(/^\/embed\/([^/?]+)/);
        if (embed) return embed[1];
      } catch {
        /* ignore */
      }
      return null;
    }

    function currentWatchVideoId() {
      if (location.pathname === '/watch') {
        return new URLSearchParams(location.search).get('v');
      }
      return location.pathname.match(/^\/shorts\/([^/?]+)/)?.[1] ?? null;
    }

    function videoIdFromCard(card) {
      const link =
        card.querySelector('a#thumbnail[href]') ??
        card.querySelector('a#video-title-link[href]') ??
        card.querySelector('a#video-title[href]') ??
        card.querySelector('a[href*="/watch"]') ??
        card.querySelector('a[href*="/shorts/"]');
      return link?.href ? videoIdFromHref(link.href) : null;
    }

    function titleFromCard(card) {
      const candidates = [];
      const titleEl =
        card.querySelector('#video-title') ??
        card.querySelector('a#video-title-link') ??
        card.querySelector('[id="video-title"]') ??
        card.querySelector('yt-formatted-string#video-title') ??
        card.querySelector('h3 a') ??
        card.querySelector('a[title]');
      if (titleEl) {
        candidates.push(titleEl.getAttribute('title'), titleEl.textContent);
      }
      const thumb = card.querySelector('a#thumbnail, a[href*="/watch"], a[href*="/shorts/"]');
      const aria = thumb?.getAttribute('aria-label');
      if (aria) candidates.push(aria.replace(/\s+by\s+.+$/i, '').trim());
      for (const candidate of candidates) {
        const cleaned = cleanTitle(candidate);
        if (cleaned) return cleaned;
      }
      return null;
    }

    function titleFromWatchPage() {
      const el =
        document.querySelector('h1.ytd-watch-metadata yt-formatted-string') ??
        document.querySelector('ytd-watch-metadata h1 yt-formatted-string') ??
        document.querySelector('h1 yt-formatted-string') ??
        document.querySelector('#title h1') ??
        document.querySelector('yt-shorts-video-title-view-model h2') ??
        document.querySelector('h2.ytd-reel-player-header-renderer');
      if (el?.textContent) return cleanTitle(el.textContent);
      return cleanTitle(document.title?.replace(/\s*-\s*YouTube\s*$/i, ''));
    }

    function findMenu(card) {
      return (
        card.querySelector('ytd-menu-renderer') ??
        card.querySelector('#menu') ??
        card.querySelector('button[aria-label="More actions"]') ??
        card.querySelector('button[aria-label="Action menu"]') ??
        card.querySelector('button[aria-label*="More"]')
      );
    }

    function findWatchMenu() {
      return (
        document.querySelector('#actions ytd-menu-renderer') ??
        document.querySelector('#actions-inner ytd-menu-renderer') ??
        document.querySelector('ytd-watch-metadata ytd-menu-renderer') ??
        document.querySelector('#menu-container ytd-menu-renderer') ??
        document.querySelector('ytd-menu-renderer.ytd-watch-metadata') ??
        document.querySelector('#actions button[aria-label="More actions"]') ??
        document.querySelector('#actions button[aria-label*="More"]') ??
        document.querySelector('ytd-reel-player-overlay-renderer ytd-menu-renderer') ??
        document.querySelector('#actions.ytd-reel-player-overlay-renderer ytd-menu-renderer') ??
        document.querySelector('ytd-shorts ytd-menu-renderer')
      );
    }

    function flash(btn) {
      btn?.classList.add('cq-sent');
      setTimeout(() => btn?.classList.remove('cq-sent'), 900);
    }

    function sendQueue(videoId, title, btn) {
      if (!videoId) {
        console.warn('[CoolholeQueue] missing videoId');
        return;
      }
      const payload = {
        videoId: String(videoId),
        title: title ?? null,
        n: Date.now(),
      };
      try {
        if (anyHostAlive()) {
          gm.setValue(queueKey, payload);
          flash(btn);
          console.log(
            `%c[CoolholeQueue]%c → queue: ${title ?? videoId}`,
            'background:#1184e8;color:#fff;font-weight:600;padding:1px 6px;border-radius:3px;',
            'color:inherit;'
          );
          return;
        }
        const hash =
          `cq_add=${encodeURIComponent(videoId)}` +
          (title ? `&cq_title=${encodeURIComponent(title)}` : '') +
          `&n=${payload.n}`;
        gm.openInTab(`https://${PRIMARY_HOST}/#${hash}`, { active: true, insert: true, setParent: true });
        gm.setValue(queueKey, payload);
        flash(btn);
      } catch (e) {
        console.error('[CoolholeQueue] send failed', e);
        alert('CoolholeQueue error: could not send video. See the browser console for details.');
      }
    }

    function sendWork(btn) {
      try {
        // Don't re-signal Work while the local charge bar is still cooling
        try {
          const cd = gm.getValue(cdKey, null);
          if (cd && cd.ready === false && cd.remainingMs > 0 && cd.startedAt) {
            const ends = cd.startedAt + Math.max(cd.totalMs || 0, cd.remainingMs);
            if (ends > Date.now() + 500) {
              const secs = Math.ceil((ends - Date.now()) / 1000);
              console.log('[CoolholeQueue] Work ignored — still cooling ~' + secs + 's');
              if (btn) btn.title = 'Work cooldown — ' + secs + 's · hold for settings';
              return;
            }
          }
        } catch {
          /* ignore */
        }
        const payload = { n: Date.now() };
        if (anyHostAlive()) {
          gm.setValue(workKey, payload);
          flash(btn);
          console.log(
            `%c[CoolholeQueue]%c → Work`,
            'background:#1184e8;color:#fff;font-weight:600;padding:1px 6px;border-radius:3px;',
            'color:inherit;'
          );
          return;
        }
        gm.openInTab(`https://${PRIMARY_HOST}/#cq_work=1&n=${payload.n}`, {
          active: true,
          insert: true,
          setParent: true,
        });
        gm.setValue(workKey, payload);
        flash(btn);
      } catch (e) {
        console.error('[CoolholeQueue] work signal failed', e);
        alert('CoolholeQueue error: could not signal Work. See the browser console.');
      }
    }

    const currentCtx = { id: null, title: null };
    let hideTimer = null;
    let sharedPill = null;

    function clearHideTimer() {
      if (hideTimer) {
        clearTimeout(hideTimer);
        hideTimer = null;
      }
    }

    function scheduleHide() {
      clearHideTimer();
      hideTimer = setTimeout(() => {
        // Don't hide while CP settings menu is open
        const menu = document.getElementById('cq-cp-menu');
        if (menu && menu.style.display === 'block') return;
        if (sharedPill) sharedPill.style.display = 'none';
      }, 250);
    }

    function hidePillNow() {
      clearHideTimer();
      const menu = document.getElementById('cq-cp-menu');
      if (menu && menu.style.display === 'block') return;
      if (sharedPill) sharedPill.style.display = 'none';
    }

    function buildSharedPill() {
      const pill = document.createElement('div');
      pill.className = 'cq-pill';
      pill.setAttribute('role', 'group');
      pill.title = 'Coolhole';

      const ch = document.createElement('button');
      ch.type = 'button';
      ch.className = 'cq-seg cq-seg-ch';
      ch.textContent = 'CH';
      ch.title = 'Queue to Coolhole';
      ch.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        event.stopImmediatePropagation();
        if (!currentCtx.id) {
          alert('Could not detect a video ID for this item.');
          return;
        }
        sendQueue(currentCtx.id, currentCtx.title, ch);
      });

      const cp = document.createElement('button');
      cp.type = 'button';
      cp.className = 'cq-seg cq-seg-cp cq-ready';
      cp.title = 'Focus Coolhole and Work (Earn CP) · hold for settings';

      // Fill layer (grows left → right). Hold layer grows right → left.
      const fill = document.createElement('span');
      fill.className = 'cq-cp-fill';
      fill.style.width = '100%';
      const holdFill = document.createElement('span');
      holdFill.className = 'cq-cp-hold';
      const label = document.createElement('span');
      label.textContent = 'CP';
      cp.append(fill, holdFill, label);

      // ── Settings + hold menu + charge bar ─────────────────────────
      let settings = loadSettings();
      let menuEl = null;
      let menuOpen = false;
      let menuOpenedAt = 0;
      let holdTimer = null;
      let holdActive = false;
      let holdOpenedMenu = false;
      let localTimer = null;
      let cycleTotal = 0;
      let cycleEndsAt = 0;
      let lastCdTs = 0;

      const applyYtOpacity = () => {
        try {
          const op = Math.min(1, Math.max(0.1, Number(settings.ytOpacity) || 0.38));
          pill.style.opacity = String(op);
        } catch {
          pill.style.opacity = '0.38';
        }
      };

      const setHoldVisual = (on) => {
        if (on) {
          cp.classList.add('cq-holding');
          holdFill.style.transition = 'width ' + HOLD_MS + 'ms linear';
          holdFill.style.width = '0%';
          void holdFill.offsetWidth;
          holdFill.style.width = '100%';
        } else {
          cp.classList.remove('cq-holding');
          holdFill.style.transition = 'none';
          holdFill.style.width = '0%';
        }
      };

      const setReady = () => {
        cp.classList.add('cq-ready');
        fill.style.width = '100%';
        cp.title = 'Focus Coolhole and Work (Earn CP) · hold for settings';
        if (localTimer) {
          clearInterval(localTimer);
          localTimer = null;
        }
      };

      const tickLocal = () => {
        const rem = Math.max(0, cycleEndsAt - Date.now());
        if (rem <= 0) {
          setReady();
          return;
        }
        cp.classList.remove('cq-ready');
        const total = Math.max(cycleTotal, 1000);
        const pct = Math.min(100, Math.max(0, ((total - rem) / total) * 100));
        fill.style.width = pct.toFixed(1) + '%';
        cp.title = 'Work cooldown — ' + Math.ceil(rem / 1000) + 's · hold for settings';
      };

      const startLocalCountdown = (state) => {
        if (!state) return;
        // Ready / blocked → only clear if we were actually done or forced ready
        if (state.ready || !(state.remainingMs > 0)) {
          if (state.ready && (!localTimer || cycleEndsAt <= Date.now())) setReady();
          return;
        }
        const ts = state.ts || state.startedAt || 0;
        if (ts && ts === lastCdTs) return;

        const started = state.startedAt || state.ts || Date.now();
        const total = Math.max(state.totalMs || 0, state.remainingMs, 1000);
        const newEndsAt = started + total;

        // If a countdown is already running for this cycle, don't reset the bar
        if (localTimer && cycleEndsAt > Date.now()) {
          const sameCycle = Math.abs(newEndsAt - cycleEndsAt) < 3000;
          if (sameCycle) {
            if (ts) lastCdTs = ts;
            return;
          }
        }

        if (ts) lastCdTs = ts;
        cycleTotal = total;
        cycleEndsAt = newEndsAt;

        cp.classList.remove('cq-ready');
        fill.style.width = '0%';
        void fill.offsetWidth;
        tickLocal();
        if (localTimer) clearInterval(localTimer);
        localTimer = setInterval(tickLocal, 200);
      };

      // --- Menu (built on first open — never "missing") ---
      const hideMenu = () => {
        if (!menuEl) return;
        menuEl.style.display = 'none';
        menuOpen = false;
        // Apply saved transparency once menu is gone
        applyYtOpacity();
      };

      const ensureMenu = () => {
        if (menuEl && menuEl.isConnected) return menuEl;
        if (menuEl && !menuEl.isConnected) {
          (document.body || document.documentElement).appendChild(menuEl);
          return menuEl;
        }

        // YouTube Trusted Types blocks innerHTML — build with DOM APIs only
        const el = document.createElement('div');
        el.id = 'cq-cp-menu';

        const head = document.createElement('div');
        head.className = 'cq-menu-head';
        head.textContent = 'CP SETTINGS';

        const body = document.createElement('div');
        body.className = 'cq-menu-body';

        const mkCheck = (key, labelText) => {
          const lab = document.createElement('label');
          lab.className = 'cq-check';
          const inp = document.createElement('input');
          inp.type = 'checkbox';
          inp.dataset.k = key;
          lab.appendChild(inp);
          lab.appendChild(document.createTextNode(' ' + labelText));
          return lab;
        };

        body.appendChild(mkCheck('work', 'Work'));
        body.appendChild(mkCheck('unAfk', 'Un-AFK'));
        body.appendChild(mkCheck('autoFocus', 'Auto-focus tab'));
        body.appendChild(mkCheck('qPlus', 'Q+ features'));

        const mkSlider = (key, labelText, defaultPct) => {
          const row = document.createElement('div');
          row.className = 'cq-slider-row';
          const span = document.createElement('span');
          span.appendChild(document.createTextNode(labelText + ' '));
          const bold = document.createElement('b');
          bold.dataset.v = key;
          bold.textContent = defaultPct + '%';
          span.appendChild(bold);
          const range = document.createElement('input');
          range.type = 'range';
          range.min = '10';
          range.max = '100';
          range.step = '1';
          range.dataset.k = key;
          range.value = String(defaultPct);
          row.appendChild(span);
          row.appendChild(range);
          return row;
        };
        body.appendChild(mkSlider('ytOpacity', 'YouTube transparency', 38));
        body.appendChild(mkSlider('holeOpacity', 'Hole transparency', 35));

        el.appendChild(head);
        el.appendChild(body);

        el.addEventListener('mousedown', (e) => e.stopPropagation());
        el.addEventListener('click', (e) => e.stopPropagation());

        // Auto-save + live apply on any change (no Save/Close buttons)
        const persistFromUI = () => {
          try {
            const next = { ...loadSettings() };
            el.querySelectorAll('input[data-k]').forEach((inp) => {
              if (inp.type === 'checkbox') next[inp.dataset.k] = inp.checked;
            });
            const ytR = el.querySelector('input[data-k="ytOpacity"]');
            const holeR = el.querySelector('input[data-k="holeOpacity"]');
            if (ytR) next.ytOpacity = Math.min(1, Math.max(0.1, Number(ytR.value) / 100));
            if (holeR) next.holeOpacity = Math.min(1, Math.max(0.1, Number(holeR.value) / 100));
            settings = next;
            saveSettings(next);
            // Same storage as Coolhole drag double-click (hide / show Hist+Q+)
            gm.setValue(collapsedKey, next.qPlus === false);
            applyYtOpacity();
          } catch (err) {
            console.warn('[CoolholeQueue] auto-save failed', err);
          }
        };

        el.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
          cb.addEventListener('change', persistFromUI);
        });

        el.querySelectorAll('input[type="range"]').forEach((r) => {
          r.addEventListener('input', () => {
            const b = el.querySelector('[data-v="' + r.dataset.k + '"]');
            if (b) b.textContent = r.value + '%';
            // Live transparency while dragging
            if (r.dataset.k === 'ytOpacity') {
              const op = Math.min(1, Math.max(0.1, Number(r.value) / 100));
              pill.style.opacity = String(op);
            }
            persistFromUI();
          });
        });

        (document.body || document.documentElement).appendChild(el);
        menuEl = el;
        return el;
      };

      const syncMenuUI = () => {
        if (!menuEl) return;
        settings = loadSettings();
        // Mirror Coolhole collapse state into Q+ features checkbox
        const collapsed = gm.getValue(collapsedKey, false) === true;
        settings = { ...settings, qPlus: !collapsed };
        menuEl.querySelectorAll('input[data-k]').forEach((inp) => {
          if (inp.type === 'checkbox') {
            if (inp.dataset.k === 'qPlus') inp.checked = !collapsed;
            else inp.checked = !!settings[inp.dataset.k];
          }
        });
        const ytR = menuEl.querySelector('input[data-k="ytOpacity"]');
        const holeR = menuEl.querySelector('input[data-k="holeOpacity"]');
        if (ytR) {
          ytR.value = String(Math.round((settings.ytOpacity || 0.38) * 100));
          const b = menuEl.querySelector('[data-v="ytOpacity"]');
          if (b) b.textContent = ytR.value + '%';
        }
        if (holeR) {
          holeR.value = String(Math.round((settings.holeOpacity || 0.35) * 100));
          const b = menuEl.querySelector('[data-v="holeOpacity"]');
          if (b) b.textContent = holeR.value + '%';
        }
      };

      document.addEventListener('mousedown', (e) => {
        if (!menuOpen || !menuEl) return;
        if (Date.now() - menuOpenedAt < 500) return;
        if (menuEl.contains(e.target) || cp.contains(e.target) || pill.contains(e.target)) return;
        hideMenu();
      });

      const openMenu = () => {
        try {
          const el = ensureMenu();
          syncMenuUI();

          clearHideTimer();
          pill.style.display = 'inline-flex';
          pill.style.opacity = '1';

          // Center under the whole CH/CP pill, clamp so it stays on-screen
          const pillRect = pill.getBoundingClientRect();
          const menuW = 260;
          const menuH = 220; // approx height for flip decision
          let left = pillRect.left + pillRect.width / 2 - menuW / 2;
          left = Math.max(8, Math.min(left, window.innerWidth - menuW - 8));

          let top = pillRect.bottom + 6;
          if (top + menuH > window.innerHeight - 8) {
            // flip above the pill if not enough room below
            top = Math.max(8, pillRect.top - menuH - 6);
          }

          el.style.cssText =
            'display:block !important;visibility:visible !important;opacity:1 !important;' +
            'position:fixed !important;z-index:2147483647 !important;' +
            'left:' + left + 'px;top:' + top + 'px;width:' + menuW + 'px;' +
            'background:#1e1e1e;border:2px solid #1184e8;border-radius:8px;' +
            'box-shadow:0 12px 40px rgba(0,0,0,.75);color:#eee;' +
            'font:12px/1.35 system-ui,sans-serif;overflow:hidden;pointer-events:auto;';

          menuOpen = true;
          menuOpenedAt = Date.now();
          holdOpenedMenu = true;
        } catch (err) {
          console.error('[CoolholeQueue] openMenu error', err);
        }
      };

      // --- Hold → menu / short press → Work ---
      // Uses press duration on release (most reliable on YouTube).
      // Also opens mid-hold once HOLD_MS is reached.
      let pressStartedAt = 0;

      const clearHold = () => {
        if (holdTimer) {
          clearTimeout(holdTimer);
          holdTimer = null;
        }
        setHoldVisual(false);
        holdActive = false;
        pressStartedAt = 0;
      };

      const onPressStart = (event) => {
        if (event.button != null && event.button !== 0) return;
        // Ignore duplicate mousedown after pointerdown
        if (holdActive && pressStartedAt && Date.now() - pressStartedAt < 50) return;
        event.preventDefault();
        event.stopPropagation();
        clearHideTimer();
        pill.style.display = 'inline-flex';
        pill.style.opacity = '1';
        if (holdTimer) clearTimeout(holdTimer);
        holdActive = true;
        holdOpenedMenu = false;
        pressStartedAt = Date.now();
        setHoldVisual(true);
        try {
          if (event.pointerId != null) cp.setPointerCapture(event.pointerId);
        } catch {
          /* ignore */
        }
        holdTimer = setTimeout(() => {
          holdTimer = null;
          if (!holdActive) return;
          holdOpenedMenu = true;
          setHoldVisual(false);
          openMenu();
        }, HOLD_MS);
      };

      let endLockUntil = 0;
      const onPressEnd = (event) => {
        if (event && event.button != null && event.button !== 0) return;
        if (!holdActive && !pressStartedAt) return;
        // Debounce duplicate pointerup + mouseup
        if (Date.now() < endLockUntil) return;
        endLockUntil = Date.now() + 80;

        const heldFor = pressStartedAt ? Date.now() - pressStartedAt : 0;
        const opened = holdOpenedMenu;
        try {
          if (event && event.pointerId != null) cp.releasePointerCapture(event.pointerId);
        } catch {
          /* ignore */
        }
        if (holdTimer) {
          clearTimeout(holdTimer);
          holdTimer = null;
        }
        setHoldVisual(false);
        holdActive = false;
        pressStartedAt = 0;

        if (opened) return;
        if (heldFor >= HOLD_MS) {
          holdOpenedMenu = true;
          openMenu();
          return;
        }
        sendWork(cp);
      };

      // Prefer pointer events; mousedown is backup for older paths
      if (window.PointerEvent) {
        cp.addEventListener('pointerdown', onPressStart);
        cp.addEventListener('pointerup', onPressEnd);
        cp.addEventListener('pointercancel', onPressEnd);
      } else {
        cp.addEventListener('mousedown', onPressStart);
        window.addEventListener('mouseup', onPressEnd);
      }
      // Always listen for mouseup as safety net when pointer capture fails
      window.addEventListener('mouseup', onPressEnd);

      cp.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        event.stopImmediatePropagation();
      });

      pill.append(ch, cp);

      const mountAll = () => {
        try {
          const root = document.body || document.documentElement;
          if (!pill.isConnected) root.appendChild(pill);
          if (menuEl && !menuEl.isConnected) root.appendChild(menuEl);
          applyYtOpacity();
        } catch (err) {
          console.error('[CoolholeQueue] pill mount failed', err);
        }
      };
      if (document.body) mountAll();
      else document.addEventListener('DOMContentLoaded', mountAll, { once: true });

      pill.addEventListener('mouseenter', () => {
        clearHideTimer();
        pill.style.opacity = '1';
      });
      pill.addEventListener('mouseleave', () => {
        // Keep pill visible while settings menu is open
        if (menuOpen) {
          pill.style.display = 'inline-flex';
          pill.style.opacity = '1';
          return;
        }
        scheduleHide();
        applyYtOpacity();
      });

      try {
        gm.onValueChange(settingsKey, (_n, _o, next) => {
          if (next && typeof next === 'object') {
            settings = { ...defaultSettings, ...next };
            applyYtOpacity();
          }
        });
      } catch {
        /* ignore */
      }

      // Charge bar: live listener + poll fallback (some managers miss value-change)
      try {
        startLocalCountdown(gm.getValue(cdKey, null));
      } catch {
        /* ignore */
      }
      try {
        gm.onValueChange(cdKey, (_n, _o, next) => startLocalCountdown(next));
      } catch {
        /* ignore */
      }
      setInterval(() => {
        try {
          const state = gm.getValue(cdKey, null);
          if (state && state.ts && state.ts !== lastCdTs) {
            startLocalCountdown(state);
          }
        } catch {
          /* ignore */
        }
      }, 1000);

      return pill;
    }

    sharedPill = buildSharedPill();

    function positionPill(anchorEl, preferAbove) {
      try {
        const rect = anchorEl.getBoundingClientRect();
        const pillRect = sharedPill.getBoundingClientRect();
        const margin = 6;
        const spaceAbove = rect.top;
        const spaceBelow = window.innerHeight - rect.bottom;
        const placeAbove = preferAbove
          ? spaceAbove >= pillRect.height + margin || spaceAbove >= spaceBelow
          : spaceBelow < pillRect.height + margin && spaceAbove > spaceBelow;
        const top = placeAbove ? rect.top - pillRect.height - margin : rect.bottom + margin;
        let left = rect.right - pillRect.width;
        if (left < 4) left = 4;
        if (left + pillRect.width > window.innerWidth - 4) {
          left = window.innerWidth - pillRect.width - 4;
        }
        sharedPill.style.top = `${Math.max(4, top)}px`;
        sharedPill.style.left = `${left}px`;
      } catch (e) {
        console.warn('[CoolholeQueue] positionPill failed', e);
      }
    }

    function showPillFor(anchorEl, ctx, preferAbove) {
      if (!ctx || !ctx.id || !anchorEl) return;
      currentCtx.id = ctx.id;
      currentCtx.title = ctx.title;
      clearHideTimer();
      sharedPill.style.visibility = 'hidden';
      sharedPill.style.display = 'inline-flex';
      positionPill(anchorEl, preferAbove);
      sharedPill.style.visibility = 'visible';
    }

    window.addEventListener('scroll', () => hidePillNow(), true);
    window.addEventListener('resize', () => hidePillNow());

    function attachHoverTrigger(triggerEl, getContext, preferAbove, positionEl) {
      if (!triggerEl || triggerEl.dataset.cqHooked === '1') return;
      triggerEl.dataset.cqHooked = '1';
      const anchor = positionEl || triggerEl;
      triggerEl.addEventListener('mouseenter', () => {
        let ctx = null;
        try {
          ctx = getContext();
        } catch (e) {
          console.warn('[CoolholeQueue] getContext failed', e);
        }
        if (ctx && ctx.id) showPillFor(anchor, ctx, preferAbove);
      });
      triggerEl.addEventListener('mouseleave', scheduleHide);
    }

    function injectUnderMenu(card) {
      if (card.dataset.cqHooked === '1') return;
      const menu = findMenu(card);
      if (!menu || !videoIdFromCard(card)) return;
      attachHoverTrigger(
        card,
        () => ({ id: videoIdFromCard(card), title: titleFromCard(card) }),
        false,
        menu
      );
    }

    function injectWatchPage() {
      const id = currentWatchVideoId();
      if (!id) return;
      const menu = findWatchMenu();
      if (!menu) return;
      attachHoverTrigger(
        menu,
        () => ({ id: currentWatchVideoId(), title: titleFromWatchPage() }),
        true
      );
    }

    function scanCards() {
      scanCount += 1;
      let touched = 0;
      for (const card of document.querySelectorAll(CARD_SELECTORS)) {
        try {
          injectUnderMenu(card);
          if (card.dataset.cqHooked === '1') touched += 1;
        } catch (e) {
          console.warn('[CoolholeQueue] card inject error', e);
        }
      }
      const total = document.querySelectorAll('[data-cq-hooked="1"]').length;
      if (scanCount <= 3 || scanCount % 10 === 0) {
        console.log(`[CoolholeQueue] scan #${scanCount} — hover triggers: ${total} (cards: ${touched})`);
      }
    }

    function shouldExpectPills() {
      const path = location.pathname ?? '';
      if (
        path === '/' ||
        path.startsWith('/feed') ||
        path.startsWith('/results') ||
        path.startsWith('/watch') ||
        path.startsWith('/shorts') ||
        path.startsWith('/channel') ||
        path.startsWith('/@') ||
        path.startsWith('/playlist')
      ) {
        return true;
      }
      return document.querySelector(CARD_SELECTORS) != null;
    }

    function collectDiagReasons() {
      const reasons = [];
      const cards = document.querySelectorAll(CARD_SELECTORS);
      const menus = document.querySelectorAll(
        'ytd-menu-renderer, #menu, button[aria-label="More actions"], button[aria-label*="More"]'
      );
      const hooks = document.querySelectorAll('[data-cq-hooked="1"]');
      if (!document.body) reasons.push('Page body not ready yet.');
      if (cards.length === 0) reasons.push('No video cards found.');
      else reasons.push(`Found ${cards.length} video card(s).`);
      if (menus.length === 0) reasons.push('No ⋮ menus found.');
      else reasons.push(`Found ${menus.length} menu control(s).`);
      if (hooks.length === 0) reasons.push('Zero hover triggers attached.');
      else reasons.push(`${hooks.length} hover trigger(s) attached.`);
      if (typeof GM_setValue !== 'function' && typeof GM?.setValue !== 'function') {
        reasons.push('Userscript storage APIs missing.');
      }
      return reasons;
    }

    function maybeShowDiagnostics() {
      if (diagShown || !shouldExpectPills()) return;
      if (document.querySelectorAll('[data-cq-hooked="1"]').length > 0) return;
      if (scanCount < 2) return;
      diagShown = true;
      const reasons = collectDiagReasons();
      console.warn('[CoolholeQueue] Diagnostics:', reasons);
      document.getElementById('cq-diag')?.remove();
      const box = document.createElement('div');
      box.id = 'cq-diag';
      box.innerHTML =
        '<strong>CoolholeQueue:</strong> buttons did not appear.<br>' +
        reasons.map((r) => `• ${r}`).join('<br>') +
        '<br><button type="button" class="cq-diag-retry">Retry scan</button>' +
        '<button type="button" class="cq-diag-dismiss">Dismiss</button>';
      (document.body ?? document.documentElement).append(box);
      box.querySelector('.cq-diag-dismiss')?.addEventListener('click', () => box.remove());
      box.querySelector('.cq-diag-retry')?.addEventListener('click', () => {
        box.remove();
        diagShown = false;
        scanCount = 0;
        scanCards();
        injectWatchPage();
        setTimeout(maybeShowDiagnostics, 1500);
      });
    }
  }

  // ======================================================================
  // COOLHOLE
  // ======================================================================
  function initCoolholeSide() {
    const SUCCESS_LINES = [
      'Queued: {title}',
      'Dropped "{title}" at the end of the line',
      'Into the hole: {title}',
      'Locked and loaded: {title}',
      'Added to the pile: {title}',
      'Queue fed. "{title}" is in.',
      'Yoink — "{title}" is yours now',
      'Slid "{title}" onto the runway',
      'Fresh meat for the playlist: {title}',
      'The hole accepts "{title}"',
      'Stashed "{title}" for later',
      'One more for the road: {title}',
      'Sealed the deal: {title}',
      'Coolhole ate "{title}"',
      'Parked "{title}" at the back',
      'Fed the filters: {title}',
      'Another offering for the hole: {title}',
      'Playlist thickened with "{title}"',
      'Chat will see "{title}" soon enough',
      'Slipped "{title}" past the gate',
    ];
    const FAIL_LINES = [
      'Could not auto-queue — open the Add panel and try again.',
      'Queue miss. Open Library / Add and retry.',
      'The hole spat it back out. Try the Add panel.',
      'Auto-queue failed. Manual Add is your friend.',
      'Filters shrugged. Try Add manually.',
      'The hole blinked — queue it by hand this time.',
    ];
    const MAX_QUEUE_MSG =
      'Your slots in the hole are full — wait for one of yours to play, then try again.';
    const ADDED_TO_Q_LINES = [
      'Parked in Q+ — next free slot is yours',
      'Holding in Q+ until the hole has room',
      'Q+ caught it — auto-queues when a slot opens',
      'Stashed in Q+ (top of the list goes in first)',
      'In the waiting hole — Q+ has it',
    ];
    const qPlusToast = () => pick(ADDED_TO_Q_LINES);

    const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
    const formatLine = (template, title) =>
      template.replaceAll('{title}', title || 'Unknown video');

    // ── storage helpers ──────────────────────────────────────────────
    function loadPending() {
      const raw = gm.getValue(pendingKey, []);
      return Array.isArray(raw) ? raw : [];
    }
    function savePending(list) {
      gm.setValue(pendingKey, list.slice(0, MAX_PENDING));
    }
    function loadHistory() {
      const raw = gm.getValue(historyKey, []);
      return Array.isArray(raw) ? raw : [];
    }
    function saveHistory(list) {
      gm.setValue(historyKey, list.slice(0, MAX_HISTORY));
    }
    function getMyLimit() {
      const n = gm.getValue(limitKey, 3);
      return typeof n === 'number' && n > 0 ? n : 3;
    }
    function setMyLimit(n) {
      if (typeof n === 'number' && n > 0) gm.setValue(limitKey, n);
    }
    function isAutoEnabled() {
      return gm.getValue(autoKey, true) !== false;
    }
    function setAutoEnabled(on) {
      gm.setValue(autoKey, !!on);
    }
    /** Q+ on unless the float is collapsed (CP “Q+ features” or drag double-click). */
    function isQPlusEnabled() {
      return !isCollapsed();
    }

    function getMyName() {
      return document.querySelector('#username')?.textContent?.trim() || null;
    }

    function getMyQueuedCount() {
      const myName = getMyName();
      if (!myName) return 0;
      let count = 0;
      document.querySelectorAll('#queue li').forEach((li) => {
        const who = li.querySelector('.q-user')?.textContent?.trim();
        if (who === myName) count += 1;
      });
      return count;
    }

    function addToPending(videoId, title) {
      const list = loadPending();
      if (list.some((x) => x.videoId === videoId)) return false;
      list.push({
        videoId: String(videoId),
        title: cleanTitle(title) || null,
        addedAt: Date.now(),
      });
      savePending(list);
      updateQBadge();
      return true;
    }

    function removeFromPending(videoId) {
      const list = loadPending().filter((x) => x.videoId !== videoId);
      savePending(list);
      updateQBadge();
    }

    function addToHistory(videoId, title) {
      const list = loadHistory().filter((x) => x.videoId !== videoId);
      list.unshift({
        videoId: String(videoId),
        title: cleanTitle(title) || null,
        queuedAt: Date.now(),
      });
      saveHistory(list);
    }

    // ── error / toast helpers (kept from 2.6.8) ──────────────────────
    function getRecentErrorText() {
      const chunks = [];
      const selectors = [
        '#messagebuffer .server-whisper',
        '#messagebuffer .server-msg',
        '#messagebuffer .action',
        '#messagebuffer .chat-msg-server',
        '.alert-danger',
        '.alert-warning',
        '#qfail',
        '#queuefail',
        '.queue-error',
      ];
      for (const sel of selectors) {
        try {
          document.querySelectorAll(sel).forEach((el) => {
            const t = el.textContent?.trim();
            if (t) chunks.push(t);
          });
        } catch {
          /* ignore */
        }
      }
      try {
        const buf = document.querySelector('#messagebuffer');
        if (buf) {
          for (const n of Array.from(buf.querySelectorAll('div, span, p, li')).slice(-15)) {
            const t = n.textContent?.trim();
            if (t) chunks.push(t);
          }
        }
      } catch {
        /* ignore */
      }
      return chunks.join('\n');
    }

    function isMaxQueueError(text) {
      if (!text) return false;
      if (/already have \d+\s+items?\s+queued/i.test(text)) return true;
      if (/wait for one to play/i.test(text)) return true;
      if (/limit\s*\d+/i.test(text) && /queued/i.test(text)) return true;
      if (/queue(?:d)?\s*(?:is\s*)?(?:full|limit)/i.test(text)) return true;
      if (/too many (videos?|items?)/i.test(text)) return true;
      return false;
    }

    function learnLimitFromError(text) {
      const m = text.match(/already have (\d+)\s+items?\s+queued/i);
      if (m) {
        const n = parseInt(m[1], 10);
        if (n > 0) setMyLimit(n);
      } else {
        // fallback: current count is the limit
        const c = getMyQueuedCount();
        if (c > 0) setMyLimit(c);
      }
    }

    function fancyLog(label, ok, gold, maxed) {
      const style = ok
        ? gold
          ? 'background:linear-gradient(90deg,#f9a825,#ffd54f,#f9a825);color:#1a1200;font-weight:700;padding:2px 8px;border-radius:4px;'
          : 'background:#1184e8;color:#fff;font-weight:600;padding:2px 8px;border-radius:4px;'
        : maxed
          ? 'background:#ef6c00;color:#fff;font-weight:600;padding:2px 8px;border-radius:4px;'
          : 'background:#c62828;color:#ffebee;font-weight:600;padding:2px 8px;border-radius:4px;';
      const tag = ok ? (gold ? '★ GOLD QUEUE' : 'QUEUE') : maxed ? 'MAX QUEUE' : 'FAIL';
      console.log(`%c[CoolholeQueue] ${tag}%c ${label ?? ''}`, style, 'color:inherit;font-weight:500;');
    }

    injectStyles(`
      #cq-toast {
        position: fixed;
        bottom: 20px;
        right: 20px;
        padding: 11px 16px;
        border-radius: 8px;
        color: #fff;
        font: 600 13px/1.4 system-ui, sans-serif;
        z-index: 999999;
        opacity: 0;
        transform: translateY(12px) scale(0.97);
        transition: opacity .28s ease, transform .28s ease;
        pointer-events: none;
        max-width: 400px;
        box-shadow: 0 4px 18px rgba(0,0,0,.4);
        border: 1px solid rgba(255,255,255,.08);
        backdrop-filter: blur(6px);
        -webkit-backdrop-filter: blur(6px);
      }
      #cq-toast.show { opacity: 1; transform: translateY(0) scale(1); }
      #cq-toast.cq-fail { animation: cq-shake .45s ease; }
      #cq-toast.cq-max {
        background: linear-gradient(135deg, #ef6c00, #fb8c00 50%, #e65100) !important;
        border-color: rgba(255, 183, 77, 0.35);
      }
      #cq-toast.cq-qplus {
        background: linear-gradient(135deg, #5e35b1, #7e57c2 45%, #4527a0) !important;
        border-color: rgba(179, 157, 219, 0.35);
      }
      #cq-toast.cq-gold {
        color: #1a1200;
        background: linear-gradient(135deg, #f9a825, #ffd54f 40%, #ffb300 70%, #f9a825);
        background-size: 200% 200%;
        animation: cq-gold-shine 1.6s ease-in-out infinite;
        box-shadow: 0 2px 14px rgba(249,168,37,.4);
        border-color: rgba(255, 224, 130, 0.5);
      }
      @keyframes cq-gold-shine {
        0% { background-position: 0% 50%; }
        50% { background-position: 100% 50%; }
        100% { background-position: 0% 50%; }
      }
      @keyframes cq-shake {
        0%, 100% { transform: translateY(0) translateX(0); }
        20% { transform: translateY(0) translateX(-6px); }
        40% { transform: translateY(0) translateX(6px); }
        60% { transform: translateY(0) translateX(-4px); }
        80% { transform: translateY(0) translateX(4px); }
      }
      @media (prefers-reduced-motion: reduce) {
        #cq-toast, #cq-toast.cq-fail, #cq-toast.cq-gold {
          animation: none !important;
          transition: opacity .15s ease !important;
        }
      }

      /* Floating Hist/Q+ pill – matches YouTube pill style */
      #cq-float {
        position: fixed;
        z-index: 999998;
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        font-family: Roboto, Arial, system-ui, sans-serif;
        user-select: none;
        opacity: 0.35;
        transition: opacity .2s ease;
      }
      #cq-float:hover {
        opacity: 1;
      }
      /* Collapsed: only a tiny restore chip */
      #cq-float.cq-collapsed {
        opacity: 0.22;
      }
      #cq-float.cq-collapsed:hover {
        opacity: 0.7;
      }
      #cq-float.cq-collapsed #cq-float-row,
      #cq-float.cq-collapsed #cq-panel {
        display: none !important;
      }
      #cq-float-restore {
        display: none;
        position: relative;
        cursor: pointer;
        border: 0;
        border-radius: 999px;
        padding: 4px 9px;
        font: 700 10px/1 system-ui, sans-serif;
        letter-spacing: 0.04em;
        color: #eee;
        background: rgba(70, 70, 70, 0.55);
        box-shadow: 0 1px 4px rgba(0,0,0,.35);
      }
      #cq-float.cq-collapsed #cq-float-restore {
        display: inline-flex;
        align-items: center;
      }
      #cq-float-restore:hover {
        background: rgba(90, 90, 90, 0.85);
      }
      #cq-restore-badge {
        position: absolute;
        top: -6px;
        right: -6px;
        min-width: 14px;
        height: 14px;
        padding: 0 3px;
        border-radius: 7px;
        background: #e53935;
        color: #fff;
        font: 700 8px/14px system-ui, sans-serif;
        text-align: center;
        pointer-events: none;
      }
      #cq-restore-badge:empty { display: none; }
      #cq-float-row {
        display: inline-flex;
        align-items: center;
        gap: 5px;
        position: relative;
      }
      /* Drag tab: invisible until hover near the pill */
      #cq-float .cq-drag {
        cursor: grab;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 20px;
        height: 20px;
        border-radius: 6px;
        background: rgba(50, 50, 50, 0.35);
        border: 1px solid transparent;
        color: transparent;
        font-size: 11px;
        line-height: 1;
        letter-spacing: -1px;
        flex-shrink: 0;
        opacity: 0;
        transition: opacity .18s ease, background .15s ease, color .15s ease, border-color .15s ease;
      }
      #cq-float-row:hover .cq-drag,
      #cq-float .cq-drag:active {
        opacity: 1;
        color: rgba(220, 220, 220, 0.85);
        background: rgba(60, 60, 60, 0.55);
        border-color: rgba(255, 255, 255, 0.12);
      }
      #cq-float .cq-drag:hover {
        background: rgba(80, 80, 80, 0.8);
        color: #fff;
      }
      #cq-float .cq-drag:active {
        cursor: grabbing;
        background: rgba(100, 100, 100, 0.9);
      }
      #cq-float-pill-wrap {
        position: relative;
        display: inline-flex;
      }
      #cq-float-pill {
        display: inline-flex;
        align-items: stretch;
        border-radius: 999px;
        overflow: hidden;
        box-shadow: 0 2px 8px rgba(0,0,0,.45), inset 0 0 0 1px rgba(255,255,255,.08);
        line-height: 1;
      }
      #cq-float-pill .cq-seg {
        cursor: pointer;
        border: 0 !important;
        margin: 0;
        padding: 6px 12px;
        font-size: 11px;
        font-weight: 700;
        letter-spacing: 0.05em;
        color: #fff !important;
        transition: background .15s ease;
        min-width: 2.6em;
        text-align: center;
        background: #4a4a4a;
        border-radius: 0 !important;
        appearance: none;
        -webkit-appearance: none;
        outline: none;
        box-shadow: none;
      }
      #cq-float-pill .cq-seg + .cq-seg {
        border-left: 1px solid rgba(0,0,0,.25) !important;
        background: #555;
      }
      #cq-float-pill .cq-seg:hover {
        background: linear-gradient(180deg, #e53935, #b71c1c);
      }
      #cq-float-pill .cq-seg.cq-seg-q:hover,
      #cq-float-pill .cq-seg.cq-seg-q.cq-active {
        background: linear-gradient(180deg, #1184e8, #0a5fad);
      }
      #cq-float-pill .cq-seg.cq-seg-hist.cq-active {
        background: linear-gradient(180deg, #e53935, #b71c1c);
      }
      /* Badge sits on the wrap so overflow:hidden on the pill does not clip it */
      #cq-q-badge {
        position: absolute;
        top: -7px;
        right: -7px;
        min-width: 16px;
        height: 16px;
        padding: 0 4px;
        border-radius: 8px;
        background: #e53935;
        color: #fff;
        font: 700 9px/16px system-ui, sans-serif;
        text-align: center;
        pointer-events: none;
        box-shadow: 0 1px 4px rgba(0,0,0,.5);
        z-index: 5;
      }
      #cq-q-badge:empty { display: none; }

      /* Panel attached under the pill */
      #cq-panel {
        display: none;
        flex-direction: column;
        width: 300px;
        margin-top: 6px;
        background: #1e1e1e;
        border: 1px solid #444;
        border-radius: 8px;
        box-shadow: 0 8px 24px rgba(0,0,0,.5);
        font: 12px/1.35 system-ui, sans-serif;
        color: #eee;
        overflow: hidden;
      }
      #cq-panel.open { display: flex; }
      #cq-panel-body {
        max-height: 108px; /* ~3 rows */
        overflow-y: auto;
        padding: 2px 0;
      }
      #cq-panel-body .cq-row {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 5px 8px;
        border-bottom: 1px solid #2a2a2a;
        min-height: 28px;
        box-sizing: border-box;
      }
      #cq-panel-body .cq-row:hover { background: #2a2a2a; }
      #cq-panel-body .cq-row.cq-dragging {
        opacity: 0.45;
        background: #333;
      }
      #cq-panel-body .cq-row.cq-drag-over {
        border-top: 2px solid #1184e8;
      }
      #cq-panel-body .cq-grip {
        flex-shrink: 0;
        cursor: grab;
        color: #777;
        font-size: 11px;
        letter-spacing: -1px;
        padding: 0 2px;
        user-select: none;
      }
      #cq-panel-body .cq-grip:active { cursor: grabbing; }
      #cq-panel-body .cq-row.cq-next .cq-title {
        color: #90caf9;
      }
      #cq-panel-body .cq-title {
        flex: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 11px;
      }
      #cq-panel-body .cq-act {
        flex-shrink: 0;
        border: 0;
        border-radius: 3px;
        padding: 2px 6px;
        font-size: 10px;
        font-weight: 600;
        cursor: pointer;
      }
      #cq-panel-body .cq-act-ch {
        background: #c62828;
        color: #fff;
      }
      #cq-panel-body .cq-act-del {
        background: #424242;
        color: #ccc;
      }
      #cq-panel-footer {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 6px 8px;
        border-top: 1px solid #333;
        background: #252525;
        font-size: 11px;
      }
      #cq-panel-footer label {
        display: flex;
        align-items: center;
        gap: 5px;
        cursor: pointer;
        user-select: none;
      }
      #cq-panel-footer button {
        border: 0;
        border-radius: 3px;
        padding: 2px 7px;
        font-size: 10px;
        font-weight: 600;
        background: #424242;
        color: #ddd;
        cursor: pointer;
      }
      #cq-panel-empty {
        padding: 16px 10px;
        text-align: center;
        color: #777;
        font-size: 11px;
      }
    `);

    function showToast(msg, ok, gold, maxed) {
      let toast = document.getElementById('cq-toast');
      if (!toast) {
        toast = document.createElement('div');
        toast.id = 'cq-toast';
        (document.body ?? document.documentElement).append(toast);
      }
      toast.textContent = msg;
      toast.classList.remove('show', 'cq-fail', 'cq-gold', 'cq-max', 'cq-qplus');
      // ok + maxed → Q+ hold (purple). !ok + maxed → hard limit (orange).
      if (maxed && ok) {
        toast.style.background = '';
        toast.classList.add('cq-qplus');
      } else if (maxed) {
        toast.style.background = '';
        toast.classList.add('cq-max');
      } else if (!ok) {
        toast.style.background = 'linear-gradient(180deg, #e53935, #b71c1c)';
        toast.classList.add('cq-fail');
      } else if (gold) {
        toast.style.background = '';
        toast.classList.add('cq-gold');
      } else {
        toast.style.background = 'linear-gradient(180deg, #1184e8, #0a5fad)';
      }
      void toast.offsetWidth;
      toast.classList.add('show');
      clearTimeout(toast._cqTimer);
      toast._cqTimer = setTimeout(() => {
        toast.classList.remove('show', 'cq-fail', 'cq-gold', 'cq-max', 'cq-qplus');
      }, maxed ? 5200 : gold ? 4200 : 3600);
    }

    // ── alive heartbeat ──────────────────────────────────────────────
    const beat = () => gm.setValue(aliveKey(host), Date.now());
    beat();
    setInterval(beat, 5000);
    window.addEventListener('beforeunload', () => gm.setValue(aliveKey(host), 0));

    // ── Work button ──────────────────────────────────────────────────
    function getWorkButton() {
      return (
        document.querySelector('#job-actions button.job-action-work') ||
        document.querySelector('button.job-action-work') ||
        document.querySelector('button[title="Earn CP"]')
      );
    }

    function collectRecentSystemTexts(limit = 6) {
      const out = [];
      try {
        const lines = document.querySelectorAll(
          '#messagebuffer .server-whisper, #messagebuffer .chat-line.server-whisper, #messagebuffer .chat-line'
        );
        for (let i = lines.length - 1; i >= 0 && out.length < limit; i--) {
          const el = lines[i];
          const isSystem =
            el.classList.contains('server-whisper') ||
            (el.querySelector && el.querySelector('.chat-user')?.textContent?.trim() === 'System');
          if (!isSystem) continue;
          const textEl = el.querySelector('.chat-text') || el;
          const text = (textEl.textContent || '').replace(/\s+/g, ' ').trim();
          if (text) out.push(text);
        }
      } catch {
        /* ignore */
      }
      return out;
    }

    function isWorkBlockedMessage(text) {
      if (!text) return false;
      const t = text.toLowerCase();
      // handle curly apostrophes etc.
      return (
        /can[\u2019']?t work/.test(t) ||
        /cannot work/.test(t) ||
        /sun saps your strength/.test(t) ||
        /wait for nightfall/.test(t) ||
        /work by day/.test(t) ||
        /not allowed to work/.test(t) ||
        /you are (dead|a ghost|ghost|jailed)/.test(t) ||
        (/error/.test(t) && /work/.test(t)) ||
        /unable to work/.test(t) ||
        /work failed/.test(t)
      );
    }

    function findNewWorkBlock(beforeSet) {
      const now = collectRecentSystemTexts(8);
      for (const msg of now) {
        if (beforeSet.has(msg)) continue;
        if (isWorkBlockedMessage(msg)) return msg;
      }
      // also accept if the newest message is a block even if text matched (re-posted)
      if (now[0] && isWorkBlockedMessage(now[0])) return now[0];
      return null;
    }

    async function doWorkAction() {
      try {
        window.focus();
      } catch {
        /* ignore */
      }

      let btn = getWorkButton();
      if (!btn) {
        for (let i = 0; i < 20 && !btn; i++) {
          await new Promise((r) => setTimeout(r, 250));
          btn = getWorkButton();
        }
      }

      if (!btn) {
        showToast('Work button not found — are you logged in with a job?', false, false, false);
        console.warn('[CoolholeQueue] Work button not in DOM');
        return;
      }

      if (btn.disabled || btn.classList.contains('work-cooling')) {
        const label = (btn.textContent || '').replace(/\s+/g, ' ').trim() || 'cooling down';
        showToast(`Work is on cooldown (${label})`, false, false, true);
        return;
      }

      const beforeSet = new Set(collectRecentSystemTexts(8));
      const buf = document.querySelector('#messagebuffer');
      let blockedMsg = null;

      // Live watcher for system lines while we wait
      let observer = null;
      if (buf && typeof MutationObserver === 'function') {
        observer = new MutationObserver(() => {
          const hit = findNewWorkBlock(beforeSet);
          if (hit) blockedMsg = hit;
        });
        try {
          observer.observe(buf, { childList: true, subtree: true, characterData: true });
        } catch {
          observer = null;
        }
      }

      btn.click();
      showToast('Work — earning CP', true, false, false);
      console.log(
        '%c[CoolholeQueue]%c Work clicked',
        'background:#1184e8;color:#fff;font-weight:700;padding:2px 8px;border-radius:4px;',
        'color:inherit;'
      );

      // Watch ~3s for system rejection (day/night, dead, jailed, etc.)
      for (let i = 0; i < 10; i++) {
        await new Promise((r) => setTimeout(r, 300));
        if (!blockedMsg) blockedMsg = findNewWorkBlock(beforeSet);
        if (blockedMsg) {
          const short =
            blockedMsg.length > 100 ? blockedMsg.slice(0, 97) + '…' : blockedMsg;
          showToast('Work failed — ' + short, false, false, false);
          console.warn('[CoolholeQueue] Work blocked by system:', blockedMsg);
          try {
            gm.setValue(cdKey, {
              ready: true,
              remainingMs: 0,
              totalMs: 0,
              startedAt: Date.now(),
              ts: Date.now(),
              blocked: true,
              reason: blockedMsg,
            });
          } catch {
            /* ignore */
          }
          break;
        }
        const b2 = getWorkButton();
        if (b2 && (b2.disabled || b2.classList.contains('work-cooling'))) {
          // accepted — cooldown started
          break;
        }
      }

      if (observer) {
        try {
          observer.disconnect();
        } catch {
          /* ignore */
        }
      }
    }

    // ── Work cooldown reporter (one-shot after a *successful* Work) ──
    function parseCountdownMs(text) {
      if (!text) return 0;
      const raw = String(text).replace(/\s+/g, ' ').trim();
      const m = raw.match(/(?:(\d+)\s*:\s*)?(\d+)\s*s?\b/i);
      if (m) {
        const mins = m[1] ? parseInt(m[1], 10) : 0;
        const secs = parseInt(m[2], 10) || 0;
        return (mins * 60 + secs) * 1000;
      }
      const n = parseInt(raw.replace(/[^\d]/g, ''), 10);
      if (n > 0 && n < 900) return n * 1000;
      return 0;
    }

    let lastCaptureAt = 0;

    function captureWorkCooldown() {
      try {
        // Never overwrite an in-progress cooldown snapshot
        if (Date.now() - lastCaptureAt < 8000) {
          const existing = gm.getValue(cdKey, null);
          if (existing && existing.ready === false && existing.remainingMs > 0) return;
        }

        const btn =
          document.querySelector('#job-actions button.job-action-work') ||
          document.querySelector('button.job-action-work') ||
          document.querySelector('button[title="Earn CP"]') ||
          document.querySelector('.job-ability-work') ||
          document.querySelector('button.job-ability-work');

        const cdEl = document.querySelector('#jcd-work');
        const cooling =
          !!btn &&
          (btn.disabled ||
            btn.classList.contains('work-cooling') ||
            (cdEl && /\d/.test(cdEl.textContent || '')));

        if (!cooling) return; // Work didn't take — leave YT bar alone

        const sources = [
          cdEl && cdEl.textContent,
          btn && btn.textContent,
          btn && btn.getAttribute('title'),
          btn && btn.querySelector('.job-ab-cd') && btn.querySelector('.job-ab-cd').textContent,
        ];

        let remainingMs = 0;
        for (const src of sources) {
          const ms = parseCountdownMs(src);
          if (ms > 0) {
            remainingMs = ms;
            break;
          }
        }
        if (remainingMs <= 0) remainingMs = 30_000;

        lastCaptureAt = Date.now();
        gm.setValue(cdKey, {
          ready: false,
          remainingMs,
          totalMs: remainingMs,
          startedAt: Date.now(),
          ts: Date.now(),
        });
      } catch (e) {
        console.warn('[CoolholeQueue] cooldown capture failed', e);
      }
    }

    gm.onValueChange(workKey, (_name, _old, next) => {
      if (!next?.n) return;
      // Snapshot ready/cooling *before* we click
      const btnBefore =
        document.querySelector('#job-actions button.job-action-work') ||
        document.querySelector('button.job-action-work') ||
        document.querySelector('button[title="Earn CP"]');
      const wasCooling =
        !!btnBefore &&
        (btnBefore.disabled || btnBefore.classList.contains('work-cooling'));

      doWorkAction().then(() => {
        // Only capture a new CD if we were ready before (successful new Work)
        if (wasCooling) return;
        setTimeout(captureWorkCooldown, 400);
        setTimeout(captureWorkCooldown, 1000);
      });
    });

    // ── Queue helpers (original + pending aware) ─────────────────────
    function getSelectors() {
      if (document.querySelector('#queue-url') || host === 'new.coolhole.org') {
        return { mediaUrl: '#queue-url', queueEnd: '#btn-queue', expandBtn: null, expandTarget: null };
      }
      return {
        mediaUrl: '#mediaurl',
        queueEnd: '#queue_end',
        expandBtn: '#showmediaurl',
        expandTarget: '#addfromurl',
      };
    }

    async function ensurePanelOpen(sel) {
      if (!sel.expandTarget) return;
      const panel = document.querySelector(sel.expandTarget);
      if (panel?.classList.contains('in') || panel?.classList.contains('show')) return;
      if (sel.expandBtn) {
        document.querySelector(sel.expandBtn)?.click();
        await new Promise((r) => setTimeout(r, 250));
      }
      if (panel) {
        panel.classList.add('in', 'show');
        panel.style.display = 'block';
        panel.style.height = 'auto';
      }
    }

    function setNativeInputValue(input, value) {
      const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
      if (descriptor?.set) descriptor.set.call(input, value);
      else input.value = value;
      input.dispatchEvent(new Event('input', { bubbles: true }));
      input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function waitFor(selector, timeoutMs = 12_000) {
      return new Promise((resolve, reject) => {
        const existing = document.querySelector(selector);
        if (existing) {
          resolve(existing);
          return;
        }
        const observer = new MutationObserver(() => {
          const found = document.querySelector(selector);
          if (found) {
            observer.disconnect();
            clearTimeout(timer);
            resolve(found);
          }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        const timer = setTimeout(() => {
          observer.disconnect();
          reject(new Error(`timed out waiting for ${selector}`));
        }, timeoutMs);
      });
    }

    async function fetchTitle(videoId) {
      try {
        const oembed = new URL('https://www.youtube.com/oembed');
        oembed.searchParams.set('url', `https://www.youtube.com/watch?v=${videoId}`);
        oembed.searchParams.set('format', 'json');
        const res = await fetch(oembed);
        if (!res.ok) return null;
        const data = await res.json();
        return cleanTitle(data?.title);
      } catch {
        return null;
      }
    }

    let lastQueued = { id: null, at: 0 };
    let autoQueueLock = false;

    async function queueVideo(videoId, title, opts = {}) {
      if (!videoId) return;
      const fromPending = !!opts.fromPending;
      const now = Date.now();
      if (lastQueued.id === videoId && now - lastQueued.at < 2000) return;
      lastQueued = { id: videoId, at: now };

      const displayTitle = cleanTitle(title) ?? (await fetchTitle(videoId)) ?? videoId;
      const myCount = getMyQueuedCount();
      const myLimit = getMyLimit();

      // Room check
      if (myCount >= myLimit) {
        if (!fromPending) {
          if (!isQPlusEnabled()) {
            // Q+ disabled (CP setting or float collapsed)
            showToast(MAX_QUEUE_MSG, false, false, true);
            fancyLog(displayTitle, false, false, true);
          } else {
            addToPending(videoId, displayTitle);
            showToast(qPlusToast(), true, false, true);
            fancyLog(displayTitle + ' → Q+', false, false, true);
            renderPanelIfOpen();
          }
        }
        return;
      }

      const sel = getSelectors();

      try {
        await ensurePanelOpen(sel);
        const input = await waitFor(sel.mediaUrl);
        const queueEndBtn = await waitFor(sel.queueEnd);

        if (queueEndBtn.disabled) {
          await new Promise((r) => setTimeout(r, 250));
          const errText = getRecentErrorText();
          if (isMaxQueueError(errText)) {
            learnLimitFromError(errText);
            if (!fromPending) {
              if (!isQPlusEnabled()) {
                showToast(MAX_QUEUE_MSG, false, false, true);
              } else {
                addToPending(videoId, displayTitle);
                showToast(qPlusToast(), true, false, true);
                renderPanelIfOpen();
              }
            }
            fancyLog(displayTitle, false, false, true);
            return;
          }
          throw new Error('Queue button is disabled');
        }

        const before = getRecentErrorText();
        setNativeInputValue(input, `https://www.youtube.com/watch?v=${videoId}`);
        await new Promise((r) => setTimeout(r, 80));
        queueEndBtn.click();
        await new Promise((r) => setTimeout(r, 700));

        const after = getRecentErrorText();
        const fresh = after.length > before.length ? after.slice(before.length) : after;

        if (isMaxQueueError(fresh) || isMaxQueueError(after)) {
          learnLimitFromError(fresh || after);
          if (!fromPending) {
            if (!isQPlusEnabled()) {
              showToast(MAX_QUEUE_MSG, false, false, true);
            } else {
              addToPending(videoId, displayTitle);
              showToast(qPlusToast(), true, false, true);
              renderPanelIfOpen();
            }
          }
          fancyLog(displayTitle, false, false, true);
          return;
        }

        // Success
        if (fromPending) removeFromPending(videoId);
        addToHistory(videoId, displayTitle);
        const gold = Math.random() < 0.12;
        showToast(formatLine(pick(SUCCESS_LINES), displayTitle), true, gold, false);
        fancyLog(displayTitle, true, gold, false);
        renderPanelIfOpen();
      } catch (err) {
        console.error('[CoolholeQueue] failed:', err);
        const errText = String(err?.message || '') + '\n' + getRecentErrorText();
        if (isMaxQueueError(errText)) {
          learnLimitFromError(errText);
          if (!fromPending) {
            if (!isQPlusEnabled()) {
              showToast(MAX_QUEUE_MSG, false, false, true);
            } else {
              addToPending(videoId, displayTitle);
              showToast(qPlusToast(), true, false, true);
              renderPanelIfOpen();
            }
          }
          fancyLog(displayTitle, false, false, true);
        } else {
          showToast(pick(FAIL_LINES), false, false, false);
          fancyLog(displayTitle, false, false, false);
        }
        if (!isCollapsed()) renderPanelIfOpen();
      }
    }

    // Incoming from YouTube
    gm.onValueChange(queueKey, (_name, _old, next) => {
      if (!next?.videoId) return;
      queueVideo(String(next.videoId), cleanTitle(next.title));
    });

    // ── Auto-queue on “one of my videos left the queue” ───────────────
    let prevMyCount = getMyQueuedCount();

    function tryAutoQueue() {
      if (autoQueueLock || !isAutoEnabled() || !isQPlusEnabled()) return;
      const pending = loadPending();
      if (pending.length === 0) return;

      const myCount = getMyQueuedCount();
      const myLimit = getMyLimit();
      if (myCount >= myLimit) return;

      const next = pending[0];
      if (!next?.videoId) return;

      autoQueueLock = true;
      queueVideo(next.videoId, next.title, { fromPending: true }).finally(() => {
        setTimeout(() => {
          autoQueueLock = false;
        }, 1500);
      });
    }

    function onQueueDomChange() {
      const now = getMyQueuedCount();
      if (now < prevMyCount) {
        // one (or more) of our videos left → try to fill the slot
        tryAutoQueue();
      }
      prevMyCount = now;
    }

    // Observe the queue list
    const queueEl = document.querySelector('#queue');
    if (queueEl) {
      new MutationObserver(onQueueDomChange).observe(queueEl, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class'],
      });
    }

    // Light safety net only while Q+ has items
    setInterval(() => {
      if (loadPending().length > 0) onQueueDomChange();
    }, 18_000);

    // ── Hist / Q+ floating pill + attached panel ─────────────────────
    const posKey = 'cq_float_pos';
    let panelTab = 'hist'; // 'hist' | 'q'
    let floatRoot = null;

    function loadPos() {
      const raw = gm.getValue(posKey, null);
      if (raw && typeof raw.x === 'number' && typeof raw.y === 'number') return raw;
      return { x: Math.max(16, window.innerWidth - 320), y: Math.max(80, window.innerHeight - 220) };
    }
    function savePos(x, y) {
      gm.setValue(posKey, { x, y });
    }
    function isCollapsed() {
      return gm.getValue(collapsedKey, false) === true;
    }
    function setCollapsed(on) {
      on = !!on;
      gm.setValue(collapsedKey, on);
      try {
        const s = loadSettings();
        if (s.qPlus === on) saveSettings({ ...s, qPlus: !on });
      } catch {
        /* ignore */
      }
      if (!floatRoot) return;
      floatRoot.classList.toggle('cq-collapsed', on);
      if (on) closePanel();
      updateQBadge();
    }

    function applyPos(x, y) {
      if (!floatRoot) return;
      const maxX = window.innerWidth - 40;
      const maxY = window.innerHeight - 40;
      x = Math.max(4, Math.min(x, maxX));
      y = Math.max(4, Math.min(y, maxY));
      floatRoot.style.left = `${x}px`;
      floatRoot.style.top = `${y}px`;
      return { x, y };
    }

    function updateQBadge() {
      // Collapsed = Q+ off → no badges, no waiting signal
      const n = !isQPlusEnabled() ? 0 : loadPending().length;
      const text = n > 0 ? String(n) : '';
      const badge = document.getElementById('cq-q-badge');
      if (badge) badge.textContent = text;
      const restoreBadge = document.getElementById('cq-restore-badge');
      if (restoreBadge) restoreBadge.textContent = text;
    }

    function renderPanelIfOpen() {
      const panel = document.getElementById('cq-panel');
      if (panel?.classList.contains('open')) renderPanel();
      updateQBadge();
    }

    function renderPanel() {
      const body = document.getElementById('cq-panel-body');
      const footer = document.getElementById('cq-panel-footer');
      if (!body || !footer) return;

      const isHist = panelTab === 'hist';
      const list = isHist ? loadHistory() : loadPending();

      body.innerHTML = '';
      if (list.length === 0) {
        body.innerHTML = `<div id="cq-panel-empty">${
          isHist ? 'No history yet — queue something!' : 'Q+ is empty — drag to reorder when it fills'
        }</div>`;
      } else {
        list.forEach((item, index) => {
          const row = document.createElement('div');
          row.className = 'cq-row' + (!isHist && index === 0 ? ' cq-next' : '');
          row.dataset.videoId = item.videoId;

          if (!isHist) {
            row.draggable = true;
            const grip = document.createElement('span');
            grip.className = 'cq-grip';
            grip.textContent = '⠿';
            grip.title = 'Drag to reorder — top queues next';
            row.append(grip);

            row.addEventListener('dragstart', (e) => {
              e.stopPropagation();
              row.classList.add('cq-dragging');
              e.dataTransfer.effectAllowed = 'move';
              e.dataTransfer.setData('text/plain', item.videoId);
            });
            row.addEventListener('dragend', () => {
              row.classList.remove('cq-dragging');
              body.querySelectorAll('.cq-drag-over').forEach((el) => el.classList.remove('cq-drag-over'));
            });
            row.addEventListener('dragover', (e) => {
              e.preventDefault();
              e.dataTransfer.dropEffect = 'move';
              body.querySelectorAll('.cq-drag-over').forEach((el) => el.classList.remove('cq-drag-over'));
              row.classList.add('cq-drag-over');
            });
            row.addEventListener('dragleave', () => row.classList.remove('cq-drag-over'));
            row.addEventListener('drop', (e) => {
              e.preventDefault();
              e.stopPropagation();
              row.classList.remove('cq-drag-over');
              const fromId = e.dataTransfer.getData('text/plain');
              const toId = item.videoId;
              if (!fromId || fromId === toId) return;
              const pending = loadPending();
              const fromIdx = pending.findIndex((x) => x.videoId === fromId);
              const toIdx = pending.findIndex((x) => x.videoId === toId);
              if (fromIdx < 0 || toIdx < 0) return;
              const [moved] = pending.splice(fromIdx, 1);
              pending.splice(toIdx, 0, moved);
              savePending(pending);
              renderPanel();
            });
          }

          const title = document.createElement('div');
          title.className = 'cq-title';
          title.textContent = item.title || item.videoId;
          title.title = isHist ? 'Open on YouTube' : (item.title || item.videoId);
          if (isHist) {
            title.style.cursor = 'pointer';
            title.addEventListener('mouseenter', () => { title.style.color = '#90caf9'; });
            title.addEventListener('mouseleave', () => { title.style.color = ''; });
            title.addEventListener('click', (e) => {
              e.stopPropagation();
              const url = `https://www.youtube.com/watch?v=${item.videoId}`;
              try {
                window.open(url, '_blank', 'noopener,noreferrer');
              } catch {
                location.assign(url);
              }
            });
          }

          const chBtn = document.createElement('button');
          chBtn.type = 'button';
          chBtn.className = 'cq-act cq-act-ch';
          chBtn.textContent = isHist ? 'CH' : 'Force';
          chBtn.title = isHist ? 'Re-queue / add to Q+' : 'Queue now';
          chBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            queueVideo(item.videoId, item.title, { fromPending: !isHist });
          });

          const delBtn = document.createElement('button');
          delBtn.type = 'button';
          delBtn.className = 'cq-act cq-act-del';
          delBtn.textContent = '×';
          delBtn.title = 'Remove';
          delBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            if (isHist) {
              saveHistory(loadHistory().filter((x) => x.videoId !== item.videoId));
            } else {
              removeFromPending(item.videoId);
            }
            renderPanel();
          });

          row.append(title, chBtn, delBtn);
          body.append(row);
        });
      }

      footer.innerHTML = '';
      const clearBtn = document.createElement('button');
      clearBtn.type = 'button';
      clearBtn.textContent = 'Clear';
      clearBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (isHist) saveHistory([]);
        else savePending([]);
        renderPanel();
        updateQBadge();
      });

      const autoLabel = document.createElement('label');
      const autoCb = document.createElement('input');
      autoCb.type = 'checkbox';
      autoCb.checked = isAutoEnabled();
      autoCb.addEventListener('change', () => {
        setAutoEnabled(autoCb.checked);
        if (autoCb.checked) tryAutoQueue();
      });
      autoLabel.append(autoCb, document.createTextNode(' Auto-queue'));
      footer.append(clearBtn, autoLabel);
    }

    function openPanel(tab) {
      panelTab = tab;
      const panel = document.getElementById('cq-panel');
      if (!panel) return;
      panel.classList.add('open');
      document.querySelectorAll('#cq-float-pill .cq-seg').forEach((b) => {
        b.classList.toggle('cq-active', b.dataset.tab === tab);
      });
      renderPanel();
    }

    function closePanel() {
      document.getElementById('cq-panel')?.classList.remove('open');
      document.querySelectorAll('#cq-float-pill .cq-seg').forEach((b) => b.classList.remove('cq-active'));
    }

    function buildFloatUI() {
      if (document.getElementById('cq-float')) return;

      const root = document.createElement('div');
      root.id = 'cq-float';

      // Row: drag tab + pill
      const row = document.createElement('div');
      row.id = 'cq-float-row';

      const drag = document.createElement('div');
      drag.className = 'cq-drag';
      drag.title = 'Drag to move';
      drag.textContent = '⠿';

      const pillWrap = document.createElement('div');
      pillWrap.id = 'cq-float-pill-wrap';

      const pill = document.createElement('div');
      pill.id = 'cq-float-pill';

      const histBtn = document.createElement('button');
      histBtn.type = 'button';
      histBtn.className = 'cq-seg cq-seg-hist';
      histBtn.dataset.tab = 'hist';
      histBtn.textContent = 'Hist';
      histBtn.title = 'Your previously queued videos';

      const qBtn = document.createElement('button');
      qBtn.type = 'button';
      qBtn.className = 'cq-seg cq-seg-q';
      qBtn.dataset.tab = 'q';
      qBtn.textContent = 'Q+';
      qBtn.title = 'Waiting list (auto-queue when a slot opens)';

      const badge = document.createElement('span');
      badge.id = 'cq-q-badge';

      pill.append(histBtn, qBtn);
      pillWrap.append(pill, badge);
      row.append(drag, pillWrap);

      // Tiny restore chip (shown only when collapsed)
      const restore = document.createElement('button');
      restore.type = 'button';
      restore.id = 'cq-float-restore';
      restore.title = 'Show Hist / Q+';
      restore.textContent = 'Q+';
      const restoreBadge = document.createElement('span');
      restoreBadge.id = 'cq-restore-badge';
      restore.append(restoreBadge);

      // Panel (attached under the row)
      const panel = document.createElement('div');
      panel.id = 'cq-panel';
      panel.innerHTML = `
        <div id="cq-panel-body"></div>
        <div id="cq-panel-footer"></div>
      `;

      root.append(row, restore, panel);
      (document.body ?? document.documentElement).append(root);
      floatRoot = root;

      // Apply saved hole transparency (from CP settings menu)
      const applyHoleOpacity = () => {
        try {
          const s = loadSettings();
          const op = Math.min(1, Math.max(0.1, Number(s.holeOpacity) || 0.35));
          root.style.opacity = String(op);
          root.addEventListener('mouseenter', () => {
            root.style.opacity = '1';
          });
          root.addEventListener('mouseleave', () => {
            if (!root.classList.contains('cq-collapsed')) {
              root.style.opacity = String(op);
            } else {
              root.style.opacity = String(Math.max(0.1, op * 0.6));
            }
          });
        } catch {
          /* ignore */
        }
      };
      applyHoleOpacity();
      gm.onValueChange(settingsKey, () => applyHoleOpacity());
      // Live-sync when CP settings toggles “Q+ features” (writes collapsedKey)
      gm.onValueChange(collapsedKey, () => {
        if (!floatRoot) return;
        const on = isCollapsed();
        floatRoot.classList.toggle('cq-collapsed', on);
        if (on) closePanel();
        updateQBadge();
      });

      // Restore position + collapsed state
      const pos = loadPos();
      applyPos(pos.x, pos.y);
      if (isCollapsed()) root.classList.add('cq-collapsed');

      // Drag logic + double-tap to collapse
      // (native dblclick is blocked by pointerdown preventDefault, so we detect it ourselves)
      let dragging = false;
      let startX = 0, startY = 0, origX = 0, origY = 0;
      let dragMoved = false;
      let lastTapAt = 0;

      const onMove = (e) => {
        if (!dragging) return;
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragMoved = true;
        applyPos(origX + dx, origY + dy);
      };
      const onUp = () => {
        if (!dragging) return;
        dragging = false;
        document.removeEventListener('pointermove', onMove);
        document.removeEventListener('pointerup', onUp);
        const rect = root.getBoundingClientRect();
        savePos(rect.left, rect.top);

        // Double-tap (no drag) → collapse
        if (!dragMoved) {
          const now = Date.now();
          if (now - lastTapAt < 380) {
            lastTapAt = 0;
            setCollapsed(true);
          } else {
            lastTapAt = now;
          }
        } else {
          lastTapAt = 0;
        }
      };

      drag.addEventListener('pointerdown', (e) => {
        e.preventDefault();
        e.stopPropagation();
        dragging = true;
        dragMoved = false;
        startX = e.clientX;
        startY = e.clientY;
        const rect = root.getBoundingClientRect();
        origX = rect.left;
        origY = rect.top;
        document.addEventListener('pointermove', onMove);
        document.addEventListener('pointerup', onUp);
      });
      drag.title = 'Drag to move · double-click to hide';

      // Click restore chip → expand
      restore.addEventListener('click', (e) => {
        e.stopPropagation();
        setCollapsed(false);
      });

      // Toggle panels
      histBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (panel.classList.contains('open') && panelTab === 'hist') closePanel();
        else openPanel('hist');
      });
      qBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (panel.classList.contains('open') && panelTab === 'q') closePanel();
        else openPanel('q');
      });

      // Click outside to close
      document.addEventListener('click', (e) => {
        if (!panel.classList.contains('open')) return;
        if (root.contains(e.target)) return;
        closePanel();
      });

      // Keep on screen on resize
      window.addEventListener('resize', () => {
        const rect = root.getBoundingClientRect();
        applyPos(rect.left, rect.top);
      });

      updateQBadge();
    }

    // Mount once body is ready
    function tryMountUI() {
      if (!document.body) return;
      buildFloatUI();
    }
    if (document.body) tryMountUI();
    else document.addEventListener('DOMContentLoaded', tryMountUI, { once: true });
    // Safety re-mount if something wipes it
    setTimeout(tryMountUI, 1500);
    setTimeout(tryMountUI, 4000);

    // ── Hash support (open-from-YouTube) ──────────────────────────────
    window.addEventListener('hashchange', processHash);
    window.addEventListener('load', () => setTimeout(processHash, 600));
    if (document.readyState !== 'loading') setTimeout(processHash, 600);

    function parseHashRequest() {
      if (/(?:^|[&#])cq_work=1(?:&|$)/.test(location.hash)) {
        return { work: true };
      }
      const add = location.hash.match(/cq_add=([^&]+)/);
      if (!add) return null;
      const titleMatch = location.hash.match(/cq_title=([^&]+)/);
      return {
        videoId: decodeURIComponent(add[1]),
        title: titleMatch ? cleanTitle(decodeURIComponent(titleMatch[1])) : null,
      };
    }

    function clearHash() {
      try {
        history.replaceState(null, '', `${location.pathname}${location.search}`);
      } catch {
        location.hash = '';
      }
    }

    function processHash() {
      const req = parseHashRequest();
      if (!req) return;
      clearHash();
      if (req.work) {
        doWorkAction();
        return;
      }
      if (req.videoId) queueVideo(req.videoId, req.title);
    }

    // Initial auto-queue attempt in case we already have pending + room
    setTimeout(tryAutoQueue, 2000);
  }
})();