pokertools-arena 0.4.5 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  | **Package** | `npx pokertools-arena` |
18
18
  | **Runtime** | Node.js ≥ 24 for tooling; any modern browser for the app |
19
19
 
20
- > **Current release: 0.4.5.** A maintenance release on top of the 0.4.0 methodology work: modal tooltips float above overflow, the launcher starts exactly once when its default port is busy, real-API diagnostics skip cleanly when repository secrets are absent, the Decision sanity suite attributes results to the correct model, seat editors show the model-derived name, close buttons use centered SVG icons, decision logs no longer repeat the typed-decision caption, and Stop immediately freezes the clock and table effects.
20
+ > **Current release: 0.4.8.** A maintenance release on top of the 0.4.0 methodology work: modal tooltips float above overflow, the launcher starts exactly once when its default port is busy, real-API diagnostics skip cleanly when repository secrets are absent, the Decision sanity suite attributes results to the correct model, seat editors show the model-derived name, close buttons use centered SVG icons, decision logs no longer repeat the typed-decision caption, Stop immediately freezes the clock and table effects, seat cards clear their last action at the start of each hand, pots fly back to winners at showdown, and the header, table chrome and small-screen board cards are more compact.
21
21
 
22
22
  ---
23
23
 
package/dist/app.js CHANGED
@@ -115737,6 +115737,7 @@ function animateBoardCards(previousCount, currentCount) {
115737
115737
  }
