Before v0.155 a captain could register a paid cup with an empty wallet and sit on the row indefinitely; no UI to back-fill payment, no "manage my team" surface, no enforcement of "everyone pays before the team can change shape". v0.155 closes this from four angles:
Lock predicate. New `isTeamLocked(cup, team)` helper returns true iff the cup is paid AND `paid_player_count < roster_size` (per_team: `paid_player_count == 0`). Every captain mutation (edit team, withdraw team, kick/add player) opens with that check and short-circuits to a typed `is_team_locked` error → 409. Pre-existing unpaid rows are LOCKED automatically; once paid in full via `/register` they unlock on next page load.
Paid-seat portability. When the captain of a paid cup kicks a teammate who's already paid, the row is NOT deleted - marked `released_at = now()` with `paid_at` and `paypal_order_id` preserved. That released row becomes addressable by `rejoinFromReleasedIfAny`, so the same player can join another team in the same cup without hitting PayPal again. `addCupPlayerByCaptain` tries the recycle path first, falls back to a fresh INSERT.
Schema. `migrations/0146_cup_players_released_at.sql` adds nullable `released_at` and rebuilds to relax UNIQUE(cup_team_id, user_id) to UNIQUE(cup_team_id, user_id, released_at). Hot-path index on (user_id, released_at, paid_at) keeps the recycle scan constant-time.
Captain management card on `/cups/<slug>`. Each roster card the viewer is captain of now renders a per-team `PAID`/`PARTIAL`/`UNPAID` badge plus a Manage sub-card with: rename (display_name, lock-gated), add teammate (with "recycles paid seat if released" hint), kick non-captain (releases the row, decrements paid_player_count if applicable), withdraw (lock-gated). Every button on a locked team is toggled off client-side; the server is still source of truth. Five new endpoints sit under `/api/cups/:id/teams/:teamId/...`: GET manage, POST edit, POST withdraw, POST players/<id>/remove, POST players/add. All open with requireAuth + captain-actor + (where applicable) isTeamLocked short-circuit; locks deliver a friendlier "Pay your team in full before editing." instead of a bare error code.
No backfill. Migration 0146 does NOT fill `paid_player_count`; existing unpaid rows stay unpaid. So immediately after deploy every roster with unpaid seats is locked.
Files touched: `src/lib/cups-db.ts` (7 new exports), `src/routes/cups.ts` (5 new routes), `src/lib/cups-pages.ts` (per-team pill + Manage card + disabled-on-lock), `migrations/0146_cup_players_released_at.sql` (schema-only), companion `_db_changelog_v0.155.sql` carries the public `/changelog` body, `package.json` bumped to 0.155.