diff --git a/.planning/phases/01-tournament-engine/01-RESEARCH.md b/.planning/phases/01-tournament-engine/01-RESEARCH.md
new file mode 100644
index 0000000..b075882
--- /dev/null
+++ b/.planning/phases/01-tournament-engine/01-RESEARCH.md
@@ -0,0 +1,903 @@
+# Phase 1: Tournament Engine - Research
+
+**Researched:** 2026-03-01
+**Domain:** Full-stack tournament management — Go backend (LibSQL, NATS JetStream, WebSocket), SvelteKit frontend (embedded via go:embed), poker domain logic (clock, blinds, financials, seating, ICM)
+**Confidence:** HIGH (core stack verified via official sources and docs)
+
+## Summary
+
+Phase 1 is a greenfield full-stack build: a single Go binary embedding LibSQL (local SQLite fork), NATS JetStream (event durability), and a WebSocket hub, serving a SvelteKit SPA via `go:embed`. The binary runs in an x86 LXC container and provides a touch-friendly Tournament Director (TD) UI for managing a complete poker tournament lifecycle.
+
+The Go backend stack is well-established. LibSQL provides a local-first SQLite-compatible database via `go-libsql` (CGO required, no tagged releases — pin to commit hash). NATS server embeds programmatically with JetStream enabled for append-only event durability (`sync_interval: always` is mandatory for single-node per the December 2025 Jepsen finding). The WebSocket hub uses `coder/websocket` (v1.8.14, formerly nhooyr.io/websocket) for idiomatic Go context support. HTTP routing uses `go-chi/chi` v5 for its lightweight middleware composition. Authentication is bcrypt-hashed PINs producing local JWTs via `golang-jwt/jwt` v5.
+
+The SvelteKit frontend uses Svelte 5 with runes (the new reactivity model replacing stores) and `adapter-static` to prerender a SPA embedded in the Go binary. Catppuccin Mocha provides the dark theme via CSS custom properties from `@catppuccin/palette`. The operator UI follows a mobile-first bottom-tab layout with FAB quick actions, persistent clock header, and 48px touch targets.
+
+The poker domain has several non-trivial algorithmic areas: ICM calculation (factorial complexity, needs Monte Carlo approximation beyond ~15 players), blind structure wizard (backward calculation from target duration to level progression), table balancing (TDA-compliant seat assignment with blind position awareness), and PKO bounty chain tracking (half bounty to hitman, half added to own bounty).
+
+**Primary recommendation:** Build as a monolithic Go binary with clean internal package boundaries (clock, financial, seating, player, audit). Use event-sourced state changes written to both NATS JetStream (for real-time broadcast) and LibSQL (for persistence and query). All financial math uses int64 cents with a CI gate test proving zero-deviation payout sums.
+
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+- **Template-first creation** — TD picks from saved tournament templates, everything pre-fills, tweak what's needed for tonight, hit Start
+- **Templates are compositions of reusable building blocks** — a template is NOT a monolithic config. It references: Chip set, Blind structure, Payout structure, Buy-in config, Points formula (venue-level, reused across seasons)
+- **Local changes by default** — when creating a tournament from a template, the TD gets a copy. Edits only affect that tournament
+- **Dedicated template management area** — create from scratch, duplicate/edit existing, save tournament config as new template
+- **Built-in starter templates** ship with the app (Turbo, Standard, Deep Stack, WSOP-style)
+- **Structure wizard** lives in template management — input player count, starting chips, duration, denominations -> generates a blind structure
+- **Minimum player threshold** — configured in tournament metadata, Start button unavailable until met
+- **Chip bonuses** — configurable per tournament: early signup bonus, punctuality bonus
+- **Late registration soft lock with admin override** — when cutoff hits, registration locks but admin can push through (logged in audit trail)
+- **Payout structure as standalone reusable building block** — entry-count brackets with tiered prizes
+- **Entry count = unique entries only** — not rebuys or add-ons
+- **Prize rounding** — round down to nearest venue-configured denomination (e.g. 50 DKK, EUR 5)
+- **Bubble prize** — fast and prominent, "Add bubble prize" easily accessible, funded by shaving top prizes
+- **Overview tab priority** — Clock > Time to break > Player count > Table balance > Financial summary > Activity feed
+- **Bust-out flow** — tap Bust -> pick table -> pick seat -> verify name -> confirm -> select hitman -> done
+- **PKO (Progressive Knockout)** — bounty transfer part of bust flow (half to hitman, half added to own bounty)
+- **Buy-in flow** — search/select player -> auto-seat -> TD override option -> confirm -> receipt
+- **Multi-tournament switching** — tabs at top (phone) or split view (tablet landscape)
+- **Undo is critical** — bust-outs, rebuys, add-ons, buy-ins can all be undone with full re-ranking
+- **Oval table view (default)** — top-down view with numbered seats, switchable to list view
+- **Balancing is TD-driven, system-assisted** — system alerts, TD requests suggestion, TD announces, assistant reports, two-tap recording
+- **Break Table is fully automatic** — system distributes evenly, TD sees result
+- **No drag-and-drop in Phase 1** — tap-tap flow for all moves
+- **Flexible chop/deal support** — ICM, custom split, partial chop, any number of players
+- **Prize money and league positions are independent**
+- **Tournament auto-closes** when one player remains
+- **Receipts configurable per venue** — off / digital / print / both
+- **Operator is the Tournament Director (TD)** — use this term consistently
+
+### Claude's Discretion
+
+- Loading skeleton and animation design
+- Exact spacing, typography, and component sizing
+- Error state handling and messaging
+- Toast notification behavior and timing
+- Activity feed formatting
+- Thermal printer receipt layout
+- Internal data structures and state management patterns
+
+### Deferred Ideas (OUT OF SCOPE)
+
+- **Drag-and-drop seat moves** — future Phase 1 enhancement or later
+- **PWA seat move notifications** — Phase 2
+- **"Keep apart" player marking** — evaluate for Phase 1 or later
+- **Chip bonus for early signup/punctuality** — captured in decisions, evaluate complexity during planning
+
+
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| ARCH-01 | Single Go binary on x86 LXC with embedded LibSQL, NATS JetStream, WebSocket hub | go-libsql local-only mode (`sql.Open("libsql", "file:"+path)`), embedded nats-server with JetStream, coder/websocket hub pattern |
+| ARCH-03 | All financial values stored as int64 cents — never float64 | Standard Go int64 arithmetic, CI gate test pattern documented |
+| ARCH-04 | NATS JetStream embedded with `sync_interval: always` for durability | Jepsen 2.12.1 finding confirms mandatory for single-node; embedded server opts `JetStream: true` + sync config |
+| ARCH-05 | WebSocket hub broadcasts within 100ms | coder/websocket concurrent writes, custom hub with connection registry pattern |
+| ARCH-06 | SvelteKit frontend via `//go:embed` | adapter-static + `//go:embed all:build` + SPA fallback handler pattern |
+| ARCH-07 | Leaf is sovereign — cloud never required | Local LibSQL file, embedded NATS, all logic in Go binary |
+| ARCH-08 | Append-only audit trail for every state-changing action | Event-sourced audit table in LibSQL + NATS JetStream publish for real-time |
+| AUTH-01 | Operator PIN login -> local JWT (bcrypt in LibSQL) | golang-jwt/jwt v5 + golang.org/x/crypto/bcrypt, offline-capable |
+| AUTH-03 | Operator roles: Admin, Floor, Viewer | JWT claims with role field, middleware enforcement on chi routes |
+| CLOCK-01 | Countdown timer per level, second-granularity display, ms-precision internal | Go time.Ticker + monotonic clock, server-authoritative time |
+| CLOCK-02 | Separate break durations with distinct visual treatment | Level type enum (round/break), frontend conditional styling |
+| CLOCK-03 | Pause/resume with visual indicator across all displays | Clock state machine (running/paused/stopped), WebSocket broadcast |
+| CLOCK-04 | Manual advance forward/backward between levels | Clock engine level index navigation |
+| CLOCK-05 | Jump to any level by number | Direct level index set on clock engine |
+| CLOCK-06 | Total elapsed time display | Tracked from tournament start, pauses excluded |
+| CLOCK-07 | Configurable warning thresholds with audio/visual | Threshold config per level, play_sound action on WebSocket, CSS animation triggers |
+| CLOCK-08 | Clock state authoritative on Leaf; 1/sec normal, 10/sec final 10s | Server-side tick emitter with dynamic interval |
+| CLOCK-09 | Reconnecting clients receive full clock state | WebSocket connection handler sends current snapshot on connect |
+| BLIND-01 | Unlimited configurable levels (round/break, game type, SB/BB, ante, duration, chip-up, notes) | Level struct with all fields, array in blind structure entity |
+| BLIND-02 | Big Blind Ante support alongside standard ante | Separate ante and bb_ante fields per level |
+| BLIND-03 | Mixed game rotation (HORSE, 8-Game) | Game type per level, rotation sequence in structure |
+| BLIND-04 | Save/load reusable blind structure templates | Template CRUD in LibSQL, building block pattern |
+| BLIND-05 | Built-in templates (Turbo, Standard, Deep Stack, WSOP-style) | Seed data on first boot |
+| BLIND-06 | Structure wizard | Algorithm: target duration -> final BB -> geometric curve -> nearest chip denominations |
+| CHIP-01 | Define denominations with colors (hex) and values | Chip set entity (venue-level building block) |
+| CHIP-02 | Chip-up tracking per break | Level flag + chip-up denomination reference |
+| CHIP-03 | Total chips in play calculation | Sum of (active players * starting chips) + rebuys + add-ons |
+| CHIP-04 | Average stack display | Total chips / remaining players |
+| FIN-01 | Buy-in configuration (amount, chips, rake, bounty, points) | Buy-in config entity as building block, int64 cents |
+| FIN-02 | Multiple rake categories (staff, league, house) | Rake split array in buy-in config |
+| FIN-03 | Late registration cutoff (by level, by time, or both) | Cutoff config with dual-condition logic |
+| FIN-04 | Re-entry support (distinct from rebuy) | Separate transaction type, new entry after bust |
+| FIN-05 | Rebuy configuration (cost, chips, rake, points, limits, cutoff, chip threshold) | Rebuy config entity with all parameters |
+| FIN-06 | Add-on configuration (cost, chips, rake, points, window) | Add-on config entity |
+| FIN-07 | Fixed bounty system (cost, chip, hitman tracking, chain tracking, cash-out) | PKO bounty entity per player, half-split on elimination, chain tracked in audit trail |
+| FIN-08 | Prize pool auto-calculation | Sum of all entries * entry amount - total rake, int64 cents |
+| FIN-09 | Guaranteed pot support (house covers shortfall) | Guarantee config field, shortfall = guarantee - actual pool |
+| FIN-10 | Payout structures (percentage, fixed, custom) with rounding | Payout building block, round-down to venue denomination, bracket selection by entry count |
+| FIN-11 | Chop/deal support (ICM, chip-chop, even-chop, custom) | ICM: Malmuth-Harville algorithm (Monte Carlo for >15 players), custom split UI |
+| FIN-12 | End-of-season withholding | Rake category marked as season reserve |
+| FIN-13 | Every financial action generates receipt | Transaction log with receipt rendering |
+| FIN-14 | Transaction editing with audit trail and receipt reprint | Undo/edit creates new audit entry referencing original |
+| PLYR-01 | Player database persistent on Leaf (LibSQL) | LibSQL local file, player table with UUID PK |
+| PLYR-02 | Search with typeahead, merge duplicates, import CSV | FTS5 (SQLite full-text search) for typeahead, merge logic, CSV import endpoint |
+| PLYR-03 | QR code generation per player | Go QR library (e.g. skip2/go-qrcode), encodes player UUID |
+| PLYR-04 | Buy-in flow (search -> confirm -> auto-seat -> receipt -> display update) | Multi-step transaction: financial entry + seat assignment + WebSocket broadcast |
+| PLYR-05 | Bust-out flow (select -> hitman -> bounty -> rank -> rebalance -> display) | Multi-step: bust record + bounty transfer + re-rank + balance check + broadcast |
+| PLYR-06 | Undo for bust-out, rebuy, add-on, buy-in with re-ranking | Reverse transaction in audit trail, recalculate rankings and balances |
+| PLYR-07 | Per-player tracking (chips, time, seat, moves, rebuys, add-ons, bounties, prize, points, net, history) | Computed from transaction log and current state |
+| SEAT-01 | Tables with configurable seat counts (6-max to 10-max) | Table entity with seat_count, name/label |
+| SEAT-02 | Table blueprints (save venue layout) | Blueprint entity as venue-level template |
+| SEAT-03 | Dealer button tracking | Button position field per table, advanced on bust/balance |
+| SEAT-04 | Random initial seating on buy-in (fills evenly) | Random seat assignment algorithm: find table with fewest players, random empty seat |
+| SEAT-05 | Auto balancing suggestions with operator confirmation | TDA-compliant algorithm: size difference threshold, move fairness, button awareness, dry-run preview |
+| SEAT-06 | Drag-and-drop manual moves on touch interface | DEFERRED in Phase 1 — tap-tap flow instead |
+| SEAT-07 | Break Table action (dissolve and distribute) | Distribute players from broken table to remaining tables evenly, respecting blind position |
+| SEAT-08 | Visual top-down table layout, list view, movement screen | Oval SVG table component, list view alternative, move confirmation screen |
+| SEAT-09 | Hand-for-hand mode | Clock pause + per-hand level decrement mode |
+| MULTI-01 | Multiple simultaneous tournaments with independent state | Tournament-scoped state (clock, financials, players all keyed by tournament ID) |
+| MULTI-02 | Tournament lobby view | List of active tournaments with status summary |
+| UI-01 | Mobile-first bottom tab bar (Overview, Players, Tables, Financials, More) | SvelteKit layout with bottom nav component |
+| UI-02 | FAB for quick actions (Bust, Buy In, Rebuy, Add-On, Pause/Resume) | Expandable FAB component with context-aware actions |
+| UI-03 | Persistent header (clock, level, blinds, player count) | Fixed header component subscribing to clock WebSocket |
+| UI-04 | Desktop/laptop sidebar navigation | Responsive layout: bottom tabs on mobile, sidebar on desktop |
+| UI-05 | Catppuccin Mocha dark theme (default), Latte light theme | @catppuccin/palette CSS custom properties, theme toggle |
+| UI-06 | 48px minimum touch targets, press-state animations, loading states | CSS touch-action, :active states, skeleton loading |
+| UI-07 | Toast notifications (success, info, warning, error) | Svelte toast store/rune with auto-dismiss timer |
+| UI-08 | Data tables with sort, sticky header, search/filter, swipe actions | Custom table component or thin wrapper, virtual scroll for large lists |
+
+
+
+## Standard Stack
+
+### Core
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| Go | 1.23+ | Backend language | Current stable, required for latest stdlib features |
+| github.com/tursodatabase/go-libsql | commit pin (no tags) | Embedded LibSQL database | SQLite-compatible, local-first, CGO required, `sql.Open("libsql", "file:"+path)` |
+| github.com/nats-io/nats-server/v2 | v2.12.4 | Embedded NATS server with JetStream | Event durability, append-only streams, embedded in Go process |
+| github.com/nats-io/nats.go | latest (v1.49+) | NATS Go client + jetstream package | New JetStream API (replaces legacy JetStreamContext) |
+| github.com/coder/websocket | v1.8.14 | WebSocket server | Minimal, idiomatic, context.Context support, concurrent writes, zero deps |
+| github.com/go-chi/chi/v5 | v5.2.5 | HTTP router | Lightweight, stdlib-compatible middleware, route grouping |
+| github.com/golang-jwt/jwt/v5 | v5.x (latest) | JWT tokens | Standard Go JWT library, 13K+ importers, HS256 signing |
+| golang.org/x/crypto | latest | bcrypt for PIN hashing | Standard library extension, production-grade bcrypt |
+| Svelte | 5.46+ | Frontend framework | Runes reactivity model, compiled output, small bundle |
+| SvelteKit | 2.x | Frontend framework/router | adapter-static for SPA, file-based routing, SSR-capable |
+| @sveltejs/adapter-static | latest | SPA build adapter | Prerenders all pages to static HTML for go:embed |
+| @catppuccin/palette | latest | Theme colors | Official Catppuccin color palette as CSS/JS, Mocha + Latte flavors |
+
+### Supporting
+
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| github.com/skip2/go-qrcode | latest | QR code generation | PLYR-03: Player QR codes for self-check-in |
+| github.com/google/uuid | latest | UUID generation | Player IDs, tournament IDs, all entity PKs |
+| FTS5 (built into LibSQL/SQLite) | N/A | Full-text search | PLYR-02: Typeahead player search |
+| @catppuccin/tailwindcss | latest | Tailwind theme plugin | Only if using Tailwind; otherwise use raw CSS vars from @catppuccin/palette |
+
+### Alternatives Considered
+
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| go-chi/chi v5 | Go 1.22+ stdlib ServeMux | Go 1.22 added method+path matching but chi has better middleware composition, route grouping, and ecosystem |
+| coder/websocket | gorilla/websocket | gorilla is more battle-tested (6+ years) but coder/websocket is more idiomatic (context support, concurrent writes) and actively maintained by Coder |
+| go-libsql | mattn/go-sqlite3 | go-sqlite3 is more mature with tagged releases but doesn't support libSQL extensions and eventual Turso sync in Phase 3 |
+| SvelteKit adapter-static | Vite vanilla SPA | SvelteKit provides file-based routing, layouts, and SSR-capability for Phase 2+ |
+
+**Installation (Go):**
+```bash
+# go.mod — pin go-libsql to specific commit
+go get github.com/tursodatabase/go-libsql@
+go get github.com/nats-io/nats-server/v2@v2.12.4
+go get github.com/nats-io/nats.go@latest
+go get github.com/coder/websocket@v1.8.14
+go get github.com/go-chi/chi/v5@v5.2.5
+go get github.com/golang-jwt/jwt/v5@latest
+go get golang.org/x/crypto@latest
+go get github.com/skip2/go-qrcode@latest
+go get github.com/google/uuid@latest
+```
+
+**Installation (Frontend):**
+```bash
+npm create svelte@latest frontend -- --template skeleton
+cd frontend
+npm install @catppuccin/palette
+npm install -D @sveltejs/adapter-static
+```
+
+## Architecture Patterns
+
+### Recommended Project Structure
+
+```
+felt/
+├── cmd/
+│ └── leaf/
+│ └── main.go # Entry point: starts NATS, LibSQL, HTTP server
+├── internal/
+│ ├── server/
+│ │ ├── server.go # HTTP server setup, chi router, middleware
+│ │ ├── ws/
+│ │ │ └── hub.go # WebSocket hub: connection registry, broadcast
+│ │ └── middleware/
+│ │ ├── auth.go # JWT validation middleware
+│ │ └── role.go # Role-based access control
+│ ├── auth/
+│ │ ├── pin.go # PIN login, bcrypt verify, JWT issue
+│ │ └── jwt.go # Token creation, validation, claims
+│ ├── clock/
+│ │ ├── engine.go # Clock state machine (running/paused/stopped)
+│ │ ├── ticker.go # Server-side tick emitter (1/sec, 10/sec final 10s)
+│ │ └── warnings.go # Threshold detection and alert emission
+│ ├── tournament/
+│ │ ├── tournament.go # Tournament lifecycle (create, start, close)
+│ │ ├── state.go # Tournament state aggregation
+│ │ └── multi.go # Multi-tournament management
+│ ├── financial/
+│ │ ├── engine.go # Buy-in, rebuy, add-on, bounty transactions
+│ │ ├── payout.go # Prize pool calculation, payout distribution
+│ │ ├── icm.go # ICM calculator (Malmuth-Harville + Monte Carlo)
+│ │ ├── chop.go # Deal/chop flows (ICM, chip-chop, custom)
+│ │ └── receipt.go # Receipt generation
+│ ├── player/
+│ │ ├── player.go # Player CRUD, search, merge
+│ │ ├── ranking.go # Live rankings, re-ranking on undo
+│ │ └── qrcode.go # QR code generation
+│ ├── seating/
+│ │ ├── table.go # Table management, seat assignment
+│ │ ├── balance.go # Table balancing algorithm
+│ │ ├── breaktable.go # Table break and redistribute
+│ │ └── blueprint.go # Venue layout blueprints
+│ ├── blind/
+│ │ ├── structure.go # Blind structure definition and CRUD
+│ │ ├── wizard.go # Structure wizard algorithm
+│ │ └── templates.go # Built-in template seed data
+│ ├── template/
+│ │ ├── tournament.go # Tournament template (composition of blocks)
+│ │ ├── chipset.go # Chip set building block
+│ │ ├── payout.go # Payout structure building block
+│ │ └── buyin.go # Buy-in config building block
+│ ├── audit/
+│ │ ├── trail.go # Append-only audit log
+│ │ └── undo.go # Undo engine (reverse operations)
+│ ├── nats/
+│ │ ├── embedded.go # Embedded NATS server startup
+│ │ └── publisher.go # Event publishing to JetStream
+│ └── store/
+│ ├── db.go # LibSQL connection setup
+│ ├── migrations/ # SQL migration files
+│ └── queries/ # SQL query files (or generated)
+├── frontend/
+│ ├── src/
+│ │ ├── lib/
+│ │ │ ├── ws.ts # WebSocket client with reconnect
+│ │ │ ├── api.ts # HTTP API client
+│ │ │ ├── stores/ # Svelte 5 runes-based state
+│ │ │ ├── components/ # Reusable UI components
+│ │ │ └── theme/ # Catppuccin theme setup
+│ │ ├── routes/
+│ │ │ ├── +layout.svelte # Root layout (header, tabs, FAB)
+│ │ │ ├── overview/ # Overview tab
+│ │ │ ├── players/ # Players tab
+│ │ │ ├── tables/ # Tables tab
+│ │ │ ├── financials/ # Financials tab
+│ │ │ └── more/ # More tab (templates, settings)
+│ │ └── app.html
+│ ├── static/
+│ │ └── sounds/ # Level change, break, bubble sounds
+│ ├── svelte.config.js
+│ ├── embed.go # //go:embed all:build
+│ └── package.json
+└── go.mod
+```
+
+### Pattern 1: Embedded NATS Server with JetStream
+
+**What:** Start NATS server in-process with JetStream enabled and `sync_interval: always` for single-node durability.
+**When to use:** Application startup in `cmd/leaf/main.go`.
+
+```go
+// Source: https://github.com/nats-io/nats-server/discussions/6242
+import (
+ "github.com/nats-io/nats-server/v2/server"
+ "github.com/nats-io/nats.go"
+)
+
+func startEmbeddedNATS(dataDir string) (*server.Server, error) {
+ opts := &server.Options{
+ DontListen: true, // In-process only, no TCP listener needed
+ JetStream: true,
+ StoreDir: dataDir,
+ JetStreamMaxMemory: 64 * 1024 * 1024, // 64MB memory limit
+ JetStreamMaxStore: 1024 * 1024 * 1024, // 1GB disk limit
+ JetStreamSyncInterval: 0, // 0 = "always" — fsync every write
+ }
+
+ ns, err := server.NewServer(opts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create NATS server: %w", err)
+ }
+
+ go ns.Start()
+
+ if !ns.ReadyForConnections(5 * time.Second) {
+ return nil, errors.New("NATS server startup timeout")
+ }
+
+ return ns, nil
+}
+```
+
+**CRITICAL:** `sync_interval: always` (or `JetStreamSyncInterval: 0`) is mandatory. The Jepsen December 2025 analysis of NATS 2.12.1 found that the default 2-minute fsync interval means recently acknowledged writes exist only in memory and are lost on power failure. For single-node (non-replicated) deployments, every write must fsync immediately.
+
+### Pattern 2: WebSocket Hub with Connection Registry
+
+**What:** Central hub managing all WebSocket connections with topic-based broadcasting.
+**When to use:** Real-time state updates to all connected operator/display clients.
+
+```go
+// Source: coder/websocket docs + standard hub pattern
+import "github.com/coder/websocket"
+
+type Hub struct {
+ mu sync.RWMutex
+ clients map[*Client]struct{}
+}
+
+type Client struct {
+ conn *websocket.Conn
+ tournamentID string // Subscribe to specific tournament
+ send chan []byte
+}
+
+func (h *Hub) Broadcast(tournamentID string, msg []byte) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ for client := range h.clients {
+ if client.tournamentID == tournamentID || client.tournamentID == "" {
+ select {
+ case client.send <- msg:
+ default:
+ // Client too slow, drop message
+ }
+ }
+ }
+}
+
+// On new WebSocket connection — send full state snapshot
+func (h *Hub) HandleConnect(w http.ResponseWriter, r *http.Request) {
+ c, err := websocket.Accept(w, r, nil)
+ if err != nil { return }
+
+ client := &Client{conn: c, send: make(chan []byte, 256)}
+ h.register(client)
+ defer h.unregister(client)
+
+ // Send full state snapshot on connect (CLOCK-09)
+ snapshot := h.getCurrentState(client.tournamentID)
+ client.send <- snapshot
+
+ // Read/write pumps...
+}
+```
+
+### Pattern 3: SvelteKit SPA Embedded in Go Binary
+
+**What:** Build SvelteKit with adapter-static, embed the output, serve with SPA fallback.
+**When to use:** Serving the frontend from the Go binary.
+
+```javascript
+// svelte.config.js
+import adapter from '@sveltejs/adapter-static';
+
+export default {
+ kit: {
+ adapter: adapter({
+ pages: 'build',
+ assets: 'build',
+ fallback: 'index.html', // SPA fallback
+ precompress: false,
+ strict: true
+ }),
+ paths: {
+ base: '' // Served from root
+ }
+ }
+};
+```
+
+```go
+// frontend/embed.go
+package frontend
+
+import (
+ "embed"
+ "io/fs"
+ "net/http"
+ "strings"
+)
+
+//go:embed all:build
+var files embed.FS
+
+func Handler() http.Handler {
+ fsys, _ := fs.Sub(files, "build")
+ fileServer := http.FileServer(http.FS(fsys))
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Try to serve static file first
+ path := strings.TrimPrefix(r.URL.Path, "/")
+ if path == "" {
+ path = "index.html"
+ }
+ if _, err := fs.Stat(fsys, path); err != nil {
+ // SPA fallback: serve index.html for client-side routing
+ r.URL.Path = "/"
+ }
+ fileServer.ServeHTTP(w, r)
+ })
+}
+```
+
+### Pattern 4: Event-Sourced Audit Trail
+
+**What:** Every state-changing action writes an immutable audit entry to both LibSQL and NATS JetStream.
+**When to use:** All mutations (buy-in, bust-out, rebuy, clock pause, seat move, etc.).
+
+```go
+type AuditEntry struct {
+ ID string `json:"id"` // UUID
+ TournamentID string `json:"tournament_id"`
+ Timestamp int64 `json:"timestamp"` // UnixNano
+ OperatorID string `json:"operator_id"`
+ Action string `json:"action"` // "player.bust", "financial.buyin", etc.
+ TargetID string `json:"target_id"` // Player/table/etc ID
+ PreviousState json.RawMessage `json:"previous_state"`
+ NewState json.RawMessage `json:"new_state"`
+ Metadata json.RawMessage `json:"metadata,omitempty"`
+ UndoneBy *string `json:"undone_by,omitempty"` // Points to undo entry
+}
+
+// Write to both LibSQL (persistence) and NATS JetStream (real-time broadcast)
+func (a *AuditTrail) Record(ctx context.Context, entry AuditEntry) error {
+ // 1. Insert into LibSQL audit table
+ if err := a.store.InsertAudit(ctx, entry); err != nil {
+ return err
+ }
+ // 2. Publish to NATS JetStream for real-time broadcast
+ data, _ := json.Marshal(entry)
+ _, err := a.js.Publish(ctx,
+ fmt.Sprintf("tournament.%s.audit", entry.TournamentID),
+ data,
+ )
+ return err
+}
+```
+
+### Pattern 5: Int64 Financial Math
+
+**What:** All monetary values stored and calculated as int64 cents. Never float64.
+**When to use:** Every financial calculation (prize pool, payouts, rake, bounties).
+
+```go
+// All money is int64 cents. 1000 = $10.00 or 10.00 EUR or 1000 DKK (10 kr)
+type Money int64
+
+func (m Money) String() string {
+ return fmt.Sprintf("%d.%02d", m/100, m%100)
+}
+
+// Prize pool: sum of all entry fees minus rake
+func CalculatePrizePool(entries []Transaction) Money {
+ var pool Money
+ for _, e := range entries {
+ pool += e.PrizeContribution // Already int64 cents
+ }
+ return pool
+}
+
+// Payout with round-down to venue denomination
+func CalculatePayouts(pool Money, structure []PayoutTier, roundTo Money) []Money {
+ payouts := make([]Money, len(structure))
+ var distributed Money
+ for i, tier := range structure {
+ raw := Money(int64(pool) * int64(tier.BasisPoints) / 10000)
+ payouts[i] = (raw / roundTo) * roundTo // Round down
+ distributed += payouts[i]
+ }
+ // Remainder goes to 1st place (standard poker convention)
+ payouts[0] += pool - distributed
+ return payouts
+}
+
+// CI GATE TEST: sum of payouts must exactly equal prize pool
+func TestPayoutSumEqualsPool(t *testing.T) {
+ // ... property-based test across thousands of random inputs
+ // assert: sum(payouts) == pool, always, zero deviation
+}
+```
+
+### Anti-Patterns to Avoid
+
+- **float64 for money:** Floating-point representation causes rounding errors. A pool of 10000 cents split 3 ways (3333 + 3333 + 3334) is exact in int64. With float64, 100.00/3 = 33.333... causes cumulative errors across a tournament.
+- **Client-authoritative clock:** The clock MUST be server-authoritative. Client clocks drift, can be manipulated, and diverge across devices. Server sends ticks, clients display.
+- **Mutable audit trail:** Audit entries must be append-only. "Undo" creates a NEW entry that references the original, never deletes or modifies existing entries.
+- **Blocking WebSocket writes:** Never write to a WebSocket connection synchronously in the broadcast loop. Use buffered channels per client with drop semantics for slow consumers.
+- **Global state instead of tournament-scoped:** Every piece of state (clock, players, tables, financials) must be scoped to a tournament ID. Global singletons break MULTI-01.
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Password hashing | Custom hash function | golang.org/x/crypto/bcrypt | Bcrypt handles salt generation, timing-safe comparison, configurable cost |
+| JWT token management | Custom token format | golang-jwt/jwt/v5 | Standard claims, expiry, signing algorithm negotiation |
+| QR code generation | Manual QR encoding | skip2/go-qrcode | Reed-Solomon error correction, multiple output formats |
+| WebSocket protocol | Raw TCP upgrade | coder/websocket | RFC 6455 compliance, compression, ping/pong, close handshake |
+| SQLite FTS | Custom search index | FTS5 (built into LibSQL) | Tokenization, ranking, prefix queries, diacritics handled |
+| UUID generation | Custom ID scheme | google/uuid | RFC 4122 compliant, v4 random, v7 time-ordered |
+| Theme colors | Manual hex values | @catppuccin/palette | 26 colors across 4 flavors, community-maintained, consistent |
+| HTTP routing + middleware | Custom mux | go-chi/chi v5 | Middleware composition, route grouping, stdlib compatible |
+
+**Key insight:** The complexity in this phase is in the poker domain logic (ICM, balancing, blind wizard, PKO chains), not in infrastructure. Use battle-tested libraries for all infrastructure concerns so implementation time is spent on domain logic.
+
+## Common Pitfalls
+
+### Pitfall 1: NATS JetStream Default sync_interval Loses Data
+
+**What goes wrong:** Acknowledged writes exist only in OS page cache for up to 2 minutes. Power failure loses all recent tournament state.
+**Why it happens:** NATS defaults to `sync_interval: 2m` for performance, relying on cluster replication for durability. Single-node deployments have no replication.
+**How to avoid:** Set `sync_interval: always` (or `JetStreamSyncInterval: 0` in Go opts) on every embedded NATS instance. Accept the ~10-20% write throughput reduction.
+**Warning signs:** Tests pass because no simulated power failure; first real power cut at a venue loses the last 2 minutes of tournament data.
+
+### Pitfall 2: go-libsql Unpinned Dependency Breaks Build
+
+**What goes wrong:** `go get github.com/tursodatabase/go-libsql@latest` resolves to a different commit on different days. Breaking changes arrive silently.
+**Why it happens:** go-libsql has no tagged releases. Go modules fall back to pseudo-versions based on commit hash.
+**How to avoid:** Pin to a specific commit hash in go.mod. Test thoroughly before updating. Document the pinned commit with a comment.
+**Warning signs:** CI builds succeed locally but fail in a clean environment, or vice versa.
+
+### Pitfall 3: ICM Calculation Factorial Explosion
+
+**What goes wrong:** Exact Malmuth-Harville ICM calculation for 20+ players takes minutes or hours. UI freezes during chop negotiation.
+**Why it happens:** ICM exhausts all possible finish permutations. For N players this is O(N!) — 15 players = 1.3 trillion permutations.
+**How to avoid:** Use exact calculation for <= 10 players. Use Monte Carlo sampling (Tysen Streib method, 2011) for 11+ players with 100K iterations — converges to <0.1% error in under 1 second.
+**Warning signs:** Chop calculation works in testing (4-5 players) but hangs in production with 8+ players.
+
+### Pitfall 4: Payout Rounding Creates Money from Nothing
+
+**What goes wrong:** Prize pool is 10000 cents, payouts sum to 10050 cents due to rounding up. Venue pays out more than collected.
+**Why it happens:** Rounding each payout independently can round up. Multiple round-ups accumulate.
+**How to avoid:** Always round DOWN to venue denomination. Assign remainder to 1st place. CI gate test: assert `sum(payouts) == prize_pool` for every possible input combination.
+**Warning signs:** Payout sum doesn't match prize pool in edge cases (odd numbers, large fields, unusual denominations).
+
+### Pitfall 5: Clock Drift Between Server and Client
+
+**What goes wrong:** Timer shows different values on different devices. Operator's phone says 45 seconds, display says 43 seconds.
+**Why it happens:** Client-side timers using `setInterval` drift due to JS event loop scheduling, tab throttling, and device sleep.
+**How to avoid:** Server is authoritative. Send absolute state (level, remaining_ms, is_paused, server_timestamp) not relative ticks. Client calculates display from server time, correcting for known RTT. Re-sync on every WebSocket message.
+**Warning signs:** Devices show slightly different times; discrepancy grows over long levels.
+
+### Pitfall 6: Undo Doesn't Re-Rank Correctly
+
+**What goes wrong:** Undoing a bust-out puts the player back but rankings are wrong. Players who busted after them have incorrect positions.
+**Why it happens:** Rankings are calculated at bust time. Undoing a bust requires recalculating ALL rankings from that point forward.
+**How to avoid:** Rankings should be derived from the ordered bust-out list, not stored as independent values. Undo removes from bust list, all subsequent positions shift. Re-derive, don't patch.
+**Warning signs:** Rankings look correct for simple undo (last bust) but break for earlier undos.
+
+### Pitfall 7: Table Balancing Race Condition
+
+**What goes wrong:** System suggests "move Player A from Table 1 to Table 4". Before TD executes, Player B busts from Table 1. Now the move makes tables unbalanced in the other direction.
+**Why it happens:** Time gap between suggestion and execution. Real tournaments have concurrent busts during hands in play.
+**How to avoid:** The CONTEXT.md already addresses this: "Suggestion is live and adaptive — if Table 1 loses a player before the move happens, system recalculates or cancels." Implement suggestions as pending proposals that are re-validated before execution.
+**Warning signs:** TD executes a stale balancing suggestion and tables end up worse than before.
+
+### Pitfall 8: SvelteKit adapter-static Path Resolution
+
+**What goes wrong:** Routes work in development but return 404 in production when embedded in Go binary.
+**Why it happens:** SvelteKit adapter-static generates files like `build/players/index.html`. The Go file server doesn't automatically resolve `/players` to `/players/index.html`.
+**How to avoid:** Configure `fallback: 'index.html'` in adapter-static config for SPA mode. The Go handler falls back to index.html for any path not found as a static file, letting SvelteKit's client-side router handle navigation.
+**Warning signs:** Direct navigation to routes works, but refresh on a nested route returns 404.
+
+## Code Examples
+
+### LibSQL Local Database Setup
+
+```go
+// Source: https://github.com/tursodatabase/go-libsql/blob/main/example/local/main.go
+import (
+ "database/sql"
+ _ "github.com/tursodatabase/go-libsql"
+)
+
+func openDB(dataDir string) (*sql.DB, error) {
+ dbPath := filepath.Join(dataDir, "felt.db")
+ db, err := sql.Open("libsql", "file:"+dbPath)
+ if err != nil {
+ return nil, fmt.Errorf("open libsql: %w", err)
+ }
+
+ // Enable WAL mode for concurrent reads during writes
+ if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
+ return nil, err
+ }
+ // Enable foreign keys
+ if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
+ return nil, err
+ }
+
+ return db, nil
+}
+```
+
+### Clock Engine State Machine
+
+```go
+type ClockState int
+const (
+ ClockStopped ClockState = iota
+ ClockRunning
+ ClockPaused
+)
+
+type ClockEngine struct {
+ mu sync.RWMutex
+ tournamentID string
+ state ClockState
+ levels []Level
+ currentLevel int
+ remainingNs int64 // Nanoseconds remaining in current level
+ lastTick time.Time // Monotonic clock reference
+ totalElapsed time.Duration
+ warnings []WarningThreshold
+ hub *ws.Hub
+}
+
+func (c *ClockEngine) Tick() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if c.state != ClockRunning { return }
+
+ now := time.Now()
+ elapsed := now.Sub(c.lastTick)
+ c.lastTick = now
+ c.remainingNs -= elapsed.Nanoseconds()
+ c.totalElapsed += elapsed
+
+ // Check for level transition
+ if c.remainingNs <= 0 {
+ c.advanceLevel()
+ }
+
+ // Check warning thresholds
+ c.checkWarnings()
+
+ // Determine tick rate: 10/sec in final 10s, 1/sec otherwise
+ remainingSec := c.remainingNs / int64(time.Second)
+ msg := c.buildStateMessage()
+
+ // Broadcast to all clients
+ c.hub.Broadcast(c.tournamentID, msg)
+}
+
+func (c *ClockEngine) Snapshot() ClockSnapshot {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ return ClockSnapshot{
+ TournamentID: c.tournamentID,
+ State: c.state,
+ CurrentLevel: c.currentLevel,
+ Level: c.levels[c.currentLevel],
+ RemainingMs: c.remainingNs / int64(time.Millisecond),
+ TotalElapsed: c.totalElapsed,
+ ServerTime: time.Now().UnixMilli(),
+ }
+}
+```
+
+### Catppuccin Mocha Theme Setup
+
+```css
+/* Source: https://catppuccin.com/palette/ — Mocha flavor */
+:root {
+ /* Base */
+ --ctp-base: #1e1e2e;
+ --ctp-mantle: #181825;
+ --ctp-crust: #11111b;
+
+ /* Text */
+ --ctp-text: #cdd6f4;
+ --ctp-subtext1: #bac2de;
+ --ctp-subtext0: #a6adc8;
+
+ /* Surface */
+ --ctp-surface2: #585b70;
+ --ctp-surface1: #45475a;
+ --ctp-surface0: #313244;
+
+ /* Overlay */
+ --ctp-overlay2: #9399b2;
+ --ctp-overlay1: #7f849c;
+ --ctp-overlay0: #6c7086;
+
+ /* Accents */
+ --ctp-rosewater: #f5e0dc;
+ --ctp-flamingo: #f2cdcd;
+ --ctp-pink: #f5c2e7;
+ --ctp-mauve: #cba6f7;
+ --ctp-red: #f38ba8;
+ --ctp-maroon: #eba0ac;
+ --ctp-peach: #fab387;
+ --ctp-yellow: #f9e2af;
+ --ctp-green: #a6e3a1;
+ --ctp-teal: #94e2d5;
+ --ctp-sky: #89dceb;
+ --ctp-sapphire: #74c7ec;
+ --ctp-blue: #89b4fa;
+ --ctp-lavender: #b4befe;
+}
+
+body {
+ background-color: var(--ctp-base);
+ color: var(--ctp-text);
+ font-family: system-ui, -apple-system, sans-serif;
+}
+```
+
+### Svelte 5 Runes WebSocket State
+
+```typescript
+// Source: Svelte 5 runes documentation
+// lib/stores/tournament.svelte.ts
+
+class TournamentState {
+ clock = $state(null);
+ players = $state([]);
+ tables = $state([]);
+ financials = $state(null);
+ activity = $state([]);
+
+ get remainingPlayers() {
+ return this.players.filter(p => p.status === 'active').length;
+ }
+
+ get isBalanced() {
+ if (this.tables.length <= 1) return true;
+ const counts = this.tables.map(t => t.players.length);
+ return Math.max(...counts) - Math.min(...counts) <= 1;
+ }
+
+ handleMessage(msg: WSMessage) {
+ switch (msg.type) {
+ case 'clock.tick':
+ this.clock = msg.data;
+ break;
+ case 'player.update':
+ // Reactive update via $state
+ const idx = this.players.findIndex(p => p.id === msg.data.id);
+ if (idx >= 0) this.players[idx] = msg.data;
+ break;
+ // ... other message types
+ }
+ }
+}
+
+export const tournament = new TournamentState();
+```
+
+### PIN Authentication Flow
+
+```go
+// PIN login -> bcrypt verify -> JWT issue
+func (a *AuthService) Login(ctx context.Context, pin string) (string, error) {
+ // Rate limiting: exponential backoff after 5 failures
+ if blocked := a.rateLimiter.Check(pin); blocked {
+ return "", ErrTooManyAttempts
+ }
+
+ operators, err := a.store.GetOperators(ctx)
+ if err != nil { return "", err }
+
+ for _, op := range operators {
+ if err := bcrypt.CompareHashAndPassword([]byte(op.PINHash), []byte(pin)); err == nil {
+ // Match found — issue JWT
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "sub": op.ID,
+ "role": op.Role, // "admin", "floor", "viewer"
+ "iat": time.Now().Unix(),
+ "exp": time.Now().Add(24 * time.Hour).Unix(),
+ })
+ return token.SignedString(a.signingKey)
+ }
+ }
+
+ a.rateLimiter.RecordFailure(pin)
+ return "", ErrInvalidPIN
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| Svelte stores (writable/readable) | Svelte 5 runes ($state, $derived, $effect) | Oct 2024 (Svelte 5.0) | All state management uses runes, stores deprecated for new code |
+| nats.JetStreamContext (legacy API) | nats.go/jetstream package (new API) | 2024 | Simpler interfaces, pull consumers, better stream management |
+| gorilla/websocket | coder/websocket (nhooyr fork) | 2024 | Coder maintains the project, context.Context support, concurrent writes |
+| Go 1.21 stdlib ServeMux | Go 1.22+ enhanced ServeMux | Feb 2024 | Method matching + path params in stdlib, but chi still better for middleware |
+| Manual ICM calculation | Monte Carlo ICM approximation | 2011 (Streib) | Practical ICM for any field size, converges quickly |
+
+**Deprecated/outdated:**
+- **Svelte stores for new code:** Still work but Svelte 5 docs recommend runes ($state) for all new code. Stores remain for backward compatibility.
+- **nats.JetStreamContext:** Legacy API. The `jetstream` package under `nats.go/jetstream` is the replacement. Don't use the old JetStreamContext methods.
+- **gorilla/websocket as default choice:** Still works but archived/maintenance-mode. coder/websocket is the actively maintained modern alternative.
+
+## Open Questions
+
+1. **go-libsql CGO Cross-Compilation for CI**
+ - What we know: go-libsql requires CGO_ENABLED=1 and precompiled libsql for linux/amd64
+ - What's unclear: Whether CI runners (GitHub Actions) have the right C toolchain pre-installed, or if we need a custom Docker build image
+ - Recommendation: Test early with a minimal CI pipeline. If problematic, use a Debian-based CI image with build-essential.
+
+2. **go-libsql Exact Commit to Pin**
+ - What we know: No tagged releases. Must pin to commit hash.
+ - What's unclear: Which commit is most stable as of March 2026
+ - Recommendation: Check the repo's main branch, pick the latest commit, verify it passes local tests, pin it with a comment in go.mod.
+
+3. **NATS JetStream Stream Configuration for Audit Trail**
+ - What we know: JetStream streams need proper retention, limits, and subject patterns
+ - What's unclear: Optimal retention policy (limits vs interest), max message size for audit entries with full state snapshots, disk space planning
+ - Recommendation: Start with limits-based retention (MaxAge: 90 days, MaxBytes: 1GB), adjust based on real tournament data sizes.
+
+4. **Svelte 5 Runes with SvelteKit adapter-static**
+ - What we know: Svelte 5 runes work for state management, adapter-static produces a SPA
+ - What's unclear: Whether $state runes in .svelte.ts files work correctly with adapter-static's prerendering (runes are runtime, prerendering is build-time)
+ - Recommendation: Use `fallback: 'index.html'` SPA mode to avoid prerendering issues. Runes are client-side only.
+
+5. **Thermal Printer Receipt Integration**
+ - What we know: FIN-13 requires receipt generation, CONTEXT.md mentions configurable receipt settings
+ - What's unclear: Which thermal printer protocol to target (ESC/POS is standard), whether to generate in Go or browser
+ - Recommendation: Generate receipt as HTML/CSS rendered in a hidden iframe for print. Thermal printer support can use ESC/POS in a later iteration. Digital receipts first.
+
+## Sources
+
+### Primary (HIGH confidence)
+
+- [go-libsql GitHub](https://github.com/tursodatabase/go-libsql) — Local-only API (`sql.Open("libsql", "file:"+path)`), CGO requirement, platform support
+- [go-libsql local example](https://github.com/tursodatabase/go-libsql/blob/main/example/local/main.go) — Verified local-only database opening pattern
+- [NATS JetStream docs](https://docs.nats.io/nats-concepts/jetstream) — JetStream configuration, sync_interval
+- [NATS server discussions #6242](https://github.com/nats-io/nats-server/discussions/6242) — Embedded JetStream account setup, PR #6261 fix
+- [Jepsen NATS 2.12.1](https://jepsen.io/analyses/nats-2.12.1) — sync_interval durability finding, single-node recommendation
+- [coder/websocket GitHub](https://github.com/coder/websocket) — v1.8.14, API, features
+- [go-chi/chi GitHub](https://github.com/go-chi/chi) — v5.2.5, middleware, routing
+- [golang-jwt/jwt v5](https://pkg.go.dev/github.com/golang-jwt/jwt/v5) — JWT library API
+- [Catppuccin palette](https://catppuccin.com/palette/) — Mocha hex values verified
+- [SvelteKit adapter-static docs](https://svelte.dev/docs/kit/adapter-static) — Configuration, fallback, prerender
+- [Svelte 5 runes blog](https://svelte.dev/blog/runes) — $state, $derived, $effect API
+- [ICM Wikipedia](https://en.wikipedia.org/wiki/Independent_Chip_Model) — Malmuth-Harville algorithm, factorial complexity
+
+### Secondary (MEDIUM confidence)
+
+- [Liip blog: Embed SvelteKit into Go](https://www.liip.ch/en/blog/embed-sveltekit-into-a-go-binary) — Complete go:embed + adapter-static pattern, verified with official docs
+- [Replay Poker table rebalancing](https://replayhelp.casino.org/hc/en-us/articles/360001878254-How-table-rebalancing-works) — TDA-compliant balancing algorithm description
+- [PokerSoup blind calculator](https://pokersoup.com/tool/blindStructureCalculator) — Blind structure wizard algorithm approach
+- [Poker TDA 2024 rules](https://www.pokertda.com/poker-tda-rules/) — Official TDA breaking/balancing rules
+- [GTO Wizard PKO theory](https://blog.gtowizard.com/the-theory-of-progressive-knockout-tournaments/) — PKO bounty mechanics (half-split)
+- [HoldemResources ICM](https://www.holdemresources.net/blog/high-accuracy-mtt-icm/) — Monte Carlo ICM approximation for large fields
+
+### Tertiary (LOW confidence)
+
+- [nats-server releases](https://github.com/nats-io/nats-server/releases) — v2.12.4 latest stable (Jan 2026), needs re-check at implementation time
+- go-libsql latest commit hash — needs to be fetched fresh at implementation time
+- Svelte exact version — reported as 5.46+ (Jan 2026), may have newer patches
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — All core libraries verified via official GitHub repos, official docs, and package registries
+- Architecture: HIGH — Patterns (go:embed SPA, embedded NATS, WebSocket hub, event sourcing) verified via multiple official examples and production usage
+- Pitfalls: HIGH — NATS Jepsen finding verified via official Jepsen report; ICM complexity verified via Wikipedia and academic sources; other pitfalls derived from verified library characteristics
+- Domain logic: MEDIUM — Poker domain (ICM, balancing, blind wizard) based on community sources and TDA rules, not on verified software implementations in Go
+
+**Research date:** 2026-03-01
+**Valid until:** 2026-03-31 (30 days — stack is stable, check go-libsql for new tagged releases)