diff --git a/PROJECT.md b/PROJECT.md
index 5d9cdae..cdf3f14 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -491,6 +491,37 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
+## Session log: THE LEAGUES, PHASE 4 — the duel learns, tin tiers, the level is you (2026-08-29, STAGING ONLY)
+
+Phase 4, the last of the built plan. STAGING ONLY — the whole feature waits on Oliver's own
+phone test before merging to main.
+
+- **THE DUEL LEARNS.** The diagnosis held: in a duel the racket zombie played with every ladder
+ term inert (chase collapsed to `racketSpeed`, read to flat `aiRead`, flub a flat 0.2, quality
+ capped 0.55) and only hp stiffened — at `duelFirst 2 / duelEvery 3` that made it a weapon
+ DISPENSER, the same inflation class the barrel economy was cured of. `gameplay.duelSkill()`
+ (0 → 1 over `TUNE.duelRampGames` 24, on `G.runGames` — gore's own clock, the league never
+ rekeys the director) now ramps its legs (`duelSpeedMax` +35%), read (`duelReadMax` 0.93),
+ whiff (`duelFlubCut`: 0.2 → 0.08) and return quality (`duelQBonus`: 0.55 → 0.75). Game 0 is
+ exactly the shipped duel (calibration rule). Losing already costs plenty (opponent game +
+ runGames++ + the 5-body surge + no tin/heal) — now it can actually happen late.
+- **THE TIN TIERS** (settled: no starting loadout). `tinLadder()` (pure, tested): a barrel weapon
+ joins the duel pot when the RUN unlocks it (as ever) OR when the LEAGUE says so —
+ `config.TIN_LEAGUE` lasers at 3, rockets at 4 — and league 5 duels pay DOUBLE tins
+ (`TIN_DOUBLE_LEAGUE`, the card says ×2N). "Weapons are only ever won" stays literal.
+- **THE LEVEL IS YOU**: `profile.bonus()` — level 1 = all factors 1 (shipped), scaling linearly
+ to the cap: melee cooldown FLOOR ×0.75 (the ceiling untouched), parry reach/arm ×1.25, hurt
+ grace ×1.35. Read at the dial sites (meleeCooldown, keyPressed's parry arm, zombies' parry
+ window + face-offset, hurtPlayer). Applies everywhere the dials do — gore included, which is
+ the cross-mode incentive the plan asked for.
+- **THE BOARD WEARS THE BADGE**: score rows carry `lg` (server clean() bounds it 1-5, default 1;
+ context, never a sort key), `stats.submit` takes it as a parameter (profile cannot be imported
+ there — cycle), main passes `prof.lg`, and drawRanks renders the little shield before the name.
+ Old rows carry none and show none — no migration.
+- Tests 121 → 124 (duel ramp shape + its caps stay fallible, tin tiers incl. the double tin and
+ the run-gate fallback, bonus() bounds + the grace landing on `G.hurtT`). Seeds fuzzed.
+ Server round-tripped live (`lg: 4` stored, served, and drawn as the navy shield on the board).
+
## Session log: THE LEAGUES, PHASE 3 — the character card, badges, the skill trail (2026-08-29, STAGING ONLY)
Phase 3. STAGING ONLY, like 1 and 2.
@@ -601,7 +632,7 @@ before the wider group) — do not merge to main until his verdict.
118, seeds fuzzed. Pane-verified end to end (identity flow, POST 200, death screen with +16 XP
and the bar; NOTE this pane session has rAF dead again — `T.step(1/60)` loops drove the clock).
-## THE LEAGUES — the meta-progression plan (2026-08-29, AGREED, PHASE 1 BUILT — see log above)
+## THE LEAGUES — the meta-progression plan (2026-08-29, AGREED; PHASES 1-4 BUILT ON STAGING — see the four logs above; phase 5 deliberately parked)
Worked out with Oliver in discussion, decisions settled below. The goal in his words: an incentive
to keep opening the game beyond beating the next opponent — levelling through play, with a clearly
diff --git a/index.html b/index.html
index d2c1070..66b05b3 100644
--- a/index.html
+++ b/index.html
@@ -290,6 +290,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
#cdBadges { display: flex; gap: 10px; align-items: flex-end; }
#cdBadges img { height: 46px; image-rendering: pixelated; }
#cdBadges img.dark { filter: grayscale(1) brightness(0.3); }
+ .rkbadge { height: 15px; margin-right: 6px; image-rendering: pixelated; flex: none; }
/* THE LEVEL STRIP: the one meta number over a thin bar, riding the splash foot. The fill colour
is the acid accent for now; the leagues will hand it the skill colour in phase 3. */
#xpRow { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; }
@@ -794,6 +795,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
-
+