115738
115738
  function processVisualEffects(s) {
115739
115739
  const events = s?.events || [];
115740
+ const previous = lastVisualState;
115740
115741
  if (["STOPPED", "ERROR"].includes(s?.status)) {
115741
115742
  if (events.length) lastProcessedEventId = events.at(-1).id;
115742
115743
  lastVisualState = s;
@@ -115748,7 +115749,6 @@ function processVisualEffects(s) {
115748
115749
  lastVisualState = s;
115749
115750
  return;
115750
115751
  }
115751
- const previous = lastVisualState;
115752
115752
  if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
115753
115753
  const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
115754
115754
  if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
@@ -115774,12 +115774,14 @@ function processVisualEffects(s) {
115774
115774
  }
115775
115775
  showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} \xB7 ${e.action?.description || type}`, type);
115776
115776
  }
115777
- if (e.type === "HAND_END" && previous?.table) {
115778
- const before = new Map((previous.table.players || []).filter(Boolean).map((p) => [p.id, Number(p.stack || 0)]));
115779
- const winners = (s?.table?.players || []).filter((p) => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0)));
115777
+ if (e.type === "HAND_END") {
115778
+ const before = new Map((previous?.table?.players || []).filter(Boolean).map((p) => [p.id, Number(p.stack || 0)]));
115779
+ const payloadIds = (e.winners || []).map((w) => w?.playerId ?? w?.id ?? w).filter(Boolean);
115780
+ const fromStacks = (s?.table?.players || []).filter((p) => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0))).map((p) => p.id);
115781
+ const winnerIds = [.../* @__PURE__ */ new Set([...payloadIds, ...fromStacks])];
115780
115782
  const pot = $(".hud-pot", document);
115781
- winners.forEach((p, i) => setTimeout(() => flyChips(pot, seatEl(p.id), 5, true), i * 130));
115782
- if (winners.length) playTableSound("winner");
115783
+ winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
115784
+ if (winnerIds.length) playTableSound("winner");
115783
115785
  }
115784
115786
  if (e.type === "TOURNAMENT_END") playTableSound("winner");
115785
115787
  }
@@ -116337,6 +116339,7 @@ var TournamentDirector = class {
116337
116339
  await this.waitIfPaused();
116338
116340
  this.pruneBustedSeats();
116339
116341
  this.handNumber++;
116342
+ for (const stat of Object.values(this.stats)) stat.lastAction = null;
116340
116343
  this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
116341
116344
  try {
116342
116345
  this.engine.deal();
@@ -117176,7 +117179,7 @@ function renderStatus(s) {
117176
117179
  els.pauseBtn.classList.toggle("hidden", !running);
117177
117180
  els.stopBtn.classList.toggle("hidden", !running);
117178
117181
  els.startTopBtn.classList.toggle("hidden", running);
117179
- els.seatsBtn.classList.toggle("hidden", running);
117182
+ els.seatsBtn.classList.toggle("hidden", running || !["STOPPED", "FINISHED", "ERROR"].includes(status));
117180
117183
  els.setupBtn.disabled = running;
117181
117184
  els.testsBtn.disabled = running;
117182
117185
  const pauseLabel = $(".action-label", els.pauseBtn);
@@ -118301,8 +118304,7 @@ els.exportBtn.addEventListener("click", () => {
118301
118304
  });
118302
118305
  els.seatsBtn.addEventListener("click", () => {
118303
118306
  if (director && ["RUNNING", "PAUSED"].includes(director.status)) return;
118304
- lobbyVisible = true;
118305
- render(currentState || { status: "IDLE", events: [] });
118307
+ void startConfiguredTournament();
118306
118308
  });
118307
118309
  els.seatsLayer.addEventListener("click", (event) => {
118308
118310
  const seat = event.target.closest("[data-lobby-seat]");
package/dist/index.html CHANGED
@@ -64,17 +64,14 @@
64
64
  <div class="brand-title">pokertools-arena</div>
65
65
  </div>
66
66
  </div>
67
+ <a id="repoLink" class="repo-link" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg></a>
67
68
  <div class="topbar-actions">
68
69
  <button id="startTopBtn" class="button primary top-action" type="button" title="Start tournament"><span class="action-icon">▶</span><span class="action-label">Start</span></button>
69
- <button id="seatsBtn" class="button ghost top-action" type="button" title="Edit seats"><span class="action-icon">♟</span><span class="action-label">Seats</span></button>
70
- <button id="setupBtn" class="button ghost top-action" type="button" title="Connections and tournament settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
71
- <button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
72
- <button id="soundBtn" class="button ghost top-action sound-button" type="button" aria-pressed="false" title="Enable table sounds"><span class="action-icon">♪</span><span class="action-label">Sound</span></button>
73
- <button id="recordBtn" class="button ghost top-action record-button" type="button" aria-pressed="false" title="Record only the poker table"><span class="action-icon">●</span><span class="action-label">Record</span></button>
74
- <button id="exportBtn" class="button ghost top-action" type="button" disabled title="Export tournament log"><span class="action-icon">↓</span><span class="action-label">Log</span></button>
70
+ <button id="seatsBtn" class="button ghost top-action hidden" type="button" title="Restart with a new tournament"><span class="action-icon">↻</span><span class="action-label">Restart</span></button>
75
71
  <button id="pauseBtn" class="button ghost top-action hidden" type="button"><span class="action-icon">Ⅱ</span><span class="action-label">Pause</span></button>
76
72
  <button id="stopBtn" class="button danger top-action hidden" type="button"><span class="action-icon">■</span><span class="action-label">Stop</span></button>
77
- <a id="repoLink" class="button ghost top-action repo-action" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="action-icon repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg><span class="action-label">GitHub</span></a>
73
+ <button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
74
+ <button id="setupBtn" class="button ghost top-action icon-only-action" type="button" title="Connections and tournament settings" aria-label="Settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
78
75
  </div>
79
76
  </header>
80
77
 
@@ -83,6 +80,10 @@
83
80
  <div class="arena-head">
84
81
  <div class="arena-status"><span id="statusDot" class="status-dot"></span><span id="statusLabel">IDLE</span></div>
85
82
  <div id="tournamentMeta" class="mono muted">Seat 2–10 models, then press Start</div>
83
+ <div class="arena-head-actions">
84
+ <button id="soundBtn" class="arena-icon-button sound-button" type="button" aria-pressed="false" title="Enable table sounds" aria-label="Enable table sounds"><span class="action-icon">♪</span></button>
85
+ <button id="recordBtn" class="arena-icon-button record-button" type="button" aria-pressed="false" title="Record only the poker table" aria-label="Record only the poker table"><span class="action-icon">●</span></button>
86
+ </div>
86
87
  </div>
87
88
 
88
89
  <div id="winnerBanner" class="winner-banner hidden" role="status" aria-live="polite"></div>
@@ -137,7 +138,12 @@
137
138
  <section class="panel-block"><div class="panel-title">Recent decisions</div><div id="decisionFeed" class="decision-feed"></div></section>
138
139
  </div>
139
140
 
140
- <div id="tab-log" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Tournament events</div><div id="eventLog" class="event-log mono"></div></section></div>
141
+ <div id="tab-log" class="tab-panel">
142
+ <section class="panel-block grow">
143
+ <div class="panel-title-row"><div class="panel-title">Tournament events</div><button id="exportBtn" class="button ghost small-button" type="button" disabled title="Download the tournament log as JSONL"><span class="action-icon">↓</span><span class="action-label">Download</span></button></div>
144
+ <div id="eventLog" class="event-log mono"></div>
145
+ </section>
146
+ </div>
141
147
  <div id="tab-stats" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Models</div><div id="statsGrid" class="stats-grid"></div></section></div>
142
148
  </aside>
143
149
  </main>
@@ -2344,6 +2344,67 @@ a.button{text-decoration:none}
2344
2344
  /* 0.4.0 — methodology, reproducibility and repository-organization release.
2345
2345
  Browser application source moved under src/; visual behavior is unchanged. */
2346
2346
 
2347
+ /* 0.4.8 — compact header, table-card parity, quieter chrome.
2348
+
2349
+ Header: brand left, a backgroundless GitHub mark, then actions right. The
2350
+ board uses the same rank-plus-center-suit card language as the hole cards on
2351
+ small screens instead of the crowded duplicated corners. */
2352
+ .topbar{flex-wrap:nowrap}
2353
+ .brandbar{flex:0 1 auto}
2354
+ .repo-link{display:inline-grid;place-items:center;width:26px;height:26px;margin-left:6px;border:0;border-radius:7px;background:none;color:var(--muted);flex:0 0 auto;transition:color .15s ease,opacity .15s ease}
2355
+ .repo-link .repo-icon{width:17px;height:17px;opacity:.9}
2356
+ .repo-link:hover{color:var(--text)}
2357
+ .repo-link:hover .repo-icon{opacity:1}
2358
+ .topbar-actions{margin-left:auto;flex:0 1 auto}
2359
+ /* Settings is an icon-only control: no label, no wasted width. */
2360
+ .icon-only-action{padding:0;width:30px;min-width:30px;justify-content:center}
2361
+ .icon-only-action .action-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
2362
+
2363
+ /* Sound and recording live with the table status they affect. */
2364
+ .arena-head{gap:10px}
2365
+ .arena-head-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto}
2366
+ .arena-icon-button{display:inline-grid;place-items:center;width:24px;height:24px;min-width:24px;padding:0;border:1px solid var(--modal-border,rgba(232,238,243,.09));border-radius:7px;background:rgba(255,255,255,.02);color:#8d98a2;font-size:10px;cursor:pointer;transition:color .14s ease,border-color .14s ease,background .14s ease}
2367
+ .arena-icon-button:hover{color:#d8e0e5;border-color:rgba(146,201,164,.3)}
2368
+ .arena-icon-button.active{color:var(--accent);border-color:rgba(114,185,138,.35);background:rgba(114,185,138,.08)}
2369
+ .arena-icon-button.record-button.active{color:var(--danger);border-color:rgba(200,94,105,.4);background:rgba(200,94,105,.1)}
2370
+ .arena-icon-button:disabled{opacity:.42;cursor:not-allowed}
2371
+ .arena-head-actions .action-icon{font-size:10px;opacity:1}
2372
+
2373
+ /* Download lives inside the Log tab it produces. */
2374
+ .panel-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px}
2375
+ .panel-title-row .panel-title{margin-bottom:0}
2376
+ .panel-title-row .button{flex:0 0 auto}
2377
+
2378
+ /* The bank clock leads the right-hand clock block instead of trailing it. */
2379
+ .clock-wrap{align-items:flex-start;text-align:left}
2380
+ #bankClock{order:-1;color:var(--muted);font-size:9px;line-height:1.2}
2381
+
2382
+ /* Board cards: rank + big centre suit, no duplicated corners. This is the
2383
+ default at small widths and every compact/tight/micro density, so table and
2384
+ hole cards read the same way. */
2385
+ @media(max-width:720px){
2386
+ .board .card .card-corner.bottom,
2387
+ .poker-table[data-density="compact"] .board .card .card-corner.bottom,
2388
+ .poker-table[data-density="tight"] .board .card .card-corner.bottom{display:none!important}
2389
+ .board .card .card-corner i,
2390
+ .poker-table[data-density="compact"] .board .card .card-corner i,
2391
+ .poker-table[data-density="tight"] .board .card .card-corner i{display:none!important}
2392
+ .board .card .card-corner,
2393
+ .poker-table[data-density="compact"] .board .card .card-corner,
2394
+ .poker-table[data-density="tight"] .board .card .card-corner{left:3px!important;top:3px!important;line-height:1!important}
2395
+ .board .card .card-corner b,
2396
+ .poker-table[data-density="compact"] .board .card .card-corner b,
2397
+ .poker-table[data-density="tight"] .board .card .card-corner b{font-size:10px!important}
2398
+ .board .card .card-suit,
2399
+ .poker-table[data-density="compact"] .board .card .card-suit,
2400
+ .poker-table[data-density="tight"] .board .card .card-suit{font-size:17px!important}
2401
+ }
2402
+ .poker-table[data-density="tight"] .board .card .card-suit{font-size:16px!important}
2403
+ .poker-table[data-density="micro"] .board .card .card-corner i,
2404
+ .poker-table[data-density="micro"] .board .card .card-corner.bottom{display:none!important}
2405
+ .poker-table[data-density="micro"] .board .card .card-suit{font-size:13px!important}
2406
+
2407
+
2347
2408
  </style>
2348
2409
  </head>
2349
2410
  <body>
@@ -2355,17 +2416,14 @@ a.button{text-decoration:none}
2355
2416
  <div class="brand-title">pokertools-arena</div>
2356
2417
  </div>
2357
2418
  </div>
2419
+ <a id="repoLink" class="repo-link" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg></a>
2358
2420
  <div class="topbar-actions">
2359
2421
  <button id="startTopBtn" class="button primary top-action" type="button" title="Start tournament"><span class="action-icon">▶</span><span class="action-label">Start</span></button>
2360
- <button id="seatsBtn" class="button ghost top-action" type="button" title="Edit seats"><span class="action-icon">♟</span><span class="action-label">Seats</span></button>
2361
- <button id="setupBtn" class="button ghost top-action" type="button" title="Connections and tournament settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
2362
- <button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
2363
- <button id="soundBtn" class="button ghost top-action sound-button" type="button" aria-pressed="false" title="Enable table sounds"><span class="action-icon">♪</span><span class="action-label">Sound</span></button>
2364
- <button id="recordBtn" class="button ghost top-action record-button" type="button" aria-pressed="false" title="Record only the poker table"><span class="action-icon">●</span><span class="action-label">Record</span></button>
2365
- <button id="exportBtn" class="button ghost top-action" type="button" disabled title="Export tournament log"><span class="action-icon">↓</span><span class="action-label">Log</span></button>
2422
+ <button id="seatsBtn" class="button ghost top-action hidden" type="button" title="Restart with a new tournament"><span class="action-icon">↻</span><span class="action-label">Restart</span></button>
2366
2423
  <button id="pauseBtn" class="button ghost top-action hidden" type="button"><span class="action-icon">Ⅱ</span><span class="action-label">Pause</span></button>
2367
2424
  <button id="stopBtn" class="button danger top-action hidden" type="button"><span class="action-icon">■</span><span class="action-label">Stop</span></button>
2368
- <a id="repoLink" class="button ghost top-action repo-action" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="action-icon repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg><span class="action-label">GitHub</span></a>
2425
+ <button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
2426
+ <button id="setupBtn" class="button ghost top-action icon-only-action" type="button" title="Connections and tournament settings" aria-label="Settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
2369
2427
  </div>
2370
2428
  </header>
2371
2429
 
@@ -2374,6 +2432,10 @@ a.button{text-decoration:none}
2374
2432
  <div class="arena-head">
2375
2433
  <div class="arena-status"><span id="statusDot" class="status-dot"></span><span id="statusLabel">IDLE</span></div>
2376
2434
  <div id="tournamentMeta" class="mono muted">Seat 2–10 models, then press Start</div>
2435
+ <div class="arena-head-actions">
2436
+ <button id="soundBtn" class="arena-icon-button sound-button" type="button" aria-pressed="false" title="Enable table sounds" aria-label="Enable table sounds"><span class="action-icon">♪</span></button>
2437
+ <button id="recordBtn" class="arena-icon-button record-button" type="button" aria-pressed="false" title="Record only the poker table" aria-label="Record only the poker table"><span class="action-icon">●</span></button>
2438
+ </div>
2377
2439
  </div>
2378
2440
 
2379
2441
  <div id="winnerBanner" class="winner-banner hidden" role="status" aria-live="polite"></div>
@@ -2428,7 +2490,12 @@ a.button{text-decoration:none}
2428
2490
  <section class="panel-block"><div class="panel-title">Recent decisions</div><div id="decisionFeed" class="decision-feed"></div></section>
2429
2491
  </div>
2430
2492
 
2431
- <div id="tab-log" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Tournament events</div><div id="eventLog" class="event-log mono"></div></section></div>
2493
+ <div id="tab-log" class="tab-panel">
2494
+ <section class="panel-block grow">
2495
+ <div class="panel-title-row"><div class="panel-title">Tournament events</div><button id="exportBtn" class="button ghost small-button" type="button" disabled title="Download the tournament log as JSONL"><span class="action-icon">↓</span><span class="action-label">Download</span></button></div>
2496
+ <div id="eventLog" class="event-log mono"></div>
2497
+ </section>
2498
+ </div>
2432
2499
  <div id="tab-stats" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Models</div><div id="statsGrid" class="stats-grid"></div></section></div>
2433
2500
  </aside>
2434
2501
  </main>
@@ -118412,6 +118479,7 @@ function animateBoardCards(previousCount, currentCount) {
118412
118479
  }
118413
118480
  function processVisualEffects(s) {
118414
118481
  const events = s?.events || [];
118482
+ const previous = lastVisualState;
118415
118483
  if (["STOPPED", "ERROR"].includes(s?.status)) {
118416
118484
  if (events.length) lastProcessedEventId = events.at(-1).id;
118417
118485
  lastVisualState = s;
@@ -118423,7 +118491,6 @@ function processVisualEffects(s) {
118423
118491
  lastVisualState = s;
118424
118492
  return;
118425
118493
  }
118426
- const previous = lastVisualState;
118427
118494
  if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
118428
118495
  const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
118429
118496
  if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
@@ -118449,12 +118516,14 @@ function processVisualEffects(s) {
118449
118516
  }
118450
118517
  showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} \xB7 ${e.action?.description || type}`, type);
