Greasy Fork is available in English.

Torn Bookie Tracker

Breaks out individual bets on the Torn bookie page and adds a status bar icon

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Torn Bookie Tracker
// @namespace    https://systoned.cc/
// @version      2.1
// @description  Breaks out individual bets on the Torn bookie page and adds a status bar icon
// @author       Systoned
// @match        https://www.torn.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const PANEL_ID = 'bookie-tracker-panel';
  const ICON_ID  = 'bookie-tracker-icon';
  const STORAGE_KEY = 'bt_data_v2';
  const POS_KEY = 'bt_panel_pos';

  const SPORT_ICONS = {
    Football:             '⚽',
    Basketball:           '🏀',
    Motorsports:          '🏎️',
    Tennis:               '🎾',
    'Horse Racing':       '🐎',
    Rugby:                '🏉',
    'Rugby League':       '🏉',
    Baseball:             '⚾',
    'American Football':  '🏈',
    'Counter-Strike':     '🎮',
    'Mixed Martial arts': '🤼',
    'Australian Football':'🏉',
    Cricket:              '🏏',
    Badminton:            '🏸',
    Boxing:               '🥊',
    'Dota 2':             '🎮',
    Handball:             '🤾',
    Hockey:               '🏒',
    'League of Legends':  '🎮',
    Overwatch:            '🎮',
    Snooker:              '🎱',
    'StarCraft 2':        '🎮',
    Volleyball:           '🏐',
  };

  const SPORT_CLASS_MAP = {
    football: 'Football', basketball: 'Basketball', motor: 'Motorsports',
    tennis: 'Tennis', horseracing: 'Horse Racing', rugby: 'Rugby',
    rugbyleague: 'Rugby League', baseball: 'Baseball',
    americanfootball: 'American Football', counterstrike: 'Counter-Strike',
    mmaufc: 'Mixed Martial arts', australianfootball: 'Australian Football',
    cricket: 'Cricket', badminton: 'Badminton', boxing: 'Boxing',
    dota2: 'Dota 2', handball: 'Handball', hockey: 'Hockey',
    leagueoflegends: 'League of Legends', overwatch: 'Overwatch',
    snooker: 'Snooker', starcraft2: 'StarCraft 2', volleyball: 'Volleyball',
  };

  // ── Styles ────────────────────────────────────────────────────────────────────

  const CSS = `
    #${ICON_ID} {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 17px;
      height: 17px;
      cursor: pointer;
      position: relative;
      vertical-align: middle;
    }
    #${ICON_ID} svg {
      width: 15px;
      height: 15px;
      fill: none;
      stroke: #c8a84b;
      stroke-width: 1.8;
      stroke-linecap: round;
      stroke-linejoin: round;
      transition: stroke 0.15s;
    }
    #${ICON_ID}:hover svg { stroke: #f0cc6a; }
    #${ICON_ID} .bt-badge {
      position: absolute;
      top: -3px; right: -4px;
      background: #c0392b;
      color: #fff;
      font-size: 8px;
      font-weight: 700;
      min-width: 12px;
      height: 12px;
      border-radius: 6px;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0 2px;
      font-family: sans-serif;
    }

    #${PANEL_ID} {
      position: fixed;
      top: 50px;
      right: 8px;
      left: 8px;
      width: auto;
      max-height: 80vh;
      z-index: 99999;
      background: #2d2d2d;
      border: 1px solid #111;
      border-radius: 5px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.7);
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-size: 12px;
      color: #ddd;
      display: none;
      flex-direction: column;
      overflow: hidden;
    }
    #${PANEL_ID}.visible { display: flex; }
    #${PANEL_ID} * { box-sizing: border-box; }
    #${PANEL_ID} { overflow-x: hidden; }
    @media (min-width: 460px) {
      #${PANEL_ID} { left: auto; width: 380px; }
    }

    .bt-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: #1a1a1a;
      padding: 7px 12px;
      border-bottom: 1px solid #111;
      flex-shrink: 0;
      cursor: move;
      user-select: none;
    }
    .bt-header-title {
      font-size: 12px;
      font-weight: 700;
      color: #fff;
    }
    .bt-close {
      cursor: pointer;
      color: #888;
      font-size: 16px;
      line-height: 1;
    }
    .bt-close:hover { color: #e55; }

    .bt-cog {
      cursor: pointer;
      color: #888;
      font-size: 14px;
      line-height: 1;
      padding: 2px 4px;
      transition: color 0.15s, transform 0.2s;
      display: inline-flex;
      align-items: center;
    }
    .bt-cog:hover { color: #c8a84b; }
    .bt-cog.active { color: #c8a84b; transform: rotate(45deg); }

    .bt-settings {
      background: #252525;
      border-bottom: 1px solid #111;
      padding: 10px 12px;
      display: none;
      flex-direction: column;
      gap: 8px;
      flex-shrink: 0;
    }
    .bt-settings.open { display: flex; }
    .bt-settings-explain {
      font-size: 11px;
      color: #999;
      line-height: 1.5;
    }
    .bt-settings-hint {
      font-size: 11px;
      color: #c8a84b;
      line-height: 1.5;
      padding: 6px 8px;
      background: #3a3000;
      border-left: 2px solid #c8a84b;
      border-radius: 2px;
    }

    .bt-footer {
      padding: 6px 12px;
      text-align: center;
      font-size: 10px;
      color: #555;
      border-top: 1px solid #222;
      background: #1a1a1a;
      flex-shrink: 0;
    }
    .bt-footer a {
      color: #888;
      text-decoration: none;
    }
    .bt-footer a:hover { color: #c8a84b; }

    .bt-reseed-notice {
      background: #3a3000;
      border-bottom: 1px solid #555200;
      padding: 8px 12px;
      font-size: 11px;
      color: #c8a84b;
      text-align: center;
      line-height: 1.5;
      flex-shrink: 0;
    }

    .bt-tabs {
      display: flex;
      background: #222;
      border-bottom: 2px solid #111;
      flex-shrink: 0;
    }
    .bt-tab {
      flex: 1;
      padding: 8px 4px;
      text-align: center;
      font-size: 11px;
      font-weight: 600;
      color: #888;
      cursor: pointer;
      border-bottom: 2px solid transparent;
      margin-bottom: -2px;
      transition: color 0.15s, border-color 0.15s;
    }
    .bt-tab:hover { color: #ccc; }
    .bt-tab.active {
      color: #fff;
      border-bottom-color: #c8a84b;
    }

    .bt-content {
      overflow-y: auto;
      flex: 1;
    }
    .bt-pane { display: none; }
    .bt-pane.active { display: block; }

    .bt-stats {
      display: grid;
      grid-template-columns: 1fr 1fr;
    }
    .bt-stat {
      padding: 10px 14px;
      border-bottom: 1px solid #222;
      border-right: 1px solid #222;
    }
    .bt-stat:nth-child(even) { border-right: none; }
    .bt-stat-label {
      font-size: 10px;
      color: #888;
      margin-bottom: 3px;
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }
    .bt-stat-value {
      font-size: 16px;
      font-weight: 700;
      color: #fff;
    }
    .bt-stat-value.green { color: #75b855; }
    .bt-stat-value.red   { color: #e05c3a; }
    .bt-stat-value.gold  { color: #c8a84b; }

    .bt-wl {
      font-size: 16px;
      font-weight: 700;
    }
    .bt-wl .w   { color: #75b855; }
    .bt-wl .sep { color: #555; margin: 0 2px; }
    .bt-wl .l   { color: #e05c3a; }

    .bt-sports {
      padding: 8px 12px 10px;
      border-bottom: 1px solid #222;
      display: flex;
      flex-wrap: wrap;
      gap: 5px;
    }
    .bt-sports-label {
      width: 100%;
      font-size: 10px;
      color: #888;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      margin-bottom: 3px;
    }
    .bt-sport-chip {
      background: #3a3a3a;
      border: 1px solid #4a4a4a;
      border-radius: 3px;
      padding: 2px 7px;
      font-size: 11px;
      color: #ccc;
    }

    .bt-event {
      border-bottom: 1px solid #222;
    }
    .bt-event-header {
      background: #383838;
      padding: 5px 12px;
      font-size: 11px;
      font-weight: 700;
      color: #fff;
      display: flex;
      align-items: flex-start;
      gap: 6px;
      word-break: break-word;
    }

    .bt-bet {
      display: grid;
      grid-template-columns: 1fr auto auto auto;
      gap: 0 6px;
      align-items: center;
      padding: 5px 10px;
      background: #2d2d2d;
    }
    .bt-bet:nth-child(even) { background: #313131; }

    .bt-bet-sel {
      font-size: 11px;
      color: #ccc;
      word-break: break-word;
    }
    .bt-bet-market {
      font-size: 10px;
      color: #777;
    }
    .bt-bet-odds {
      font-size: 11px;
      color: #999;
      white-space: nowrap;
    }
    .bt-bet-stake {
      font-size: 11px;
      color: #bbb;
      white-space: nowrap;
      text-align: right;
    }
    .bt-bet-result {
      font-size: 11px;
      font-weight: 700;
      white-space: nowrap;
      text-align: right;
    }
    .bt-bet-result.won      { color: #75b855; }
    .bt-bet-result.lost     { color: #e05c3a; }
    .bt-bet-result.pending  { color: #c8a84b; }
    .bt-bet-result.refunded { color: #888; }

    .bt-event-total {
      display: flex;
      justify-content: flex-end;
      align-items: center;
      gap: 8px;
      padding: 4px 12px;
      background: #272727;
      font-size: 11px;
      border-top: 1px solid #222;
    }
    .bt-event-total-label { color: #666; }
    .bt-event-total-val { font-weight: 700; }
    .bt-event-total-val.green { color: #75b855; }
    .bt-event-total-val.red   { color: #e05c3a; }
    .bt-event-total-val.gold  { color: #c8a84b; }

    .bt-pane-summary {
      display: flex;
      gap: 0;
      border-bottom: 2px solid #111;
      flex-shrink: 0;
    }
    .bt-pane-stat {
      flex: 1;
      padding: 8px 12px;
      background: #252525;
      border-right: 1px solid #1a1a1a;
    }
    .bt-pane-stat:last-child { border-right: none; }
    .bt-pane-stat-label {
      display: block;
      font-size: 10px;
      color: #888;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      margin-bottom: 2px;
    }
    .bt-pane-stat-value {
      display: block;
      font-size: 14px;
      font-weight: 700;
      color: #fff;
    }
    .bt-pane-stat-value.green { color: #75b855; }
    .bt-pane-stat-value.red   { color: #e05c3a; }
    .bt-pane-stat-value.gold  { color: #c8a84b; }

    .bt-empty {
      padding: 20px;
      text-align: center;
      color: #666;
      font-size: 12px;
      line-height: 1.6;
    }

    .bt-update-bar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: #222;
      padding: 5px 12px;
      border-bottom: 1px solid #111;
      flex-shrink: 0;
    }
    .bt-saved-at {
      font-size: 10px;
      color: #666;
    }
    .bt-update-btn {
      background: #3a3000;
      border: 1px solid #c8a84b;
      border-radius: 3px;
      color: #c8a84b;
      font-size: 11px;
      font-weight: 600;
      padding: 3px 10px;
      cursor: pointer;
      font-family: inherit;
    }
    .bt-update-btn:hover {
      background: #4a3d00;
      color: #f0cc6a;
    }
    .bt-update-btn:disabled {
      background: #2a2a2a;
      border-color: #444;
      color: #555;
      cursor: not-allowed;
    }
    .bt-update-btn:disabled:hover {
      background: #2a2a2a;
      color: #555;
    }

    .bt-sport-row {
      display: grid;
      grid-template-columns: 1fr auto auto auto;
      gap: 0 10px;
      align-items: center;
      padding: 8px 12px;
      border-bottom: 1px solid #222;
      background: #2d2d2d;
    }
    .bt-sport-row:nth-child(even) { background: #313131; }
    .bt-sport-name {
      font-size: 12px;
      font-weight: 600;
      color: #ddd;
      display: flex;
      align-items: center;
      gap: 6px;
    }
    .bt-sport-wl {
      font-size: 11px;
      font-weight: 700;
      white-space: nowrap;
    }
    .bt-sport-wl .w { color: #75b855; }
    .bt-sport-wl .sep { color: #555; margin: 0 2px; }
    .bt-sport-wl .l { color: #e05c3a; }
    .bt-sport-stake {
      font-size: 11px;
      color: #999;
      white-space: nowrap;
      text-align: right;
    }
    .bt-sport-pnl {
      font-size: 12px;
      font-weight: 700;
      white-space: nowrap;
      text-align: right;
      min-width: 70px;
    }
    .bt-sport-pnl.green { color: #75b855; }
    .bt-sport-pnl.red   { color: #e05c3a; }
    .bt-sport-pnl.gold  { color: #c8a84b; }
    .bt-sport-row-header {
      display: grid;
      grid-template-columns: 1fr auto auto auto;
      gap: 0 10px;
      padding: 6px 12px;
      background: #1a1a1a;
      border-bottom: 1px solid #111;
      font-size: 10px;
      color: #888;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      font-weight: 600;
    }
    .bt-sport-row-header > div:nth-child(2),
    .bt-sport-row-header > div:nth-child(3),
    .bt-sport-row-header > div:nth-child(4) {
      text-align: right;
    }
  `;

  // ── Helpers ───────────────────────────────────────────────────────────────────

  function fmt(n) {
    if (Math.abs(n) >= 1_000_000_000) return (n / 1_000_000_000).toFixed(2) + 'b';
    if (Math.abs(n) >= 1_000_000)     return (n / 1_000_000).toFixed(2) + 'm';
    if (Math.abs(n) >= 1_000)         return Math.round(n / 1_000) + 'k';
    return Math.round(n).toLocaleString();
  }

  function fmtSigned(n) {
    if (n === 0) return '$0';
    return (n > 0 ? '+$' : '-$') + fmt(Math.abs(n));
  }

  function colorClass(n) {
    if (n > 0) return 'green';
    if (n < 0) return 'red';
    return 'gold';
  }

  function parseBetTitle(titleStr, defaultStatus) {
    const lines = titleStr.split(/<br\s*\/?>/i).map(s => s.trim()).filter(Boolean);
    return lines.map(line => {
      const statusMatch = line.match(/^(Won|Lost|Pending|Refunded)\s+/i);
      const status = statusMatch ? statusMatch[1].toLowerCase() : defaultStatus;
      line = line.replace(/^(Won|Lost|Pending|Refunded)\s+/i, '');

      const dollarMatch = line.match(/^\$([\d,]+)/);
      let firstDollar = 0;
      if (dollarMatch) {
        firstDollar = parseFloat(dollarMatch[1].replace(/,/g, ''));
        line = line.slice(dollarMatch[0].length).trim();
      }

      const oddsMatch = line.match(/^\(x([\d.]+)\)/);
      let odds = null;
      if (oddsMatch) {
        odds = parseFloat(oddsMatch[1]);
        line = line.slice(oddsMatch[0].length).trim();
      }

      // For won bets: title is "Won $WINNINGS (xODDS) from a $STAKE bet on..."
      // For everything else: title is "$STAKE (xODDS) bet on..."
      let stake = firstDollar;
      const fromAMatch = line.match(/^from\s+a\s+\$([\d,]+)\s+bet\s+/i);
      if (fromAMatch) {
        stake = parseFloat(fromAMatch[1].replace(/,/g, ''));
        line = line.slice(fromAMatch[0].length).trim();
      } else {
        line = line.replace(/^bet on\s*/i, '');
      }

      const marketMatch = line.match(/^(.*?)\s*\(([^)]+)\)\s*$/);
      let selection = line;
      let market = '';
      if (marketMatch) {
        selection = marketMatch[1].trim();
        market = marketMatch[2].trim();
      }

      let pnl = 0;
      if (status === 'won' && odds)  pnl = Math.round(stake * (odds - 1));
      if (status === 'lost')         pnl = -stake;

      return { status, stake, odds, selection, market, pnl };
    });
  }

  // Stable key for a bet
  function betKey(matchId, bet) {
    return [matchId || 'nomatch', bet.selection, bet.market, bet.stake].join('|');
  }

  // ── Scraping ─────────────────────────────────────────────────────────────────

  // Walk ul.pop-list. Section headers are <li> with class "title" plus one of
  // "live", "upcomming" (sic), or "completed". Everything else between headers
  // is a match row.
  function scrapeBets() {
    const bets = {};
    let bucket = null;

    document.querySelectorAll('ul.pop-list > li').forEach(li => {
      if (li.classList.contains('title')) {
        if (li.classList.contains('completed')) bucket = 'completed';
        else if (li.classList.contains('upcomming') || li.classList.contains('live')) bucket = 'pending';
        else bucket = null;
        return;
      }
      if (!bucket) return;

      const anchor = li.querySelector('a[href*="/your-bets/"]');
      if (!anchor) return;
      // i-data is stable per match (e.g. i_192_375_784_33). The href contains
      // a bet ID which changes whenever you add another bet to the same match,
      // so it can't be used as a match identifier.
      const matchId = anchor.getAttribute('i-data') || null;

      const matchNameEl = li.querySelector('[class*="matchName"] p');
      const matchName   = matchNameEl ? (matchNameEl.title || matchNameEl.textContent.trim()) : 'Unknown';

      const sportEl = li.querySelector('[class*="game"] i');
      let sport = 'Unknown';
      if (sportEl) {
        const m = (sportEl.className || '').match(/gm-([a-z]+)-icon/);
        if (m) sport = SPORT_CLASS_MAP[m[1]] || m[1];
      }

      li.querySelectorAll('.stick').forEach(stick => {
        const textEl = stick.querySelector('.text');
        if (!textEl) return;
        const title = textEl.getAttribute('title') || '';
        if (!title) return;

        let defaultStatus = 'pending';
        if (stick.classList.contains('won'))           defaultStatus = 'won';
        else if (stick.classList.contains('lost'))     defaultStatus = 'lost';
        else if (stick.classList.contains('refunded')) defaultStatus = 'refunded';

        parseBetTitle(title, defaultStatus).forEach(parsed => {
          const key = betKey(matchId, parsed);
          bets[key] = { ...parsed, matchId, matchName, sport };
        });
      });
    });

    return bets;
  }

  // ── Storage ───────────────────────────────────────────────────────────────────

  function loadData() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { bets: {}, savedAt: null };
    } catch (e) {
      return { bets: {}, savedAt: null };
    }
  }

  function saveData(data) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
  }

  function hasSavedData() {
    const d = loadData();
    return d.bets && Object.keys(d.bets).length > 0;
  }

  function savePos(pos) {
    try { localStorage.setItem(POS_KEY, JSON.stringify(pos)); } catch (e) {}
  }

  function loadPos() {
    try { return JSON.parse(localStorage.getItem(POS_KEY)) || null; } catch (e) { return null; }
  }

  function getValidPos() {
    const pos = loadPos();
    if (!pos) return null;
    if (pos.left < 0 || pos.top < 0) return null;
    if (pos.left > window.innerWidth - 40 || pos.top > window.innerHeight - 40) return null;
    return pos;
  }

  // ── Bucketing for render ──────────────────────────────────────────────────────

  // Turn the flat bets object into pending/completed event groups for rendering.
  // A bet is pending if its status is 'pending', otherwise it's completed.
  // Groups by match name (the only stable per-match identifier on Torn) and
  // dedupes bets within a match by selection+market+stake+odds, so identical
  // bets that ended up stored under different keys only render once.
  function groupByEvent(bets) {
    const pending = new Map();
    const completed = new Map();

    Object.values(bets).forEach(b => {
      const target = b.status === 'pending' ? pending : completed;
      const key = b.matchName;
      if (!target.has(key)) {
        target.set(key, { matchName: b.matchName, sport: b.sport, bets: [], seen: new Set() });
      }
      const entry = target.get(key);
      const betDedupKey = [b.selection, b.market, b.stake, b.odds].join('|');
      if (entry.seen.has(betDedupKey)) return;
      entry.seen.add(betDedupKey);
      entry.bets.push(b);
    });

    // Strip the seen set before returning
    const strip = list => list.map(({ seen, ...rest }) => rest);
    return { pending: strip([...pending.values()]), completed: strip([...completed.values()]) };
  }

  // ── Render ────────────────────────────────────────────────────────────────────

  function renderOverview(pending, completed) {
    const pendingCount  = pending.reduce((n, e) => n + e.bets.length, 0);
    const pendingStake  = pending.reduce((s, e) => s + e.bets.reduce((b, bet) => b + bet.stake, 0), 0);
    const pendingSports = [...new Set(pending.map(e => e.sport))];

    let wins = 0, losses = 0, pnl = 0;
    completed.forEach(e => e.bets.forEach(b => {
      if (b.status === 'won')  wins++;
      if (b.status === 'lost') losses++;
      if (b.status !== 'refunded') pnl += b.pnl;
    }));

    const sportChips = pendingSports.map(s =>
      `<span class="bt-sport-chip">${SPORT_ICONS[s] || '🎲'} ${s}</span>`
    ).join('');

    return `
      <div class="bt-stats">
        <div class="bt-stat">
          <div class="bt-stat-label">Pending bets</div>
          <div class="bt-stat-value gold">${pendingCount}</div>
        </div>
        <div class="bt-stat">
          <div class="bt-stat-label">At stake</div>
          <div class="bt-stat-value gold">$${fmt(pendingStake)}</div>
        </div>
        <div class="bt-stat">
          <div class="bt-stat-label">Win / Loss</div>
          <div class="bt-wl"><span class="w">${wins}W</span><span class="sep">/</span><span class="l">${losses}L</span></div>
        </div>
        <div class="bt-stat">
          <div class="bt-stat-label">Total P&amp;L</div>
          <div class="bt-stat-value ${colorClass(pnl)}">${fmtSigned(pnl)}</div>
        </div>
      </div>
      ${pendingSports.length ? `
        <div class="bt-sports">
          <div class="bt-sports-label">Sports in play</div>
          ${sportChips}
        </div>` : ''}
    `;
  }

  function renderEvents(events, isPending) {
    if (!events.length) return `<div class="bt-empty">No bets found.<br>Scroll the bookie page to load them first.</div>`;

    const allBets = events.flatMap(e => e.bets);
    const totalStake = allBets.reduce((s, b) => s + b.stake, 0);

    let summaryHtml;
    if (isPending) {
      const totalReturn = allBets.reduce((s, b) => s + (b.odds ? Math.round(b.stake * b.odds) : b.stake), 0);
      summaryHtml = `
        <div class="bt-pane-summary">
          <div class="bt-pane-stat"><span class="bt-pane-stat-label">Total staked</span><span class="bt-pane-stat-value">$${fmt(totalStake)}</span></div>
          <div class="bt-pane-stat"><span class="bt-pane-stat-label">Potential return</span><span class="bt-pane-stat-value gold">$${fmt(totalReturn)}</span></div>
        </div>`;
    } else {
      const totalPnl = allBets.filter(b => b.status !== 'refunded').reduce((s, b) => s + b.pnl, 0);
      summaryHtml = `
        <div class="bt-pane-summary">
          <div class="bt-pane-stat"><span class="bt-pane-stat-label">Total staked</span><span class="bt-pane-stat-value">$${fmt(totalStake)}</span></div>
          <div class="bt-pane-stat"><span class="bt-pane-stat-label">Total P&amp;L</span><span class="bt-pane-stat-value ${colorClass(totalPnl)}">${fmtSigned(totalPnl)}</span></div>
        </div>`;
    }

    const eventsHtml = events.map(ev => {
      const icon = SPORT_ICONS[ev.sport] || '🎲';
      const eventPnl  = ev.bets.reduce((s, b) => s + b.pnl, 0);
      const showTotal = ev.bets.length > 1 && !isPending;

      const betRows = ev.bets.map(b => {
        let resultStr, resultClass;
        if (b.status === 'won')           { resultStr = '+$' + fmt(b.pnl);   resultClass = 'won'; }
        else if (b.status === 'lost')     { resultStr = '-$' + fmt(b.stake); resultClass = 'lost'; }
        else if (b.status === 'pending')  { resultStr = 'Pending';           resultClass = 'pending'; }
        else                              { resultStr = 'Refunded';          resultClass = 'refunded'; }

        return `
          <div class="bt-bet">
            <div>
              <div class="bt-bet-sel" title="${b.selection}">${b.selection}</div>
              ${b.market ? `<div class="bt-bet-market">${b.market}</div>` : ''}
            </div>
            <div class="bt-bet-odds">${b.odds ? 'x' + b.odds : ''}</div>
            <div class="bt-bet-stake">$${fmt(b.stake)}</div>
            <div class="bt-bet-result ${resultClass}">${resultStr}</div>
          </div>`;
      }).join('');

      const totalRow = showTotal ? `
        <div class="bt-event-total">
          <span class="bt-event-total-label">Event total</span>
          <span class="bt-event-total-val ${colorClass(eventPnl)}">${fmtSigned(eventPnl)}</span>
        </div>` : '';

      return `
        <div class="bt-event">
          <div class="bt-event-header">${icon} ${ev.matchName}</div>
          ${betRows}
          ${totalRow}
        </div>`;
    }).join('');

    return summaryHtml + eventsHtml;
  }

  function renderBySport(completed) {
    if (!completed.length) {
      return `<div class="bt-empty">No completed bets yet.<br>Click update to load them.</div>`;
    }

    const bySport = new Map();
    completed.forEach(ev => {
      ev.bets.forEach(b => {
        if (b.status === 'refunded') return;
        const row = bySport.get(ev.sport) || { sport: ev.sport, wins: 0, losses: 0, stake: 0, pnl: 0 };
        if (b.status === 'won')  row.wins++;
        if (b.status === 'lost') row.losses++;
        row.stake += b.stake;
        row.pnl   += b.pnl;
        bySport.set(ev.sport, row);
      });
    });

    if (!bySport.size) {
      return `<div class="bt-empty">No completed bets yet.</div>`;
    }

    const rows = [...bySport.values()].sort((a, b) => b.pnl - a.pnl);
    const totalPnl   = rows.reduce((s, r) => s + r.pnl, 0);
    const totalStake = rows.reduce((s, r) => s + r.stake, 0);

    const summary = `
      <div class="bt-pane-summary">
        <div class="bt-pane-stat"><span class="bt-pane-stat-label">Sports tracked</span><span class="bt-pane-stat-value">${rows.length}</span></div>
        <div class="bt-pane-stat"><span class="bt-pane-stat-label">Total staked</span><span class="bt-pane-stat-value">$${fmt(totalStake)}</span></div>
        <div class="bt-pane-stat"><span class="bt-pane-stat-label">Total P&amp;L</span><span class="bt-pane-stat-value ${colorClass(totalPnl)}">${fmtSigned(totalPnl)}</span></div>
      </div>`;

    const header = `
      <div class="bt-sport-row-header">
        <div>Sport</div>
        <div>W/L</div>
        <div>Staked</div>
        <div>P&amp;L</div>
      </div>`;

    const rowsHtml = rows.map(r => `
      <div class="bt-sport-row">
        <div class="bt-sport-name">${SPORT_ICONS[r.sport] || '🎲'} ${r.sport}</div>
        <div class="bt-sport-wl"><span class="w">${r.wins}W</span><span class="sep">/</span><span class="l">${r.losses}L</span></div>
        <div class="bt-sport-stake">$${fmt(r.stake)}</div>
        <div class="bt-sport-pnl ${colorClass(r.pnl)}">${fmtSigned(r.pnl)}</div>
      </div>`).join('');

    return summary + header + rowsHtml;
  }

  // ── Panel ─────────────────────────────────────────────────────────────────────

  function isBookiePage() {
    return location.href.includes('sid=bookie');
  }

  function buildPanel() {
    let panel = document.getElementById(PANEL_ID);
    if (!panel) {
      panel = document.createElement('div');
      panel.id = PANEL_ID;
      document.body.appendChild(panel);
    }
    renderPanel(panel);
    applyPos(panel);
    return panel;
  }

  function applyPos(panel) {
    const pos = getValidPos();
    if (pos) {
      panel.style.left = pos.left + 'px';
      panel.style.top  = pos.top  + 'px';
      panel.style.right = 'auto';
    }
  }

  let dragState = null;

  function ensureDocDragListeners() {
    if (ensureDocDragListeners.bound) return;
    ensureDocDragListeners.bound = true;
    document.addEventListener('mousemove', (e) => {
      if (!dragState) return;
      const { panel, startX, startY, startLeft, startTop } = dragState;
      let newLeft = startLeft + (e.clientX - startX);
      let newTop  = startTop  + (e.clientY - startY);
      const maxLeft = window.innerWidth  - 40;
      const maxTop  = window.innerHeight - 40;
      newLeft = Math.max(0, Math.min(newLeft, maxLeft));
      newTop  = Math.max(0, Math.min(newTop,  maxTop));
      panel.style.left = newLeft + 'px';
      panel.style.top  = newTop  + 'px';
    });
    document.addEventListener('mouseup', () => {
      if (!dragState) return;
      const { panel } = dragState;
      dragState = null;
      const rect = panel.getBoundingClientRect();
      savePos({ left: rect.left, top: rect.top });
    });
  }

  function attachDrag(panel) {
    const header = panel.querySelector('.bt-header');
    if (!header) return;
    ensureDocDragListeners();

    header.addEventListener('mousedown', (e) => {
      if (e.target.closest('.bt-close')) return;
      const rect = panel.getBoundingClientRect();
      dragState = {
        panel,
        startX: e.clientX,
        startY: e.clientY,
        startLeft: rect.left,
        startTop:  rect.top,
      };
      panel.style.left  = rect.left + 'px';
      panel.style.top   = rect.top  + 'px';
      panel.style.right = 'auto';
      e.preventDefault();
    });
  }

  function renderPanel(panel) {
    const data = loadData();
    const { pending, completed } = groupByEvent(data.bets);
    const pendingCount   = pending.reduce((n, e) => n + e.bets.length, 0);
    const completedCount = completed.reduce((n, e) => n + e.bets.length, 0);
    const savedStr = data.savedAt ? 'Updated ' + new Date(data.savedAt).toLocaleTimeString() : 'No data yet';
    const onBookie = isBookiePage();
    const empty = !hasSavedData();
    const hint = empty
      ? `Scroll to the bottom of the bookie page first to load all your bets, then click update.`
      : `If you feel you're missing data, scroll to the bottom of the bookie page and manually click update.`;

    panel.innerHTML = `
      <div class="bt-header">
        <span class="bt-header-title">Bookie Tracker</span>
        <span class="bt-close" id="bt-close-btn">&#x2715;</span>
      </div>
      <div class="bt-update-bar">
        <span class="bt-saved-at">${savedStr}</span>
        <div style="display:flex;align-items:center;gap:8px">
          <button class="bt-update-btn" id="bt-update-btn" ${onBookie ? '' : 'disabled title="Go to the bookie page to update"'}>Update bets</button>
          <span class="bt-cog" id="bt-cog-btn" title="Settings">&#9881;</span>
        </div>
      </div>
      <div class="bt-settings" id="bt-settings">
        <div class="bt-settings-explain">
          Bookie Tracker stores your Torn bookie history locally in your browser. Click Update on the bookie page to refresh your data.
        </div>
        <div class="bt-settings-hint">${hint}</div>
      </div>
      ${empty ? `
        <div class="bt-reseed-notice">
          Scroll to the bottom of the bookie page and click Update to collect your data.
        </div>
      ` : ''}
      <div class="bt-tabs">
        <div class="bt-tab active" data-tab="overview">Overview</div>
        <div class="bt-tab" data-tab="pending">Pending (${pendingCount})</div>
        <div class="bt-tab" data-tab="completed">Completed (${completedCount})</div>
        <div class="bt-tab" data-tab="bysport">By sport</div>
      </div>
      <div class="bt-content">
        <div class="bt-pane active" data-pane="overview">${renderOverview(pending, completed)}</div>
        <div class="bt-pane" data-pane="pending">${renderEvents(pending, true)}</div>
        <div class="bt-pane" data-pane="completed">${renderEvents(completed, false)}</div>
        <div class="bt-pane" data-pane="bysport">${renderBySport(completed)}</div>
      </div>
      <div class="bt-footer">
        Made by <a href="https://www.torn.com/profiles.php?XID=3583736" target="_blank">Systoned</a>
      </div>
    `;

    panel.querySelector('#bt-close-btn').addEventListener('click', () => {
      panel.classList.remove('visible');
    });

    const cogBtn = panel.querySelector('#bt-cog-btn');
    const settings = panel.querySelector('#bt-settings');
    cogBtn.addEventListener('click', () => {
      const opened = settings.classList.toggle('open');
      cogBtn.classList.toggle('active', opened);
    });

    const updateBtn = panel.querySelector('#bt-update-btn');
    if (updateBtn) {
      updateBtn.addEventListener('click', () => {
        runUpdate();
        renderPanel(panel);
      });
    }

    panel.querySelectorAll('.bt-tab').forEach(tab => {
      tab.addEventListener('click', () => {
        panel.querySelectorAll('.bt-tab').forEach(t => t.classList.remove('active'));
        panel.querySelectorAll('.bt-pane').forEach(p => p.classList.remove('active'));
        tab.classList.add('active');
        panel.querySelector(`[data-pane="${tab.dataset.tab}"]`).classList.add('active');
      });
    });

    attachDrag(panel);
  }

  // Scrape the page. Pending in storage is replaced by pending on the page
  // (stale pending entries are removed). Completed bets accumulate.
  function runUpdate() {
    if (!isBookiePage()) return false;
    const fresh = scrapeBets();
    const data = loadData();

    // Remove stored pending bets that are no longer on the page.
    Object.keys(data.bets).forEach(key => {
      if (data.bets[key].status === 'pending' && !(key in fresh)) {
        delete data.bets[key];
      }
    });

    // Merge fresh scrape into storage (adds new bets, updates existing).
    Object.assign(data.bets, fresh);

    data.savedAt = Date.now();
    saveData(data);
    updateBadge();
    return true;
  }

  function togglePanel() {
    let panel = document.getElementById(PANEL_ID);
    if (panel && panel.classList.contains('visible')) {
      panel.classList.remove('visible');
      return;
    }
    panel = buildPanel();
    panel.classList.add('visible');
  }

  // ── Badge ─────────────────────────────────────────────────────────────────────

  function updateBadge() {
    const icon = document.getElementById(ICON_ID);
    if (!icon) return;
    const badge = icon.querySelector('.bt-badge');
    if (!badge) return;
    const data = loadData();
    const { pending } = groupByEvent(data.bets);
    const count = pending.reduce((n, e) => n + e.bets.length, 0);
    badge.textContent = count;
    badge.style.display = count > 0 ? 'flex' : 'none';
  }

  // ── Status bar icon ───────────────────────────────────────────────────────────

  function injectIcon() {
    if (document.getElementById(ICON_ID)) return;
    const statusBar = document.querySelector('ul[class*="status-icons"]');
    if (!statusBar) return;

    const li = document.createElement('li');
    li.id = ICON_ID;
    li.title = 'Bookie Tracker';
    li.innerHTML = `
      <svg viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
        <rect x="1.5" y="1.5" width="12" height="12" rx="1.5"/>
        <line x1="4" y1="5"   x2="11" y2="5"/>
        <line x1="4" y1="7.5" x2="11" y2="7.5"/>
        <line x1="4" y1="10"  x2="8"  y2="10"/>
      </svg>
      <span class="bt-badge" style="display:none"></span>
    `;
    li.addEventListener('click', togglePanel);
    statusBar.appendChild(li);
    updateBadge();
  }

  // ── Init ──────────────────────────────────────────────────────────────────────

  function injectStyles() {
    if (document.getElementById('bt-styles')) return;
    const style = document.createElement('style');
    style.id = 'bt-styles';
    style.textContent = CSS;
    document.head.appendChild(style);
  }

  function init() {
    injectStyles();
    if (document.querySelector('ul[class*="status-icons"]')) {
      injectIcon();
    } else {
      const observer = new MutationObserver(() => {
        if (document.querySelector('ul[class*="status-icons"]')) {
          observer.disconnect();
          injectIcon();
        }
      });
      observer.observe(document.body, { childList: true, subtree: true });
    }
  }

  if (document.readyState !== 'loading') {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }

})();