diff --git a/src/App.jsx b/src/App.jsx
index c8ea22f..c5e965d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -675,6 +675,7 @@ export default function App() {
bpm={bpm}
currentChord={currentChord}
onFocusChord={setJamFocusChord}
+ onChordClick={setSelectedChord}
/>
)
diff --git a/src/components/ExplorePanel.jsx b/src/components/ExplorePanel.jsx
index 55bc780..61f5676 100644
--- a/src/components/ExplorePanel.jsx
+++ b/src/components/ExplorePanel.jsx
@@ -1,6 +1,24 @@
-import { useState } from 'react'
+// ExplorePanel — refactored into Knowledge Center parts (task L-22, per
+// docs/design/knowledge-center.md §7 step 1).
+//
+// This file now exports the named building blocks the Knowledge Center shell
+// (JamGuide.jsx) composes:
+//
+// — the shared foundation/intermediate filter
+// — controlled root × quality picker row
+// — KB progression browser + famous progressions
+// — picker (follows the live chord) → VoicingBrowser
+//
+// The default export remains a thin standalone composition of the parts (the
+// panel is verified-orphaned — no importer — so it exists only so the file
+// stays a complete, mountable component). GuitarGrid/PianoGrid are kept as
+// exported no-audio fallbacks per the D-20 IA map (§2).
+
+import { useEffect, useMemo, useState } from 'react'
import ChordBox from './ChordBox'
import MiniPiano from './MiniPiano'
+import VoicingBrowser from './VoicingBrowser'
+import kb from '../data/kb/index.js'
import { getGuitarVoicings, getPianoTechniques, parseChord } from '../lib/voicings'
import { CHORD_TYPES, NOTES, getChordsInKey, toRomanNumeral } from '../lib/theory'
import { FAMOUS_PROGRESSIONS, progressionInKey } from '../lib/education'
@@ -24,6 +42,66 @@ const CHORD_TYPE_OPTIONS = [
const MAJOR_TYPES = new Set(['maj','maj7','maj6','add9','sus4','sus2','aug','dom7'])
+// Progressions/licks without a `level` count as foundation (D-20 §4).
+const levelOf = (item) => (item?.level === 'intermediate' ? 'intermediate' : 'foundation')
+
+// ─── Level filter chips (shared by Explore + Licks toolbars) ──────────────────
+// Two toggle chips, both on by default. The SHELL owns the `levels` state
+// ({foundation, intermediate}) and enforces "both can't be off"; the chip for
+// the last active level advertises the no-op via its title.
+export function LevelChips({ levels = {}, onToggle }) {
+ const defs = [
+ { key: 'foundation', label: 'Foundation' },
+ { key: 'intermediate', label: 'Intermediate' },
+ ]
+ return (
+
+ {defs.map(d => {
+ const active = !!levels[d.key]
+ const lastActive = active && !defs.some(o => o.key !== d.key && levels[o.key])
+ return (
+ onToggle?.(d.key)}
+ title={lastActive
+ ? 'At least one level stays on'
+ : `${active ? 'Hide' : 'Show'} ${d.label.toLowerCase()} material`}
+ className={`min-h-[32px] px-2.5 py-1 rounded-lg border text-xs transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent ${
+ active
+ ? 'bg-accent/20 border-accent text-accent font-semibold'
+ : 'bg-surface border-border text-gray-400 hover:text-gray-200 hover:border-gray-500'
+ }`}
+ >
+ {d.label}
+
+ )
+ })}
+
+ )
+}
+
+// Level badge on cards — mirrors LickCard's badge treatment (amber = the
+// existing secondary-tone token; foundation stays quiet).
+function LevelBadge({ level }) {
+ if (level === 'intermediate') {
+ return (
+
+ intermediate
+
+ )
+ }
+ if (level === 'foundation') {
+ return (
+
+ foundation
+
+ )
+ }
+ return null
+}
+
// ─── Quick-pick chip row ──────────────────────────────────────────────────────
function ChipRow({ label, chords, active, keyInfo, onSelect }) {
if (!chords?.length) return null
@@ -35,7 +113,7 @@ function ChipRow({ label, chords, active, keyInfo, onSelect }) {
const rn = keyInfo?.root ? toRomanNumeral(chord, keyInfo.root, keyInfo.mode) : ''
return (
onSelect(chord)}
- className={`flex flex-col items-center px-2.5 py-1 rounded-lg border text-xs font-bold transition-all ${
+ className={`flex flex-col items-center px-2.5 py-1 rounded-lg border text-xs font-bold transition-all outline-none focus-visible:ring-2 focus-visible:ring-accent ${
active === chord
? 'bg-accent border-accent text-white'
: 'bg-surface border-border text-gray-300 hover:border-accent/50 hover:text-accent'
@@ -50,8 +128,38 @@ function ChipRow({ label, chords, active, keyInfo, onSelect }) {
)
}
-// ─── Guitar voicings grid ─────────────────────────────────────────────────────
-function GuitarGrid({ chordName }) {
+// ─── Chord picker toolbar (controlled: root × quality) ───────────────────────
+export function ChordPickerToolbar({ root, typeKey, onRootChange, onTypeChange }) {
+ const chordName = root + (CHORD_TYPES[typeKey]?.suffix ?? '')
+ return (
+
+
+ {NOTES.map(n => (
+ onRootChange?.(n)}
+ aria-pressed={root === n}
+ className={`px-2 py-0.5 rounded text-xs font-bold transition-all outline-none focus-visible:ring-2 focus-visible:ring-accent ${
+ root === n ? 'bg-accent text-white' : 'bg-border text-gray-400 hover:text-white'
+ }`}>
+ {n}
+
+ ))}
+
+
+
+ onTypeChange?.(e.target.value)}
+ aria-label="Chord quality"
+ className="appearance-none bg-panel border border-border rounded-lg pl-2 pr-6 py-1 text-xs text-gray-200 cursor-pointer focus:outline-none focus:border-accent">
+ {CHORD_TYPE_OPTIONS.map(o => {o.label} )}
+
+ ▾
+
+
{chordName}
+
+ )
+}
+
+// ─── Guitar voicings grid (no-audio fallback; superseded by VoicingBrowser) ──
+export function GuitarGrid({ chordName }) {
const voicings = getGuitarVoicings(chordName)
if (!voicings.length) return No voicings for {chordName}.
return (
@@ -71,8 +179,8 @@ function GuitarGrid({ chordName }) {
)
}
-// ─── Piano techniques grid ────────────────────────────────────────────────────
-function PianoGrid({ chordName }) {
+// ─── Piano techniques grid (no-audio fallback; superseded by VoicingBrowser) ─
+export function PianoGrid({ chordName }) {
const parsed = parseChord(chordName)
const techniques = getPianoTechniques(chordName)
const rootPc = parsed?.rootPc ?? 0
@@ -98,6 +206,8 @@ function PianoGrid({ chordName }) {
}
// ─── Famous progressions using this chord as tonic ───────────────────────────
+// NOTE (D-20 §4, recorded Maestro call): FAMOUS_PROGRESSIONS carries no `level`
+// field — these cards show no badge and are EXEMPT from the level filter.
function ProgressionCards({ chordName, onChordClick }) {
const parsed = parseChord(chordName)
if (!parsed) return null
@@ -131,7 +241,7 @@ function ProgressionCards({ chordName, onChordClick }) {
{chordsHere.map((c, i) => (
onChordClick?.(c)}
- className={`px-2.5 py-1 rounded-lg font-bold text-sm border transition-all ${
+ className={`px-2.5 py-1 rounded-lg font-bold text-sm border transition-all outline-none focus-visible:ring-2 focus-visible:ring-accent ${
i === 0
? 'bg-accent border-accent text-white'
: 'bg-panel border-border text-gray-200 hover:border-accent/50 hover:text-accent'
@@ -153,15 +263,132 @@ function ProgressionCards({ chordName, onChordClick }) {
)
}
-// ─── Main panel ───────────────────────────────────────────────────────────────
-export default function ExplorePanel({ keyInfo, chordHistory, onChordClick }) {
- const [open, setOpen] = useState(false)
+// ─── One KB progression card (the Explore browser hero) ──────────────────────
+function KbProgressionCard({ prog, keyRootPc, onChordClick }) {
+ const degrees = prog?.degrees ?? []
+ const qualities = prog?.qualities ?? []
+ const chords = degrees.map((deg, i) => {
+ const pc = (((keyRootPc + deg) % 12) + 12) % 12
+ return `${NOTES[pc]}${CHORD_TYPES[qualities[i]]?.suffix ?? ''}`
+ })
+ const songs = Array.isArray(prog?.songs) ? prog.songs : []
+ return (
+
+
+ {prog?.name ?? prog?.id ?? 'Untitled'}
+ {Array.isArray(prog?.rn) && prog.rn.length > 0 && (
+ {prog.rn.join(' – ')}
+ )}
+
+
+ {chords.length > 0 && (
+
+ {chords.map((c, i) => (
+
+ onChordClick?.(c)}
+ title={`Open ${c} details`}
+ className="px-2.5 py-1 rounded-lg font-bold text-sm border bg-panel border-border text-gray-200 transition-all outline-none hover:border-accent/50 hover:text-accent focus-visible:ring-2 focus-visible:ring-accent">
+ {c}
+
+ {i < chords.length - 1 && → }
+
+ ))}
+
+ )}
+ {prog?.tip &&
{prog.tip}
}
+ {songs.length > 0 && (
+
{songs.slice(0, 3).join(' · ')}
+ )}
+
+ )
+}
+
+// ─── Explore section — KB progression browser + famous progressions ──────────
+// Props: keyInfo (chords render in the detected key; C until one is known),
+// levels + onToggleLevel (shell-owned shared filter), onChordClick (chord name
+// string → ChordDetailModal).
+export function ExploreSection({ keyInfo, levels, onToggleLevel, onChordClick }) {
+ const styles = useMemo(
+ () => Object.entries(kb ?? {}).map(([id, s]) => ({ id, label: s?.meta?.label ?? id })),
+ []
+ )
+ const [styleOverride, setStyleOverride] = useState(null)
+ const activeStyle = styleOverride ?? styles[0]?.id
+
+ const keyRootPc = parseChord(keyInfo?.root ?? '')?.rootPc ?? 0
+ const keyMode = keyInfo?.mode === 'minor' ? 'minor' : 'major'
+ const tonicName = `${NOTES[keyRootPc]}${keyMode === 'minor' ? 'm' : ''}`
+
+ const progressions = kb?.[activeStyle]?.progressions ?? []
+ const visible = progressions.filter(p => levels?.[levelOf(p)])
+
+ return (
+
+ {/* Toolbar: style chips + shared level filter */}
+
+
+ {styles.map(s => {
+ const active = s.id === activeStyle
+ return (
+ setStyleOverride(s.id)}
+ className={`px-2.5 py-1 min-h-[32px] rounded-lg text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent ${
+ active
+ ? 'bg-accent/20 border border-accent text-accent font-semibold'
+ : 'border border-transparent text-gray-400 hover:text-gray-200 hover:border-border'
+ }`}>
+ {s.label}
+
+ )
+ })}
+
+
+
+
+
+
+ Chords shown in {NOTES[keyRootPc]} {keyMode}{keyInfo?.root ? '' : ' (no key detected yet)'} · tap any chord for voicings
+
+
+ {/* KB progression cards */}
+ {visible.length > 0 ? (
+
+ {visible.map((p, i) => (
+
+ ))}
+
+ ) : (
+
+ {progressions.length === 0
+ ? 'No progressions authored for this style yet.'
+ : 'Nothing at the selected level for this style — flip the level filter back on.'}
+
+ )}
+
+ {/* Famous progressions (exempt from the level filter — untagged corpus) */}
+
+
+ Famous progressions · not affected by the level filter
+
+
+
+
+ )
+}
+
+// ─── Voicings section — picker (follows the live chord) → VoicingBrowser ─────
+// Props: keyInfo + chordHistory feed the quick-pick chips; currentChord re-aims
+// the picker whenever a new chord commits (manual picks hold until then).
+export function VoicingsSection({ keyInfo, chordHistory, currentChord }) {
const [root, setRoot] = useState('C')
const [typeKey, setTypeKey] = useState('maj')
- const [view, setView] = useState('guitar') // guitar | piano | progressions
const [active, setActive] = useState('')
- const chordName = root + (CHORD_TYPES[typeKey]?.suffix ?? '')
+ useEffect(() => {
+ if (!currentChord) return
+ const p = parseChord(currentChord)
+ if (p) { setRoot(NOTES[p.rootPc]); setTypeKey(p.type); setActive(currentChord) }
+ }, [currentChord])
function selectChord(chord) {
setActive(chord)
@@ -169,79 +396,58 @@ export default function ExplorePanel({ keyInfo, chordHistory, onChordClick }) {
if (p) { setRoot(NOTES[p.rootPc]); setTypeKey(p.type) }
}
- // Context-aware quick-picks
- const recentChords = [...new Set([...(chordHistory ?? [])].reverse())].slice(0, 12)
- const keyChords = keyInfo?.root ? getChordsInKey(keyInfo.root, keyInfo.mode ?? 'major') : []
+ const recentChords = [...new Set([...(chordHistory ?? [])].reverse())].slice(0, 12)
+ const keyChords = keyInfo?.root ? getChordsInKey(keyInfo.root, keyInfo.mode ?? 'major') : []
+ const rootPc = parseChord(root)?.rootPc ?? 0
+
+ return (
+
+ {(recentChords.length > 0 || keyChords.length > 0) && (
+
+
+ {keyChords.length > 0 && recentChords.length > 0 &&
}
+ {keyChords.length > 0 && (
+
+ )}
+
+ )}
+
+
{ setRoot(n); setActive('') }}
+ onTypeChange={k => { setTypeKey(k); setActive('') }}
+ />
+
+
+
+ )
+}
+
+// ─── Standalone panel (thin composition; orphaned — kept mountable) ──────────
+export default function ExplorePanel({ keyInfo, chordHistory, currentChord, onChordClick }) {
+ const [open, setOpen] = useState(false)
+ const [levels, setLevels] = useState({ foundation: true, intermediate: true })
+ const toggleLevel = (key) => setLevels(prev => {
+ const next = { ...prev, [key]: !prev[key] }
+ return (next.foundation || next.intermediate) ? next : prev // both can't be off
+ })
return (
-
setOpen(v => !v)}
+ setOpen(v => !v)} aria-expanded={open}
className="w-full flex items-center justify-between px-4 py-2 text-sm text-gray-400 hover:text-gray-200 transition-all">
EXPLORE ANY CHORD
{open ? '▲' : '▼'}
{open && (
-
-
- {/* ── Context quick-picks ── */}
- {(recentChords.length > 0 || keyChords.length > 0) && (
-
-
- {keyChords.length > 0 && recentChords.length > 0 &&
}
- {keyChords.length > 0 && (
-
- )}
-
- )}
-
- {/* ── Manual chord picker ── */}
-
-
- {NOTES.map(n => (
- { setRoot(n); setActive('') }}
- className={`px-2 py-0.5 rounded text-xs font-bold transition-all ${
- root === n ? 'bg-accent text-white' : 'bg-border text-gray-400 hover:text-white'
- }`}>
- {n}
-
- ))}
-
-
-
- { setTypeKey(e.target.value); setActive('') }}
- className="appearance-none bg-panel border border-border rounded-lg pl-2 pr-6 py-1 text-xs text-gray-200 cursor-pointer focus:outline-none focus:border-accent">
- {CHORD_TYPE_OPTIONS.map(o => {o.label} )}
-
- ▾
-
-
{chordName}
-
-
- {/* ── View tabs ── */}
-
- {[
- { key: 'guitar', label: '🎸 Guitar Voicings' },
- { key: 'piano', label: '🎹 Piano Techniques' },
- { key: 'progressions', label: '🎵 Progressions' },
- ].map(t => (
- setView(t.key)}
- className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all whitespace-nowrap ${
- view === t.key ? 'bg-accent text-white' : 'text-gray-400 hover:text-white'
- }`}>
- {t.label}
-
- ))}
-
-
- {/* ── Content ── */}
- {view === 'guitar' &&
}
- {view === 'piano' &&
}
- {view === 'progressions' &&
{ selectChord(c); onChordClick?.(c) }} />}
-
+
+
+
)}
diff --git a/src/components/JamGuide.jsx b/src/components/JamGuide.jsx
index a70026a..6dcb92c 100644
--- a/src/components/JamGuide.jsx
+++ b/src/components/JamGuide.jsx
@@ -6,25 +6,34 @@ import RoadmapTrack from './RoadmapTrack'
import ChordDiagram from './ChordDiagram'
import MiniPiano from './MiniPiano'
import VoicingBrowser from './VoicingBrowser'
+import LickCard, { TechniqueLegend } from './LickCard'
+import { ExploreSection, VoicingsSection, LevelChips } from './ExplorePanel'
import { pianoVoicingChain } from '../lib/piano'
-// ─── JamGuide — the Roadmap bottom dock ───────────────────────────────────────
+// ─── JamGuide — the Knowledge Center bottom dock ──────────────────────────────
//
-// The large bottom panel of JamBuddy. This is the SHELL (task L-02): the
-// collapsed header bar, instrument + style tabs (derived from the KB registry),
-// live loop → KB progression resolution, and a clearly-marked placeholder slot
-// where the Roadmap visualization (RoadmapTrack + ChordDiagram, task D-02) will
-// be wired in afterwards.
+// The large bottom panel of JamBuddy. Originally the Roadmap Jam Guide dock
+// (tasks L-02/D-02/L-11); task L-22 grew it into the KNOWLEDGE CENTER shell per
+// docs/design/knowledge-center.md — one dock, four sections behind a pill nav:
//
-// This component does NOT import RoadmapTrack or ChordDiagram — sibling tasks
-// build those in parallel; D-02 fills the [data-roadmap-slot] left here.
+// Jam Guide (live) — the original Roadmap body, moved verbatim (default)
+// Explore — KB progression browser + famous progressions
+// Voicings — chord picker (follows the live chord) → VoicingBrowser
+// Licks & Techniques — per-style LickCard grid + technique legend
//
-// Props (the contract D-02 relies on):
+// Explore/Voicings parts come from ExplorePanel.jsx (refactored to named
+// exports); the shell owns the shared foundation/intermediate level filter
+// consumed by Explore + Licks. Collapsed-bar behaviour is unchanged apart from
+// the "Knowledge Center" name.
+//
+// Props:
// detectedProgression : string[] | null — the live detected loop (chord names)
// keyInfo : { root, mode, confidence } | null — effective key
// chordHistory : string[] — committed chord history (for position)
// bpm : number | null — live tempo from the onset pipeline
// currentChord : string | undefined — most recent committed chord
+// onFocusChord : fn({rootPc,quality}|null) — Fretboard guide-tone link (D-03)
+// onChordClick : fn(chordName) — opens ChordDetailModal (additive, L-22)
// Display order for instrument tabs; availability is derived from the KB, not
// hardcoded — EXCEPT piano, which is always available: its voicings are COMPUTED
@@ -37,9 +46,27 @@ const INSTRUMENTS = [
]
const COMPUTED_INSTRUMENTS = new Set(['piano'])
-export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], bpm, currentChord, onFocusChord }) {
+// Knowledge Center sections (D-20 §1). 'jam' is the default landing section.
+const SECTIONS = [
+ { id: 'jam', label: 'Jam Guide' },
+ { id: 'explore', label: 'Explore' },
+ { id: 'voicings', label: 'Voicings' },
+ { id: 'licks', label: 'Licks & Techniques' },
+]
+
+export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], bpm, currentChord, onFocusChord, onChordClick }) {
const [open, setOpen] = useState(false)
+ // ── Knowledge Center shell state ────────────────────────────────────────────
+ // Active section + the shared level filter (Explore + Licks toolbars, D-20 §4).
+ // Both levels on by default; both can never be off (last-chip tap is a no-op).
+ const [section, setSection] = useState('jam')
+ const [levels, setLevels] = useState({ foundation: true, intermediate: true })
+ const toggleLevel = (key) => setLevels(prev => {
+ const next = { ...prev, [key]: !prev[key] }
+ return (next.foundation || next.intermediate) ? next : prev
+ })
+
// Which instruments have at least one KB pack across the registry.
const availableInstruments = useMemo(() => {
const set = new Set()
@@ -186,7 +213,7 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
>
🎸
- Jam Guide
+ Knowledge Center
—
{headerLabel}
{match.matched && keyInfo?.root && (
@@ -202,6 +229,40 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
{open && (
+ {/* ── Section nav (Knowledge Center pills, D-20 §1) ── */}
+
+ {SECTIONS.map(s => {
+ const active = section === s.id
+ const live = s.id === 'jam' && match.matched
+ return (
+ setSection(s.id)}
+ className={`shrink-0 flex items-center gap-1.5 px-3 py-1 min-h-[32px] rounded-lg text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent ${
+ active
+ ? 'bg-accent/20 border border-accent text-accent font-semibold'
+ : 'border border-border text-gray-300 hover:border-gray-500 hover:text-gray-100'
+ }`}
+ >
+ {s.label}
+ {live && (
+
+ )}
+
+ )
+ })}
+
+
+ {/* ── Section 1: Jam Guide (live) — the Roadmap body, moved verbatim ── */}
+ {section === 'jam' && (
+ <>
{/* ── Tab rows ── */}
@@ -284,6 +345,131 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory =
)}
+ >
+ )}
+
+ {/* ── Section 2: Explore — KB progression browser + famous progressions ── */}
+ {section === 'explore' && (
+
+
+
+ )}
+
+ {/* ── Section 3: Voicings — picker (follows live chord) → VoicingBrowser ── */}
+ {section === 'voicings' && (
+
+
+
+ )}
+
+ {/* ── Section 4: Licks & Techniques — per-style LickCard grid ── */}
+ {section === 'licks' && (
+
+
+
+ )}
+
+ )}
+
+ )
+}
+
+// ─── LicksSection — per-style structured-lick grid (D-20 §1 section 4) ────────
+//
+// Reads the STRUCTURED top-level `kb[style].instruments.guitar.licks ?? []`
+// (C-20 schema; P-21 authors blues/jazz/funk concurrently — the section must
+// work whether or not that data has landed, hence the defensive reads and the
+// honest per-style empty states). One TechniqueLegend per grid, never per card.
+function LicksSection({ styles, levels, onToggleLevel }) {
+ const licksFor = (id) => {
+ const l = kb?.[id]?.instruments?.guitar?.licks
+ return Array.isArray(l) ? l : []
+ }
+ const stylesWithLicks = useMemo(
+ () => styles.filter(s => licksFor(s.id).length > 0),
+ [styles] // kb is a static module import
+ )
+
+ const [styleOverride, setStyleOverride] = useState(null)
+ const activeStyle = styleOverride ?? stylesWithLicks[0]?.id ?? styles[0]?.id
+ const activeLabel = styles.find(s => s.id === activeStyle)?.label ?? activeStyle
+
+ const all = licksFor(activeStyle)
+ // Licks without a `level` count as foundation (D-20 §4).
+ const visible = all.filter(l => levels[l?.level === 'intermediate' ? 'intermediate' : 'foundation'])
+
+ return (
+
+ {/* Toolbar: style chips + shared level filter */}
+
+
+ {styles.map(s => {
+ const active = s.id === activeStyle
+ const has = stylesWithLicks.some(w => w.id === s.id)
+ return (
+ setStyleOverride(s.id)}
+ title={has ? s.label : `${s.label} — no licks authored yet`}
+ className={`px-2.5 py-1 min-h-[32px] rounded-lg text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent ${
+ active
+ ? 'bg-accent/20 border border-accent text-accent font-semibold'
+ : has
+ ? 'border border-transparent text-gray-400 hover:text-gray-200 hover:border-border'
+ : 'border border-transparent text-gray-600 hover:text-gray-400 hover:border-border'
+ }`}>
+ {s.label}
+
+ )
+ })}
+
+
+
+
+
+
+ Guitar licks · tab reads high e on top · amber marks = techniques (legend below)
+
+
+ {visible.length > 0 ? (
+ <>
+
+ {visible.map((l, i) => (
+
+ ))}
+
+ {/* Glyph key — once per grid (D-20 §3), not per card */}
+
+
+
+ >
+ ) : (
+
+ {all.length === 0 ? (
+ <>
+
No licks authored for {activeLabel} yet.
+
+ {stylesWithLicks.length > 0
+ ? `${stylesWithLicks.map(s => s.label).join(', ')} ${stylesWithLicks.length === 1 ? 'has' : 'have'} them — pick one above.`
+ : 'Lick packs are landing style by style — check back soon.'}
+
+ >
+ ) : (
+
+ Nothing at the selected level for {activeLabel} — flip the level filter back on.
+
+ )}
)}