diff --git a/PROJECT.md b/PROJECT.md
index d222e4a..5d9cdae 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -491,6 +491,34 @@ 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 3 — the character card, badges, the skill trail (2026-08-29, STAGING ONLY)
+
+Phase 3. STAGING ONLY, like 1 and 2.
+
+- **THE CHARACTER CARD** (`#card`, main.buildCard): the splash's LEVEL STRIP is the door (tap it —
+ it wears `cursor: pointer` and nothing else changed on the splash). Name as the screen title,
+ the live sprite (drawLook, third canvas on refreshLook), the current league's crest + name in
+ the league colour, LEVEL in Bangers over the gradient bar with "N XP · M TO LEVEL X", an
+ HONOURS row, and THE LEDGER (two .figs rows off stats.read per mode: kills / multi-kills /
+ deaths, best run / most felled / runs). EDIT PLAYER borrows the identity screen and returns to
+ the card (`playerReturn`). Deviation from the plan: no death-screen entry — the death screen
+ already carries the XP moment inline, and its tap is the restart.
+- **The honours row is a COLLECTION**: all five league shields plus the champion's laurel always
+ visible, earned bright, unearned dark (`filter: grayscale brightness`) — the empty slots are
+ the incentive. A shield is earned by FINISHING its league (being past it); league five's lights
+ with the champion crown, since there is no league six to be past it.
+- **Badges rolled via OpenRouter** (one sheet on #FF00FF, cut by connected components + chroma
+ key, 4x NEAREST downscale): `badge_l1..l5.png` (terracotta / slate / grass / navy-ice /
+ marble-gold shields, each with the crossed-rackets motif and a chip of decay) + `badge_champ.png`
+ (gold laurel + cup). DOM `` only — not in SPR, nothing draws them on canvas yet.
+- **THE SKILL TRAIL**: `LEAGUES` rows carry `color` (each riding its court's palette), and the
+ player's OWN sprint dust wears it — `dustPuff` grew an optional colour, the mover's emit passes
+ `skillDust()`. League 1 keeps the shipped court dust: the tint itself is a progression reward.
+ Read off the profile (not `G.league`), so the gore mode shows your colour too. The aura stays
+ made of MOTION, never paint — the full-body glow idea was dropped against the painted-layer veto.
+- Pane-verified: card at league 5 (four bright shields, dark L5 + laurel, ledger numbers, edit
+ round trip). Suite unchanged at 121 — the card is DOM, the dust is a colour.
+
## Session log: THE LEAGUES, PHASE 2 — bands, pace tiers, courts, the promotion (2026-08-29, STAGING ONLY)
Phase 2 of the plan. STAGING ONLY, like phase 1.
diff --git a/assets/badge_champ.png b/assets/badge_champ.png
new file mode 100644
index 0000000..c76e0c8
Binary files /dev/null and b/assets/badge_champ.png differ
diff --git a/assets/badge_l1.png b/assets/badge_l1.png
new file mode 100644
index 0000000..d550140
Binary files /dev/null and b/assets/badge_l1.png differ
diff --git a/assets/badge_l2.png b/assets/badge_l2.png
new file mode 100644
index 0000000..a12cce7
Binary files /dev/null and b/assets/badge_l2.png differ
diff --git a/assets/badge_l3.png b/assets/badge_l3.png
new file mode 100644
index 0000000..90b73f0
Binary files /dev/null and b/assets/badge_l3.png differ
diff --git a/assets/badge_l4.png b/assets/badge_l4.png
new file mode 100644
index 0000000..e849bdd
Binary files /dev/null and b/assets/badge_l4.png differ
diff --git a/assets/badge_l5.png b/assets/badge_l5.png
new file mode 100644
index 0000000..dbe7e10
Binary files /dev/null and b/assets/badge_l5.png differ
diff --git a/index.html b/index.html
index a53ffc5..d2c1070 100644
--- a/index.html
+++ b/index.html
@@ -174,7 +174,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
court through between menus. */
- #splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #bracket, #vs, #train, #player, #promo { display: none; }
+ #splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #bracket, #vs, #train, #player, #promo, #card { display: none; }
#intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; }
/* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a
screen whose content does not fill it */
@@ -190,6 +190,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
#intro[data-stage="vs"] #vs,
#intro[data-stage="player"] #player,
#intro[data-stage="promo"] #promo,
+ #intro[data-stage="card"] #card,
#intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1;
animation: drop 0.3s ease-out; }
/* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the
@@ -274,14 +275,31 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
image-rendering: pixelated; animation: drop 0.5s ease-out 0.5s backwards; }
#prCourtName { margin: 0; font: 400 10px Silkscreen, monospace; color: var(--acid);
letter-spacing: 1.5px; animation: drop 0.5s ease-out 0.62s backwards; }
+ /* THE CHARACTER CARD (the leagues, phase 3): the player writ large — sprite, crest, level,
+ honours, ledger. The honours row is a collection with visible empty slots: the unearned
+ badges sit dark, which is the whole incentive. */
+ #cdTop { display: flex; gap: 14px; align-items: flex-start; margin-top: 14px; }
+ #cdLook { position: static; display: block; flex: none; margin: 0; width: auto; height: 200px;
+ touch-action: auto; image-rendering: pixelated; }
+ #cdMeta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
+ #cdBadge { width: 56px; image-rendering: pixelated; }
+ #cdLeague { font: 700 10px Silkscreen, monospace; text-decoration: none; letter-spacing: 1.5px; }
+ #cdLevel { font: 400 30px Bangers, cursive; color: var(--cream); letter-spacing: 1.5px; }
+ #cdXp { display: block; margin-top: 5px; font: 400 9px Silkscreen, monospace; color: var(--dim);
+ text-decoration: none; letter-spacing: 1px; }
+ #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); }
/* 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; }
#xpRow u { font: 700 9px Silkscreen, monospace; color: var(--cream); text-decoration: none;
letter-spacing: 1px; white-space: nowrap; }
- #xpRow s { flex: 1; height: 6px; background: rgba(255,255,255,0.10); border: 1px solid var(--line);
+ #xpRow { cursor: pointer; } /* the strip IS the door to the character card */
+ #xpRow s, #cdBar s { flex: 1; height: 6px; background: rgba(255,255,255,0.10); border: 1px solid var(--line);
text-decoration: none; display: block; }
- #xpRow s i { display: block; height: 100%; width: 0%;
+ #cdBar s { height: 8px; }
+ #xpRow s i, #cdBar s i { display: block; height: 100%; width: 0%;
background: linear-gradient(90deg, #3f9560, var(--acid) 55%, #e8b23a);
transition: width 0.4s ease; }
/* KIT and SKIN: a row of colour squares, under a label that names whatever is selected. That is
@@ -629,6 +647,30 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
+
+