Perplexity – Usage Limits & Settings

Draggable, resizable usage-limits panel with dark/light theme memory

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Perplexity – Usage Limits & Settings
// @namespace    https://perplexity.ai/
// @version      5.0.4
// @license MIT 
// @description  Draggable, resizable usage-limits panel with dark/light theme memory
// @author       userscript
// @match        https://www.perplexity.ai/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ── CONFIG ─────────────────────────────────────────────────────────────────
  const SEARCH_LIMITS = {
    remaining_pro:              { label: 'Pro',      total: 200  },
    remaining_research:         { label: 'Research', total: 20   },
    remaining_agentic_research: { label: 'Agentic',  total: null },
    remaining_labs:             { label: 'Labs',     total: 25   },
  };

  const SETTINGS_FIELDS = [
    { key: 'upload_limit',                   label: 'Upload Limit',      type: 'number' },
    { key: 'pages_limit',                    label: 'Pages Limit',       type: 'number' },
    { key: 'create_limit',                   label: 'Create Limit',      type: 'number' },
    { key: 'article_image_upload_limit',     label: 'Img Upload Limit',  type: 'number' },
    { key: 'daily_attachment_limit',         label: 'Daily Attachments', type: 'number' },
    { key: 'disable_training',               label: 'Training Disabled', type: 'bool'   },
    { key: 'has_ai_profile',                 label: 'AI Profile',        type: 'bool'   },
    { key: 'default_model',                  label: 'Default Model',     type: 'string' },
    { key: 'default_image_generation_model', label: 'Image Model',       type: 'string' },
    { key: 'default_video_generation_model', label: 'Video Model',       type: 'string' },
  ];

  const MCP_NAMES = {
    asana_mcp_merge:           'Asana',           box:                       'Box',
    cbinsights_mcp_cashmere:   'CBInsights',      confluence_mcp_merge:      'Confluence',
    crunchbase:                'Crunchbase',       dropbox:                   'Dropbox',
    factset:                   'FactSet',          gcal:                      'Google Calendar',
    github_mcp_direct:         'GitHub',           google_drive:              'Google Drive',
    jira_mcp_merge:            'Jira',             linear_alt:                'Linear',
    microsoft_teams_mcp_merge: 'MS Teams',         notion_mcp:                'Notion',
    onedrive:                  'OneDrive',         org:                       'Organization',
    outlook:                   'Outlook',          pitchbook_mcp_cashmere:    'Pitchbook',
    scholar:                   'Scholar',          sharepoint:                'SharePoint',
    slack_direct:              'Slack',            social:                    'Social',
    statista_mcp_cashmere:     'Statista',         web:                       'Web',
    wiley_mcp_cashmere:        'Wiley',
  };

  const INPUT_SEL = [
    'textarea[placeholder]', '[data-testid="search-input"]',
    'div[contenteditable="true"]', '[role="textbox"]', 'form textarea',
  ].join(', ');

  // ── PREFERENCES (localStorage) ─────────────────────────────────────────────
  const PREFS_KEY = 'pplx_panel_v5';
  const loadPrefs  = () => { try { return JSON.parse(localStorage.getItem(PREFS_KEY)) || {}; } catch { return {}; } };
  const savePrefs  = patch => { try { localStorage.setItem(PREFS_KEY, JSON.stringify({ ...loadPrefs(), ...patch })); } catch {} };

  // ── CSS ────────────────────────────────────────────────────────────────────
  document.head.appendChild(Object.assign(document.createElement('style'), { textContent: `

    /* ── CSS variables — dark (default) ─────────────────────── */
    #pplx-panel {
      --bg:        #1c1c1c;
      --bg-head:   rgba(255,255,255,0.025);
      --bg-foot:   rgba(255,255,255,0.01);
      --border:    rgba(255,255,255,0.09);
      --sep:       rgba(255,255,255,0.06);
      --track:     rgba(255,255,255,0.08);
      --hover:     rgba(255,255,255,0.035);
      --text:      #ededed;
      --lbl:       #95959d;
      --sec:       #a3a3a4;
      --dim:       #a0a0aa;
      --shadow:    0 0 0 0.5px rgba(255,255,255,0.04), 0 4px 8px rgba(0,0,0,0.3), 0 14px 32px rgba(0,0,0,0.5);
      --resize-bg: rgba(255,255,255,0.12);
      --cursor-drag: grab;
    }

    /* ── CSS variables — light ───────────────────────────────── */
    #pplx-panel.pp-light {
      --bg:        #ffffff;
      --bg-head:   rgba(0,0,0,0.015);
      --bg-foot:   rgba(0,0,0,0.01);
      --border:    rgba(0,0,0,0.1);
      --sep:       rgba(0,0,0,0.07);
      --track:     rgba(0,0,0,0.09);
      --hover:     rgba(0,0,0,0.03);
      --text:      #3f4145;
      --lbl:       #6b7280;
      --sec:       #5f636c;
      --dim:       #cecece;
      --time:      #616872
      --shadow:    0 0 0 1px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.1), 0 12px 28px rgba(0,0,0,0.08);
      --resize-bg: rgba(0,0,0,0.12);
      --cursor-drag: grab;
    }

    /* ── Panel shell ─────────────────────────────────────────── */
    #pplx-panel {
      position: fixed;
      z-index: 99999;
      width: 222px;
      min-width: 185px;
      min-height: 180px;
      max-height: calc(100vh - 20px);
      display: flex;
      flex-direction: column;
      background: var(--bg);
      border: 1px solid var(--border);
      border-radius: 14px;
      overflow: hidden;
      color: var(--text);
      font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
      font-size: 12.5px;
      line-height: 1.45;
      box-shadow: var(--shadow);
      transition: opacity 0.2s ease, box-shadow 0.2s ease;
      user-select: none;
    }
    #pplx-panel.pp-hidden { opacity: 0; pointer-events: none; }
    #pplx-panel.pp-dragging { transition: none; cursor: grabbing !important; box-shadow: var(--shadow), 0 0 0 2px #20b2aa44; }

    /* ── Header ─────────────────────────────────────────────── */
    .pp-head {
      flex-shrink: 0;
      display: flex; align-items: center; justify-content: space-between;
      padding: 9px 11px 9px 13px;
      background: var(--bg-head);
      border-bottom: 1px solid var(--sep);
      cursor: var(--cursor-drag);
    }
    .pp-head-left { display: flex; align-items: center; gap: 7px; }
    .pp-head-label {
      font-size: 11px; font-weight: 600; letter-spacing: 0.3px;
      color: var(--lbl);
      pointer-events: none;
    }
    #pplx-countdown { font-size: 10px; font-weight: 400; color: #fbbf24; margin-left: 1px; }

    .pp-dot {
      flex-shrink: 0;
      width: 6px; height: 6px; border-radius: 50%;
      background: var(--dim);
      transition: background 0.35s, box-shadow 0.35s;
      pointer-events: none;
    }
    .pp-dot.live    { background: #20b2aa; box-shadow: 0 0 0 3px rgba(32,178,170,0.2); }
    .pp-dot.pending { background: #fbbf24; box-shadow: 0 0 0 3px rgba(251,191,36,0.2); }

    .pp-head-actions { display: flex; gap: 1px; flex-shrink: 0; }
    .pp-btn-icon {
      appearance: none; background: none; border: none;
      padding: 3px 5px; border-radius: 6px; cursor: pointer;
      color: var(--sec); font-size: 13px; line-height: 1;
      transition: color 0.15s, background 0.15s;
    }
    .pp-btn-icon:hover { color: var(--text); background: var(--hover); }

    /* ── Scrollable body ─────────────────────────────────────── */
    .pp-body {
      flex: 1;
      overflow-y: auto;
      overflow-x: hidden;
      min-height: 60px;
      padding: 4px 0 2px;
      scrollbar-width: thin;
      scrollbar-color: var(--dim) transparent;
    }

    /* ── Section label ───────────────────────────────────────── */
    .pp-sec {
      padding: 9px 13px 4px;
      font-size: 9.5px; font-weight: 700;
      letter-spacing: 0.9px; text-transform: uppercase;
      color: var(--sec);
    }
    .pp-sec.first { padding-top: 6px; }
    .pp-sep { height: 1px; margin: 4px 0; background: var(--sep); }

    /* ── Standard row ────────────────────────────────────────── */
    .pp-row {
      display: flex; align-items: center; justify-content: space-between;
      padding: 3.5px 13px; gap: 8px; min-height: 24px;
    }
    .pp-row:hover { background: var(--hover); }
    .pp-lbl { color: var(--lbl); font-size: 11.5px; white-space: nowrap; flex-shrink: 0; }
    .pp-val { font-size: 12px; font-weight: 600; text-align: right; color: var(--text); }

    .pp-val.v-zero  { color: var(--dim); font-weight: 400; font-style: italic; }
    .pp-val.v-crit  { color: #f87171; }
    .pp-val.v-low   { color: #fb923c; }
    .pp-val.v-warn  { color: #fbbf24; }
    .pp-val.v-bool-y { color: #20b2aa; }
    .pp-val.v-bool-n { color: var(--lbl); font-weight: 400; }
    .pp-val.v-str    { color: #818cf8; font-weight: 500; font-size: 11px; }

    /* ── Search / MCP row (with bar) ─────────────────────────── */
    .pp-bar-row {
      padding: 4px 13px 7px;
    }
    .pp-bar-row:hover { background: var(--hover); }
    .pp-bar-top {
      display: flex; align-items: baseline;
      justify-content: space-between;
      margin-bottom: 4px;
    }
    .pp-bar-count { font-size: 12px; font-weight: 600; color: var(--text); }
    .pp-bar-total { font-size: 10px; font-weight: 400; color: var(--dim); }
    .pp-track {
      height: 3px;
      background: var(--track);
      border-radius: 99px; overflow: hidden;
    }
    .pp-fill { height: 100%; border-radius: 99px; transition: width 0.55s cubic-bezier(.4,0,.2,1); }

    /* ── Footer ─────────────────────────────────────────────── */
    .pp-foot {
      flex-shrink: 0;
      display: flex; align-items: center; justify-content: space-between;
      padding: 5px 11px 6px 13px;
      border-top: 1px solid var(--sep);
      background: var(--bg-foot);
    }
    .pp-timestamp { font-size: 9.5px; color: var(--time); }
    .pp-btn-sm {
      appearance: none; background: none; cursor: pointer;
      border: 1px solid var(--border);
      border-radius: 6px; padding: 2px 9px;
      font-size: 10px; color: var(--sec);
      font-family: inherit;
      transition: border-color 0.15s, color 0.15s;
    }
    .pp-btn-sm:hover { border-color: var(--lbl); color: var(--text); }

    /* ── Resize handle (bottom-right corner) ─────────────────── */
    .pp-resize {
      position: absolute;
      bottom: 0; right: 0;
      width: 16px; height: 16px;
      cursor: se-resize;
      z-index: 10;
      /* subtle diagonal lines matching Perplexity's corner affordance */
      background:
        linear-gradient(135deg,
          transparent 30%,
          var(--resize-bg) 30%, var(--resize-bg) 40%,
          transparent 40%,
          transparent 55%,
          var(--resize-bg) 55%, var(--resize-bg) 65%,
          transparent 65%
        );
      border-radius: 0 0 14px 0;
    }

    /* ── Toggle pill ─────────────────────────────────────────── */
    #pplx-toggle {
      position: fixed; z-index: 99998;
      background: var(--bg, #1c1c1c);
      border: 1px solid var(--border, rgba(255,255,255,0.09));
      border-radius: 99px;
      height: 27px; padding: 0 11px;
      display: flex; align-items: center; gap: 5px;
      cursor: pointer;
      color: var(--sec, #52525b);
      font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
      font-size: 10.5px; font-weight: 600; letter-spacing: 0.3px;
      white-space: nowrap;
      box-shadow: 0 2px 8px rgba(0,0,0,0.35);
      transition: border-color 0.15s, color 0.15s;
    }
    #pplx-toggle.pp-light {
      background: #ffffff;
      border-color: rgba(0,0,0,0.1);
      color: #6b7280;
      box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    }
    #pplx-toggle:hover { border-color: var(--lbl, #71717a); color: var(--text, #ededed); }
    .pp-toggle-dot {
      width: 5px; height: 5px; border-radius: 50%;
      background: #3f3f46;
      transition: background 0.35s;
      flex-shrink: 0;
    }

    /* ── States ─────────────────────────────────────────────── */
    .pp-loading { padding: 18px 13px; text-align: center; font-size: 11px; color: var(--sec); }
    .pp-error   { padding: 8px 13px; font-size: 11px; color: #f87171; }
  ` }));

  // ── HELPERS ────────────────────────────────────────────────────────────────
  const fillColor = pct => pct > 60 ? '#20b2aa' : pct > 25 ? '#fbbf24' : '#f87171';

  function searchValClass(n) {
    if (n === 0)  return 'v-zero';
    if (n <= 3)   return 'v-crit';
    if (n <= 10)  return 'v-low';
    if (n <= 25)  return 'v-warn';
    return '';
  }

  function settingRender(v, type) {
    if (v === null || v === undefined) return { html: '—', cls: 'v-zero' };
    if (type === 'bool')   return { html: v ? '✓' : '✗', cls: v ? 'v-bool-y' : 'v-bool-n' };
    if (type === 'string') return { html: v, cls: 'v-str' };
    return { html: String(v), cls: '' };
  }

  function barRowHTML(label, remaining, total) {
    const pct  = total > 0 ? Math.round((remaining / total) * 100) : 0;
    const col  = fillColor(pct);
    const vcls = searchValClass(remaining);
    return `<div class="pp-bar-row">
      <div class="pp-bar-top">
        <span class="pp-lbl">${label}</span>
        <span class="pp-bar-count ${vcls}">${remaining}<span class="pp-bar-total"> / ${total}</span></span>
      </div>
      <div class="pp-track"><div class="pp-fill" style="width:${pct}%;background:${col}"></div></div>
    </div>`;
  }

  // ── BUILD HTML ─────────────────────────────────────────────────────────────
  function buildHTML(rate, settings) {
    let h = '<div class="pp-sec first">Searches</div>';

    if (rate) {
      Object.entries(SEARCH_LIMITS).forEach(([key, { label, total }]) => {
        const remaining = rate[key];
        if (remaining === undefined) return;
        if (total !== null) {
          h += barRowHTML(label, remaining, total);
        } else {
          const vcls = searchValClass(remaining);
          h += `<div class="pp-row"><span class="pp-lbl">${label}</span><span class="pp-val ${vcls}">${remaining}</span></div>`;
        }
      });
    } else {
      h += '<div class="pp-error">⚠ Rate limit data unavailable</div>';
    }

    const mcpRaw = rate?.sources?.source_to_limit;
    if (mcpRaw) {
      const active = Object.entries(mcpRaw).filter(([, v]) => v.monthly_limit !== null && v.monthly_limit !== 0);
      if (active.length) {
        h += '<div class="pp-sep"></div><div class="pp-sec">MCP Sources</div>';
        active.forEach(([key, { monthly_limit, remaining }]) => {
          h += barRowHTML(MCP_NAMES[key] || key, remaining, monthly_limit);
        });
      }
    }

    h += '<div class="pp-sep"></div><div class="pp-sec">Settings</div>';
    if (settings) {
      SETTINGS_FIELDS.forEach(({ key, label, type }) => {
        const raw = settings[key];
        if (raw === undefined) return;
        const { html: vhtml, cls } = settingRender(raw, type);
        h += `<div class="pp-row"><span class="pp-lbl">${label}</span><span class="pp-val ${cls}">${vhtml}</span></div>`;
      });
    } else {
      h += '<div class="pp-error">⚠ Settings data unavailable</div>';
    }
    return h;
  }

  // ── FETCH & RENDER ─────────────────────────────────────────────────────────
  async function fetchAndRender() {
    const panel = document.getElementById('pplx-panel');
    if (!panel) return;
    const body  = panel.querySelector('.pp-body');
    const dot   = panel.querySelector('.pp-dot');
    const tdot  = document.querySelector('.pp-toggle-dot');
    const ts    = panel.querySelector('.pp-timestamp');
    const cd    = document.getElementById('pplx-countdown');

    if (dot)  dot.classList.remove('live', 'pending');
    if (tdot) tdot.style.background = '#3f3f46';

    const [r1, r2] = await Promise.allSettled([
      fetch('/rest/rate-limit/all', { credentials: 'include' }).then(r => r.json()),
      fetch('/rest/user/settings',  { credentials: 'include' }).then(r => r.json()),
    ]);

    const rate     = r1.status === 'fulfilled' ? r1.value : null;
    const settings = r2.status === 'fulfilled' ? r2.value : null;

    if (body) body.innerHTML = buildHTML(rate, settings);
    if (dot)  dot.classList.add('live');
    if (tdot) tdot.style.background = '#20b2aa';
    if (ts)   ts.textContent = new Date().toLocaleTimeString('en-CA', { hour:'2-digit', minute:'2-digit', second:'2-digit' });
    if (cd)   cd.textContent = '';
  }

  // ── POST-QUERY REFRESH ─────────────────────────────────────────────────────
  let _rt = null, _ct = null;

  function scheduleRefresh() {
    clearTimeout(_rt); clearInterval(_ct);
    const dot  = document.querySelector('#pplx-panel .pp-dot');
    const tdot = document.querySelector('.pp-toggle-dot');
    const cd   = document.getElementById('pplx-countdown');
    if (dot)  { dot.classList.remove('live'); dot.classList.add('pending'); }
    if (tdot) tdot.style.background = '#fbbf24';
    let s = 3;
    if (cd) cd.textContent = ` (${s}s)`;
    _ct = setInterval(() => { s--; if (cd) cd.textContent = s > 0 ? ` (${s}s)` : ''; if (s <= 0) clearInterval(_ct); }, 1000);
    _rt = setTimeout(() => { clearInterval(_ct); fetchAndRender(); }, 3000);
  }

  function hookSubmitEvents() {
    const _orig = window.fetch;
    window.fetch = function (...a) {
      try {
        const url = typeof a[0] === 'string' ? a[0] : a[0] instanceof Request ? a[0].url : '';
        if ((a[1]?.method || 'GET').toUpperCase() === 'POST' &&
            url.includes('/rest/') && !url.includes('/rest/rate-limit') && !url.includes('/rest/user/settings'))
          scheduleRefresh();
      } catch (_) {}
      return _orig.apply(this, a);
    };
    document.addEventListener('keydown', e => {
      if (e.key === 'Enter' && !e.shiftKey &&
          (e.target.matches(INPUT_SEL) || !!e.target.closest('[role="textbox"],form')))
        scheduleRefresh();
    }, true);
    document.addEventListener('click', e => {
      const b = e.target.closest('button');
      if (!b) return;
      const lbl = (b.getAttribute('aria-label') || '').toLowerCase();
      if (b.type === 'submit' || lbl.includes('send') || lbl.includes('submit') || lbl.includes('search'))
        scheduleRefresh();
    }, true);
  }

  // ── THEME ──────────────────────────────────────────────────────────────────
  function applyTheme(isDark) {
    const panel  = document.getElementById('pplx-panel');
    const toggle = document.getElementById('pplx-toggle');
    if (!panel) return;
    panel.classList.toggle('pp-light', !isDark);
    if (toggle) toggle.classList.toggle('pp-light', !isDark);
    const btn = panel.querySelector('[data-a="theme"]');
    if (btn) btn.textContent = isDark ? '☀' : '☾';
  }

  function toggleTheme() {
    const panel = document.getElementById('pplx-panel');
    if (!panel) return;
    const nowDark = panel.classList.contains('pp-light'); // was light → going dark
    applyTheme(nowDark);
    savePrefs({ dark: nowDark });
  }

  // ── DRAG ───────────────────────────────────────────────────────────────────
  function makeDraggable(panel) {
    const handle = panel.querySelector('.pp-head');
    let sx = 0, sy = 0;

    handle.addEventListener('mousedown', e => {
      if (e.target.closest('button')) return;
      e.preventDefault();
      const rect = panel.getBoundingClientRect();
      sx = e.clientX - rect.left;
      sy = e.clientY - rect.top;
      panel.classList.add('pp-dragging');

      const onMove = e => {
        let x = e.clientX - sx;
        let y = e.clientY - sy;
        x = Math.max(0, Math.min(window.innerWidth  - panel.offsetWidth,  x));
        y = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, y));
        panel.style.left   = x + 'px';
        panel.style.top    = y + 'px';
        panel.style.bottom = 'auto';
        panel.style.right  = 'auto';
      };
      const onUp = () => {
        panel.classList.remove('pp-dragging');
        savePrefs({ x: parseInt(panel.style.left), y: parseInt(panel.style.top), anchored: true });
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup',   onUp);
      };
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup',   onUp);
    });
  }

  // ── RESIZE ─────────────────────────────────────────────────────────────────
  function makeResizable(panel) {
    const handle = panel.querySelector('.pp-resize');
    handle.addEventListener('mousedown', e => {
      e.preventDefault(); e.stopPropagation();
      const sx = e.clientX, sy = e.clientY;
      const sw = panel.offsetWidth, sh = panel.offsetHeight;

      const onMove = e => {
        const w = Math.max(185, sw + e.clientX - sx);
        const h = Math.max(180, sh + e.clientY - sy);
        panel.style.width  = w + 'px';
        panel.style.height = h + 'px';
      };
      const onUp = () => {
        savePrefs({ w: panel.offsetWidth, h: panel.offsetHeight });
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup',   onUp);
      };
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup',   onUp);
    });
  }

  // ── POSITION (respects saved anchor) ───────────────────────────────────────
  function positionPanel() {
    const panel  = document.getElementById('pplx-panel');
    const toggle = document.getElementById('pplx-toggle');
    if (!panel || !toggle) return;

    const prefs = loadPrefs();

    // Panel: use saved position if user moved it, else auto-anchor to input
    if (prefs.anchored && prefs.x !== undefined) {
      const x = Math.max(0, Math.min(window.innerWidth  - panel.offsetWidth,  prefs.x));
      const y = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, prefs.y || 0));
      panel.style.left = x + 'px'; panel.style.top = y + 'px'; panel.style.bottom = 'auto';
    } else {
      const inp = document.querySelector(INPUT_SEL);
      if (inp) {
        const r   = inp.getBoundingClientRect();
        const pw  = panel.offsetWidth || 222;
        const lft = Math.max(8, r.left - pw - 12);
        const ph  = panel.offsetHeight || 380;
        const top = Math.min(window.innerHeight - ph - 8, Math.max(8, r.top + r.height / 2 - ph / 2));
        panel.style.left = lft + 'px'; panel.style.top = top + 'px'; panel.style.bottom = 'auto';
      } else {
        panel.style.left = '12px'; panel.style.bottom = '90px'; panel.style.top = 'auto';
      }
    }

    // Toggle always tracks the input box
    const inp = document.querySelector(INPUT_SEL);
    if (inp) {
      const r   = inp.getBoundingClientRect();
      const pw  = panel.offsetWidth || 222;
      const lft = Math.max(8, r.left - pw - 12);
      toggle.style.left   = lft + 'px';
      toggle.style.bottom = (window.innerHeight - r.top + 10) + 'px';
      toggle.style.top    = 'auto';
    } else {
      toggle.style.left = '12px'; toggle.style.bottom = '58px'; toggle.style.top = 'auto';
    }
  }

  // ── INJECT ─────────────────────────────────────────────────────────────────
  function inject() {
    if (document.getElementById('pplx-panel')) return;

    const prefs  = loadPrefs();
    const isDark = prefs.dark !== false;

    const panel = document.createElement('div');
    panel.id = 'pplx-panel';
    panel.innerHTML = `
      <div class="pp-head">
        <div class="pp-head-left">
          <span class="pp-dot"></span>
          <span class="pp-head-label">Usage Limits</span>
          <span id="pplx-countdown"></span>
        </div>
        <div class="pp-head-actions">
          <button class="pp-btn-icon" data-a="theme"   title="Toggle theme">☀</button>
          <button class="pp-btn-icon" data-a="refresh" title="Refresh">↻</button>
          <button class="pp-btn-icon" data-a="close"   title="Hide">✕</button>
        </div>
      </div>
      <div class="pp-body"><div class="pp-loading">Loading…</div></div>
      <div class="pp-foot">
        <span class="pp-timestamp">—</span>
        <button class="pp-btn-sm" data-a="refresh">Refresh</button>
      </div>
      <div class="pp-resize" title="Resize"></div>
    `;

    // Restore saved size
    if (prefs.w) panel.style.width  = prefs.w + 'px';
    if (prefs.h) panel.style.height = prefs.h + 'px';

    document.body.appendChild(panel);

    const toggle = document.createElement('button');
    toggle.id = 'pplx-toggle';
    toggle.innerHTML = '<span class="pp-toggle-dot"></span>Limits';
    document.body.appendChild(toggle);

    // Apply saved theme
    applyTheme(isDark);

    // Events
    panel.addEventListener('click', e => {
      const a = e.target.closest('[data-a]')?.dataset.a;
      if (a === 'close')   panel.classList.add('pp-hidden');
      if (a === 'refresh') fetchAndRender();
      if (a === 'theme')   toggleTheme();
    });
    toggle.addEventListener('click', () => panel.classList.toggle('pp-hidden'));

    makeDraggable(panel);
    makeResizable(panel);
    positionPanel();
    fetchAndRender();
    window.addEventListener('resize', positionPanel);
  }

  // ── BOOT ───────────────────────────────────────────────────────────────────
  hookSubmitEvents();
  const boot = () => setTimeout(inject, 1600);
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  else boot();

  let _lastUrl = location.href;
  new MutationObserver(() => {
    if (location.href !== _lastUrl) {
      _lastUrl = location.href;
      setTimeout(() => { if (!document.getElementById('pplx-panel')) inject(); else positionPanel(); }, 1200);
    } else positionPanel();
  }).observe(document.body, { childList: true, subtree: false });
  window.addEventListener('popstate', () => setTimeout(positionPanel, 600));

})();