Perplexity – Usage Limits & Settings

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

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

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

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

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

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

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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));

})();