118451
118518
  }
118452
- if (e.type === "HAND_END" && previous?.table) {
118453
- const before = new Map((previous.table.players || []).filter(Boolean).map((p) => [p.id, Number(p.stack || 0)]));
118454
- const winners = (s?.table?.players || []).filter((p) => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0)));
118519
+ if (e.type === "HAND_END") {
118520
+ const before = new Map((previous?.table?.players || []).filter(Boolean).map((p) => [p.id, Number(p.stack || 0)]));
118521
+ const payloadIds = (e.winners || []).map((w) => w?.playerId ?? w?.id ?? w).filter(Boolean);
118522
+ const fromStacks = (s?.table?.players || []).filter((p) => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0))).map((p) => p.id);
118523
+ const winnerIds = [.../* @__PURE__ */ new Set([...payloadIds, ...fromStacks])];
118455
118524
  const pot = $(".hud-pot", document);
118456
- winners.forEach((p, i) => setTimeout(() => flyChips(pot, seatEl(p.id), 5, true), i * 130));
118457
- if (winners.length) playTableSound("winner");
118525
+ winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
118526
+ if (winnerIds.length) playTableSound("winner");
118458
118527
  }
118459
118528
  if (e.type === "TOURNAMENT_END") playTableSound("winner");
118460
118529
  }
