CLAUSAGE

A compact floating widget that displays your Claude AI usage limits in real time on chat pages

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         CLAUSAGE
// @namespace    https://github.com/Yorkian/CLAUSAGE
// @version      1.2.2
// @description  A compact floating widget that displays your Claude AI usage limits in real time on chat pages
// @author       Yorkian
// @homepage     https://github.com/Yorkian/CLAUSAGE
// @homepageURL  https://github.com/Yorkian/CLAUSAGE
// @supportURL   https://github.com/Yorkian/CLAUSAGE/issues
// @icon         https://raw.githubusercontent.com/Yorkian/CLAUSAGE/main/icon48.png
// @match        https://claude.ai/*
// @run-at       document-idle
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

/* eslint-disable */
(function () {
  'use strict';

  // Do not run inside the hidden same-origin iframe we create for usage polling.
  // This short-circuit prevents recursive widget creation and recursive iframes.
  if (window.top !== window.self) return;

  // Inject styles (single call — keeps the stylesheet in one place).
  const CUW_CSS = `/* ── Claude Usage Widget ── */
#claude-usage-widget {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 2147483647;
  width: 252px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 12px;
  color: #1a1a1a;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 10px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.10), 0 1px 4px rgba(0, 0, 0, 0.06);
  overflow: hidden;
  user-select: none;
  transition: box-shadow 0.2s, opacity 0.2s, width 0.2s;
}

#claude-usage-widget.cuw-dragging {
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
  opacity: 0.92;
}

#claude-usage-widget.cuw-collapsed .cuw-body-wrap,
#claude-usage-widget.cuw-collapsed #cuw-body {
  display: none;
}

/* ── Header ── */
.cuw-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 10px;
  cursor: grab;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  background: rgba(0, 0, 0, 0.02);
}

.cuw-header:active {
  cursor: grabbing;
}

.cuw-title {
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: #666;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1;
}

.cuw-title-brand {
  /* Default: shown. Narrow + has-plan hides it. */
}

.cuw-plan-sep {
  text-transform: none;
  font-weight: 500;
  opacity: 0.75;
}

.cuw-plan {
  text-transform: none;
  font-weight: 500;
  opacity: 0.75;
}

.cuw-actions {
  display: flex;
  gap: 2px;
  flex-shrink: 0;
}

.cuw-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: #888;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s, color 0.15s;
}

.cuw-btn:hover {
  background: rgba(0, 0, 0, 0.07);
  color: #333;
}

#cuw-refresh:active svg {
  transform: rotate(-180deg);
  transition: transform 0.3s ease;
}

/* ── Body ── */
#cuw-body {
  padding: 8px 10px 6px;
}

.cuw-loading {
  display: flex;
  align-items: center;
  gap: 6px;
  color: #999;
  font-size: 11px;
  padding: 4px 0;
}

.cuw-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 1.5px solid rgba(0,0,0,0.1);
  border-top-color: #6b8afd;
  border-radius: 50%;
  animation: cuw-spin 0.7s linear infinite;
}

@keyframes cuw-spin {
  to { transform: rotate(360deg); }
}

.cuw-empty {
  color: #aaa;
  font-size: 11px;
  padding: 4px 0;
}

/* ── Usage Row ── */
.cuw-row {
  margin-bottom: 6px;
}

.cuw-row:last-child,
.cuw-row:last-of-type {
  margin-bottom: 2px;
}

.cuw-row-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 3px;
}

.cuw-label {
  font-weight: 500;
  font-size: 11.5px;
  color: #333;
}

.cuw-pct {
  font-weight: 600;
  font-size: 11.5px;
  color: #333;
  font-variant-numeric: tabular-nums;
}

.cuw-bar-track {
  height: 5px;
  background: rgba(0, 0, 0, 0.06);
  border-radius: 3px;
  overflow: hidden;
}

.cuw-bar-fill {
  height: 100%;
  border-radius: 3px;
  transition: width 0.4s ease;
}

.cuw-reset {
  font-size: 10px;
  color: #999;
  margin-top: 2px;
}

/* Default: only full reset label is shown */
.cuw-reset-short {
  display: none;
}

/* Default: only full row label is shown */
.cuw-label-short {
  display: none;
}

.cuw-balance {
  color: #22c55e;
}

.cuw-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid rgba(0, 0, 0, 0.04);
}

.cuw-updated {
  font-size: 10px;
  color: #bbb;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.cuw-github {
  display: flex;
  align-items: center;
  color: #ccc;
  text-decoration: none;
  transition: color 0.15s;
  line-height: 1;
  flex-shrink: 0;
}

.cuw-github:hover {
  color: #888;
}

/* ───────────────────────────────────────── */
/* ── Narrow mode: browser width < 1300 ── */
/* ───────────────────────────────────────── */
#claude-usage-widget.cuw-narrow {
  width: 84px;
}

/* Title: hide brand + sep when plan is available. Otherwise keep brand. */
.cuw-narrow .cuw-title.has-plan .cuw-title-brand,
.cuw-narrow .cuw-title.has-plan .cuw-plan-sep {
  display: none;
}

.cuw-narrow .cuw-title {
  font-size: 10px;
  letter-spacing: 0.2px;
}

/* Narrow-mode plan: base name only (e.g. "Pro", "Max"); multiplier like "(5x)" is hidden. */
.cuw-narrow .cuw-plan {
  font-weight: 600;
  color: #666;
}

.cuw-narrow .cuw-plan-mult-wide {
  display: none;
}

.cuw-narrow .cuw-header {
  padding: 5px 6px;
}

.cuw-narrow .cuw-btn {
  width: 18px;
  height: 18px;
}

.cuw-narrow #cuw-body {
  padding: 6px 6px 4px;
}

.cuw-narrow .cuw-row {
  margin-bottom: 8px;
}

.cuw-narrow .cuw-row:last-child {
  margin-bottom: 2px;
}

/* Stack label / value / reset vertically */
.cuw-narrow .cuw-row-top {
  display: block;
  margin-bottom: 0;
}

.cuw-narrow .cuw-label {
  display: block;
  font-size: 10px;
  font-weight: 500;
  color: #666;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.2;
}

.cuw-narrow .cuw-pct {
  display: block;
  font-size: 15px;
  font-weight: 600;
  color: #222;
  line-height: 1.2;
  margin-top: 1px;
}

/* Hide progress bar entirely in narrow mode */
.cuw-narrow .cuw-bar-track {
  display: none;
}

/* Swap reset label: show the short form */
.cuw-narrow .cuw-reset-full {
  display: none;
}

.cuw-narrow .cuw-reset-short {
  display: inline;
}

/* Swap row label: show the short form (Current, Design, Add'l Feat.) */
.cuw-narrow .cuw-label-full {
  display: none;
}

.cuw-narrow .cuw-label-short {
  display: inline;
}

.cuw-narrow .cuw-reset {
  font-size: 9px;
  margin-top: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cuw-narrow .cuw-footer {
  margin-top: 3px;
  padding-top: 3px;
}

.cuw-narrow .cuw-updated {
  font-size: 9px;
}

/* ── Dark mode (claude.ai uses dark theme) ── */
@media (prefers-color-scheme: dark) {
  #claude-usage-widget {
    background: rgba(32, 33, 36, 0.92);
    border-color: rgba(255, 255, 255, 0.08);
    color: #e0e0e0;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2);
  }
  .cuw-header {
    border-bottom-color: rgba(255, 255, 255, 0.06);
    background: rgba(255, 255, 255, 0.02);
  }
  .cuw-title { color: #aaa; }
  .cuw-btn { color: #888; }
  .cuw-btn:hover { background: rgba(255, 255, 255, 0.08); color: #ddd; }
  .cuw-label { color: #ddd; }
  .cuw-pct { color: #ddd; }
  .cuw-bar-track { background: rgba(255, 255, 255, 0.08); }
  .cuw-reset { color: #777; }
  .cuw-balance { color: #4ade80; }
  .cuw-footer { border-top-color: rgba(255, 255, 255, 0.06); }
  .cuw-updated { color: #666; }
  .cuw-github { color: #555; }
  .cuw-github:hover { color: #aaa; }
  .cuw-loading { color: #777; }
  .cuw-empty { color: #666; }

  .cuw-narrow .cuw-label { color: #aaa; }
  .cuw-narrow .cuw-pct { color: #eee; }
  .cuw-narrow .cuw-plan { color: #aaa; }
}

/* ── Also detect claude.ai's own dark class ── */
html[data-theme="dark"] #claude-usage-widget,
body.dark #claude-usage-widget,
[data-mode="dark"] #claude-usage-widget {
  background: rgba(32, 33, 36, 0.92);
  border-color: rgba(255, 255, 255, 0.08);
  color: #e0e0e0;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.2);
}

html[data-theme="dark"] .cuw-header,
body.dark .cuw-header,
[data-mode="dark"] .cuw-header {
  border-bottom-color: rgba(255, 255, 255, 0.06);
  background: rgba(255, 255, 255, 0.02);
}

html[data-theme="dark"] .cuw-title,
body.dark .cuw-title,
[data-mode="dark"] .cuw-title { color: #aaa; }

html[data-theme="dark"] .cuw-btn,
body.dark .cuw-btn,
[data-mode="dark"] .cuw-btn { color: #888; }

html[data-theme="dark"] .cuw-label,
body.dark .cuw-label,
[data-mode="dark"] .cuw-label,
html[data-theme="dark"] .cuw-pct,
body.dark .cuw-pct,
[data-mode="dark"] .cuw-pct { color: #ddd; }

html[data-theme="dark"] .cuw-bar-track,
body.dark .cuw-bar-track,
[data-mode="dark"] .cuw-bar-track { background: rgba(255, 255, 255, 0.08); }

html[data-theme="dark"] .cuw-reset,
body.dark .cuw-reset,
[data-mode="dark"] .cuw-reset { color: #777; }

html[data-theme="dark"] .cuw-balance,
body.dark .cuw-balance,
[data-mode="dark"] .cuw-balance { color: #4ade80; }

html[data-theme="dark"] .cuw-footer,
body.dark .cuw-footer,
[data-mode="dark"] .cuw-footer { border-top-color: rgba(255, 255, 255, 0.06); }

html[data-theme="dark"] .cuw-updated,
body.dark .cuw-updated,
[data-mode="dark"] .cuw-updated { color: #666; }

html[data-theme="dark"] .cuw-github,
body.dark .cuw-github,
[data-mode="dark"] .cuw-github { color: #555; }

html[data-theme="dark"] .cuw-github:hover,
body.dark .cuw-github:hover,
[data-mode="dark"] .cuw-github:hover { color: #aaa; }

html[data-theme="dark"] .cuw-narrow .cuw-label,
body.dark .cuw-narrow .cuw-label,
[data-mode="dark"] .cuw-narrow .cuw-label { color: #aaa; }

html[data-theme="dark"] .cuw-narrow .cuw-pct,
body.dark .cuw-narrow .cuw-pct,
[data-mode="dark"] .cuw-narrow .cuw-pct { color: #eee; }

html[data-theme="dark"] .cuw-narrow .cuw-plan,
body.dark .cuw-narrow .cuw-plan,
[data-mode="dark"] .cuw-narrow .cuw-plan { color: #aaa; }
`;
  if (typeof GM_addStyle === 'function') {
    GM_addStyle(CUW_CSS);
  } else {
    const styleEl = document.createElement('style');
    styleEl.textContent = CUW_CSS;
    (document.head || document.documentElement).appendChild(styleEl);
  }

  const WIDGET_ID = 'claude-usage-widget';
    const IFRAME_ID = 'claude-usage-iframe';
    const STORAGE_KEY = 'claude-usage-widget-pos';
    const MARGIN_RIGHT = 16;
    // Default initial placement for users who haven't dragged the widget:
    // the widget's top edge sits this many pixels above the window's bottom edge.
    // Falls back to top: 0 when the window is too short to accommodate this offset.
    const DEFAULT_TOP_FROM_BOTTOM = 500;
    const POLL_INTERVAL = 500;
    const MAX_POLLS = 40;
    const AUTO_REFRESH_MS = 5 * 60 * 1000;
    const NARROW_THRESHOLD = 1300;

    let autoRefreshTimer = null;
    let lastRefreshTime = null;
    let updatedTickerTimer = null;
    let wasDragged = false;
    let savedTop = null;

    const GITHUB_SVG = '<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z"/></svg>';

    // ── SPA Navigation ──
    function isOnChatPage() {
      return location.pathname.startsWith('/chat/');
    }

    function showWidget() {
      var w = document.getElementById(WIDGET_ID);
      if (w) w.style.display = '';
    }

    function hideWidget() {
      var w = document.getElementById(WIDGET_ID);
      if (w) w.style.display = 'none';
    }

    function monitorNavigation() {
      var lastPath = location.pathname;
      var check = function () {
        var curPath = location.pathname;
        if (curPath === lastPath) return;
        lastPath = curPath;
        onRouteChange();
      };
      var origPush = history.pushState;
      history.pushState = function () {
        origPush.apply(this, arguments);
        setTimeout(check, 0);
      };
      var origReplace = history.replaceState;
      history.replaceState = function () {
        origReplace.apply(this, arguments);
        setTimeout(check, 0);
      };
      window.addEventListener('popstate', check);
      setInterval(check, 1000);
    }

    function onRouteChange() {
      if (isOnChatPage()) {
        ensureWidget();
        showWidget();
      } else {
        hideWidget();
      }
    }

    // ── Narrow-mode helpers ──
    function isNarrowMode() {
      return window.innerWidth < NARROW_THRESHOLD;
    }

    function updateNarrowMode() {
      var w = document.getElementById(WIDGET_ID);
      if (!w) return;
      w.classList.toggle('cuw-narrow', isNarrowMode());
    }

    // ── Widget creation ──
    function ensureWidget() {
      if (document.getElementById(WIDGET_ID)) return;
      createWidget();
      fetchUsageData();
      scheduleAutoRefresh();
    }

    function createWidget() {
      var wrap = document.createElement('div');
      wrap.id = WIDGET_ID;
      wrap.innerHTML =
        '<div class="cuw-header" id="cuw-drag-handle">' +
          '<span class="cuw-title">' +
            '<span class="cuw-title-brand">CLAUSAGE</span>' +
          '</span>' +
          '<div class="cuw-actions">' +
            '<button class="cuw-btn" id="cuw-refresh" title="Refresh">' +
              '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
                '<path d="M1.5 2v4.5H6"/>' +
                '<path d="M2.2 10.2a6 6 0 1 0 .6-6.7L1.5 6.5"/>' +
              '</svg>' +
            '</button>' +
            '<button class="cuw-btn" id="cuw-minimize" title="Minimize">' +
              '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">' +
                '<line x1="3" y1="8" x2="13" y2="8"/>' +
              '</svg>' +
            '</button>' +
          '</div>' +
        '</div>' +
        '<div class="cuw-body" id="cuw-body">' +
          '<div class="cuw-loading">Loading…</div>' +
        '</div>';
      document.body.appendChild(wrap);

      // Restore position
      try {
        var saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
        if (saved && saved.wasDragged) {
          wasDragged = true;
          savedTop = saved.top;
        }
      } catch (_) {}

      updateNarrowMode();
      applyPosition(wrap);

      initDrag(wrap, document.getElementById('cuw-drag-handle'));

      window.addEventListener('resize', function () {
        var w = document.getElementById(WIDGET_ID);
        if (!w) return;
        updateNarrowMode();
        applyPosition(w);
        var line = document.getElementById('cuw-updated-line');
        if (line) line.textContent = getRelativeTimeText();
      });

      var minBtn = document.getElementById('cuw-minimize');
      minBtn.addEventListener('click', function (e) {
        e.stopPropagation();
        var collapsed = wrap.classList.toggle('cuw-collapsed');
        minBtn.title = collapsed ? 'Expand' : 'Minimize';
        minBtn.innerHTML = collapsed
          ? '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="3" x2="8" y2="13"/><line x1="3" y1="8" x2="13" y2="8"/></svg>'
          : '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="8" x2="13" y2="8"/></svg>';
      });

      document.getElementById('cuw-refresh').addEventListener('click', function (e) {
        e.stopPropagation();
        fetchUsageData();
        scheduleAutoRefresh();
      });

      return wrap;
    }

    // ── Position logic ──
    function applyPosition(el) {
      if (!wasDragged) {
        // Horizontal: pin to the right edge (unchanged).
        // Vertical: top edge sits DEFAULT_TOP_FROM_BOTTOM px above the window's bottom.
        //   If the window is too short for that offset, clamp to top: 0 (widget hugs the top).
        var desiredTop = window.innerHeight - DEFAULT_TOP_FROM_BOTTOM;
        if (desiredTop < 0) desiredTop = 0;
        el.style.right = MARGIN_RIGHT + 'px';
        el.style.left = 'auto';
        el.style.bottom = 'auto';
        el.style.top = desiredTop + 'px';
        return;
      }
      var rect = el.getBoundingClientRect();
      var left = window.innerWidth - rect.width - MARGIN_RIGHT;
      el.style.right = 'auto';
      el.style.bottom = 'auto';
      el.style.left = left + 'px';
      var t = savedTop;
      if (t < 0) t = 0;
      if (t + rect.height > window.innerHeight) t = window.innerHeight - rect.height;
      el.style.top = t + 'px';
    }

    // ── Drag logic ──
    function initDrag(el, handle) {
      var startX, startY, startLeft, startTop, dragging = false;

      handle.addEventListener('mousedown', function (e) {
        if (e.target.closest('.cuw-btn')) return;
        e.preventDefault();
        dragging = true;
        var rect = el.getBoundingClientRect();
        startX = e.clientX;
        startY = e.clientY;
        startLeft = rect.left;
        startTop = rect.top;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
        el.style.left = rect.left + 'px';
        el.style.top = rect.top + 'px';
        el.classList.add('cuw-dragging');
      });

      document.addEventListener('mousemove', function (e) {
        if (!dragging) return;
        el.style.left = (startLeft + (e.clientX - startX)) + 'px';
        el.style.top = (startTop + (e.clientY - startY)) + 'px';
      });

      document.addEventListener('mouseup', function () {
        if (!dragging) return;
        dragging = false;
        el.classList.remove('cuw-dragging');
        clampToViewport(el);
        var rect = el.getBoundingClientRect();
        wasDragged = true;
        savedTop = rect.top;
        localStorage.setItem(STORAGE_KEY, JSON.stringify({ top: savedTop, wasDragged: true }));
        applyPosition(el);
      });
    }

    function clampToViewport(el) {
      var rect = el.getBoundingClientRect();
      var vw = window.innerWidth, vh = window.innerHeight;
      var l = rect.left, t = rect.top;
      if (l < 0) l = 0;
      if (t < 0) t = 0;
      if (l + rect.width > vw) l = vw - rect.width;
      if (t + rect.height > vh) t = vh - rect.height;
      el.style.left = l + 'px';
      el.style.top = t + 'px';
    }

    // ── Parse usage data ──
    function parseUsage(doc) {
      var data = { sections: [], plan: null };
      var allText = doc.body.innerText;
      if (!allText || allText.includes('Sign in') || allText.includes('Log in')) return null;

      // ── Extract plan name (e.g. "Pro", "Team", "Free", "Max", "Max (5x)", "Max (10x)") ──
      // Matches the whole cell text. The (\d+x) group captures multiplier variants for Max.
      var planRE = /^(Free|Pro|Team|Enterprise|Max)(?:\s*\((\d+x)\))?$/i;
      var planWalker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
      while (planWalker.nextNode()) {
        var ptxt = planWalker.currentNode.textContent.trim();
        var m = ptxt.match(planRE);
        if (m) {
          // Normalize casing: first letter uppercase, rest lowercase (Pro, Max, Team…)
          var base = m[1].charAt(0).toUpperCase() + m[1].slice(1).toLowerCase();
          var mult = m[2] ? m[2].toLowerCase() : null;
          data.plan = { base: base, multiplier: mult };
          break;
        }
      }

      // ── Pass 1: "% used" rows (Current session, All models, Claude Design, Extra usage $ spent) ──
      var walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
      var usageTexts = [];
      while (walker.nextNode()) {
        if (walker.currentNode.textContent.trim().includes('% used')) usageTexts.push(walker.currentNode);
      }

      usageTexts.forEach(function (node) {
        var pctMatch = node.textContent.match(/(\d+)%\s*used/);
        if (!pctMatch) return;
        var pct = parseInt(pctMatch[1], 10);
        var container = findAncestorBlock(node);
        if (!container) return;
        var blockText = container.innerText || '';
        var lines = blockText.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);

        var label = '', resetInfo = '';
        lines.forEach(function (line) {
          if (/current session|all models|claude design/i.test(line) && !label) label = line;
          if (/resets?\s/i.test(line) && !resetInfo) resetInfo = line;
        });
        if (!label) {
          label = lines.find(function (l) {
            return !l.includes('% used') && !l.includes('Resets') && !l.includes('Learn more');
          }) || 'Unknown';
        }

        // Filter rule: Current session / All models always show; everything else hide when 0%.
        var alwaysShow = /current session|all models/i.test(label);
        if (!alwaysShow && pct === 0) return;

        data.sections.push({
          type: 'pct',
          label: label,
          pct: pct,
          resetInfo: resetInfo
        });
      });

      // ── Pass 2: Additional features "X / Y" rows (e.g. Daily included routine runs) ──
      var routineWalker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null);
      while (routineWalker.nextNode()) {
        var rtxt = routineWalker.currentNode.textContent.trim();
        if (/routine runs?/i.test(rtxt)) {
          var blockEl = routineWalker.currentNode.parentElement;
          for (var d = 0; d < 8 && blockEl; d++) {
            var btext = blockEl.innerText || '';
            var fracMatch = btext.match(/(\d+)\s*\/\s*(\d+)/);
            if (fracMatch) {
              var cur = parseInt(fracMatch[1], 10);
              var tot = parseInt(fracMatch[2], 10);
              // Filter rule: Additional features hide if value is 0.
              if (cur > 0 && tot > 0) {
                data.sections.push({
                  type: 'frac',
                  label: 'Routine runs',
                  current: cur,
                  total: tot,
                  resetInfo: ''
                });
              }
              break;
            }
            blockEl = blockEl.parentElement;
          }
          break;
        }
      }

      return data.sections.length > 0 ? data : null;
    }

    // Find the tightest ancestor that still only contains ONE "% used" occurrence.
    // As we walk upward, once we hit a level with 2+ "% used" we've crossed into
    // a sibling row — return the last single-occurrence candidate.
    function findAncestorBlock(node) {
      var el = node.parentElement;
      var candidate = null;
      var depth = 0;
      while (el && depth < 15) {
        var text = el.innerText || '';
        var count = (text.match(/% used/g) || []).length;
        if (count >= 2) {
          return candidate || el;
        }
        if (count === 1) {
          candidate = el;
        }
        el = el.parentElement;
        depth++;
      }
      return candidate;
    }

    // ── Relative time + ticker ──
    function getRelativeTimeText() {
      if (!lastRefreshTime) return '';
      var elapsed = Math.floor((Date.now() - lastRefreshTime) / 1000);
      if (isNarrowMode()) {
        if (elapsed < 30)  return 'just now';
        if (elapsed < 60)  return '30s ago';
        if (elapsed < 120) return '1m ago';
        if (elapsed < 180) return '2m ago';
        if (elapsed < 240) return '3m ago';
        if (elapsed < 290) return '4m ago';
        return 'refreshing…';
      }
      if (elapsed < 30)  return 'Updated: just now';
      if (elapsed < 60)  return 'Updated: half a minute ago';
      if (elapsed < 120) return 'Updated: a minute ago';
      if (elapsed < 180) return 'Updated: 2 minutes ago';
      if (elapsed < 240) return 'Updated: 3 minutes ago';
      if (elapsed < 290) return 'Updated: 4 minutes ago';
      return 'Refreshing soon';
    }

    function stopUpdatedTicker() {
      if (updatedTickerTimer) { clearInterval(updatedTickerTimer); updatedTickerTimer = null; }
    }

    function startUpdatedTicker() {
      stopUpdatedTicker();
      updatedTickerTimer = setInterval(function () {
        var el = document.getElementById('cuw-updated-line');
        if (el) el.textContent = getRelativeTimeText();
      }, 1000);
    }

    // ── Shorten reset info for narrow mode ──
    function shortenReset(s) {
      return (s || '')
        .replace(/^Resets\s+/i, '')
        .replace(/\s*(AM|PM)\s*$/i, '');
    }

    // ── Shorten row label for narrow mode (Current session → Current, etc.) ──
    function shortenLabel(s) {
      var t = (s || '').trim();
      if (/^current session$/i.test(t)) return 'Current';
      if (/^claude design$/i.test(t)) return 'Design';
      // "Daily included routine runs" header / or its shortened pre-render form
      if (/^routine runs?$/i.test(t)) return "Add'l Feat.";
      if (/^daily included routine runs?$/i.test(t)) return "Add'l Feat.";
      return t;
    }

    // ── Render data ──
    function renderData(data) {
      var body = document.getElementById('cuw-body');
      if (!data) {
        body.innerHTML = '<div class="cuw-empty">Unable to load</div>';
        return;
      }

      lastRefreshTime = Date.now();

      // Update title with plan name
      var titleEl = document.querySelector('#' + WIDGET_ID + ' .cuw-title');
      if (titleEl) {
        if (data.plan) {
          titleEl.classList.add('has-plan');
          // Wide mode:  CLAUSAGE | Max (5x)   — literal "(5x)" matches the settings page.
          // Narrow mode: shows just the plan base (e.g. "Max"); the multiplier span is hidden via CSS.
          var planBase = escapeHtml(data.plan.base);
          var planHTML = '<span class="cuw-plan-base">' + planBase + '</span>';
          if (data.plan.multiplier) {
            var mult = escapeHtml(data.plan.multiplier);
            planHTML += '<span class="cuw-plan-mult-wide"> (' + mult + ')</span>';
          }
          titleEl.innerHTML =
            '<span class="cuw-title-brand">CLAUSAGE</span>' +
            '<span class="cuw-plan-sep"> | </span>' +
            '<span class="cuw-plan">' + planHTML + '</span>';
        } else {
          titleEl.classList.remove('has-plan');
          titleEl.innerHTML = '<span class="cuw-title-brand">CLAUSAGE</span>';
        }
      }

      var html = '';
      data.sections.forEach(function (s) {
        var barWidth, rightText;
        if (s.type === 'frac') {
          barWidth = s.total > 0 ? Math.round((s.current / s.total) * 100) : 0;
          rightText = s.current + '/' + s.total;
        } else {
          barWidth = s.pct;
          rightText = s.pct + '%';
        }
        var barColor = barWidth >= 90 ? '#ef4444' : barWidth >= 70 ? '#f59e0b' : '#6b8afd';
        var shortR = shortenReset(s.resetInfo);
        var shortLbl = shortenLabel(s.label);
        html +=
          '<div class="cuw-row">' +
            '<div class="cuw-row-top">' +
              '<span class="cuw-label">' +
                '<span class="cuw-label-full">' + escapeHtml(s.label) + '</span>' +
                '<span class="cuw-label-short">' + escapeHtml(shortLbl) + '</span>' +
              '</span>' +
              '<span class="cuw-pct">' + escapeHtml(rightText) + '</span>' +
            '</div>' +
            '<div class="cuw-bar-track">' +
              '<div class="cuw-bar-fill" style="width:' + barWidth + '%;background:' + barColor + '"></div>' +
            '</div>' +
            (s.resetInfo
              ? '<div class="cuw-reset">' +
                  '<span class="cuw-reset-full">' + escapeHtml(s.resetInfo) + '</span>' +
                  '<span class="cuw-reset-short">' + escapeHtml(shortR) + '</span>' +
                '</div>'
              : '') +
          '</div>';
      });

      html +=
        '<div class="cuw-footer">' +
          '<span class="cuw-updated" id="cuw-updated-line">' + getRelativeTimeText() + '</span>' +
          '<a class="cuw-github" href="https://github.com/Yorkian/CLAUSAGE" target="_blank" title="GitHub">' + GITHUB_SVG + '</a>' +
        '</div>';

      body.innerHTML = html;
      startUpdatedTicker();
    }

    function escapeHtml(s) {
      var d = document.createElement('div');
      d.textContent = s;
      return d.innerHTML;
    }

    // ── Fetch data via hidden iframe ──
    function fetchUsageData() {
      stopUpdatedTicker();

      var body = document.getElementById('cuw-body');
      if (body) body.innerHTML = '<div class="cuw-loading"><span class="cuw-spinner"></span> Loading…</div>';

      var old = document.getElementById(IFRAME_ID);
      if (old) old.remove();

      var iframe = document.createElement('iframe');
      iframe.id = IFRAME_ID;
      iframe.src = 'https://claude.ai/settings/usage';
      iframe.style.cssText = 'position:fixed;width:0;height:0;border:none;opacity:0;pointer-events:none;top:-9999px;left:-9999px;';
      document.body.appendChild(iframe);

      var polls = 0;
      var timer = setInterval(function () {
        polls++;
        try {
          var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
          if (!doc || !doc.body) {
            if (polls >= MAX_POLLS) { clearInterval(timer); iframe.remove(); renderData(null); }
            return;
          }

          var currentUrl = (iframe.contentWindow && iframe.contentWindow.location && iframe.contentWindow.location.href) || '';
          if (currentUrl.includes('/login') || currentUrl.includes('/signin')) {
            clearInterval(timer); iframe.remove(); hideWidget(); return;
          }

          var text = doc.body.innerText || '';

          if (text.includes('% used')) {
            clearInterval(timer);
            var data = parseUsage(doc);
            renderData(data);
            iframe.remove();
            return;
          }

          if (text.includes('Sign in') || text.includes('Log in') || text.includes('Continue with')) {
            clearInterval(timer); iframe.remove(); hideWidget(); return;
          }

        } catch (e) {
          if (e.name === 'SecurityError' || (e.message && e.message.includes('cross-origin'))) {
            clearInterval(timer); iframe.remove(); hideWidget(); return;
          }
        }

        if (polls >= MAX_POLLS) { clearInterval(timer); iframe.remove(); renderData(null); }
      }, POLL_INTERVAL);
    }

    // ── Auto-refresh ──
    function scheduleAutoRefresh() {
      if (autoRefreshTimer) clearInterval(autoRefreshTimer);
      autoRefreshTimer = setInterval(function () {
        fetchUsageData();
      }, AUTO_REFRESH_MS);
    }

    // ── Init ──
    monitorNavigation();
    if (isOnChatPage()) {
      ensureWidget();
    }

})();