
Le billet précédent s’arrêtait à T3 (suggestions ancrées). Cette vague ferme la promesse : tout ce que le handoff design 2a décrit — paquets par degré, rivière qui remonte, panneaux latéraux — est en code sur la branche essai/ui-ingame-2a. Le monde 3D n’a pas été touché.
Ce qui manquait au handoff après T3

Trois sections entières du design restaient à implémenter :
- §2 Paquets (besaces par degré, jauges, respiration du plus rempli).
- §3 Rivière (rubans qui montent de
y=800ày=150, deux passes par couloir). - §6 Panneaux latéraux (
DERNIERE NOTEà gauche,JOURNALà droite, accents#52d6d1/#7cf59a).
Le runtime avait déjà D-STATE (compteurs par degré, pastilles en vol, dernière note, journal) — la moitié du travail côté données. Il restait à peindre la peau.
Vague design 2a complète — sept chantiers
Plan : PLAN-ORCH-DESIGN-2a-COMPLET.md. Sept chantiers en trois vagues :
| Vague | Chantiers |
|---|---|
| 1 (parallèle) | D-POLISH, D-STATE |
| 2 (parallèle) | D-POUCH, D-RIVER, D-PANELS |
| 3 (séquentiel) | D-MOUNT, D-VERIFY |
D-POLISH a refermé les fuites de la fidélité : Cinzel 500 chargé, regroupement gap:14px du bloc droit de la topbar, transition de teinte des couloirs via @property --lane-color.
D-STATE a exposé runtime.pouchCounts, runtime.climbers, runtime.lastNote, runtime.journal — quatre getters purs, sans réécriture du routage MIDI. La causalité « la pastille arrive → le paquet grossit » est portée par settleClimbers (appelé dans le tick()).
D-POUCH — PouchRow.tsx
Une pochette par degré actif, centrée sur x(i). Pas un compteur abstrait : une forme (84×74, border-radius organique 42%/30%), un col, une jauge interne qui passe de 18px à 78px selon la part de session, un compte Space Grotesk 700 15 avec text-shadow: 0 0 12px #000. Le paquet le plus rempli respire via rjBreathe 3.4s ease-in-out.
Positionnement : le composant s’ancre lui-même dans la bande utile left:120 width:1040 et appelle laneCenterX — la même géométrie que les couloirs et les suggestions. Aucun risque de désynchronisation.
const centerXPct = ((laneCenterX(index, n) - BAND_LEFT) / BAND_WIDTH) * 100
D-RIVER — RiverOverlay.tsx
Une décision structurante au passage : pas de rewrite 3D pour la rivière qui monte. Le canon produit (profondeur = temps) tient debout ; les couloirs 2a doivent se lire à plat, pas en projection 3D.
Donc : un SVG plein cadre (viewBox 0 0 1280 800), plein position: absolute; inset: 0; z-index: 1, avec un ruban par couloir. Deux passes :
- halo :
stroke-opacity: .3,stroke-width: 34, filtrefeGaussianBlur stdDeviation=6; - courant :
stroke-opacity: .55,stroke-width: 2,stroke-dasharray: 12 16,rjFlow7…13s linéaire.
Le chemin part et arrive sur cx :
M cx 800 C cx-16 660 cx+16 520 cx 400 C cx-12 320 cx+12 250 cx 150
Le balancement gauche/droite est léger — il donne du courant sans déplacer la moyenne. Si la position du ruban dérivait, le X de la note ne tomberait plus sur le centre du couloir, et tout le reste se casserait. Le test riverPath vérifie que M cx 800 ouvre et cx 150 ferme.
prefers-reduced-motion coupe rjFlow, le SVG reste statique. Le monde 3D en arrière-plan n’est pas modifié.
D-PANELS — LastNotePanel + JournalPanel
Deux FloatingPanel 214px de large, top: 470px, à 18px des bords gauche / droit. Accents 2px :
- gauche
DERNIERE NOTE: bordure#52d6d1, nom enCinzel 700 30à la couleur de la note avectext-shadow: 0 0 24px #52d6d155. Une ligne▲ V · appui · couloir 3, une lignetension 0.15 · velocite 90. - droite
JOURNAL: bordure#7cf59a, lignesdt/ddJetBrains Mono 10,safePct/explorePct(destyleScores.computeStyleScores), paquet le plus rempli (fullestChroma/fullestCount), total recueilli.
Toutes les données viennent de runtime.lastNote / runtime.journal. Aucune métrique inventée : safePct est null tant que styleSampleSize === 0, et le panneau affiche — dans ce cas.
Le calcul du couloir passe par laneIndexForMidi(midi, tonic, mask) — la même fonction que DegreeCorridors et SuggestionStrip. Si FG change la gamme, la mention « couloir N » suit, ou « hors gamme » si la note n’est plus dans le mask.
Ce qui n’est pas fait
| Élément | Statut |
|---|---|
| Climbers DOM (pastilles qui montent, handoff §4) | hors essai 2a — runtime prêt, rendu visuel à faire si FG le demande |
Pulse rjFill à l’arrivée (handoff §2 « pochette pulse une fois ») | non animé — la transition height .35s suffit pour la causalité |
| Proposition rewrite axes 3D | hors essai (canon profondeur = temps) |
| Gate visuelle FG 1280×800 | recommandée avant de déclarer T4/T5 verts |
Migration des layouts persistés rainbowjourney.panel.* | non demandée |
Contôles réellement exécutés
node node_modules/typescript/bin/tsc -b --force: OK.npm run lint: OK.npx vitest run: 215/215 (30 fichiers ; +8 testsriverGeometry, +1 testhudDefaults).npm run build: OK (CSS 99 kB, JS 1.08 MB gzip 301 kB ; avertissements Zod et bundle volumineux préexistants).- Serveur
npx vite --port 5183 --strictPort: HTTP 200,Journey.tsxet ses nouveaux imports servis sans erreur.
Pas de Playwright dans cette passe (E2E complet = zone outillage, hors essai). Smoke DOM 1280×800 reste à faire par FG.
Pourquoi publier maintenant
Parce que la peau du jeu changeait sans qu’aucun billet public ne le raconte. Trois sections entières du design 2a, c’est un visage différent — pas une variation cosmétique. Le runtime porte la causalité, la géométrie est partagée, les panneaux latéraux sortent le joueur des panneaux flottants persistés pour entrer dans la composition 2a.
La branche essai/ui-ingame-2a reste en working tree après ce billet. Aucun commit n’a été poussé vers main ni vers le remote sans l’ordre explicite FG. La prochaine étape est la gate visuelle 1280×800 : c’est FG qui décide si le rendu matche, si la rivière SVG est trop chargée, si les paquets respirent au bon tempo.
Sources : docs/plans/essai-ui-ingame-2a/PLAN-ORCH-DESIGN-2a-COMPLET.md, handoffs/D-POUCH.md, handoffs/D-RIVER.md, handoffs/D-PANELS.md, handoffs/D-MOUNT.md, handoffs/D-VERIFY.md, handoffs/2026-09-15-design-2a-complet-handoff.md. Branche : essai/ui-ingame-2a. Miroir local : RainbowPiano/docs/devblog/.