diff --git a/frontend/src/lib/components/BubblePrize.svelte b/frontend/src/lib/components/BubblePrize.svelte
new file mode 100644
index 0000000..abd7c58
--- /dev/null
+++ b/frontend/src/lib/components/BubblePrize.svelte
@@ -0,0 +1,330 @@
+
+
+
+ {#if flowState === 'idle'}
+
+
+ {:else if flowState === 'input'}
+
+
+
Bubble Prize
+
Enter the bubble prize amount (pre-filled with buy-in).
+
+
+
+
+
+
+
+
+
+
+
+ {:else if flowState === 'preview' && preview}
+
+
+
Bubble Prize Preview
+
+ Bubble: {formatCurrency(preview.bubble_amount)}
+
+
+
+
+
+ | Position |
+ Original |
+ Adjusted |
+
+
+
+ {#each preview.payouts.slice(0, 5) as p}
+
+ | {formatPosition(p.position)} |
+ {formatCurrency(p.original)} |
+
+ {formatCurrency(p.adjusted)}
+ |
+
+ {/each}
+
+
+
+
+
+
+
+
+ {:else if flowState === 'preview' && !preview}
+
+
+ {:else if flowState === 'submitting'}
+
+
+
Adding bubble prize...
+
+ {/if}
+
+
+
diff --git a/frontend/src/lib/components/DealFlow.svelte b/frontend/src/lib/components/DealFlow.svelte
new file mode 100644
index 0000000..360c12a
--- /dev/null
+++ b/frontend/src/lib/components/DealFlow.svelte
@@ -0,0 +1,519 @@
+
+
+
+ {#if step === 'idle'}
+ {#if remainingPlayers >= 2}
+
+ {/if}
+ {:else if step === 'type'}
+
+
+
+
+
+ Prize money and league positions are independent.
+
+
+
+ {#each dealTypes as dt}
+
+ {/each}
+
+
+ {:else if step === 'input'}
+
+
+
+
+ {#if selectedType === 'icm' || selectedType === 'chip_chop'}
+
Enter chip stacks for each remaining player.
+
+ Chip stacks will be loaded from the tournament when connected.
+ Enter manual overrides if needed.
+
+ {:else if selectedType === 'custom'}
+
Enter payout amount for each player.
+
+ Total must equal the prize pool ({formatCurrency(prizePool)}).
+
+ {:else if selectedType === 'partial_chop'}
+
How much to split now?
+
+
+
+
+ Remaining {formatCurrency(prizePool - partialSplitAmount)} stays in play.
+
+
+ {/if}
+
+
+
+
+
+
+ {:else if step === 'review'}
+
+
+
+
+ {#if proposal}
+
+ {dealTypes.find((d) => d.value === selectedType)?.label ?? selectedType}
+
+
+
+
+
+ | Player |
+ Chips |
+ Original |
+ Proposed |
+
+
+
+ {#each proposal.players as p}
+
+ | {p.player_name} |
+ {formatCurrency(p.chips)} |
+ {formatCurrency(p.original_payout)} |
+ {formatCurrency(p.proposed_payout)} |
+
+ {/each}
+
+
+
+ {#if selectedType === 'partial_chop'}
+
+ Split: {formatCurrency(proposal.amount_to_split)} |
+ Remaining in play: {formatCurrency(proposal.amount_in_play - proposal.amount_to_split)}
+
+ {/if}
+
+
+ Prize money and league positions are independent.
+
+
+
+
+
+
+ {:else}
+
Calculating proposal...
+ {/if}
+
+ {:else if step === 'submitting'}
+
+ {/if}
+
+
+
diff --git a/frontend/src/lib/components/PrizePoolCard.svelte b/frontend/src/lib/components/PrizePoolCard.svelte
new file mode 100644
index 0000000..ef7bd95
--- /dev/null
+++ b/frontend/src/lib/components/PrizePoolCard.svelte
@@ -0,0 +1,281 @@
+
+
+
+
+
+ {#if expanded}
+
+
+
+
+
+ | Entries |
+ {financials.buyin_count ?? 0} |
+ × |
+ {formatCurrency(financials.buyin_amount ?? 0)} |
+ {formatCurrency(financials.total_buyin)} |
+
+
+ {#if (financials.rebuy_count ?? 0) > 0}
+
+ | Rebuys |
+ {financials.rebuy_count} |
+ × |
+ {formatCurrency(financials.rebuy_amount ?? 0)} |
+ {formatCurrency(financials.total_rebuys)} |
+
+ {/if}
+
+ {#if (financials.addon_count ?? 0) > 0}
+
+ | Add-ons |
+ {financials.addon_count} |
+ × |
+ {formatCurrency(financials.addon_amount ?? 0)} |
+ {formatCurrency(financials.total_addons)} |
+
+ {/if}
+
+ {#if (financials.reentry_count ?? 0) > 0}
+
+ | Re-entries |
+ {financials.reentry_count} |
+ × |
+ {formatCurrency(financials.reentry_amount ?? 0)} |
+ {formatCurrency(financials.total_reentries ?? 0)} |
+
+ {/if}
+
+
+ | Total Contributions |
+ {formatCurrency(financials.total_collected)} |
+
+
+
+ | Rake |
+ -{formatCurrency(financials.house_fee)} |
+
+
+ {#if financials.rake_breakdown && financials.rake_breakdown.length > 0}
+ {#each financials.rake_breakdown as rb}
+
+ | {rb.category} |
+ -{formatCurrency(rb.amount)} |
+
+ {/each}
+ {/if}
+
+ {#if financials.season_reserve > 0}
+
+ | Season Reserve |
+ -{formatCurrency(financials.season_reserve)} |
+
+ {/if}
+
+
+ | Prize Pool |
+ {formatCurrency(financials.prize_pool)} |
+
+
+
+
+ {/if}
+
+
+ {#if financials.guarantee && financials.guarantee > 0}
+
0}>
+ {#if financials.guarantee_shortfall > 0}
+ Guarantee: {formatCurrency(financials.guarantee)} (house covers {formatCurrency(financials.guarantee_shortfall)})
+ {:else}
+ Guarantee: {formatCurrency(financials.guarantee)} (met)
+ {/if}
+
+ {/if}
+
+
+
diff --git a/frontend/src/lib/components/TransactionList.svelte b/frontend/src/lib/components/TransactionList.svelte
new file mode 100644
index 0000000..0238307
--- /dev/null
+++ b/frontend/src/lib/components/TransactionList.svelte
@@ -0,0 +1,210 @@
+
+
+
-
Financials
-
Prize pool and payout information.
-
{#if tournament.financials}
{@const fin = tournament.financials}
-
-
- Total Buy-ins
- {fin.total_buyin.toLocaleString()}
-
-
- Total Rebuys
- {fin.total_rebuys.toLocaleString()}
-
-
- Total Add-ons
- {fin.total_addons.toLocaleString()}
-
-
- Prize Pool
- {fin.prize_pool.toLocaleString()}
-
-
- House Fee
- {fin.house_fee.toLocaleString()}
-
-
-
Paid Positions
-
{fin.paid_positions}
+
+
+
+
+
+ {#if fin.payouts && fin.payouts.length > 0}
+
+
Payout Preview
+
+
+
+ | Position |
+ Percentage |
+ Amount |
+
+
+
+ {#each fin.payouts as payout}
+
+ |
+ {payout.position}{ordinalSuffix(payout.position)}
+ {#if payout.player_name}
+ ({payout.player_name})
+ {/if}
+ |
+
+ {fin.prize_pool > 0
+ ? ((payout.amount / fin.prize_pool) * 100).toFixed(1)
+ : '0.0'}%
+ |
+ {formatCurrency(payout.amount)} |
+
+ {/each}
+
+
+ {#if fin.rounding_denomination > 0}
+
+ Rounded to nearest {formatCurrency(fin.rounding_denomination)}
+
+ {/if}
+ {/if}
+
+
+ {#if tournament.id}
+
+ {/if}
+
+
+ {#if tournament.id}
+
+ {/if}
+
+
+
+
Transactions
+
+
{:else}
+
+
No financial data available yet.
{/if}