27 KiB
Dashboard polish — rail + licks (task D-70)
Concept doc for sprint-dashboard-polish. Revises the rail/licks layout that
docs/design/one-screen.md §4 and D-51 established, per the user directive of
2026-07-13 ("this looks amazing" + six refinements). No code here — this specs
the two bounded implementation tasks L-70 (rail) and L-71 (licks) and
proves the numbers.
Design tokens only (tailwind.config.js): surface #0f0f0f, panel #1a1a1a,
border #2a2a2a, accent #a855f7, plus the SVG note language already mirrored
in MiniPiano/ChordDiagram/LickCard (amber #f59e0b secondary, ACCENT_SOFT
#c084fc chord-tone, BASS_RING #fbbf24). No new colour is introduced.
The column geometry we build against (from D-51 / one-screen.md §4, unchanged):
right column 500px
− section border+p-2 −18
− row border+p-1.5 −14
− vertical scrollbar −17 (classic Windows) … −8 (the thin bar we add in §2)
= row interior 451px worst case (classic) · 460px (thin) · 468 (none)
All fits below are proven against the conservative 451px interior; the thin scrollbar we introduce in §2 only ever makes them more comfortable.
0. Scope boundary — dashboard only (user refinement 2026-07-13)
"perhaps within the knowledge center it could be nice but leave it for now as is, only in the main JAM FULL SCREEN we need to optimize what we show."
The net boundary (two settled user clarifications, 2026-07-13):
- Dashboard (always-open band + ⛶ jam-view fullscreen) = full visual optimisation: guitar ≤4, 2×2 smaller piano, uniform licks, dark scoped scrollbars, no ▶.
- Knowledge Center dock (
KnowledgeDock's VoicingsSection + LicksSection, the Circle of Fifths) andChordDetailModal= layout UNCHANGED (all shapes, normal piano, current lick size) except the ▶ play buttons are removed — the user decided "then leave them off, better not," so ▶ comes off everywhere, not just the rail.
So there are two kinds of change with two scopes: the visual layout changes are dashboard-only (gated behind the dashboard fork), while the ▶ removal is a deliberate global change across every mount.
The trap is shared components:
| component | dashboard mount | Knowledge Center / modal mount | how they differ today |
|---|---|---|---|
VoicingBrowser |
GlanceRail rows + heard-live (dense) |
VoicingsSection + ChordDetailModal (no dense) |
the dense flag already forks them |
LickCard |
LicksStrip (size="thumb") |
LicksSection (size="full") |
the size prop already forks them |
MiniPiano |
rail thumbs (size="thumb") |
modal/ExplorePanel (size="thumb"/"full") |
needs a new size="mini" variant |
PianoLickCard |
LicksStrip (new wiring) | never mounted | no KC concern at all |
The rule: gate the visual optimisations behind the dashboard fork and change no default there; remove ▶ globally. Concretely:
VoicingBrowser— visual, dashboard-only: guitar ≤4 + piano 2×2 apply only underdense(the flag only dashboard mounts pass); non-dense (VoicingsSection + ChordDetailModal) keeps all shapes + the normal piano layout. Global: the ▶ is removed from the component outright (§3) — no mount wants it anymore.MiniPiano: the smaller keyboard is a new additivesize="mini";thumbandfullare byte-untouched, so the modal/ExplorePanel render identically.LickCard: the uniform footprint is imposed at the dashboard-strip mount (a wrapper box), not inLickCard— its default footprint is unchanged.LickCarditself is edited only if it must lose a ▶ (it has none — see §3.3), so the dock's LicksSection (size="full") renders unchanged.PianoLickCardis all-new to the strip.- Scrollbar CSS: a scoped
.dark-scrollclass on the dashboard scrollers only — the KC's own scrolling keeps the OS default.
Restated L-70/L-71 byte-identity DoD: the dock's VoicingsSection /
LicksSection and ChordDetailModal render byte-identical to today EXCEPT the
▶ buttons are gone (the D-41/D-51 non-dense-mount precedent — Critic
diff-checks it: the only permitted diff in those mounts is the removed play
button + its now-dead wiring).
1. Guitar: ≤4 shapes, recommended-first (dashboard dense only)
1.1 What changes structurally
Today the rail's guitar station shows a separate own-cell (the KB play's
recommended shape, accent border, "play" badge — GlanceRail StationRow
:215-231) plus VoicingBrowser's full gallery of every placeable shape
(matchingShapes :76 returns all). Two problems: (a) the recommended shape is
usually also in matchingShapes, so it renders twice; (b) maj/min/dom7
have 5-6 placeable shapes, so own-cell + gallery = 6-7 cells that wrap to a
second line.
Decision: centralise the ≤4 rule inside VoicingBrowser and delete the
separate own-cell. GlanceRail passes the recommended shape down; the browser
renders it as the badged first cell and caps the total.
- New
VoicingBrowserprops (additive, default-off so modal/dock stay byte-identical):recommended(a shape object) andmax(number). GlanceRail(dense rail) passesrecommended={st.shape}andmax={4}.- Non-dense mounts (
ChordDetailModalGuitar tab,KnowledgeDockVoicingsSection) pass neither → they keep showing all shapes with no cap (a detail view should be exhaustive; the ≤4 fit constraint is the rail's alone).
This removes the pre-existing duplicate and guarantees ≤4 in one place.
1.2 The top-4 selection rule (exact ordering + tiebreak)
matchingShapes(quality, rootPc) still returns every placeable shape. The rail
then orders and slices:
- Recommended first — the KB play's shape (
recommended, matched into the list bylabel; if it was filtered out as unplaceable, prepend it anyway). Badgedplay, accent border — "the answer" prominence the old own-cell had, now cell #1 of the row. - Open-position forms — shapes with an
Array.isArray(shape.frets)grip (open chords). They sit at the nut, sound the most idiomatic, and are what a player reaches for first. - Movable / barre forms by lowest base fret ascending — closest to the nut
first (easiest hand position, most common voicing). Base fret is the value
matchingShapesalready computes (mod12(rootPc − OPEN_PCS[idx]), 0→12). - Tiebreak at equal category/base fret: fewer muted strings first
(count of
'x'infrets/offsets— a fuller voicing wins), then the shape's declared order inGUITAR_SHAPES[quality](stable, honours the KB author's priority). Deterministic — no reflow flicker as the root moves.
Take the first 4 (recommended + up to 3 more, recommended deduped so it never repeats). Qualities with <4 placeable shapes just show what they have — the rule is a cap, never padding; nothing is invented to reach four.
1.3 It fits one line
Guitar GalleryCell (dense, no play button — see §3): ChordDiagram thumb
75px + p-1.5 (12) + border (2) = 89px box. Four across:
4×89 + 3×6 (gap-1.5) = 356 + 18 = 374 ≤ 451 ✓ (a 5th would be 469 ✗)
Four shapes on one horizontal line, no wrap, no scroll — exactly the user's "4 guitar options visible so it would fit without scrolling."
2. Piano: 2×2 of compact MiniPianos (dashboard dense only)
2.1 The grid
Under dense the four pianoVoicing styles (root / shell / rootlessA
/ rootlessB, PIANO_STYLES :67) render as a 2-column × 2-row CSS grid
(grid-cols-2 gap-1.5) — one clean 2×2 per chord. Non-dense (VoicingsSection,
ChordDetailModal) keeps today's flex-wrap gallery at MiniPiano size="thumb"
(0.8) with ▶ — byte-identical. As with guitar, the separate own-cell is
dropped for piano; the 2×2 is the chord's voicings. If the station's
authored voicing corresponds to one of the four styles (match on
voicing.style), that cell gets the accent border — the "recommended" signal,
for free, without a fifth cell. (Fuzzy-authored voicings that match no style →
no highlight; honest.)
Because the own-cell is gone, the piano grid spans the full row interior
(the basis-[320px] flex-1 gallery column grows to ~451px), so each of the two
grid columns is (451 − 6)/2 = 222.5px.
2.2 The scale — arithmetic
MiniPiano VOICING thumb geometry: baseW = 22·(7·OCTAVES + 1) + 2,
SVG_W = baseW · scale, SVG_H = (60 + 4)·scale. Today thumb scale = 0.8:
| crop | baseW | SVG_W @0.8 | cell box @0.8 (+14) | two-up + gap |
|---|---|---|---|---|
| 1-octave | 178 | 142.4 | 156.4 | 318.8 ≤ 451 ✓ |
| 2-octave | 332 | 265.6 | 279.6 | 565.2 > 451 ✗ |
So 0.8 cannot put two 2-octave crops side by side (the brief's 532>456). The
2-octave crop (the rootless 7th-chord voicings — rootlessA/B of min7/dom7/
maj7 whose span pushes past one octave) dictates the scale. Solve for a column
of 222.5px:
332·scale + 14 ≤ 222.5 → scale ≤ 208.5 / 332 = 0.628 (the ceiling)
Chosen: a new uniform size="mini" at scale 0.60 — below the 0.628 ceiling,
leaving a robust ~19px margin for sub-pixel rounding and the classic-scrollbar
worst case:
| crop @ 0.60 | SVG_W | cell box (+14) | two-up + gap (6) | fits 451? |
|---|---|---|---|---|
| 1-octave | 106.8 | 120.8 | 247.6 | ✓ (203 spare) |
| 2-octave | 199.2 | 213.2 | 432.4 | ✓ (18.6 spare) |
grid-cols-2 sizes every cell to the 222.5px column, so 1- and 2-octave cells
share one uniform grid — a true 2×2 at both crops. A thumb crop never exceeds
2 octaves (a 4-note pianoVoicing spans ≤24 semitones; OCTAVES = ceil((maxNote − octStart·12)/12) ≤ 2), so 2-octave is the proven worst case —
no 3-octave escape hatch needed.
2.3 Legibility at 0.60 — the marks stay honest
White key = 22·0.60 = 13.2px wide (vs ChordDiagram's 11px string gap —
comparable, legibly playable). The root/bass signal is carried primarily by
colour, which does not shrink in meaning: root key = full accent #a855f7, other
tones = ACCENT_SOFT #c084fc, bass = amber ring #fbbf24 — the established tier
language. The "R" glyph and the amber ring are secondary reinforcement. Because
the SVG viewBox scales text with the keys, at 0.60 the R renders ~4.8px and the
ring ~1.2px — thin. So size="mini" bumps the in-SVG R fontSize (8→10 in
viewBox units) and the bass-ring strokeWidth (2→2.5) so both stay readable at
the smaller render. This is an additive branch in MiniPiano.jsx (Muse-owned SVG
renderer); the existing thumb/full paths are byte-untouched, so the modal and
dock render identically.
2.4 Row height (piano)
MiniPiano mini SVG_H = 64·0.60 = 38.4px. Piano GalleryCell (dense, no
play): caption ~13 + gap-1.5 (6) + SVG 38.4 + p-1.5 (12) + border (2) = 71.4px.
Two rows + grid row-gap (6) = 148.8px for the 2×2 block — versus the old
gallery's ~3 wrapped cell-lines of ~118px each (~366px). Nearly halved (§6).
3. Remove ▶ everywhere (deliberate global removal)
Standing principle (memory): glance over audio. The user settled it — "then
leave them off, better not." So ▶ comes off every mount: the dashboard rail,
the dashboard strip, and the Knowledge Center + ChordDetailModal. Because no
mount wants playback, the machinery is deleted outright, not gated — this is
intended, not a side-effect.
3.1 VoicingBrowser.jsx (L-70) — delete the play machinery
- The
PlayButtoncomponent (:103-121). import { playVoicing, guitarShapeToNotes } from '../lib/chordAudio'(:62).handleRef+stopCurrent(:184-188) and theuseEffect(() => stopCurrent, [chordKey])cleanup (:191) — both now dead.playGuitar/playPianohandlers (:197-211).- The
<PlayButton>insideGalleryCell(:149) and itsplayLabel/onPlayprops; theplayLabel/onPlaypassed at each call site (:243-244, :280-281). - The mic-feedback microcopy (:300-309) — nothing plays anywhere now, so the caveat is false; delete it.
- Now-unused after the above: the
useRef/useEffectimports and thechordKeylocal (only the deleted effect read it). Update the file-header comment.
This is the only permitted change to the non-dense (VoicingsSection /
ChordDetailModal) render — those mounts are otherwise byte-identical (all shapes,
normal piano layout); the visual dense-fork of §1/§2 leaves them alone.
3.2 GlanceRail.jsx (L-70) — dashboard-exclusive
GlanceRail is mounted only by the JamGuide rail, so its edits never leak:
- Delete the footer microcopy "▶ previews play through your speakers…" (:292-295); replace with nothing (or a quiet "voicings follow the loop").
- Delete the own-cell
<figure>block (:215-231) — folded intoVoicingBrowserper §1; passrecommended={st.shape}+max={4}. - Update the header comment (":45 …every ▶ lives inside the gallery").
3.3 The licks strip (L-71)
LickCard.jsxhas no play path — the guitar tab card never had a ▶ (verified: noPlayButton, nochordAudioimport). Nothing to remove there; the dock'ssize="full"LickCards are unchanged.PianoLickCard.jsxis the only lick ▶. Remove:import { playVoicing, stopAll } from '../lib/chordAudio'(:73); thePREVIEW_BPM/currentSeq/stopLick/playLicksequencer (:167-198); thePlayButtoncomponent (:484-502) and its render (:606-609); theuseEffect(() => () => stopLick(), [])unmount silence (:530, then drop the now-unuseduseEffectimport). Keep theresolveDegreeimport — realization needs it.
3.4 chordAudio.js stays — verified importers
grep chordAudio src/** → importers are VoicingBrowser.jsx (▶ deleted in
§3.1), PianoLickCard.jsx (▶ deleted in §3.3), and BassPatternCard.jsx
(untouched — the bass-rail pattern previews the user did not ask to remove,
out of both locks); the rest are doc comments in voicings.js.
ChordDetailModal.jsx does not import it directly — it plays via
VoicingBrowser, so deleting that ▶ removes the modal's too, exactly as the
user's global decision intends. So chordAudio.js stays (for BassPatternCard),
but nothing in the voicing/lick UI plays.
3.5 The shared-sequencer blocker is eliminated
D-60 flagged that BassPatternCard and PianoLickCard each hold a private
module-level currentSeq, so their previews could layer once both mount — the
reason PianoLickCard was never wired into the strip. Removing PianoLickCard's
playback deletes its currentSeq entirely: there is no piano sequencer left to
conflict with. The strip becomes purely visual; BassPatternCard's sequencer
lives only under the BASS selector (where the strip is hidden anyway). So
PianoLickCard wires into the strip (§5) with no currentSeq collision —
confirmed.
4. Hidden-but-scrollable dark scrollbars
4.1 Mechanism — a scoped utility, defined once in index.css (L-70)
/* Thin, dark, overlay-feel scrollbars for the jam dashboard's scrollers.
Scoped (a class), NOT global — the KnowledgeDock/Debug/Tuner/Settings
below keep the OS default. Raw hex is unavoidable here: ::-webkit-scrollbar
pseudo-elements are not reachable by Tailwind utilities. The values mirror
the design tokens (border #2a2a2a, panel #1a1a1a). */
.dark-scroll {
scrollbar-width: thin; /* Firefox */
scrollbar-color: #2a2a2a transparent; /* thumb=border token · track transparent */
}
.dark-scroll::-webkit-scrollbar { width: 8px; height: 8px; } /* WebKit/Blink */
.dark-scroll::-webkit-scrollbar-track { background: transparent; }
.dark-scroll::-webkit-scrollbar-thumb {
background: #2a2a2a; border-radius: 4px; /* border token */
}
.dark-scroll::-webkit-scrollbar-thumb:hover { background: #3a3a3a; }
- Scoped, not global (
*): the recommendation is a.dark-scrollopt-in class. Global would restyle every scroller in the app (the whole normal-mode page, the dock, Settings' full-screen scroller). The user's ask is about the dashboard's rail; keep the blast radius there. - Raw-hex exception, flagged to Maestro: the DoD says "no raw hex outside
tailwind.config.js," but::-webkit-scrollbarcannot consume a Tailwind class.index.css(which already carries the raw#0f0f0fbody background) is the correct home; the values are documented as mirroring thebordertoken. No new colour — #3a3a3a hover is a one-step lift of the same family; if Maestro prefers, usepanel#1a1a1a for the base andborder#2a2a2a for hover instead (both existing tokens). Either is fine. - Contrast: a #2a2a2a thumb on the #0f0f0f/#1a1a1a surface is intentionally quiet — it is chrome, not content, so it is exempt from AA text contrast; it is still clearly grabbable (the point of "make them black or something").
4.2 Where it applies — every dashboard scroll container
| # | scroller | file:line | task | note |
|---|---|---|---|---|
| 1 | rail right column xl:overflow-y-auto |
JamGuide.jsx:484 |
L-71 | the headline visible bar |
| 2 | left column (jam-view) xl:overflow-y-auto |
JamGuide.jsx:475 |
L-71 | consistency |
| 3 | LicksStrip horizontal overflow-x-auto |
JamGuide.jsx:888 |
L-71 | the licks row |
| 4 | per-cell overflow-x-auto (voicing) |
VoicingBrowser.jsx:148 |
L-70 | rarely triggers now (§2) — apply for the edge case |
| 5 | timeline/keyboard overflow-x-auto |
PianoLickCard.jsx:576,585 |
L-71 | wide-lick edge case |
File-collision resolution (the brief's ask): the rail's outer vertical
scroller lives in JamGuide.jsx, which is L-71's exclusive lock — not
GlanceRail, not App.jsx. So the class definition ships in index.css
(L-70), and each className="… dark-scroll" application is made by whichever
task owns the file it lives in: L-70 applies it in VoicingBrowser (#4), L-71
applies it in JamGuide (#1-3) and PianoLickCard (#5). The class exists before
L-71 runs because L-71 depends-on L-70 (already in the ledger). Zero shared
files. The visible rail bar is styled in L-71 — acceptable, because that scroller
was always a JamGuide-column concern, not a GlanceRail one.
5. Licks: uniform size + follow the instrument
5.1 The strip follows the global instrument
instrument already reaches the strip: App's global selector → JamGuide
instrument prop (:177) → LicksStrip({ …, instrument }) (:433-445, :863).
Today the strip always renders LickCard (guitar), even under PIANO, with an
apologetic heading "(no piano licks in the KB yet)" (:885). Change:
guitar→LickCard(unchanged renderer).piano→PianoLickCard(wire it in — the D-60 component, never mounted). NeedsrootPc+chordLabelfrom the playheadcontextstation (which carriesrootPc/quality/label), and each lick carries its ownquality.bass→ the strip is already gated off (:432/:439instrument !== 'bass'); render a slim honest line "No bass licks in the KB yet" instead of nothing, so the section doesn't silently vanish when a bassist is selected.
The licks reader licksFor(id) (:136) is hardcoded to
instruments.guitar.licks — L-71 generalises it to
licksFor(id, instrument), reading instruments.piano.licks under piano, and
filtering piano licks to the structured ones (those with a notes array) so
prose-only entries don't render as PianoLickCard placeholders (see §5.3).
5.2 Uniform footprint — measure, target, change
Both are wrapped by the strip in w-[220px] (closed) / w-[340px] (open)
(:894), so width is already uniform. The gap is height — the two SVGs
auto-size by different aspect ratios:
LickCardthumb (layoutTab): viewBox 96 units tall (17 + 5·14 + 9),TabSvgmaxWidth = width·1.3; inside a 204px inner box it renders ~125px tall. Card ≈ 163px.PianoLickCardthumb (layoutLick): height = 14 +plotH+ 8 (+10 beat row),plotH = max(26, range·semi)— varies with the lick's pitch range, so a wide lick is much taller than a narrow one and neither matches LickCard.
Target: a shared thumb card of 220 × 150px (closed), 340px wide (open).
Imposed at the dashboard-strip mount, not inside the cards (per the scope
refinement — do not touch LickCard's default footprint). The LicksStrip
wrapper (JamGuide, L-71) that already sets w-[220px]/w-[340px] (:894) also
sets a fixed content height and normalises the card's SVG to fill it, e.g.
class="… h-[150px] [&_svg]:!h-[104px] [&_svg]:!w-full". The cards' SVGs already
carry a viewBox, so preserveAspectRatio="xMidYMid meet" (the SVG default)
scales the tab / timeline to the 104px box and centres it — zero edit to
LickCard or PianoLickCard internals. Every card — guitar tab or piano roll —
then occupies the same box, "all the same size," and flips instrument in
place. (The maxWidth: width·1.3/1.4 inline caps mean a short lick renders
narrower than 220 and centres — the card box is uniform; content is centred.)
Scope proof (KC untouched): the fixed-box CSS lives on the LicksStrip
wrapper only — a dashboard-exclusive element. LickCard's and PianoLickCard's
own geometry is unchanged, so the dock's LicksSection size="full" LickCards
(:817) render byte-identical (the L-71 DoD asserts it). PianoLickCard has no
KC mount at all.
5.3 KB coverage + empty-per-style behaviour
Verified against the KB (instruments.piano.licks with a structured notes
array — PianoLickCard-renderable):
| style | guitar licks | piano licks (structured) |
|---|---|---|
| jazz, blues, gospel, rnb | ✓ | ✓ (P-60/P-61) |
| pop | ✓ | prose only (over/description) — not renderable |
| bossa, country, funk, reggae, rock | ✓ | ✗ |
- Guitar: all 10 styles have tab licks — the strip works everywhere.
- Piano: structured, renderable licks exist for jazz, blues, gospel, rnb
only. Pop's
piano.licksare the old prose education entries (nonotes) — the structured filter drops them, so pop reads as empty under piano too. - Empty-per-style: today the strip returns
nullwhen empty (hides). Keep hide-on-empty for guitar (the default; a silent gap is fine). For piano with zero structured licks (pop + the 5 styles above), show a slim honest line — "No {style} piano licks yet" — because the user actively switched to piano and a vanished section is confusing there. Same slim line for bass.
6. Recomputed budget
Per-station row (row p-1.5 12 + border 2 + header block ~54 + gallery):
| gallery block | new row | pre-polish row | |
|---|---|---|---|
| Guitar | 4 cells ×1 line, no ▶ = 105px | ~173px | ~207px (5-6 cells, ▶, wraps) |
| Piano | 2×2 mini, no ▶ = 148.8px | ~217px | ~445px (0.8, ▶, 3 wrapped lines) |
Rail overhead (section p-2 16 + border 2 + h4 title ~22 + footer ~40) ≈ 80px;
rows joined by gap-2 (8px).
| loop | instrument | new total | pre-polish | Δ |
|---|---|---|---|---|
| 4-chord | guitar | 80 + 4·173 + 24 = 796 | ~932 | −15% |
| 4-chord | piano | 80 + 4·217 + 24 = 972 | ~1884 | −48% |
| 8-chord | guitar | 80 + 8·173 + 56 = 1520 | ~1720 | −12% |
| 8-chord | piano | 80 + 8·217 + 56 = 1872 | ~3640 | −49% |
Headline: the piano rail nearly halves (−48%), and the guitar row is now a guaranteed single line (≤4, never wraps). Every total still exceeds the 500px column, so the rail remains the dashboard's one vertical scroller — which is exactly what §2's dark thin scrollbar dresses, and what the user licensed ("it can scroll if we need to"). The polish makes each row compact and the scroll pretty; it does not (and need not) fit a whole loop in 500px.
7. Migration order + bounded scopes
L-70 — the rail. Files (file-disjoint from L-71):
src/components/GlanceRail.jsx, src/components/VoicingBrowser.jsx,
src/components/MiniPiano.jsx (new size="mini" + R/ring bump — additive),
src/index.css (define .dark-scroll).
Does: guitar ≤4 recommended-first, dense-only (§1); piano 2×2 mini @0.60,
dense-only (§2); ▶ deleted globally from VoicingBrowser, own-cell +
footer from GlanceRail (§3.1-3.2); .dark-scroll defined + applied to
VoicingBrowser's cell scroller (§4 #4). MiniPiano.jsx is safe to lock here — no
other polish task touches it (PianoLickCard/LickCard carry their own
keyboard geometry and do not import MiniPiano).
DoD byte-identity: the non-dense VoicingBrowser render (VoicingsSection +
ChordDetailModal) is byte-identical to today except the ▶ is removed;
MiniPiano thumb/full byte-untouched (new mini is additive).
L-71 — the licks. Files: src/components/JamGuide.jsx (LicksStrip +
licksFor + the fixed-box wrapper + the scroller classNames),
src/components/PianoLickCard.jsx. LickCard.jsx is not edited — it has no
▶ and its footprint is imposed from the strip wrapper (§5.2), so it drops out of
the lock (dock LicksSection unaffected).
Does: uniform 220×150 thumb via the strip wrapper (§5.2); instrument branch +
PianoLickCard wiring + piano/bass empty states (§5.1/5.3); ▶ deleted from
PianoLickCard (§3.3); .dark-scroll applied to JamGuide's three scrollers +
PianoLickCard's (§4 #1-3,5).
DoD byte-identity: the dock LicksSection's size="full" LickCards render
byte-identical to today (no ▶ existed to remove there).
Serialise L-70 → L-71 (already the ledger's depends-on): they are
file-disjoint (no shared file — the .dark-scroll class is defined in
L-70's index.css and merely referenced by L-71's JSX, which is not a file
edit collision), and L-71 needs L-70's class to exist. JamGuide.jsx belongs to
exactly one task (L-71), so the rail-column scrollbar has a single owner — no
shared-file clash. Critic gates each; C-70 closes the sprint.
Rejected alternatives
- Shrink
MiniPiano's globalthumbscale (0.8→0.6) instead of a newmini. Rejected — it also shrinks theChordDetailModaland dock thumbs, which have room to spare and benefit from the larger keys; a scopedminivariant keeps those byte-identical and confines the change to the rail. - Give 2-octave piano crops their own full-width row (mixed cell sizes) at a larger scale (~0.7). Rejected — it breaks the clean "2×2 for each chord" the user asked for; a uniform 0.60 grid keeps every chord a tidy 2×2 and 0.60 is still legibly playable (13.2px keys ≈ the guitar diagram's string gap).
- Keep the separate own-cell and cap the gallery to 3. Rejected — it leaves
the ≤4 rule split across two components (own-cell in
GlanceRail, cap inVoicingBrowser) and preserves the today's recommended/gallery duplicate; centralising inVoicingBrowser(§1.1) is one place, one rule, no dupe. - Global
*scrollbar styling. Rejected — restyles the entire app (dock/Debug/Tuner/Settings) for a dashboard-scoped ask; a.dark-scrollopt-in class is surgical.