@@ -119012,6 +119081,7 @@ var TournamentDirector = class {
119012
119081
  await this.waitIfPaused();
119013
119082
  this.pruneBustedSeats();
119014
119083
  this.handNumber++;
119084
+ for (const stat of Object.values(this.stats)) stat.lastAction = null;
119015
119085
  this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map((p) => [p.id, playerStack(p)]));
119016
119086
  try {
119017
119087
  this.engine.deal();
@@ -119851,7 +119921,7 @@ function renderStatus(s) {
119851
119921
  els.pauseBtn.classList.toggle("hidden", !running);
119852
119922
  els.stopBtn.classList.toggle("hidden", !running);
119853
119923
  els.startTopBtn.classList.toggle("hidden", running);
119854
- els.seatsBtn.classList.toggle("hidden", running);
119924
+ els.seatsBtn.classList.toggle("hidden", running || !["STOPPED", "FINISHED", "ERROR"].includes(status));
119855
119925
  els.setupBtn.disabled = running;
119856
119926
  els.testsBtn.disabled = running;
119857
119927
  const pauseLabel = $(".action-label", els.pauseBtn);
@@ -120976,8 +121046,7 @@ els.exportBtn.addEventListener("click", () => {
120976
121046
  });
120977
121047
  els.seatsBtn.addEventListener("click", () => {
120978
121048
  if (director && ["RUNNING", "PAUSED"].includes(director.status)) return;
120979
- lobbyVisible = true;
120980
- render(currentState || { status: "IDLE", events: [] });
121049
+ void startConfiguredTournament();
120981
121050
  });
120982
121051
  els.seatsLayer.addEventListener("click", (event) => {
120983
121052
  const seat = event.target.closest("[data-lobby-seat]");
package/dist/styles.css CHANGED
@@ -2287,3 +2287,64 @@ a.button{text-decoration:none}
2287
2287
 
2288
2288
  /* 0.4.0 — methodology, reproducibility and repository-organization release.
2289
2289
  Browser application source moved under src/; visual behavior is unchanged. */
2290
+
2291
+ /* 0.4.8 — compact header, table-card parity, quieter chrome.
2292
+
2293
+ Header: brand left, a backgroundless GitHub mark, then actions right. The
2294
+ board uses the same rank-plus-center-suit card language as the hole cards on
2295
+ small screens instead of the crowded duplicated corners. */
2296
+ .topbar{flex-wrap:nowrap}
2297
+ .brandbar{flex:0 1 auto}
2298
+ .repo-link{display:inline-grid;place-items:center;width:26px;height:26px;margin-left:6px;border:0;border-radius:7px;background:none;color:var(--muted);flex:0 0 auto;transition:color .15s ease,opacity .15s ease}
2299
+ .repo-link .repo-icon{width:17px;height:17px;opacity:.9}
2300
+ .repo-link:hover{color:var(--text)}
2301
+ .repo-link:hover .repo-icon{opacity:1}
2302
+ .topbar-actions{margin-left:auto;flex:0 1 auto}
2303
+ /* Settings is an icon-only control: no label, no wasted width. */
2304
+ .icon-only-action{padding:0;width:30px;min-width:30px;justify-content:center}
2305
+ .icon-only-action .action-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
2306
+
2307
+ /* Sound and recording live with the table status they affect. */
2308
+ .arena-head{gap:10px}
2309
+ .arena-head-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto}
2310
+ .arena-icon-button{display:inline-grid;place-items:center;width:24px;height:24px;min-width:24px;padding:0;border:1px solid var(--modal-border,rgba(232,238,243,.09));border-radius:7px;background:rgba(255,255,255,.02);color:#8d98a2;font-size:10px;cursor:pointer;transition:color .14s ease,border-color .14s ease,background .14s ease}
2311
+ .arena-icon-button:hover{color:#d8e0e5;border-color:rgba(146,201,164,.3)}
2312
+ .arena-icon-button.active{color:var(--accent);border-color:rgba(114,185,138,.35);background:rgba(114,185,138,.08)}
2313
+ .arena-icon-button.record-button.active{color:var(--danger);border-color:rgba(200,94,105,.4);background:rgba(200,94,105,.1)}
2314
+ .arena-icon-button:disabled{opacity:.42;cursor:not-allowed}
2315
+ .arena-head-actions .action-icon{font-size:10px;opacity:1}
2316
+
2317
+ /* Download lives inside the Log tab it produces. */
2318
+ .panel-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px}
2319
+ .panel-title-row .panel-title{margin-bottom:0}
2320
+ .panel-title-row .button{flex:0 0 auto}
2321
+
2322
+ /* The bank clock leads the right-hand clock block instead of trailing it. */
2323
+ .clock-wrap{align-items:flex-start;text-align:left}
2324
+ #bankClock{order:-1;color:var(--muted);font-size:9px;line-height:1.2}
2325
+
2326
+ /* Board cards: rank + big centre suit, no duplicated corners. This is the
2327
+ default at small widths and every compact/tight/micro density, so table and
2328
+ hole cards read the same way. */
2329
+ @media(max-width:720px){
2330
+ .board .card .card-corner.bottom,
2331
+ .poker-table[data-density="compact"] .board .card .card-corner.bottom,
2332
+ .poker-table[data-density="tight"] .board .card .card-corner.bottom{display:none!important}
2333
+ .board .card .card-corner i,
2334
+ .poker-table[data-density="compact"] .board .card .card-corner i,
2335
+ .poker-table[data-density="tight"] .board .card .card-corner i{display:none!important}
2336
+ .board .card .card-corner,
2337
+ .poker-table[data-density="compact"] .board .card .card-corner,
2338
+ .poker-table[data-density="tight"] .board .card .card-corner{left:3px!important;top:3px!important;line-height:1!important}
2339
+ .board .card .card-corner b,
2340
+ .poker-table[data-density="compact"] .board .card .card-corner b,
2341
+ .poker-table[data-density="tight"] .board .card .card-corner b{font-size:10px!important}
2342
+ .board .card .card-suit,
2343
+ .poker-table[data-density="compact"] .board .card .card-suit,
2344
+ .poker-table[data-density="tight"] .board .card .card-suit{font-size:17px!important}
2345
+ }
2346
+ .poker-table[data-density="tight"] .board .card .card-suit{font-size:16px!important}
2347
+ .poker-table[data-density="micro"] .board .card .card-corner i,
2348
+ .poker-table[data-density="micro"] .board .card .card-corner.bottom{display:none!important}
2349
+ .poker-table[data-density="micro"] .board .card .card-suit{font-size:13px!important}
2350
+
@@ -1,4 +1,4 @@
1
- # pokertools-arena 0.4.5 — benchmark methodology & results
1
+ # pokertools-arena 0.4.8 — benchmark methodology & results
2
2
 
3
3
  Generated: 2026-09-19T18:19:58.352Z
4
4
  Methodology: paired fixed-state corpus; deterministic interleaving with recorded experiment seed.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.4.5",
3
+ "version": "0.4.8",
4
4
  "generatedAt": "2026-09-19T18:19:58.352Z",
5
5
  "experiments": [
6
6
  "paired"
@@ -1,5 +1,36 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.4.8 — Maintenance
4
+
5
+ - **Compact header.** Left: the logo, `pokertools-arena`, and a backgroundless
6
+ GitHub mark. Right: Start / Restart / Pause / Stop, Tests, and an icon-only
7
+ Settings control. The `♟ Seats` button became **Restart**, shown only after a
8
+ run has started and stopped or finished.
9
+ - **Sound and recording** moved into the table header (`arena-head`) beside the
10
+ status they affect; **log download** moved into the `Log` tab that produces it.
11
+ - **Bank clock** now leads the right-hand clock block instead of trailing it.
12
+ - **Board cards on small screens** use the same rank-plus-center-suit language as
13
+ the hole cards: the duplicated bottom-right corner and inline suit glyph are
14
+ dropped at narrow widths and at compact/tight/micro densities.
15
+
16
+ ## 0.4.7 — Maintenance
17
+
18
+ - **Pot pays out to the winner.** The pot→winner chip animation never fired.
19
+ `processVisualEffects` read `lastVisualState` *after* `animateNewHand` and
20
+ `animateBoardCards` had already advanced it, so at a hand boundary the
21
+ `HAND_END` stack diff compared a state against itself and matched no winners.
22
+ The previous state is now snapshotted before any helper runs, and the payout
23
+ is driven by the `HAND_END` winner payload (with the stack diff as fallback),
24
+ so chips fly from the pot to every winner and the win sound plays.
25
+
26
+ ## 0.4.6 — Maintenance
27
+
28
+ - **Seat cards clear their last action each hand.** `stats.lastAction` was only
29
+ ever written, never reset, so every player card kept showing the previous
30
+ hand's action into the next deal. `startHand` now clears the last action for
31
+ every seat. Eliminations still display, because they are derived from the
32
+ elimination list at render time rather than stored as `lastAction`.
33
+
3
34
  ## 0.4.5 — Maintenance
4
35
 
5
36
  - **Stop now freezes the display.** Stopping a tournament clears the decision
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pokertools-arena",
3
- "version": "0.4.5",
3
+ "version": "0.4.8",
4
4
  "description": "Browser-first AI poker benchmark powered by @pokertools/engine with generic OpenAI-compatible endpoints, optional OpenRouter/Jev Decisions routing, deterministic hierarchical decisions, paired fixed-state methodology, and reproducible reporting.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -180,6 +180,11 @@ function animateBoardCards(previousCount, currentCount) {
180
180
  }
181
181
  function processVisualEffects(s) {
182
182
  const events = s?.events || [];
183
+ // Snapshot the previous state BEFORE any helper runs. animateNewHand and
184
+ // animateBoardCards advance lastVisualState internally, so reading it later
185
+ // made the HAND_END winner diff compare a state against itself and the
186
+ // pot→winner payout animation never fired.
187
+ const previous = lastVisualState;
183
188
  // Once a run is stopped (or errored) no further effects may be scheduled.
184
189
  // Without this, already-live chip flies, flashes and sounds continued after
185
190
  // Stop, and an in-flight broadcast could replay the last decision's effects.
@@ -194,7 +199,6 @@ function processVisualEffects(s) {
194
199
  lastVisualState = s;
195
200
  return;
196
201
  }
197
- const previous = lastVisualState;
198
202
  if (s?.table && (!previous?.table || s.table.handNumber !== previous.table.handNumber)) animateNewHand(s);
199
203
  const prevBoard = previous?.table?.board?.length || 0, nextBoard = s?.table?.board?.length || 0;
200
204
  if (s?.table?.handNumber === previous?.table?.handNumber) animateBoardCards(prevBoard, nextBoard);
@@ -210,11 +214,19 @@ function processVisualEffects(s) {
210
214
  if (type === ACTION.CHECK && seat) { seat.classList.add('check-flash'); setTimeout(() => seat.classList.remove('check-flash'), 500); playTableSound('check'); }
211
215
  showActionToast(`${displayModelName(e.configuredModel || e.resolvedModel || e.playerName)} · ${e.action?.description || type}`, type);
212
216
  }
213
- if (e.type === 'HAND_END' && previous?.table) {
214
- const before = new Map((previous.table.players || []).filter(Boolean).map(p => [p.id, Number(p.stack || 0)]));
215
- const winners = (s?.table?.players || []).filter(p => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0)));
216
- const pot = $('.hud-pot', document); winners.forEach((p, i) => setTimeout(() => flyChips(pot, seatEl(p.id), 5, true), i * 130));
217
- if (winners.length) playTableSound('winner');
217
+ if (e.type === 'HAND_END') {
218
+ // Prefer the event payload: `previous` can be missing on a resumed or
219
+ // re-rendered state, so the stack diff is only a fallback. Chips fly back
220
+ // from the pot to every winner at showdown, and the hand is announced.
221
+ const before = new Map((previous?.table?.players || []).filter(Boolean).map(p => [p.id, Number(p.stack || 0)]));
222
+ const payloadIds = (e.winners || []).map(w => w?.playerId ?? w?.id ?? w).filter(Boolean);
223
+ const fromStacks = (s?.table?.players || [])
224
+ .filter(p => p && Number(p.stack || 0) > (before.get(p.id) ?? Number(p.stack || 0)))
225
+ .map(p => p.id);
226
+ const winnerIds = [...new Set([...payloadIds, ...fromStacks])];
227
+ const pot = $('.hud-pot', document);
228
+ winnerIds.forEach((playerId, i) => setTimeout(() => flyChips(pot, seatEl(playerId), 5, true), i * 130));
229
+ if (winnerIds.length) playTableSound('winner');
218
230
  }
219
231
  if (e.type === 'TOURNAMENT_END') playTableSound('winner');
220
232
  }
@@ -642,6 +654,10 @@ class TournamentDirector {
642
654
  }
643
655
  async startHand() {
644
656
  await this.waitIfPaused(); this.pruneBustedSeats(); this.handNumber++;
657
+ // A new hand clears every seat's "last action" so a card never shows what a
658
+ // player did in the previous hand. Elimination stamps are presentation-only
659
+ // and are applied in renderTable, so they survive this reset.
660
+ for (const stat of Object.values(this.stats)) stat.lastAction = null;
645
661
  this.handStartStacks = Object.fromEntries((this.engine?.state?.players ?? []).filter(Boolean).map(p => [p.id, playerStack(p)]));
646
662
  try { this.engine.deal(); }
647
663
  catch {
@@ -1384,7 +1400,9 @@ function renderStatus(s) {
1384
1400
  els.pauseBtn.classList.toggle('hidden', !running);
1385
1401
  els.stopBtn.classList.toggle('hidden', !running);
1386
1402
  els.startTopBtn.classList.toggle('hidden', running);
1387
- els.seatsBtn.classList.toggle('hidden', running);
1403
+ // Restart is offered only after a run has actually started and stopped or
1404
+ // finished; before the first Start the primary action is Start itself.
1405
+ els.seatsBtn.classList.toggle('hidden', running || !['STOPPED', 'FINISHED', 'ERROR'].includes(status));
1388
1406
  els.setupBtn.disabled = running;
1389
1407
  els.testsBtn.disabled = running;
1390
1408
  const pauseLabel = $('.action-label', els.pauseBtn);
@@ -2356,9 +2374,10 @@ els.stopBtn.addEventListener('click', () => {
2356
2374
  });
2357
2375
  els.exportBtn.addEventListener('click', () => { if (!director?.events?.length) return; downloadText(`${director.config?.id || 'pokertools-arena'}.jsonl`, director.exportJsonl()); });
2358
2376
  els.seatsBtn.addEventListener('click', () => {
2377
+ // This button only appears once a run has stopped or finished, where it
2378
+ // restarts the tournament from the current seats.
2359
2379
  if (director && ['RUNNING', 'PAUSED'].includes(director.status)) return;
2360
- lobbyVisible = true;
2361
- render(currentState || { status: 'IDLE', events: [] });
2380
+ void startConfiguredTournament();
2362
2381
  });
2363
2382
  els.seatsLayer.addEventListener('click', event => {
2364
2383
  const seat = event.target.closest('[data-lobby-seat]');
package/src/index.html CHANGED
@@ -64,17 +64,14 @@
64
64
  <div class="brand-title">pokertools-arena</div>
65
65
  </div>
66
66
  </div>
67
+ <a id="repoLink" class="repo-link" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg></a>
67
68
  <div class="topbar-actions">
68
69
  <button id="startTopBtn" class="button primary top-action" type="button" title="Start tournament"><span class="action-icon">▶</span><span class="action-label">Start</span></button>
69
- <button id="seatsBtn" class="button ghost top-action" type="button" title="Edit seats"><span class="action-icon">♟</span><span class="action-label">Seats</span></button>
70
- <button id="setupBtn" class="button ghost top-action" type="button" title="Connections and tournament settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
71
- <button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
72
- <button id="soundBtn" class="button ghost top-action sound-button" type="button" aria-pressed="false" title="Enable table sounds"><span class="action-icon">♪</span><span class="action-label">Sound</span></button>
73
- <button id="recordBtn" class="button ghost top-action record-button" type="button" aria-pressed="false" title="Record only the poker table"><span class="action-icon">●</span><span class="action-label">Record</span></button>
74
- <button id="exportBtn" class="button ghost top-action" type="button" disabled title="Export tournament log"><span class="action-icon">↓</span><span class="action-label">Log</span></button>
70
+ <button id="seatsBtn" class="button ghost top-action hidden" type="button" title="Restart with a new tournament"><span class="action-icon">↻</span><span class="action-label">Restart</span></button>
75
71
  <button id="pauseBtn" class="button ghost top-action hidden" type="button"><span class="action-icon">Ⅱ</span><span class="action-label">Pause</span></button>
76
72
  <button id="stopBtn" class="button danger top-action hidden" type="button"><span class="action-icon">■</span><span class="action-label">Stop</span></button>
77
- <a id="repoLink" class="button ghost top-action repo-action" href="https://github.com/pokertools-arena/pokertools-arena.github.io" target="_blank" rel="noopener noreferrer" title="View source on GitHub" aria-label="View source on GitHub"><svg class="action-icon repo-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg><span class="action-label">GitHub</span></a>
73
+ <button id="testsBtn" class="button ghost top-action" type="button" title="Run the 10-spot decision sanity suite"><span class="action-icon">◇</span><span class="action-label">Tests</span></button>
74
+ <button id="setupBtn" class="button ghost top-action icon-only-action" type="button" title="Connections and tournament settings" aria-label="Settings"><span class="action-icon">⚙</span><span class="action-label">Settings</span></button>
78
75
  </div>
79
76
  </header>
80
77
 
@@ -83,6 +80,10 @@
83
80
  <div class="arena-head">
84
81
  <div class="arena-status"><span id="statusDot" class="status-dot"></span><span id="statusLabel">IDLE</span></div>
85
82
  <div id="tournamentMeta" class="mono muted">Seat 2–10 models, then press Start</div>
83
+ <div class="arena-head-actions">
84
+ <button id="soundBtn" class="arena-icon-button sound-button" type="button" aria-pressed="false" title="Enable table sounds" aria-label="Enable table sounds"><span class="action-icon">♪</span></button>
85
+ <button id="recordBtn" class="arena-icon-button record-button" type="button" aria-pressed="false" title="Record only the poker table" aria-label="Record only the poker table"><span class="action-icon">●</span></button>
86
+ </div>
86
87
  </div>
87
88
 
88
89
  <div id="winnerBanner" class="winner-banner hidden" role="status" aria-live="polite"></div>
@@ -137,7 +138,12 @@
137
138
  <section class="panel-block"><div class="panel-title">Recent decisions</div><div id="decisionFeed" class="decision-feed"></div></section>
138
139
  </div>
139
140
 
140
- <div id="tab-log" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Tournament events</div><div id="eventLog" class="event-log mono"></div></section></div>
141
+ <div id="tab-log" class="tab-panel">
142
+ <section class="panel-block grow">
143
+ <div class="panel-title-row"><div class="panel-title">Tournament events</div><button id="exportBtn" class="button ghost small-button" type="button" disabled title="Download the tournament log as JSONL"><span class="action-icon">↓</span><span class="action-label">Download</span></button></div>
144
+ <div id="eventLog" class="event-log mono"></div>
145
+ </section>
146
+ </div>
141
147
  <div id="tab-stats" class="tab-panel"><section class="panel-block grow"><div class="panel-title">Models</div><div id="statsGrid" class="stats-grid"></div></section></div>
142
148
  </aside>
143
149
  </main>
package/src/styles.css CHANGED
@@ -2287,3 +2287,64 @@ a.button{text-decoration:none}
2287
2287
 
2288
2288
  /* 0.4.0 — methodology, reproducibility and repository-organization release.
2289
2289
  Browser application source moved under src/; visual behavior is unchanged. */
2290
+
2291
+ /* 0.4.8 — compact header, table-card parity, quieter chrome.
2292
+
2293
+ Header: brand left, a backgroundless GitHub mark, then actions right. The
2294
+ board uses the same rank-plus-center-suit card language as the hole cards on
2295
+ small screens instead of the crowded duplicated corners. */
2296
+ .topbar{flex-wrap:nowrap}
2297
+ .brandbar{flex:0 1 auto}
2298
+ .repo-link{display:inline-grid;place-items:center;width:26px;height:26px;margin-left:6px;border:0;border-radius:7px;background:none;color:var(--muted);flex:0 0 auto;transition:color .15s ease,opacity .15s ease}
2299
+ .repo-link .repo-icon{width:17px;height:17px;opacity:.9}
2300
+ .repo-link:hover{color:var(--text)}
2301
+ .repo-link:hover .repo-icon{opacity:1}
2302
+ .topbar-actions{margin-left:auto;flex:0 1 auto}
2303
+ /* Settings is an icon-only control: no label, no wasted width. */
2304
+ .icon-only-action{padding:0;width:30px;min-width:30px;justify-content:center}
2305
+ .icon-only-action .action-label{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
2306
+
2307
+ /* Sound and recording live with the table status they affect. */
2308
+ .arena-head{gap:10px}
2309
+ .arena-head-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto;margin-left:auto}
2310
+ .arena-icon-button{display:inline-grid;place-items:center;width:24px;height:24px;min-width:24px;padding:0;border:1px solid var(--modal-border,rgba(232,238,243,.09));border-radius:7px;background:rgba(255,255,255,.02);color:#8d98a2;font-size:10px;cursor:pointer;transition:color .14s ease,border-color .14s ease,background .14s ease}
2311
+ .arena-icon-button:hover{color:#d8e0e5;border-color:rgba(146,201,164,.3)}
2312
+ .arena-icon-button.active{color:var(--accent);border-color:rgba(114,185,138,.35);background:rgba(114,185,138,.08)}
2313
+ .arena-icon-button.record-button.active{color:var(--danger);border-color:rgba(200,94,105,.4);background:rgba(200,94,105,.1)}
2314
+ .arena-icon-button:disabled{opacity:.42;cursor:not-allowed}
2315
+ .arena-head-actions .action-icon{font-size:10px;opacity:1}
2316
+
2317
+ /* Download lives inside the Log tab it produces. */
2318
+ .panel-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:11px}
2319
+ .panel-title-row .panel-title{margin-bottom:0}
2320
+ .panel-title-row .button{flex:0 0 auto}
2321
+
2322
+ /* The bank clock leads the right-hand clock block instead of trailing it. */
2323
+ .clock-wrap{align-items:flex-start;text-align:left}
2324
+ #bankClock{order:-1;color:var(--muted);font-size:9px;line-height:1.2}
2325
+
2326
+ /* Board cards: rank + big centre suit, no duplicated corners. This is the
2327
+ default at small widths and every compact/tight/micro density, so table and
2328
+ hole cards read the same way. */
2329
+ @media(max-width:720px){
2330
+ .board .card .card-corner.bottom,
2331
+ .poker-table[data-density="compact"] .board .card .card-corner.bottom,
2332
+ .poker-table[data-density="tight"] .board .card .card-corner.bottom{display:none!important}
2333
+ .board .card .card-corner i,
2334
+ .poker-table[data-density="compact"] .board .card .card-corner i,
2335
+ .poker-table[data-density="tight"] .board .card .card-corner i{display:none!important}
2336
+ .board .card .card-corner,
2337
+ .poker-table[data-density="compact"] .board .card .card-corner,
2338
+ .poker-table[data-density="tight"] .board .card .card-corner{left:3px!important;top:3px!important;line-height:1!important}
2339
+ .board .card .card-corner b,
2340
+ .poker-table[data-density="compact"] .board .card .card-corner b,
2341
+ .poker-table[data-density="tight"] .board .card .card-corner b{font-size:10px!important}
2342
+ .board .card .card-suit,
2343
+ .poker-table[data-density="compact"] .board .card .card-suit,
2344
+ .poker-table[data-density="tight"] .board .card .card-suit{font-size:17px!important}
2345
+ }
2346
+ .poker-table[data-density="tight"] .board .card .card-suit{font-size:16px!important}
2347
+ .poker-table[data-density="micro"] .board .card .card-corner i,
2348
+ .poker-table[data-density="micro"] .board .card .card-corner.bottom{display:none!important}
2349
+ .poker-table[data-density="micro"] .board .card .card-suit{font-size:13px!important}
2350
+