# 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) **and `ChordDetailModal`** = **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 under `dense`** (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 additive `size="mini"`**; `thumb` and `full` are byte-untouched, so the modal/ExplorePanel render identically. - `LickCard`: the uniform footprint is imposed **at the dashboard-strip mount** (a wrapper box), **not** in `LickCard` — its default footprint is unchanged. `LickCard` itself is edited only if it must lose a ▶ (it has none — see §3.3), so the dock's LicksSection (`size="full"`) renders unchanged. `PianoLickCard` is all-new to the strip. - Scrollbar CSS: a **scoped `.dark-scroll` class** 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 `VoicingBrowser` props (additive, default-off so modal/dock stay byte-identical): `recommended` (a shape object) and `max` (number). - `GlanceRail` (dense rail) passes `recommended={st.shape}` and `max={4}`. - Non-dense mounts (`ChordDetailModal` Guitar tab, `KnowledgeDock` VoicingsSection) 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: 1. **Recommended first** — the KB play's shape (`recommended`, matched into the list by `label`; if it was filtered out as unplaceable, prepend it anyway). Badged `play`, accent border — "the answer" prominence the old own-cell had, now cell #1 of the row. 2. **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. 3. **Movable / barre forms by lowest base fret ascending** — closest to the nut first (easiest hand position, most common voicing). Base fret is the value `matchingShapes` already computes (`mod12(rootPc − OPEN_PCS[idx])`, 0→12). 4. **Tiebreak** at equal category/base fret: **fewer muted strings first** (count of `'x'` in `frets`/`offsets` — a fuller voicing wins), then the shape's **declared order in `GUITAR_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 `PlayButton` component (:103-121). - `import { playVoicing, guitarShapeToNotes } from '../lib/chordAudio'` (:62). - `handleRef` + `stopCurrent` (:184-188) and the `useEffect(() => stopCurrent, [chordKey])` cleanup (:191) — both now dead. - `playGuitar` / `playPiano` handlers (:197-211). - The `` inside `GalleryCell` (:149) and its `playLabel`/`onPlay` props; the `playLabel`/`onPlay` passed 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`/`useEffect` imports and the `chordKey` local (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 `
` block (:215-231) — folded into `VoicingBrowser` per §1; pass `recommended={st.shape}` + `max={4}`. - Update the header comment (":45 …every ▶ lives inside the gallery"). ### 3.3 The licks strip (L-71) - **`LickCard.jsx` has no play path** — the guitar tab card never had a ▶ (verified: no `PlayButton`, no `chordAudio` import). Nothing to remove there; the dock's `size="full"` LickCards are unchanged. - **`PianoLickCard.jsx` is the only lick ▶.** Remove: `import { playVoicing, stopAll } from '../lib/chordAudio'` (:73); the `PREVIEW_BPM`/`currentSeq`/ `stopLick`/`playLick` sequencer (:167-198); the `PlayButton` component (:484-502) and its render (:606-609); the `useEffect(() => () => stopLick(), [])` unmount silence (:530, then drop the now-unused `useEffect` import). Keep the `resolveDegree` import — 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) ```css /* 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-scroll` opt-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-scrollbar` cannot consume a Tailwind class. `index.css` (which already carries the raw `#0f0f0f` body background) is the correct home; the values are documented as mirroring the `border` token. No *new* colour — #3a3a3a hover is a one-step lift of the same family; if Maestro prefers, use `panel` #1a1a1a for the base and `border` #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). Needs `rootPc` + `chordLabel` from the playhead `context` station (which carries `rootPc`/`quality`/`label`), and each lick carries its own `quality`. - `bass` → the strip is already gated off (:432/:439 `instrument !== '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: - **`LickCard` thumb** (`layoutTab`): viewBox 96 units tall (17 + 5·14 + 9), `TabSvg` `maxWidth = width·1.3`; inside a 204px inner box it renders ~125px tall. Card ≈ 163px. - **`PianoLickCard` thumb** (`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.licks` are the old prose education entries (no `notes`) — the structured filter drops them, so pop reads as empty under piano too. - **Empty-per-style:** today the strip returns `null` when 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 1. **Shrink `MiniPiano`'s global `thumb` scale (0.8→0.6) instead of a new `mini`.** Rejected — it also shrinks the `ChordDetailModal` and dock thumbs, which have room to spare and benefit from the larger keys; a scoped `mini` variant keeps those byte-identical and confines the change to the rail. 2. **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). 3. **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 in `VoicingBrowser`) and preserves the today's recommended/gallery duplicate; centralising in `VoicingBrowser` (§1.1) is one place, one rule, no dupe. 4. **Global `*` scrollbar styling.** Rejected — restyles the entire app (dock/Debug/Tuner/Settings) for a dashboard-scoped ask; a `.dark-scroll` opt-in class is surgical.