aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-19 21:50:08 -0500
committerCraig Jennings <c@cjennings.net>2026-07-19 21:50:08 -0500
commit7483eb390188005e54d1e5f1e9d2789b64ad8fe4 (patch)
tree41c75ed9a3db210acc41e05a20df13e57c9af798
parent18bcd567d1e7770b4e28c43eeefbf81696e2f4a3 (diff)
downloaddotemacs-7483eb390188005e54d1e5f1e9d2789b64ad8fe4.tar.gz
dotemacs-7483eb390188005e54d1e5f1e9d2789b64ad8fe4.zip
chore: version active working artifacts
I made working/ a versioned home for in-progress project artifacts and moved disposable scratch to temp/. This preserves the full music UI design history and the deferred EAT XTWINOPS upstream patch.
-rw-r--r--.gitattributes2
-rw-r--r--.gitignore4
-rw-r--r--working/eat-sixel-patch/eat-xtwinops.patch46
-rw-r--r--working/eat-sixel-patch/rulesets-companion-note.org5
-rw-r--r--working/eat-sixel-patch/takuzu-original-bug-report.org17
-rw-r--r--working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg87
-rw-r--r--working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org923
-rw-r--r--working/music-svg-app-design/README.org426
-rw-r--r--working/music-svg-app-design/assets/vinyl-placeholder.svg20
-rw-r--r--working/music-svg-app-design/concept-board.html25
-rw-r--r--working/music-svg-app-design/concepts/01-champagne-receiver.pngbin0 -> 2064469 bytes
-rw-r--r--working/music-svg-app-design/concepts/02-le-mans-night-cluster.pngbin0 -> 2451131 bytes
-rw-r--r--working/music-svg-app-design/concepts/03-geneva-playback-chronograph.pngbin0 -> 3097288 bytes
-rw-r--r--working/music-svg-app-design/concepts/04-mastering-room-reel-console.pngbin0 -> 2243918 bytes
-rw-r--r--working/music-svg-app-design/concepts/05-perpetual-calendar-salon.pngbin0 -> 2360111 bytes
-rw-r--r--working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.pngbin0 -> 1897511 bytes
-rw-r--r--working/music-svg-app-design/concepts/07-functional-black-glass-receiver.pngbin0 -> 1890841 bytes
-rw-r--r--working/music-svg-app-design/concepts/08-functional-black-glass-flipped.pngbin0 -> 1804000 bytes
-rw-r--r--working/music-svg-app-design/concepts/09-functional-black-glass-retro.pngbin0 -> 2226236 bytes
-rw-r--r--working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.pngbin0 -> 1872741 bytes
-rw-r--r--working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.pngbin0 -> 1950069 bytes
-rw-r--r--working/music-svg-app-design/concepts/12-consolidated-functional-receiver.pngbin0 -> 1884863 bytes
-rw-r--r--working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.pngbin0 -> 2119461 bytes
-rw-r--r--working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.pngbin0 -> 2061668 bytes
-rw-r--r--working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.pngbin0 -> 2010447 bytes
-rw-r--r--working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.pngbin0 -> 1995487 bytes
-rw-r--r--working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.pngbin0 -> 2027803 bytes
-rw-r--r--working/music-svg-app-design/concepts/18-dupre-branding-control-study.pngbin0 -> 2071824 bytes
-rw-r--r--working/music-svg-app-design/concepts/19a-dupre-playlist-state.pngbin0 -> 2013519 bytes
-rw-r--r--working/music-svg-app-design/concepts/19b-dupre-radio-state.pngbin0 -> 1954409 bytes
-rw-r--r--working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.pngbin0 -> 3976510 bytes
-rw-r--r--working/music-svg-app-design/concepts/20a-dupre-long-playlist.pngbin0 -> 2051749 bytes
-rw-r--r--working/music-svg-app-design/concepts/20b-dupre-compact-radio.pngbin0 -> 2015466 bytes
-rw-r--r--working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.pngbin0 -> 4015119 bytes
-rw-r--r--working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.pngbin0 -> 2084254 bytes
-rw-r--r--working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.pngbin0 -> 2015323 bytes
-rw-r--r--working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.pngbin0 -> 4029125 bytes
-rw-r--r--working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.pngbin0 -> 2091641 bytes
-rw-r--r--working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.pngbin0 -> 2024997 bytes
-rw-r--r--working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.pngbin0 -> 3733539 bytes
-rw-r--r--working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.pngbin0 -> 1959413 bytes
-rw-r--r--working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.pngbin0 -> 1860779 bytes
-rw-r--r--working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.pngbin0 -> 3843296 bytes
-rw-r--r--working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.pngbin0 -> 2013020 bytes
-rw-r--r--working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.pngbin0 -> 1911285 bytes
-rw-r--r--working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.pngbin0 -> 3847925 bytes
-rw-r--r--working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.pngbin0 -> 2037192 bytes
-rw-r--r--working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.pngbin0 -> 1895006 bytes
-rw-r--r--working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.pngbin0 -> 3795106 bytes
-rw-r--r--working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.pngbin0 -> 1979416 bytes
-rw-r--r--working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.pngbin0 -> 1896697 bytes
-rw-r--r--working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.pngbin0 -> 4153861 bytes
-rw-r--r--working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.pngbin0 -> 2148939 bytes
-rw-r--r--working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.pngbin0 -> 2101436 bytes
-rw-r--r--working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.pngbin0 -> 4163731 bytes
-rw-r--r--working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.pngbin0 -> 2144998 bytes
-rw-r--r--working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.pngbin0 -> 2111994 bytes
-rw-r--r--working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.pngbin0 -> 2176613 bytes
-rw-r--r--working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcfbin0 -> 2303994 bytes
-rw-r--r--working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.pngbin0 -> 2288732 bytes
-rw-r--r--working/music-svg-app-design/music-svg-directions.html35
-rw-r--r--working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org61
-rw-r--r--working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html1450
-rw-r--r--working/music-svg-app-design/references/29-alignment-guide.pngbin0 -> 2328747 bytes
-rw-r--r--working/music-svg-app-design/references/dupre-gallery-tall.pngbin0 -> 2154502 bytes
-rw-r--r--working/music-svg-app-design/references/dupre-gallery.pngbin0 -> 287964 bytes
-rw-r--r--working/music-svg-app-design/references/waybar-redesign-tall.pngbin0 -> 735385 bytes
-rw-r--r--working/music-svg-app-design/references/waybar-redesign.pngbin0 -> 205084 bytes
68 files changed, 3100 insertions, 1 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..fd4eb2ad
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Unified diff context lines can carry a required trailing space marker.
+working/**/*.patch whitespace=-blank-at-eol
diff --git a/.gitignore b/.gitignore
index 16ada780..b92678f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -110,4 +110,6 @@ todo.org
# Claude Code: task archive (follows todo file privacy)
/archive/task-archive.org
takuzu-stats.eld
-/working/
+
+# disposable project-local scratch; active task artifacts live in versioned working/
+/temp/
diff --git a/working/eat-sixel-patch/eat-xtwinops.patch b/working/eat-sixel-patch/eat-xtwinops.patch
new file mode 100644
index 00000000..4347d729
--- /dev/null
+++ b/working/eat-sixel-patch/eat-xtwinops.patch
@@ -0,0 +1,46 @@
+--- a/eat.el
++++ b/eat.el
+@@ -2809,6 +2809,33 @@
+ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term
+ "\e[>0;0;0c")))))
+
++(defun eat--t-send-window-size-report (n)
++ "Respond to XTWINOPS window size report request N.
++
++N is 14 (text area size in pixels), 16 (cell size in pixels) or 18
++\(text area size in characters). Other operations are ignored.
++Multiplexers like tmux send \\='CSI 14 t\\=' to discover the cell
++pixel size they need before they will emit Sixel to a terminal."
++ (let ((disp (eat--t-term-display eat--t-term)))
++ (pcase n
++ (14
++ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term
++ (format "\e[4;%i;%it"
++ (* (eat--t-disp-height disp)
++ (eat--t-term-char-height eat--t-term))
++ (* (eat--t-disp-width disp)
++ (eat--t-term-char-width eat--t-term)))))
++ (16
++ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term
++ (format "\e[6;%i;%it"
++ (eat--t-term-char-height eat--t-term)
++ (eat--t-term-char-width eat--t-term))))
++ (18
++ (funcall (eat--t-term-input-fn eat--t-term) eat--t-term
++ (format "\e[8;%i;%it"
++ (eat--t-disp-height disp)
++ (eat--t-disp-width disp)))))))
++
+ (defun eat--t-send-graphics-attrs (attr operation)
+ "Send graphics attributes.
+
+@@ -3632,6 +3659,9 @@
+ ;; CSI s.
+ (`((?s) nil nil)
+ (eat--t-save-cur))
++ ;; CSI <n> t.
++ (`((?t) nil ((,n)))
++ (eat--t-send-window-size-report n))
+ ;; CSI u.
+ (`((?u) nil nil)
+ (eat--t-restore-cur)))))))
diff --git a/working/eat-sixel-patch/rulesets-companion-note.org b/working/eat-sixel-patch/rulesets-companion-note.org
new file mode 100644
index 00000000..2df33273
--- /dev/null
+++ b/working/eat-sixel-patch/rulesets-companion-note.org
@@ -0,0 +1,5 @@
+#+TITLE: EAT XTWINOPS patch companion note
+#+SOURCE: from rulesets
+#+DATE: 2026-07-13 10:58:54 -0500
+
+EAT XTWINOPS patch (companion file eat-xtwinops.patch, sent separately just now): verified live on ratio 2026-07-13, this makes sixel images render AND persist in tmux inside EAT. Root cause chain: tmux 3.7b has native sixel but refuses to transmit unless it knows the client's cell pixel size; it asks via CSI 14 t (XTWINOPS), which EAT 0.9.4 silently ignores (its CSI dispatch has no 't' case). The patch adds eat--t-send-window-size-report answering CSI 14/16/18 t from eat--t-term-char-width/height and the display dims — same fields EAT's own XTSMGRAPHICS reply already uses. With it, tmux learns the geometry at client attach, ingests raw sixel into its grid, and re-emits on every redraw — images survive window switches, scrolling, and resizing (Craig verified visually). Needs on the emacsd side: (1) a durable home for the patch — the elpa file is version-pinned, so likely a load-path shadow or an advice-free vendored fix, your call, plus wiring so both daily drivers load it (velox verified ready: same tmux build, imagemagick present); (2) written test-first per testing.md when productionizing — this was a timeboxed spike, treat the diff as evidence not deliverable; (3) strongly consider sending it upstream to EAT (codeberg akib/emacs-eat) so 0.9.5+ ships it stock. tmux side is separate (two conf lines, handoff sent to dotfiles). Ask rulesets for the full investigation log if useful.
diff --git a/working/eat-sixel-patch/takuzu-original-bug-report.org b/working/eat-sixel-patch/takuzu-original-bug-report.org
new file mode 100644
index 00000000..02107a88
--- /dev/null
+++ b/working/eat-sixel-patch/takuzu-original-bug-report.org
@@ -0,0 +1,17 @@
+#+TITLE: Bug/investigate: images from Claude Code don't display in th
+#+SOURCE: from takuzu
+#+DATE: 2026-07-11 14:41:39 -0500
+
+Bug/investigate: images from Claude Code don't display in the EAT terminal.
+
+Context: running Claude Code inside EAT (eat 0.9.4 from elpa). When the agent posts an image to the terminal, nothing renders — Craig can't see it.
+
+Finding: EAT *does* have Sixel image support. eat.el defines eat-sixel-scale, eat-sixel-aspect-ratio, eat-sixel-render-formats, and a full Sixel decode/render path (sixel-buffer, sixel-palette, sixel-render-format, etc.). So the capability exists; this isn't a plain 'EAT can't do images' limitation.
+
+Likely causes to check:
+1. Protocol mismatch: Claude Code CLI probably emits the kitty graphics protocol or the iTerm2 inline-image protocol (OSC 1337), NOT Sixel. EAT renders Sixel only, so a kitty/iTerm image sequence is dropped. Confirm which protocol Claude Code uses (env like TERM, or its terminal-capability detection) and whether it can be told to emit Sixel.
+2. Sixel render-format availability: eat-sixel-render-formats lists preferred render backends; if none is available in this Emacs build, Sixel silently won't display. Test EAT with a known-good sixel image (e.g. img2sixel output) to confirm EAT-side rendering works at all.
+
+Repro: in an EAT session, have Claude Code (or any tool) post an image; observe nothing renders. Next step: identify the emitted escape sequence (capture raw bytes) to confirm protocol.
+
+From the takuzu session, 2026-07-11 — surfaced because the agent kept posting game screenshots the terminal couldn't show.
diff --git a/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg
new file mode 100644
index 00000000..aad6f746
--- /dev/null
+++ b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-render-fixture.svg
@@ -0,0 +1,87 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ viewBox="0 0 1916 821" width="1916" height="821">
+ <defs>
+ <radialGradient id="glassGlow" cx="40%" cy="0" r="90%">
+ <stop offset="0" stop-color="#22231f" stop-opacity=".28"/>
+ <stop offset="1" stop-color="#050605" stop-opacity="0"/>
+ </radialGradient>
+ <linearGradient id="headerBrass" x1="0" x2="1">
+ <stop offset="0" stop-color="#c4aa89"/>
+ <stop offset=".5" stop-color="#ae9374"/>
+ <stop offset="1" stop-color="#c2a27d"/>
+ </linearGradient>
+ <clipPath id="metadataClip"><rect x="780" y="104" width="350" height="180"/></clipPath>
+ <clipPath id="playlistClip"><rect x="1188" y="126" width="610" height="520"/></clipPath>
+ </defs>
+
+ <image x="0" y="0" width="1916" height="821"
+ xlink:href="concepts/30a-dupre-studios-user-refined-playlist.png"/>
+
+ <rect x="605" y="96" width="540" height="250" fill="#070807"/>
+ <rect x="605" y="96" width="540" height="250" fill="url(#glassGlow)"/>
+ <image x="612" y="108" width="146" height="173" xlink:href="assets/vinyl-placeholder.svg"/>
+ <g clip-path="url(#metadataClip)" fill="#ebe5d9" font-family="Berkeley Mono, monospace" font-size="20" letter-spacing="1.4">
+ <text x="780" y="130">A CHICKEN WITH ITS HEAD CUT OFF — REMASTERED ANNIVERSARY EDITION</text>
+ <text x="780" y="166">THE MAGNETIC FIELDS AND THE LONG-LOST ORCHESTRA</text>
+ <text x="780" y="202">69 LOVE SONGS: THE COMPLETE THREE-VOLUME COLLECTION</text>
+ <text x="780" y="238">MERGE RECORDS</text>
+ <text x="780" y="274">1999</text>
+ </g>
+ <text x="610" y="322" fill="#ddd4c5" font-family="Berkeley Mono, monospace" font-size="19">01:30</text>
+ <line x1="679" y1="316" x2="1023" y2="316" stroke="#3b3a37" stroke-width="2"/>
+ <line x1="679" y1="316" x2="850" y2="316" stroke="#f2a900" stroke-width="2"/>
+ <circle cx="850" cy="316" r="9" fill="#c79a60" stroke="#f0d4a7" stroke-width="1"/>
+ <text x="1040" y="322" fill="#ddd4c5" font-family="Berkeley Mono, monospace" font-size="19">02:59</text>
+
+ <rect x="1172" y="52" width="688" height="65" fill="url(#headerBrass)"/>
+ <g fill="#251c14" font-family="Berkeley Mono, monospace" font-size="20" letter-spacing="1.5">
+ <text x="1202" y="94">PLAYLIST</text>
+ <text x="1316" y="94">|</text>
+ <text x="1355" y="94">69 LOVE SONGS — COMPLETE</text>
+ <text x="1731" y="94">69 TRACKS</text>
+ </g>
+ <rect x="1172" y="118" width="688" height="545" fill="#070807"/>
+ <rect x="1172" y="118" width="688" height="545" fill="url(#glassGlow)"/>
+ <g clip-path="url(#playlistClip)" font-family="Berkeley Mono, monospace" font-size="17" letter-spacing=".8">
+ <g fill="#ddd8cf">
+ <text x="1192" y="153">01</text><text x="1237" y="153">ABSOLUTELY CUCKOO — THE MAGNETIC FIELDS</text><text x="1760" y="153">02:02</text>
+ <text x="1192" y="190">02</text><text x="1237" y="190">I DON’T BELIEVE IN THE SUN — THE MAGNETIC FIELDS</text><text x="1760" y="190">02:50</text>
+ <text x="1192" y="227">03</text><text x="1237" y="227">ALL MY LITTLE WORDS — THE MAGNETIC FIELDS</text><text x="1760" y="227">02:03</text>
+ </g>
+ <rect x="1175" y="238" width="650" height="37" fill="#f2a900" fill-opacity=".07"/>
+ <g fill="#f2a900">
+ <text x="1174" y="264">▶</text><text x="1192" y="264">04</text><text x="1237" y="264">A CHICKEN WITH ITS HEAD CUT OFF — THE MAGNETIC FIELDS</text><text x="1760" y="264">02:59</text>
+ </g>
+ <g fill="#ddd8cf">
+ <text x="1192" y="301">05</text><text x="1237" y="301">(I’M) DAZOTA — THE MAGNETIC FIELDS</text><text x="1760" y="301">02:38</text>
+ <text x="1192" y="338">06</text><text x="1237" y="338">I DON’T WANT TO GET OVER YOU — THE MAGNETIC FIELDS</text><text x="1760" y="338">02:50</text>
+ <text x="1192" y="375">07</text><text x="1237" y="375">COME BACK FROM SAN FRANCISCO — THE MAGNETIC FIELDS</text><text x="1760" y="375">02:37</text>
+ <text x="1192" y="412">08</text><text x="1237" y="412">THE LUCKIEST GUY ON THE LOWER EAST SIDE — THE MAGNETIC FIELDS</text><text x="1760" y="412">01:32</text>
+ <text x="1192" y="449">09</text><text x="1237" y="449">LET’S PRETEND WE’RE BUNNY RABBITS — THE MAGNETIC FIELDS</text><text x="1760" y="449">02:10</text>
+ <text x="1192" y="486">10</text><text x="1237" y="486">THE CACTUS WHERE YOUR HEART SHOULD BE — THE MAGNETIC FIELDS</text><text x="1760" y="486">02:38</text>
+ <text x="1192" y="523">11</text><text x="1237" y="523">I THINK I NEED A NEW HEART — THE MAGNETIC FIELDS</text><text x="1760" y="523">02:58</text>
+ <text x="1192" y="560">12</text><text x="1237" y="560">THE BOOK OF LOVE — THE MAGNETIC FIELDS</text><text x="1760" y="560">02:56</text>
+ <text x="1192" y="597">13</text><text x="1237" y="597">FIDO, YOUR LEASH IS TOO LONG — THE MAGNETIC FIELDS</text><text x="1760" y="597">02:13</text>
+ <text x="1192" y="634">14</text><text x="1237" y="634">HOW FUCKING ROMANTIC — THE MAGNETIC FIELDS</text><text x="1760" y="634">00:58</text>
+ </g>
+ </g>
+
+ <g fill="#171714" stroke="#6b5a3c" stroke-width="1.5">
+ <circle cx="862" cy="614" r="6"/><circle cx="850" cy="588" r="6"/><circle cx="846" cy="559" r="6"/>
+ <circle cx="850" cy="530" r="6"/><circle cx="861" cy="502" r="6"/><circle cx="878" cy="479" r="6"/>
+ <circle cx="901" cy="461" r="6"/><circle cx="928" cy="451" r="6"/><circle cx="957" cy="447" r="6"/>
+ <circle cx="986" cy="451" r="6"/><circle cx="1013" cy="461" r="6"/><circle cx="1036" cy="479" r="6"/>
+ <circle cx="1053" cy="502" r="6"/><circle cx="1064" cy="530" r="6"/><circle cx="1068" cy="559" r="6"/>
+ <circle cx="1064" cy="588" r="6"/><circle cx="1052" cy="614" r="6"/>
+ </g>
+ <g fill="#ffb23c" stroke="#fff0bd" stroke-width="1.5">
+ <circle cx="862" cy="614" r="6"/><circle cx="850" cy="588" r="6"/><circle cx="846" cy="559" r="6"/>
+ <circle cx="850" cy="530" r="6"/><circle cx="861" cy="502" r="6"/><circle cx="878" cy="479" r="6"/>
+ <circle cx="901" cy="461" r="6"/><circle cx="928" cy="451" r="6"/><circle cx="957" cy="447" r="6"/>
+ <circle cx="986" cy="451" r="6"/><circle cx="1013" cy="461" r="6"/>
+ </g>
+
+ <line x1="227" y1="537" x2="245" y2="467" stroke="#f3d59c" stroke-width="2"/>
+ <line x1="480" y1="537" x2="496" y2="466" stroke="#f3d59c" stroke-width="2"/>
+</svg>
diff --git a/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org
new file mode 100644
index 00000000..6e68e104
--- /dev/null
+++ b/working/music-svg-app-design/2026-07-19-music-config-ui-remodel-spec.org
@@ -0,0 +1,923 @@
+#+TITLE: Music Config UI Remodel — Spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-19
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Music Config UI Remodel
+:PROPERTIES:
+:ID: bcbb9342-1884-4281-8821-89b06f6f8793
+:END:
+- 2026-07-19 Sun @ 16:03:13 -0500 — drafted from the approved Dupre Studios design session.
+
+** Prototype iterations
+
+The design session started with working HTML directions, moved through six
+distinct visual concepts, and then refined one receiver layout through paired
+playlist/radio boards. The last board is Craig's GIMP refinement and is the
+visual authority for this spec.
+
+- [[file:music-svg-directions.html][Initial working SVG directions]]
+- [[file:concept-board.html][Six-direction comparison board]]
+- [[file:concepts/01-champagne-receiver.png][01 Champagne Receiver]]
+- [[file:concepts/02-le-mans-night-cluster.png][02 Le Mans Night Cluster]]
+- [[file:concepts/03-geneva-playback-chronograph.png][03 Geneva Playback Chronograph]]
+- [[file:concepts/04-mastering-room-reel-console.png][04 Mastering Room Reel Console]]
+- [[file:concepts/05-perpetual-calendar-salon.png][05 Perpetual Calendar Salon]]
+- [[file:concepts/06-transatlantic-broadcast-navigator.png][06 Transatlantic Broadcast Navigator]]
+- [[file:concepts/07-functional-black-glass-receiver.png][07 Functional black-glass receiver]]
+- [[file:concepts/08-functional-black-glass-flipped.png][08 Playlist-flipped receiver]]
+- [[file:concepts/09-functional-black-glass-retro.png][09 Warmer retro receiver]]
+- [[file:concepts/10-warm-black-glass-player-volume.png][10 Player-volume receiver]]
+- [[file:concepts/11-illuminated-black-glass-controls.png][11 Illuminated controls]]
+- [[file:concepts/12-consolidated-functional-receiver.png][12 Consolidated functional receiver]]
+- [[file:concepts/13-black-silver-chronograph-receiver.png][13 Black-silver chronograph]]
+- [[file:concepts/14-corrected-luxury-chronograph-receiver.png][14 Corrected luxury chronograph]]
+- [[file:concepts/15-champagne-brass-digital-hifi.png][15 Champagne digital hi-fi]]
+- [[file:concepts/16-champagne-aluminum-radio-groups.png][16 Explicit radio grouping]]
+- [[file:concepts/17-dupre-coltrane-scrollbar.png][17 Long-playlist and scrollbar stress test]]
+- [[file:concepts/18-dupre-branding-control-study.png][18 Branding and control study]]
+- [[file:concepts/19a-dupre-playlist-state.png][19a Playlist state]] and [[file:concepts/19b-dupre-radio-state.png][19b radio state]]
+- [[file:concepts/20-dupre-playlist-radio-comparison.png][20 Long playlist / compact radio comparison]]
+- [[file:concepts/21-dupre-semantic-controls-comparison.png][21 Semantic control comparison]]
+- [[file:concepts/22-dupre-identity-bay-comparison.png][22 Identity-bay comparison]]
+- [[file:concepts/23-dupre-swapped-info-comparison.png][23 Swapped information comparison]]
+- [[file:concepts/24-dupre-full-brass-vu-comparison.png][24 Full-brass VU comparison]]
+- [[file:concepts/25-dupre-seamless-flush-comparison.png][25 Seamless flush comparison]]
+- [[file:concepts/26-dupre-studios-rounded-chrome-comparison.png][26 Dupre Studios comparison]]
+- [[file:concepts/27-dupre-studios-leather-analog-header-comparison.png][27 Leather and analog-header comparison]]
+- [[file:concepts/28-dupre-studios-balanced-controls-comparison.png][28 Balanced-control comparison]]
+- [[file:concepts/29a-dupre-studios-brass-seek-playlist.png][29 Brass seek-thumb pass]]
+- [[file:concepts/30a-dupre-studios-user-refined-playlist.png][30 Craig's near-final playlist board (visual authority)]]
+- [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][31 Functional local-playlist / radio prototype (acceptance candidate)]]
+
+* Metadata
+| Status | Not ready — first review found one functional-prototype blocker |
+|----------+------------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+------------------------------------------------------------------------|
+| Reviewer | Craig Jennings |
+|----------+------------------------------------------------------------------------|
+| Related | [[file:../../todo.org][music: SVG player application design and delivery]]; [[file:../../docs/specs/2026-07-06-fancy-music-player-ui-spec.org][implemented fancy player spec]]; [[file:../../docs/specs/music-config-without-emms-spec.org][EMMS-free architecture spec]] |
+|----------+------------------------------------------------------------------------|
+
+* Summary
+
+Replace the current text-and-image EMMS playlist surface with a realistic
+Dupre Studios receiver rendered inside Emacs. The interface uses a raster skin
+for leather, glass, brass, and machined controls, then draws all changing
+information and illumination in SVG. Emacs Lisp remains the application
+runtime and maps the SVG surface to the existing music-config commands.
+
+The remodel preserves the plain and fancy text views as fallbacks. It does not
+change M3U ownership, radio-browser behavior, or the current EMMS playback model.
+
+* Problem / Context
+
+The implemented fancy player improved names, artwork, and progress, but it is
+still a text playlist with a multi-line overlay. It cannot present the player
+as one coherent instrument. Controls are key hints rather than controls,
+playlist management is visually detached from playback, and active state is
+limited to text faces and a character-cell progress bar.
+
+Craig wants a bottom-docked application that looks like a manufactured object:
+black leather, black glass, champagne brass, lit analog meters, and discreet
+controls. The beauty has to explain state. A lamp, depressed key, moving
+needle, seek position, or selected row must correspond to real music-config or
+mpv behavior. Decorative AM/FM, tone, power, reload, manual URL, and other
+unsupported faceplate controls are prohibited.
+
+The final 1916×821 board settles the visual direction. The remaining design
+problem is how to preserve its realism without baking live state into a picture.
+An all-vector redraw would lose much of the leather and metal detail. A single
+clickable PNG would preserve the appearance but freeze track data, meter state,
+playlist rows, and button feedback. The chosen design separates the physical
+object from its information and behavior.
+
+* Goals and Non-Goals
+
+** Goals
+
+- Match Craig's near-final board closely enough that the running player reads as
+ the same manufactured receiver, not a simplified diagram of it.
+- Preserve the full music-config workflow: playback, seek, player volume,
+ playlist browsing and persistence, radio search, repeat/single/random/consume,
+ reordering, add, new, load, save, and delete.
+- Make every visible state honest. Meter needles use real program levels,
+ volume lamps use mpv volume, progress uses mpv position, and mode lamps use
+ current mode state.
+- Keep the hot path compact: album information and controls on the left,
+ fourteen playlist rows and playlist actions on the right.
+- Keep keyboard operation complete while adding direct mouse interaction.
+- Preserve the existing text render for TTY frames, unsupported SVG builds, and
+ opt-out/rollback.
+- Keep the renderer independent of EMMS objects so the planned EMMS-free/VAMP
+ work can replace the controller without replacing the visual layer.
+
+** Non-Goals
+
+- No music library browser, tag editor, lyrics view, waveform editor, or album
+ collection database.
+- No streaming-service integration.
+- No fake receiver functions such as AM/FM tuning, bass, treble, speakers, or
+ power.
+- No permanent faceplate controls for reload, editing the M3U file, or manual URL
+ entry. Existing keyboard and M-x access may remain.
+- No all-vector imitation of leather, brushed metal, or photographic reflections.
+- No EMMS removal or VAMP extraction in this project. Those remain separate
+ architecture efforts.
+- No alternate narrow/mobile layout in v1. The player scales as one panoramic
+ instrument.
+
+** Scope tiers
+
+- v1: neutral raster skin, SVG renderer, dedicated graphical view buffer,
+ complete keyboard/mouse command surface, playlist viewport, seek, persistent
+ mpv volume, real stereo VU telemetry, local-file and radio states, diagnostics,
+ fallbacks, tests, and live verification.
+- Out of scope: library browsing, ratings, per-track menus, editable metadata,
+ alternate skins, responsive reflow, and remote control.
+- vNext: multiple visual skins, a narrower alternate layout, richer motion
+ transitions, and reuse in a future standalone VAMP package.
+
+* Design
+
+** User view
+
+F10 opens the player in the existing bottom side-window position. In a
+graphical frame with the remodel enabled, the buffer is the Dupre Studios
+receiver. The same keys still work. The visible buttons also work with the
+mouse, the playlist scrolls inside its fourteen-row aperture, and selecting a
+row starts that track.
+
+A local file shows album art, title, artist, album, label, and year on separate
+lines when those fields exist. It shows elapsed and total time with a draggable
+seek control. A live station shows station art and two ON AIR indications: one
+under the art and one in the active station row. It never shows a seek control
+or invented time for a stream. Missing metadata produces no placeholder word.
+
+The interface behaves like hardware. Play/Pause sits depressed and glows green
+while playing. Momentary transport controls brighten while pressed. Active
+play modes illuminate the circumference of their brass buttons. Volume lamps
+fill continuously to the current mpv volume. VU needles move from the actual
+left and right audio levels.
+
+The interaction model is available in the [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional browser prototype]]. It
+switches between a 69-track local fixture and an 18-station radio fixture and
+wires transport, playback modes, radio search, playlist actions, row selection,
+seek, volume, wheel/keyboard browsing, scrollbar drag, empty state, and delete
+confirmation. Its built-in twelve-check behavior pass is green. Craig's
+hands-on acceptance remains the last prototype gate.
+
+** Research and visual sources
+
+The design session compared NAD restraint, Marshall's black-and-brass control
+language, McIntosh black glass and meter hierarchy, vintage stereo transport
+controls, automobile instrument clusters, and black/silver chronographs. The
+useful lessons were consistent: controls need one visual family, status lights
+need stable color meaning, labels belong on the faceplate, and ornament should
+make state easier to read.
+
+The local Dupre references are the implementation sources:
+
+- [[file:references/dupre-gallery.png][Dupre component gallery capture]]
+- [[file:references/waybar-redesign.png][Waybar redesign capture]]
+- [[file:README.org][Design-session decisions and critique log]]
+- [[file:concepts/30a-dupre-studios-user-refined-playlist.png][Near-final visual authority]]
+- [[file:../../../code/archsetup/docs/prototypes/gallery-widget.el][svg.el Dupre renderer proof of concept]]
+
+** Canonical geometry
+
+The source canvas is 1916×821 with a viewBox of =0 0 1916 821=. The renderer
+preserves that aspect ratio and scales it to the available window width. It
+does not reflow the playlist under the controls or move controls at runtime.
+Extra vertical room stays black below the instrument. A window too short for
+legible rendering falls back to the existing fancy text view rather than
+shrinking controls into unusable targets.
+
+Craig's final board defines the visual positions. Phase 1 records them in one
+geometry table, including every visual rectangle, label anchor, hit target, and
+playlist row. Rendering and hit testing use the same constants. No second
+hand-maintained map is allowed.
+
+The principal regions are:
+
+| Region | Approximate source bounds |
+|------------------------+---------------------------|
+| Complete instrument | 0,0 to 1916,821 |
+| Player upper glass | 50,50 to 1170,368 |
+| Lower brass deck | 69,372 to 1147,744 |
+| Playlist glass | 1170,52 to 1862,744 |
+| Branding field | 150,115 to 500,275 |
+| Artwork | 609,108 to 755,281 |
+| Metadata | 780,100 to 1125,275 |
+| Seek / ON AIR line | 608,302 to 1125,330 |
+| Playlist viewport | 1175,126 to 1835,658 |
+| Playlist actions | 1173,671 to 1850,733 |
+
+Exact measured values replace these orientation bounds in the geometry table.
+
+** Rendering layers
+
+The SVG is a scene graph with four layers.
+
+1. The physical shell is raster. It contains the leather chassis, compressed
+ seams, black glass texture, fine low-contrast champagne micro-brush, recessed
+ grooves, fixed branding,
+ engraved labels, neutral control bodies, neutral volume knob, and empty VU
+ scales. The application embeds the assets with =svg-embed= and caches their
+ encoded data for the Emacs session.
+
+2. Static vector geometry supplies clip paths, hit-region outlines used only in
+ debug mode, row separators, and any simple lines that need to stay sharp at
+ different scales.
+
+3. Dynamic SVG supplies album/station art, metadata, playlist header and rows,
+ progress, brass seek thumb, scrollbar, transport glyphs, pressed states,
+ lamps, VU needles, and meter light. No live text, needle, lamp, selected row,
+ scrollbar, or progress state is baked into the raster shell.
+
+4. Emacs Lisp supplies behavior. SVG rendered through librsvg does not run
+ browser JavaScript. A buffer-local keymap receives mouse events,
+ =posn-object-x-y= returns pixel coordinates inside the displayed image, and
+ one scale transform maps them back to the source coordinate system.
+
+The near-final PNG cannot be embedded unchanged. Phase 1 creates a neutral
+layered source by removing metadata, playlist text, active lamps, VU needles,
+progress, and selection from the final board. The source stays editable. The
+editable source stays one full-resolution instrument. Runtime rendering uses
+three aligned image regions: upper player glass, lower control deck, and
+playlist. A compact control sprite sheet and neutral VU face supplement those
+regions. The current 1916×821 art is treated as a two-times source for a typical
+roughly 958×410 rendered dock. Opaque runtime crops may be exported at their
+display size after a PNG/JPEG visual comparison. V1 does not upscale beyond the
+native source pixels.
+
+** Module and state boundary
+
+The SVG view lives in a new =modules/music-svg-ui.el= module. It requires
+=svg=, =dom=, and ordinary Emacs image support, but it does not require EMMS.
+Its pure entry points accept plain data:
+
+- =cj/music-svg-render= takes a state snapshot and viewport.
+- =cj/music-svg-hit-target= maps source coordinates to an action description.
+- =cj/music-svg-truncate= fits display text to a fixed pixel/character budget.
+- =cj/music-svg-scrollbar= computes thumb length and position.
+- =cj/music-svg-vu-angle= maps a smoothed dB value to the meter scale.
+
+=modules/music-config.el= remains the controller. It builds an ephemeral
+=cj/music-ui-state= snapshot from the current EMMS playlist and player state,
+then dispatches renderer actions back to existing commands. The snapshot owns
+no durable music data. EMMS and M3U files remain authoritative in v1.
+
+The snapshot contains:
+
+- playback status and track kind
+- current track identity and metadata
+- art path, elapsed time, duration, and seekability
+- playlist name/file, total count, rows, selected index, and viewport offset
+- repeat, single, random, and consume state
+- player volume
+- left/right meter values
+- transient pressed/focused target
+
+The graphical view is a dedicated =*Music Player*= special-mode buffer. The
+hidden EMMS playlist buffer continues to own playlist markers and track order.
+Controller helpers translate a visible row index to an EMMS buffer position
+before selection, playback, reorder, or deletion. This avoids making a single
+image character pretend to be fourteen editable EMMS rows, and it leaves a
+clean seam for the EMMS-free state API later.
+
+** Metadata and typography
+
+Berkeley Mono is the default instrument typeface because it is the Dupre token
+font and is installed on the target system. Missing font support falls back to
+=monospace=. The cursive Dupre Studios mark remains raster in the physical
+shell.
+
+Title, artist, album, label, and year each get one line. Empty values disappear
+without closing the line spacing above them. The title receives the largest
+width budget and truncates with a literal ellipsis. The renderer uses the
+fixed-width font metrics and the measured metadata box, not a guessed character
+count from the current window.
+
+The playlist shows fourteen one-line rows. Each row is number, title, artist,
+and duration on one line. The row text truncates before the duration column.
+Inactive text is soft white. The active row is amber and carries the triangular
+play marker. There are no decorative status dots.
+
+The header is the only coffee/cream instrument card. It shows the current
+collection or search name and item count, but never labels the source as a
+playlist or radio. Four concealed warm-light pools are part of the static skin,
+but the text is SVG because the name and count change. Its champagne surface is
+smooth metal with a fine linear micro-brush, never coarse or wavy wood grain.
+
+** Playlist viewport
+
+The UI keeps a buffer-local first-visible index. The current track is kept in
+view after track changes unless the user is actively browsing elsewhere. Mouse
+wheel moves three rows. Up/Down moves the UI cursor one row. Page Up/Page Down
+moves fourteen. Home/End goes to the first/last track. A row click selects and
+starts that track. The existing keyboard play command remains available.
+
+For =N= tracks and fourteen visible rows, the scrollbar thumb fraction is
+=min(1, 14/N)=. Its position is =offset/(N-14)= when =N > 14=. Dragging the
+thumb changes the viewport but never changes the playing track. No scrollbar
+appears for fourteen or fewer tracks.
+
+** Control behavior
+
+The visible controls map to existing behavior:
+
+| Surface | Action |
+|---------------+--------|
+| PREV | =cj/music-previous= |
+| PLAY/PAUSE | start selected/current track, or =emms-pause= while playing |
+| STOP | =emms-stop= |
+| NEXT | =cj/music-next= |
+| REPEAT | =emms-toggle-repeat-playlist= |
+| SINGLE | =emms-toggle-repeat-track= |
+| RANDOM | =emms-toggle-random-playlist= |
+| CONSUME | =cj/music-toggle-consume= |
+| NAME | =cj/music-radio-search-by-name= |
+| TAGS | =cj/music-radio-search-by-tag= |
+| ADD | =cj/music-fuzzy-select-and-add= |
+| NEW | =cj/music-playlist-clear=, including its file-association reset |
+| LOAD | =cj/music-playlist-load= |
+| SAVE | =cj/music-playlist-save= |
+| DELETE | =cj/music-delete-playlist= with its existing strong confirmation |
+
+Mouse press immediately renders the physical pressed state. Release inside the
+same target dispatches the command. Release elsewhere cancels it. The
+Play/Pause key remains depressed while playback is active. PREV and NEXT only
+brighten during a press. STOP has no latched light.
+
+The four play-mode controls are alternate-action pushbuttons, not lamps or
+rotary controls. Each has an unmarked spun-brass cap matching the volume cap, a
+fixed collar, and a narrow dark travel seam; the faceplate label identifies its
+function. Off stands slightly proud and casts a small lower shadow. On sits
+nearly flush, collapses that shadow, and illuminates a thin desaturated-green
+ring inside the cap/collar gap. Play/Pause uses the same warm instrument green
+through its cut-out symbol with a tight internal bloom; the depressed key
+remains the primary playing cue.
+
+The SVG mode retains the full existing keyboard surface, including commands not
+placed on the faceplate: manual station creation, reload, edit, append to another
+playlist, shuffle, track removal, and track reordering. Which-key/help text
+documents those commands. The faceplate stays limited to the approved controls.
+
+** Seek and volume
+
+Local files show elapsed time, total duration, an amber progress line, and a
+narrow vertical machined-brass grip. Clicking or dragging the slider sends an
+mpv =seek= command in
+=absolute-percent= mode. The displayed thumb is computed from mpv =time-pos=
+and =duration=. While dragging, the local preview follows the pointer; the next
+mpv response confirms or corrects it.
+
+URL tracks always use the radio presentation. The slider and times disappear.
+One red =ON AIR= line appears under the station art, and the active playlist row
+contains the second =ON AIR=. No other ON AIR label is rendered.
+
+Player volume is separate from the system mixer. Add
+=cj/music-player-volume=, default 100, and apply it to every new mpv process.
+The knob has no number or pointer. Its equally spaced lamps show the stored
+0–100 value. Click/drag around the arc sets volume, the mouse wheel changes it
+by five, and the existing +/− bindings call the same player-volume commands.
+Setting volume writes mpv's =volume= property and updates the stored value so a
+track change does not reset it.
+
+Every volume position is one fixed lamp well. Dark and illuminated states use
+the same center and mounting bezel; the lit state replaces the dark lens rather
+than adding another dot or adjacent glow. The neutral raster skin contains no
+baked lamp cores or light spill.
+
+** Real VU telemetry
+
+mpv starts with a labeled FFmpeg audio filter:
+
+#+begin_src text
+--af-add=@meter:lavfi=[astats=metadata=1:reset=1:measure_perchannel=RMS_level+Peak_level:measure_overall=none]
+#+end_src
+
+The UI reads =af-metadata/meter= over mpv JSON IPC. FFmpeg supplies
+=lavfi.astats.1.RMS_level= and =lavfi.astats.2.RMS_level= (plus peak values).
+Mono duplicates channel one to both meters. Missing or nonnumeric data rests
+the needles and records one diagnostic; it never invents motion.
+
+This path was verified locally on 2026-07-19 with mpv 0.41.0 and FFmpeg 8.1.2.
+A synthetic stereo source returned distinct left/right RMS and peak values
+through the labeled property. The official references are:
+
+- [[https://mpv.io/manual/master/#command-interface-af-metadata][mpv af-metadata property]]
+- [[https://ffmpeg.org/ffmpeg-filters.html#astats-1][FFmpeg astats filter]]
+
+The renderer maps dB to the printed meter scale (−40 through +3). A 300 ms
+attack and slower 600 ms release approximate a physical VU movement. Values
+below the scale rest at the left stop. Values above +3 clamp at the right stop.
+
+Telemetry uses one persistent, nonblocking IPC connection with request IDs and
+line-buffered JSON replies. It polls only while the player view is visible and
+audio is running. It closes when playback stops or the view is hidden. The
+existing one-shot IPC helpers remain valid for infrequent commands.
+
+** Refresh and render budget
+
+Hooks trigger immediate redraws for track, playlist, playback, mode, and art
+changes. Progress updates four times per second while a seekable file plays.
+Meters target ten updates per second while visible and playing. Paused, stopped,
+hidden, and TTY states run no animation timer.
+
+The [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][prototype benchmark]] measured full-image and tiled refresh for one minute
+each at the real dock size. Full refresh failed at 133.616 ms median and
+139.822 ms p95. A display-sized lower-deck tile passed at 35.513 ms median and
+38.221 ms p95. The application therefore uses three aligned images: upper
+player glass, lower control deck, and playlist. Each image includes the
+matching raster crop. Meter ticks refresh only the lower deck; hooks refresh
+the other regions. The source geometry and user-visible layout do not change.
+
+** Errors and empty states
+
+An empty queue shows the receiver, =UNTITLED=, =0 TRACKS=, and fourteen blank
+rows. Playlist actions and radio search remain enabled. Transport, seek, and
+mode actions that require a track are visibly inactive and report a short
+message if invoked by key.
+
+Missing artwork uses the existing vinyl placeholder. Missing metadata is
+omitted. The UI never displays =Unknown=, =N/A=, a raw nil, or a raw stream URL
+as program information.
+
+If SVG, librsvg, the raster shell, or a minimum legible viewport is unavailable,
+F10 opens the implemented fancy/text playlist instead and reports why once. If
+mpv IPC is unavailable, seek, volume telemetry, and meters disable without
+stopping playback. A command reports the failed operation and the next step,
+for example: =Music seek unavailable: mpv IPC socket is not connected; start a
+track and try again.=
+
+=M-x cj/music-ui-doctor= reports graphical/SVG support, resolved assets and font,
+view size, last render time, mpv socket state, meter filter state, telemetry
+timer state, and the last telemetry error. It does not log private file paths
+or stream URLs unless called with a debug prefix.
+
+* Alternatives Considered
+
+** Hybrid raster skin plus dynamic SVG (chosen)
+
+- Good, because it preserves the leather, glass, brass grain, and machined
+ highlights from the approved board while keeping every changing value honest.
+- Bad, because the neutral raster source and SVG geometry must stay aligned.
+- Neutral, because the result is still one SVG application document even though
+ the document embeds raster assets.
+
+** Rebuild the complete receiver as vector geometry
+
+- Good, because every surface would scale without raster limits and theme tokens
+ could recolor everything.
+- Bad, because reproducing leather, brushed brass, glass, and irregular reflected
+ light would take much longer and would look less like the approved board.
+- Neutral, because simple Dupre controls still supply useful vector geometry
+ inside the hybrid renderer.
+
+** Use the final PNG as one clickable background
+
+- Good, because it would match the approved image immediately.
+- Bad, because the PNG contains one track, one playlist, lit volume segments,
+ meter needles, progress, and button state. Painting over those areas would
+ flatten the material texture and produce visible patches.
+- Neutral, because a cleaned neutral derivative of the PNG is the chosen shell.
+
+** Browser/xwidget application
+
+- Good, because browser SVG supports native DOM events and partial element
+ updates.
+- Bad, because it introduces a browser runtime inside Emacs, complicates window
+ integration, and weakens TTY and configuration portability.
+- Neutral, because browser prototypes remain useful before the svg.el port.
+
+** Replace the EMMS playlist buffer in place
+
+- Good, because no second buffer name or adapter is needed.
+- Bad, because EMMS commands depend on point and text markers while the SVG is
+ one displayed image glyph. Mixing invisible rows and image coordinates would
+ be brittle.
+- Neutral, because the dedicated view still delegates every operation to the
+ existing EMMS buffer in v1.
+
+* Decisions [10/10]
+
+** DONE Hybrid raster/SVG rendering
+- Context: the final board's realism comes from material texture and reflected
+ light, while live state must remain replaceable.
+- Decision: We will embed a neutral raster skin in an SVG scene and render all
+ changing information, needles, lights, and selection as SVG.
+- Consequences: easier, the running UI can closely match the board. Harder, the
+ asset export and SVG geometry need one measured coordinate system.
+
+** DONE Craig's final board is the visual authority
+- Context: the design session produced many useful directions, but implementation
+ needs one source for proportion and material treatment.
+- Decision: We will use concept 30, Craig's 1916×821 GIMP refinement, as the
+ layout and appearance authority.
+- Consequences: easier, visual disputes can be checked against one board. Harder,
+ the radio state must be derived without drifting from that hardware.
+
+** DONE Neutral physical shell
+- Context: the final flattened PNG contains transient information and active
+ state.
+- Decision: We will create an editable neutral source and export a clean shell,
+ control sprites, and neutral VU face. No live state remains in those assets.
+- Consequences: easier, texture stays realistic. Harder, Phase 1 includes careful
+ image cleanup before application code can look finished.
+
+** DONE Dedicated graphical view buffer
+- Context: EMMS playlist operations are point/marker based and cannot treat one
+ SVG image as fourteen text rows.
+- Decision: We will render into =*Music Player*= and use controller adapters to
+ operate on the hidden EMMS playlist buffer.
+- Consequences: easier, the SVG renderer stays clean and future-backend ready.
+ Harder, selection and reorder commands need explicit index-to-marker adapters.
+
+** DONE One geometry table for drawing and input
+- Context: separate hand-maintained visual and hit maps will drift.
+- Decision: We will measure the final board once and derive rendering, hit tests,
+ clipping, and scaling from the same constants.
+- Consequences: easier, controls stay clickable after scaling. Harder, geometry
+ extraction is a named deliverable rather than ad hoc coordinates in draw code.
+
+** DONE Direct control semantics
+- Context: every faceplate control must perform real music-config behavior.
+- Decision: We will map the approved transport, modes, radio search, playlist
+ actions, seek, and volume controls directly to their existing commands or the
+ named mpv additions in this spec.
+- Consequences: easier, the interface is self-explanatory. Harder, commands not
+ approved for the faceplate remain keyboard/M-x only and need discoverable help.
+
+** DONE Persistent per-player volume
+- Context: the existing +/- path can change the system mixer, while the design's
+ knob is the player's volume and mpv restarts for each track.
+- Decision: We will store volume in music-config, write mpv's volume property,
+ and apply the stored value when every new mpv process starts.
+- Consequences: easier, the knob is truthful and track changes preserve level.
+ Harder, one more piece of runtime state must stay synchronized with mpv.
+
+** DONE Real meter data through mpv astats
+- Context: random needle animation would violate the design's functional-lighting
+ rule.
+- Decision: We will read left/right RMS metadata from a labeled FFmpeg astats
+ filter over mpv IPC and apply physical needle smoothing.
+- Consequences: easier, the VUs convey real program level. Harder, a visible
+ view needs a small persistent IPC telemetry client and bounded refresh timer.
+
+** DONE Current text views remain fallbacks
+- Context: TTY frames and SVG/asset failures need a usable player, and the
+ implemented fancy text UI already works.
+- Decision: We will add an SVG enable flag and fall back to the current fancy or
+ plain text path without changing M3U or playback state.
+- Consequences: easier, rollout and rollback are safe. Harder, the text path
+ remains maintained alongside the SVG application.
+
+** DONE Three-region render path after measurement
+- Context: =svg-possibly-update-image= rerasterizes an SVG image, so ten full
+ receiver renders per second may exceed the dock's CPU budget.
+- Decision: The prototype benchmark measured the full receiver at 133.616 ms
+ median / 139.822 ms p95 and the display-sized lower-deck tile at 35.513 ms
+ median / 38.221 ms p95. We will use three aligned regions and refresh only
+ the lower deck at meter cadence.
+- Consequences: easier, meter animation passes the render budget with margin.
+ Harder, the renderer places three images and the asset export must hide their
+ boundaries exactly.
+
+* Review findings [0/1]
+
+** TODO The final interaction model has no working two-state prototype :blocking:
+
+The design session produced extensive static boards and established concept 30
+as the visual authority, but the final geometry has not been exercised as a
+working local-playlist and radio prototype. That leaves click targets, drag
+behavior, playlist scrolling, long-text clipping, local/radio state changes,
+and the full-image render budget unproven. Starting implementation now would
+make the first build phase answer product and performance questions that belong
+in the design gate.
+
+Smallest resolution: create one functional prototype using concept 30's
+geometry and neutral skin. It may be a browser prototype or an Emacs SVG proof,
+but it must switch between a long local playlist and a live-radio fixture; wire
+transport, modes, radio search, playlist actions, row and scrollbar navigation,
+seek, and volume interaction; show all pressed and latched states; stress long
+metadata; and record one minute of render timing at the intended dock size.
+Link the accepted prototype under Prototype iterations and Design, record any
+geometry or performance changes here, then repeat spec review.
+
+Progress on 2026-07-19: [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][prototype 1]] now supplies both fixtures and the named
+interactions; its twelve-check behavior pass is green. The [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][librsvg benchmark]]
+records a full-render failure and a passing lower-deck tile, so the spec now
+requires the three-region renderer. Craig's hands-on acceptance is the only
+remaining part of this finding.
+
+* Implementation phases
+
+** Phase 1 — Neutral skin and geometry manifest
+
+Create the editable neutral source from concept 30. Export aligned upper-glass,
+lower-deck, and playlist crops plus control sprites and the VU face under final
+dated asset names. Record measured
+source rectangles, anchors, clip boxes, and hit targets in one Lisp geometry
+table. Add an asset/geometry debug command that renders target outlines over the
+skin. The phase ends with a static SVG in Emacs that matches the final board and
+contains no baked live state.
+
+** Phase 2 — Pure SVG renderer and view mode
+
+Add =modules/music-svg-ui.el=, the state structs, pure layout helpers, raster
+embedding cache, text clipping, dynamic node construction, and
+=cj/music-svg-mode=. F10 can show/hide the static graphical view while the
+existing fancy/text path remains available. Add unit tests for scale transforms,
+hit testing, truncation, scrollbar math, mode colors, and VU angle mapping.
+
+** Phase 3 — Controller snapshot and playlist viewport
+
+Build state snapshots from EMMS without exposing EMMS objects to the renderer.
+Render metadata, album art, header, fourteen rows, selection, and the exact
+scrollbar. Implement keyboard and mouse scrolling, row selection/play, and
+index-to-EMMS-marker adapters. Characterize existing reorder/removal behavior
+before routing those keys through the graphical view.
+
+** Phase 4 — Transport, modes, radio search, and playlist actions
+
+Wire the approved faceplate controls and pressed/latched rendering. Preserve
+the remaining keyboard-only commands and add a concise help surface. Cover
+empty-queue behavior, destructive Delete confirmation, command errors, and
+state refresh hooks. The phase ends with a complete functional control surface
+using the existing backend.
+
+** Phase 5 — Seek and persistent mpv volume
+
+Add player-owned volume state and apply it on process start. Route +/−, wheel,
+click, and drag through the same volume setters. Add absolute seek and drag
+confirmation using mpv time/duration. Render local progress and the exact brass
+thumb. Unit-test value/coordinate mapping and use a fake IPC ledger for command
+order.
+
+** Phase 6 — Stereo telemetry and timed refresh
+
+Add the labeled astats filter, persistent nonblocking telemetry connection,
+JSON request/reply routing, meter parsing, dB mapping, and needle ballistics.
+Run progress and meter timers only while visible and active. Measure the render
+budget for one minute and use the documented three-region fallback if needed.
+Test with the synthetic no-audio-output source used during spec research.
+
+** Phase 7 — Radio state, failures, and diagnostics
+
+Render the matched live-radio state with exactly two ON AIR labels and no seek
+surface. Handle missing art/metadata, unavailable SVG/assets/font, lost mpv IPC,
+missing meter metadata, hidden-window timer shutdown, and an empty queue. Add
+=cj/music-ui-doctor= and verify that fallback never mutates playback or playlist
+state.
+
+** Phase 8 — Integration, documentation, and live verification
+
+Run the focused music tests, batch load, byte compilation, TTY fallback, and GUI
+daemon verification. Exercise a long local playlist, a short radio playlist,
+seek, volume across track changes, every control, scrolling, reorder, deletion,
+and one-hour timer stability. Update music documentation and key hints. Make
+the SVG view the graphical default only after Craig accepts the live result.
+
+* Acceptance criteria
+
+- [ ] F10 opens the Dupre Studios receiver in the bottom side window on a
+ graphical frame and toggles it closed without changing playback.
+- [ ] The running receiver matches concept 30 in proportion, material character,
+ spacing, typography, and control placement at the normal dock size.
+- [ ] Leather, glass, brass, fixed engraving, and neutral hardware come from the
+ raster skin; live information and illumination are not baked into it.
+- [ ] The UI remains fully usable from the keyboard.
+- [ ] Every visible button maps to the command named in this spec.
+- [ ] Pressed, playing, and latched mode states are physically and chromatically
+ distinct.
+- [ ] Local metadata uses separate lines and omits missing values. No visible
+ program field says Unknown or N/A.
+- [ ] Long metadata and playlist rows truncate with an ellipsis before colliding
+ with adjacent fields.
+- [ ] The analog header card shows only the collection/search name and item
+ count; it never adds a PLAYLIST or RADIO type label.
+- [ ] Champagne surfaces use a fine low-contrast metallic micro-brush and never
+ read as wood grain.
+- [ ] The playlist shows fourteen one-line rows with artist inline, selected row
+ amber, and a functional scrollbar whose size and position follow the formulas.
+- [ ] Mouse wheel, keyboard navigation, scrollbar drag, row click/play, reorder,
+ and removal operate on the intended EMMS tracks.
+- [ ] ADD, NEW, LOAD, SAVE, and DELETE preserve current playlist/M3U semantics.
+- [ ] DELETE uses the existing strong confirmation and reports which playlist was
+ deleted.
+- [ ] A local track displays actual elapsed/duration and supports click/drag seek.
+- [ ] The seek thumb is brass and its position comes from mpv state.
+- [ ] A live station has no slider or times and shows exactly two ON AIR labels.
+- [ ] The volume knob controls mpv, not the system mixer, and persists across
+ track changes.
+- [ ] Volume segments fill cumulatively to the stored mpv volume and have no
+ numeric display.
+- [ ] VU needles use distinct real left/right RMS values, rest on missing data,
+ and never use random animation.
+- [ ] Paused, stopped, hidden, and TTY states run no meter/progress timer.
+- [ ] Full or tiled rendering meets the median/95th-percentile budget during a
+ one-minute meter run.
+- [ ] Empty playlist, missing art, missing metadata, missing SVG/assets, and lost
+ mpv IPC each have the fallback or actionable error defined in this spec.
+- [ ] =cj/music-ui-doctor= reports the renderer and telemetry state without
+ exposing file paths or stream URLs by default.
+- [ ] Setting the SVG enable flag off restores the implemented fancy/text UI.
+- [ ] Existing music-config tests pass, and new pure tests cover geometry,
+ snapshots, hit targets, clipping, scrolling, volume, seek, meter mapping, and
+ timer lifecycle.
+- [ ] Craig accepts both a long local-playlist state and a live-radio state in the
+ real Emacs side window before the SVG view becomes the default.
+
+* Readiness dimensions
+
+- Data model & ownership: EMMS and M3U remain authoritative. The SVG snapshot,
+ pressed target, viewport offset, meter smoothing, and render timing are
+ generated runtime state. Player volume is music-config runtime state applied
+ to each mpv process. Raster source assets are project-authored and versioned.
+- Errors, empty states & failure: defined in the Errors and empty states section.
+ Playback continues if rendering or meter telemetry fails. Destructive
+ playlist deletion retains the existing strong confirmation.
+- Security & privacy: no credentials are added. Diagnostics omit local paths and
+ stream URLs by default. Album art and radio favicons stay in the existing
+ local cache.
+- Observability: =cj/music-ui-doctor= exposes assets, font, viewport, render
+ timing, socket/filter/timer state, and last telemetry error. The UI itself
+ exposes player state through lamps, needles, progress, and selection.
+- Performance & scale: fourteen visible rows from playlists tested through at
+ least 1,000 tracks. SVG refresh has explicit median/95th-percentile budgets,
+ a one-minute test, and a pre-approved tiled fallback. Hidden views do no timed
+ work.
+- Reuse & lost opportunities: reuses the current music commands, EMMS state,
+ M3U/radio/art code, bottom side window, Dupre tokens, =svg.el=, and the gallery
+ widget proof. It deliberately does not wait for the EMMS-free/VAMP rewrite.
+- Architecture fit & weak points: new renderer module is pure at its boundary;
+ music-config owns EMMS adaptation and mpv effects. Weak points are raster/SVG
+ alignment, point-based EMMS adapters, full-image rerasterization, and telemetry
+ lifecycle. Geometry sharing, adapter tests, tiled fallback, and visibility-
+ scoped IPC mitigate them.
+- Config surface: add =cj/music-svg-ui= (boolean, default off until live acceptance,
+ then on) and =cj/music-player-volume= (integer 0–100, default 100). Keep
+ =cj/music-fancy-ui= as the fallback selector. Refresh rates and geometry are
+ internal until real use proves a customization need.
+- Documentation plan: update music-config commentary/docstrings, F10/keybinding
+ notes, the music VERIFY task, and the design README. The final spec and dated
+ runtime assets remain linked from the parent task.
+- Dev tooling: use the existing ERT/byte-compile commands, focused music test
+ files, Emacs daemon live verification, and a deterministic synthetic mpv meter
+ script/fixture with =ao=null=. No network or audible output in the normal
+ suite.
+- Rollout, compatibility & rollback: SVG is additive and initially disabled.
+ Fallback is automatic. Rollback is setting =cj/music-svg-ui= nil or reverting
+ the new renderer/controller changes; M3U files and cached art do not migrate.
+- External APIs & deps: built-in Emacs 30.2 =svg.el= and librsvg are the render
+ path. mpv 0.41 and FFmpeg 8.1 were checked locally. Official mpv and FFmpeg
+ documentation confirms labeled audio-filter metadata and astats keys.
+
+* Risks, Rabbit Holes, and Drawbacks
+
+- Cleaning a neutral skin from a flattened final board can produce visible
+ texture patches. Keep the layered source, use broad material samples rather
+ than small clone spots, and approve the neutral shell before wiring state.
+- Full-image librsvg refresh may be too expensive at meter cadence. Measure in
+ the prototype. Use the specified three-region split rather than lowering the
+ meter to a decorative crawl.
+- EMMS commands expect point in the playlist buffer. Keep every index-to-marker
+ translation in one controller adapter and test reorder/remove against the same
+ logical track.
+- SVG text metrics can differ from Emacs face metrics. Use the installed Dupre
+ fixed-width font, fixed boxes, clipping paths, and visual stress fixtures with
+ long titles.
+- An mpv filter may fail for an unusual audio format. Playback wins: leave the
+ meter at rest, show the failure in the doctor, and never stop the track.
+- The raster skin limits infinite scaling and alternate themes. V1 targets the
+ current dock and approved aesthetic. Multiple skins and responsive reflow are
+ separate work.
+
+* Testing / Verification / Rollout
+
+Pure ERT tests cover layout transforms, geometry/hit agreement, text fitting,
+playlist window/scrollbar math, state-to-color mapping, volume/seek conversion,
+VU dB/angle mapping, ballistics, and timer state. Controller tests use an EMMS
+fixture or stubs and an mpv IPC ledger so they do not play sound.
+
+Integration tests build representative local and radio snapshots, render SVG
+strings, and assert required IDs, text, clip paths, and state classes. They do
+not use pixel-perfect screenshots as the main contract because librsvg/font
+versions can change antialiasing. A small set of manual reference screenshots
+supports the visual acceptance step.
+
+The live pass runs in the real bottom side window with the normal long playlist
+and a radio playlist. It covers every faceplate control, keyboard parity,
+scrolling, long strings, seek, persistent volume, two-channel meters, exactly two
+ON AIR states, hidden-window timer shutdown, and fallback. The SVG flag changes
+to default-on only after that pass.
+
+* References / Appendix
+
+- [[file:../../docs/specs/2026-07-06-fancy-music-player-ui-spec.org][Implemented fancy music-player UI spec]]
+- [[file:../../docs/specs/music-config-without-emms-spec.org][Music-config without EMMS architecture spec]]
+- [[file:README.org][Music SVG design record]]
+- [[file:concepts/30a-dupre-studios-user-refined-playlist.png][Near-final playlist board]]
+- [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][Functional two-state prototype]]
+- [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][Prototype librsvg benchmark]]
+- [[https://www.gnu.org/software/emacs/manual/html_node/elisp/SVG-Images.html][Emacs Lisp SVG images]]
+- [[https://mpv.io/manual/master/#json-ipc][mpv JSON IPC]]
+- [[https://mpv.io/manual/master/#command-interface-af-metadata][mpv audio-filter metadata]]
+- [[https://ffmpeg.org/ffmpeg-filters.html#astats-1][FFmpeg astats]]
+
+* Review and iteration history
+
+** 2026-07-19 Sun @ 19:11:57 -0500 — Codex (emacs-d) — hardware-depth correction
+- What: changed the upper boundary of the lower deck from a raised champagne
+ rim to a dark glass reveal descending into the brass faceplate. Unified all
+ four play-mode and both radio-search controls as blank, circularly machined
+ brass pushbutton caps. Removed the returned search glyphs and expanded then
+ tightened the source crop to preserve the volume knob's complete black
+ knurled skirt without importing the old faceplate halo.
+- Why: the prior neutral-faceplate overlay flattened the glass/metal depth,
+ allowed the small-control treatments to diverge, and clipped the knob at a
+ perfect circle that hid its physical knurling. These changes restore a
+ believable assembly hierarchy while retaining state-owned lamps.
+- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; twelve-check interaction regression green.
+
+** 2026-07-19 Sun @ 18:43:00 -0500 — Codex (emacs-d) — prototype refinement
+- What: replaced the baked lower-deck faceplate with a neutral SVG-owned
+ champagne micro-brush surface, then restored only the approved volume knob
+ through a circular crop. Re-engraved the fixed control labels on the new
+ surface and reduced the volume arc to small dark wells with state-owned amber
+ cores and local glow. Verified both local and radio layouts and reran the
+ twelve-check headless interaction pass successfully.
+- Why: masking individual source-image lamps left visible baked illumination and
+ made the faceplate look patched. Owning the complete neutral deck surface in
+ SVG removes the conflicting lamp state and the woodlike raster grain while
+ retaining the realistic machined knob from the approved visual authority.
+- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; local and radio state screenshots.
+
+** 2026-07-19 Sun @ 18:39:41 -0500 — Codex (emacs-d) — prototype refinement
+- What: applied Craig's second hands-on pass. Removed source-type words and the
+ separator from the analog card; changed radio fixture naming to =NAME SEARCH=;
+ replaced the circular seek ornament with a vertical machined-brass grip;
+ removed all mode-cap glyphs and matched their smooth spun-brass finish to the
+ volume cap; and rebuilt the volume arc as dark recessed wells whose amber core
+ and glow exist only for lit positions.
+- Why: the card should identify the current collection/search rather than report
+ an internal playlist/radio type. The prior seek coin had no hardware model,
+ the mode symbols duplicated their faceplate labels, and baked light spill made
+ some nominally dark volume positions glow. The spec now also rejects coarse,
+ wavy champagne texture that reads as wood grain.
+- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; Craig's 18:29–18:32 screenshots.
+
+** 2026-07-19 Sun @ 18:19:28 -0500 — Codex (emacs-d) — prototype refinement
+- What: incorporated Craig's first hands-on notes. Rebuilt play modes as
+ alternate-action brass pushbuttons with engraved glyphs, visible travel, and
+ a restrained inset green ring; tightened Play/Pause illumination; aligned
+ each volume lamp's dark and lit state to one physical well; restored four
+ concealed warm-light pools on a darker coffee/cream playlist card; and made
+ the prototype's synthetic-VU / production-signal distinction explicit.
+- Why: the prior controls read as glowing domes, the green looked like a neon
+ outline, the flattened source and live volume arcs produced double lamps, and
+ the playlist card had lost its analog illumination model. Production VUs
+ remain tied to real mpv astats RMS data as already specified.
+- Artifacts: updated [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; Craig's 17:59–18:03 screenshots.
+
+** 2026-07-19 Sun @ 17:56:27 -0500 — Codex (emacs-d) — prototype author
+- What: built functional prototype 1 from concept 30 with local and radio
+ fixtures, all approved faceplate controls, long-string stress data, exactly
+ fourteen visible rows, scrollbar and seek/volume drag, pressed/latched states,
+ empty/delete flows, keyboard navigation, and synthetic stereo needle motion.
+ Its built-in twelve-check behavior pass is green. Measured librsvg for one
+ minute per path: full receiver 133.616 ms median / 139.822 ms p95 (fail),
+ display-sized lower deck 35.513 ms median / 38.221 ms p95 (pass).
+- Why: this supplies the interaction and performance evidence requested by the
+ first review finding. The measured result makes the three-region renderer a
+ requirement rather than a contingency. Craig's hands-on prototype acceptance
+ remains pending, so the finding stays open and the spec stays DRAFT.
+- Artifacts: [[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][functional prototype 1]]; [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][benchmark record]];
+ [[file:2026-07-19-music-config-ui-remodel-render-fixture.svg][full-render fixture template]].
+
+** 2026-07-19 Sun @ 16:16:57 -0500 — Codex (emacs-d) — reviewer
+- What: completed the first implementation-readiness review after reading the
+ current music-config render, EMMS playlist, radio, mpv IPC, and volume paths.
+ Assigned =Not ready= and recorded one blocking finding: the approved final
+ board is static, so its two application states, hit/drag/scroll behavior, and
+ rendering budget have not passed the project's functional-prototype gate.
+- Why: the architecture, ownership, failure handling, telemetry source, rollout,
+ acceptance criteria, and eight phase boundaries are otherwise specific enough
+ to implement. A working prototype is the smallest remaining test of the
+ design rather than an invitation to redesign it during Phase 1.
+- Artifacts: =* Review findings [0/1]= in this spec; source checks in
+ =modules/music-config.el=; Dupre =gallery-widget.el= SVG proof; verified mpv
+ =af-metadata/meter= / FFmpeg =astats= telemetry path.
+
+** 2026-07-19 Sun @ 16:03:13 -0500 — Codex (emacs-d) — author
+- What: drafted the Music Config UI Remodel spec from Craig's selected concept,
+ manual GIMP refinement, and hybrid raster/SVG decision. Verified the real VU
+ telemetry path against the installed mpv/FFmpeg stack and primary docs.
+- Why: the visual design is settled closely enough to define the renderer,
+ controller boundary, interaction surface, phases, and test contract before
+ implementation.
+- Artifacts: [[file:concepts/30a-dupre-studios-user-refined-playlist.png][concept 30]]; local mpv astats IPC check; parent music SVG task.
diff --git a/working/music-svg-app-design/README.org b/working/music-svg-app-design/README.org
new file mode 100644
index 00000000..5ef6d67d
--- /dev/null
+++ b/working/music-svg-app-design/README.org
@@ -0,0 +1,426 @@
+#+TITLE: Music SVG Application Design Working Set
+#+AUTHOR: Craig Jennings
+
+* Purpose
+
+Six functional HTML/SVG prototypes for the music-config application design session.
+They explore the Dupre chronometer, hi-fi stereo, and automobile-dashboard visual
+language while preserving the current player, queue, radio, and playlist workflows.
+
+This directory owns the complete in-progress remodel package while the design
+continues. The implementation spec is
+[[file:2026-07-19-music-config-ui-remodel-spec.org][2026-07-19-music-config-ui-remodel-spec.org]].
+The current interactive and rendering evidence lives under [[file:prototypes/][prototypes/]].
+Concept boards, source artwork, and visual references stay beside them so local
+iteration does not depend on files scattered through =docs/=. When the design
+is accepted, the spec, prototypes, benchmark, render fixture, and selected final
+assets graduate together to their permanent =docs/= and =assets/= homes.
+
+The selected receiver now has a final interaction candidate at
+[[file:prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html][prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html]]. It uses
+concept 30 as the physical shell and overlays the changing local-playlist and
+radio state. The paired [[file:prototypes/2026-07-19-music-config-ui-remodel-benchmark.org][librsvg benchmark]] makes the three-region production
+renderer mandatory: full-instrument meter refresh missed the budget, while the
+lower control tile passed.
+
+* Inputs
+
+- [[file:../../docs/specs/2026-07-06-fancy-music-player-ui-spec.org][Existing music player UI spec]]
+- [[file:../../docs/specs/music-config-without-emms-spec.org][EMMS-free architecture spec]]
+- [[file:../../../code/archsetup/docs/prototypes/panel-widget-gallery.html][Dupre panel-widget gallery]]
+- [[file:../../../code/archsetup/docs/prototypes/waybar-redesign-prototype.html][Waybar redesign prototype]]
+
+* Prototype Directions
+
+1. Gold-pinstripe tuner console
+2. Touring-car instrument cluster
+3. Chronometer record deck
+4. Studio rack and tape machine
+5. Perpetual-calendar salon receiver
+6. Mission-control radio navigator
+
+* High-Fidelity Concept Boards — Iteration Two
+
+The second pass deliberately separates visual design from implementation. These
+boards establish materials, lighting, silhouette, hierarchy, and instrument
+semantics before the selected direction is translated into Dupre components.
+
+1. [[file:concepts/01-champagne-receiver.png][Champagne Receiver]]
+2. [[file:concepts/02-le-mans-night-cluster.png][Le Mans Night Cluster]]
+3. [[file:concepts/03-geneva-playback-chronograph.png][Geneva Playback Chronograph]]
+4. [[file:concepts/04-mastering-room-reel-console.png][Mastering Room Reel Console]]
+5. [[file:concepts/05-perpetual-calendar-salon.png][Perpetual Calendar Salon]]
+6. [[file:concepts/06-transatlantic-broadcast-navigator.png][Transatlantic Broadcast Navigator]]
+7. [[file:concepts/07-functional-black-glass-receiver.png][Functional Black Glass Receiver]]
+8. [[file:concepts/08-functional-black-glass-flipped.png][Functional Black Glass Receiver — flipped]]
+9. [[file:concepts/09-functional-black-glass-retro.png][Functional Black Glass Receiver — retro]]
+10. [[file:concepts/10-warm-black-glass-player-volume.png][Warm Black Glass Receiver — player volume]]
+11. [[file:concepts/11-illuminated-black-glass-controls.png][Illuminated Black Glass Receiver — varied controls]]
+12. [[file:concepts/12-consolidated-functional-receiver.png][Consolidated Functional Receiver]]
+13. [[file:concepts/13-black-silver-chronograph-receiver.png][Black and Silver Chronograph Receiver]]
+14. [[file:concepts/14-corrected-luxury-chronograph-receiver.png][Corrected Luxury Chronograph Receiver]]
+15. [[file:concepts/15-champagne-brass-digital-hifi.png][Champagne-Brass Digital Hi-Fi]]
+16. [[file:concepts/16-champagne-aluminum-radio-groups.png][Champagne-Aluminum Radio Groups]]
+17. [[file:concepts/17-dupre-coltrane-scrollbar.png][Dupre Coltrane Scrollbar State]]
+18. [[file:concepts/18-dupre-branding-control-study.png][Dupre Branding and Compact-Control Study]]
+19. [[file:concepts/19a-dupre-playlist-state.png][Dupre Playlist State]] / [[file:concepts/19b-dupre-radio-state.png][Dupre Radio State]]
+20. [[file:concepts/20-dupre-playlist-radio-comparison.png][Dupre Long-Playlist / Compact-Radio Comparison]] ([[file:concepts/20a-dupre-long-playlist.png][playlist source]] / [[file:concepts/20b-dupre-compact-radio.png][radio source]])
+21. [[file:concepts/21-dupre-semantic-controls-comparison.png][Dupre Semantic-Controls Comparison]] ([[file:concepts/21a-dupre-mode-lamps-playlist.png][playlist source]] / [[file:concepts/21b-dupre-two-on-air-radio.png][radio source]])
+
+- [[file:concept-board.html][Open the six-board review sheet]]
+
+The original functional HTML pass remains at
+[[file:music-svg-directions.html][music-svg-directions.html]] for interaction and
+workflow reference; it is not the material-design target.
+
+* Current Design Decisions
+
+- Beauty must communicate real application function; no decorative controls.
+- Preserve the Champagne Receiver's now-playing, progress, transport, queue,
+ and playlist-management regions as the functional nucleus.
+- Remove AM/FM, tone, speaker, loudness, power, and other receiver controls
+ without a =music-config.el= command or state.
+- Target a compact panoramic panel in the bottom portion of a predominantly
+ portrait Emacs frame; playlist and player controls must sit side by side.
+- Prefer a neutral black-glass, warm-meter, polished-chrome language with
+ restrained warm metal and tactile black leather; keep whitespace low without
+ returning to clutter. Do not use cold blue illumination.
+- Permanent radio controls map only to the implemented name and tag searches.
+ Manual URL entry remains an Emacs command rather than faceplate hardware.
+- Place the player on the left and playlist on the right.
+- The dots around the volume knob are a cumulative illuminated scale driven by
+ actual volume; the last lit dot is the precise pointer. They never chase or
+ flash.
+- The knob controls dedicated mpv player volume rather than the system mixer.
+ Persist the level in Emacs, pass it to each newly started per-track mpv
+ process, and read/write mpv's =volume= property over the existing IPC socket.
+- Drive the stereo VU meter from real mpv audio analysis. A labeled lavfi
+ =astats= filter exposes per-channel RMS and peak readings through
+ =af-metadata/<label>=; apply analog needle ballistics in the UI. Treat the
+ meter as pre-volume program level so it describes the recording independently
+ of listening volume.
+- Prefer neutral black glass, chrome, and pebbled leather with warm ivory,
+ amber, and restrained jewel illumination; avoid cold blue light.
+- All playlist and now-playing typography is self-luminous behind smoked glass,
+ never merely engraved. Compose album metadata conditionally from album,
+ label, year/date, genre, composer, performer, track, and disc; omit missing
+ values and separators, and never display =Unknown= or =N/A=.
+- Do not display a numeric player-volume value. The physical knob pointer,
+ ticks, and cumulative lamps communicate level.
+- Omit manual URL entry and playlist reload from the permanent faceplate; the
+ commands remain available through Emacs.
+- For seekable tracks, the progress indicator is an interactive slider. For a
+ non-seekable live stream, replace the entire slider/time treatment with an
+ =ON AIR= state; never show both states together.
+- With the slider handling seeking, transport contains only previous,
+ play/pause, stop, and next. Labels live on the faceplate beneath compact
+ physical controls.
+- Put repeat, single, random, and consume lamps/toggles on the lower player
+ control rail. Put radio name/tag search there as well. The right side is
+ reserved for the large playlist and visible playlist actions.
+- Present now-playing metadata as separate illuminated lines and omit the
+ =NOW PLAYING= caption.
+- Use one consistent family of small round chrome transport pushbuttons. Mode
+ controls use four identical low-profile luxury latching pushbuttons with
+ polished-steel collars, smoked-black enamel centers, and integrated faceted
+ jewels: pressed and lit means on; raised and dark means off. Bat toggles are
+ prohibited because they read as utilitarian rather than luxury hardware.
+- Build depth with a realistic two-step polished-chrome bezel and dark gasket
+ around both the VU meter and complete player where they meet black leather.
+ Avoid excessive parallel chrome rails.
+- Use a coherent brushed-silver lower control faceplate. Chrome and silver
+ dominate; brass remains a hairline accent.
+- The playlist shows roughly fourteen compact one-line rows with artist inline,
+ an explicit illuminated playlist name, and square matte-anodized action keys.
+ Visible actions are add, new, load, save, and delete. Delete is a normal
+ matte-red key; edit is not permanent faceplate functionality.
+- Put the =ON AIR= lamp inside the radio control bank. It remains dark for a
+ local file and lights only for a live radio source.
+- Do not use the drum/tape roller as a saved-playlist selector. Its unlabeled
+ adjacent digits were mistaken for a counter and its purpose was not legible;
+ the labeled =LOAD= action is sufficient. If a drum mechanism is revisited,
+ it must expose a unique function that is understood without explanation.
+- Volume segments are identical and evenly spaced; light continuously from the
+ rightmost minimum through and including the pointer-aligned segment.
+- Allocate approximately 42 percent of usable width to the player and 58
+ percent to the playlist. When all fourteen rows fit, show neither a scrollbar
+ nor another redundant track-count mechanism.
+- Remove the current-track =01 / 14= complication entirely: the highlighted
+ playlist row communicates the same information more directly.
+- Put the mode/status/radio rail above the transport rail. Align =ON AIR= with
+ the other mode jewels and give it the same visible diameter, while retaining
+ its non-pressable status-only behavior.
+- Transport hardware is a coherent bank of four shallow, closely spaced,
+ machined-metal hi-fi piano keys rather than separate round buttons.
+- Use distinctly digital, self-luminous typography for player metadata and the
+ playlist; keep faceplate labels engraved rather than digital.
+- A pale brushed champagne-brass control faceplate is viable so long as black
+ glass and polished chrome remain dominant and the brass never becomes bronze,
+ copper, brown, or sepia.
+- Prefer authentic pale champagne-anodized aluminum over brass: fine horizontal
+ brushing, cool metallic highlights, and crisp machined edges retain warmth
+ while matching real high-end audio construction.
+- Fasteners are small, discreet, countersunk details rather than prominent
+ corner bolts.
+- Make radio membership explicit on the faceplate. A restrained engraved
+ =RADIO= heading and hairline bracket span exactly =ON AIR=, =BY NAME=, and
+ =BY TAG=; a separate =PLAY MODES= heading owns repeat, single, random, and
+ consume. Do not use another heavy enclosure.
+- Mode controls combine an unmistakably pressable shallow aluminum cap with a
+ smaller inset jewel. =ON AIR= uses the same jewel diameter and baseline but
+ no cap or travel, preserving its status-only meaning.
+- For a seekable track, slider position must be computed from elapsed/duration
+ rather than independently illustrated. Concept 16 still places the handle
+ too far right for =02:37 / 05:35=; implementation must place it at 47 percent.
+- Color carries stable semantics: warm ivory is ordinary information; amber is
+ active playback, progress, and volume; green is an enabled playback mode;
+ neutral silver is keyboard focus; red is reserved for delete, live =ON AIR=,
+ and faults. Do not color ordinary playlist actions arbitrarily.
+- Use conventional volume geometry: minimum is lower left, maximum lower right,
+ and illumination advances clockwise from minimum through the knob pointer.
+ Segments after the pointer toward maximum remain dark.
+- Playing state must be physical as well as chromatic: the =PLAY/PAUSE= key sits
+ visibly lower with its shadow withdrawn; a restrained amber edge is secondary
+ confirmation.
+- The faceplate may carry one modest maker's plaque: coarse black crosshatch
+ that survives small SVG rendering, with a raised cursive =Dupre= wordmark.
+ It is a badge, not a fake speaker grille, and must not justify extra height.
+- For lists longer than the fourteen-row viewport, show a narrow functional
+ scrollbar. Thumb length represents =visible rows / total rows= and position
+ represents the top visible row. Concept 17 demonstrates the long-list state
+ but its generated thumb remains shorter than the required 14/32 (44 percent).
+- The next review set is a matched two-state pair: one seekable normal-playlist
+ player and one live-radio player using identical hardware. The radio state
+ replaces slider and times with =ON AIR=, lights the red broadcast lamp, and
+ conditionally shows only available station/stream metadata.
+- Do not assume short metadata. Recover display width by reducing album art and
+ VU width roughly 20 percent; allow modest automatic font condensation before
+ ellipsis. Do not marquee by default. Full truncated values remain available
+ through normal Emacs help/minibuffer affordances.
+- Compact playback modes into a 2×2 internally illuminated-legend block. The
+ engraved symbol itself glows; do not add a separate lamp or oversized lip.
+ Keep explicit micro-labels because =CONSUME= has no universal symbol.
+- Place a similarly compact radio block immediately beside play modes: =NAME=
+ and =TAG= momentary buttons above a flush =ON AIR= window. This leaves the
+ right side visually quiet around volume.
+- Branding is part of the faceplate surface, never a plate atop another plate.
+ Concept 18 compares: black-enamel inscription (A), bright-cut inscription
+ (B), same-plane crosshatched negative-space field (C), and direct-mounted
+ chrome script without backing (D).
+- Select concept 18 option D: a moderately enlarged direct-mounted chrome
+ cursive =Dupre= script at the faceplate's upper left, with no backing. Put
+ compact proportional =NAME=, =TAG=, and =ON AIR= radio controls below it.
+- Transport adopts the same internally illuminated-legend construction as the
+ mode keys. The symbol itself is translucent: amber while active, dim ivory
+ while inactive. Do not add a separate under-key light strip. Concepts 19a/b
+ still contain a generated amber strip beneath Play and are non-authoritative
+ on that detail.
+- Treat local and live playback as matched states of identical hardware. A
+ seekable file shows slider and times and keeps =ON AIR= dark; a live stream
+ removes all seek/time hardware, displays =ON AIR / LIVE STREAM= in that region,
+ and lights the faceplate broadcast window red.
+- Scrollbar thumbs remain mathematical UI, not illustrative ornament: 14/32 is
+ 44 percent and 14/18 is 78 percent. Both generated concept-19 thumbs are too
+ short despite their otherwise useful long-list demonstrations.
+- Reserve a clear brand field at the far left of the faceplate and optically
+ center =Dupre= within that invisible rectangle. All functional controls stay
+ outside it.
+- Immediately right of the brand field, stack =NAME=, =TAG=, and =ON AIR= in a
+ narrow vertical Radio column, right-justified tightly against an even smaller
+ 2×2 Play Modes block. Volume retains the quiet right-hand field.
+- Transport state is symbol-only illumination. The area beneath every key is
+ plain faceplate plus its engraved label; no amber strip, lamp, or glow.
+- Stress-test metadata with realistic long values. Concept 20 uses =A CHICKEN
+ WITH ITS HEAD CUT OFF= / =THE MAGNETIC FIELDS= and the 69-track =69 LOVE SONGS=
+ list without marquee or truncation.
+- Review normal and live states together by composing their independently
+ generated panoramic sources. This preserves the true bottom-dock form factor
+ while making both states simultaneously visible.
+- Concept 20's scrollbar rendering remains non-authoritative: the 14/69 playlist
+ thumb should be 20 percent, and the 14/18 radio thumb should be 78 percent and
+ clearly visible. The image model made the former too short and omitted the
+ latter's filled thumb.
+- Live playback contains exactly two =ON AIR= presentations: a red bottom-most
+ metadata line and the current station row. Remove the faceplate status window
+ and any centered banner or =LIVE STREAM= caption.
+- The compact controls form a measured 3×2 grid above transport. Every small
+ control is half a transport key's width and equal to its height; stacked Radio
+ occupies column one and the 2×2 Play Modes block columns two and three. The
+ grid's right edge aligns with =NEXT=.
+- Radio =NAME= and =TAG= are momentary search buttons with magnifying-glass
+ glyphs and no lamps. Persistent playback modes use a separate tiny status
+ lamp above a compact champagne/brass actuator, borrowing the Heston preset
+ language without numeric legends.
+- Branding is a true faceplate aperture: champagne aluminum is cut away to
+ reveal a recessed dark woven/diamond-textured subplate with centered warm-white
+ cursive =Dupre=. It is not a badge attached atop the faceplate.
+- =PLAY/PAUSE= glows green through its symbol while playing. Previous, Stop, and
+ Next stay neutral and merely brighten ivory during activation.
+- Volume uses a deeply knurled black body and machined champagne/brass cap with no
+ pointer. The cumulative lamp arc is the sole indicator and its final lit
+ segment communicates the level.
+- =ADD= and =NEW= may share a restrained OD-green anodized tint. =LOAD= and
+ =SAVE= remain neutral; =DELETE= remains matte red.
+- Concept 21 again renders scrollbar fill incorrectly; the exact 20/78 percent
+ requirements remain implementation constraints rather than image guidance.
+- Tone =DELETE= down from saturated red to muted, low-saturation oxblood or
+ burgundy. It remains unmistakably destructive without becoming the loudest
+ object in the application.
+- Playback modes use four small circular brass pushbuttons mounted directly on
+ the metal. Remove the black switch tiles and separate lamps; illuminate only
+ a thin circumference around an active brass actuator. A single hairline
+ engraved boundary contains the 2×2 group and its geometry aligns with the
+ transport grid ending at =NEXT=.
+- Split the lower player into two genuine material zones rather than stacking
+ branding plates. The left 28--32 percent is uninterrupted black woven grille
+ continuous with the chassis; center a restrained warm-ivory/champagne cursive
+ =Dupre= directly in that negative space with no border, plaque, or fasteners.
+ The right 68--72 percent is one compact self-contained champagne-anodized
+ control island containing Radio, Play Modes, transport, and Volume.
+- Concept 22 validates this identity-bay/control-island architecture in matched
+ playlist and radio states. Its compact balance is authoritative; the image
+ model's engraved Play Modes boundary does not yet reach the =NEXT= alignment
+ datum, so the exact grid alignment remains an implementation constraint.
+- Lock the concept-22 champagne faceplate and every component mounted on it:
+ Radio keys, direct-mounted brass Play Modes matrix and engraved boundary,
+ pointerless cumulative Volume dial, transport keys, fasteners, typography,
+ materials, spacing, and illumination semantics. Subsequent exploration may
+ change surrounding glass and layout, but not this control component.
+- Swap the information and identity zones. Put reduced album/station art and
+ separate-line metadata in the lower black-glass bay beside the locked
+ faceplate. Put =Dupre= directly on the upper black glass with no backing
+ texture, beside substantially larger VU meters.
+- Light each VU face with a subtle warm pool rising from a hidden lamp directly
+ below its =VU= legend. The fixture is not visible; it only explains the
+ meter's restrained internal illumination.
+- Give the player approximately 49 percent of total width and the list 51
+ percent. Keep fourteen rows; ellipsize only the title/artist field when it
+ exceeds its allocation, while preserving the row number, current-state marker,
+ duration, and live status.
+- In radio state, place the first approved =ON AIR= directly underneath the
+ reduced station art. The second remains in the active station row; all
+ inactive status cells are empty. Concept 23 is authoritative for this
+ swapped-information arrangement.
+- Divide the player itself into two equal-height tiers. The upper tier is one
+ continuous black-glass identity/information display; =Dupre= stays at far left
+ while reduced art, separate-line metadata, and conditional seek information
+ occupy the remaining width.
+- The lower tier is one uninterrupted full-width champagne-brass faceplate, not
+ a small plate stacked over another material. Recess modest dual VU meters
+ into its left side and integrate the locked Radio, Play Modes, Volume, and
+ transport hardware on its right with proportional spacing.
+- Return the VU meters to approximately concept-22 scale or slightly smaller.
+ They are black instrument windows with narrow chrome bezels physically set
+ into the brass; their concealed lower lamps remain a soft wash rather than a
+ visible point source.
+- Concept 24 demonstrates this full-brass lower tier in matched playlist and
+ radio states. State-dependent information changes only in the upper glass
+ and control illumination; the underlying manufactured unit is identical.
+- Remove fasteners and corner radii from every interior surface. The upper
+ information glass, lower brass faceplate, and right playlist are three flush
+ rectangular fields separated only by one horizontal and one vertical rule.
+ Rounded depth remains exclusively in the outer leather/chrome chassis.
+- Align the left edge of album or station art exactly with the left edge of the
+ stacked Radio buttons below. This is a shared vertical datum and reserves a
+ larger uninterrupted field for =Dupre=. Confine local seek time, line, and
+ handle to the information block beginning at that datum; nothing extends
+ beneath the wordmark. Live =ON AIR= occupies the corresponding position below
+ station art without a seek affordance.
+- Render ordinary album metadata and inactive list rows in soft neutral white.
+ Amber is reserved for the list header/rule and the currently selected row;
+ inactive rows have no lamps, glow, dots, or amber tint.
+- VU windows are flush or slightly recessed apertures in the brass, with only a
+ hairline inlaid edge and inner shadow. They must never read as raised boxes or
+ chrome housings attached to the faceplate.
+- Concept 25 demonstrates the seamless square-panel and white-type direction.
+ The image generator still places art left of the Radio datum despite a
+ targeted correction; exact shared-edge alignment remains authoritative for
+ the implemented SVG.
+- Reintroduce rounded depth selectively rather than through internal cards. The
+ two-step chrome trim encircling the complete unit has clearly rounded corners,
+ and the flush VU apertures may use modest rounded corners.
+- Rename the maker signature to a two-line =Dupre Studios=: large cursive
+ =Dupre= above smaller widely tracked uppercase =STUDIOS=, centered as one
+ optical lockup directly on the black glass.
+- Move album/station art, metadata, and the conditional seek/live treatment
+ farther right to enlarge the calm manufacturer field.
+- Remove the upper vertical player/list separator entirely. Branding, program
+ information, and playlist share one continuous black-glass plane; header rules
+ and column alignment alone establish list hierarchy.
+- Define the lower brass faceplate as a distinct receiver module without adding
+ fasteners: a narrow recessed dark groove followed by slim deep chrome encircles
+ all four sides of the brass. Its surround uses graceful rounded corners while
+ the single brass field remains uninterrupted.
+- Concept 26 demonstrates the =Dupre Studios= lockup, continuous upper glass,
+ rounded outer chrome, rounded flush VU apertures, and fully encircled brass
+ control module in matched playlist and radio states.
+- Remove the complete unit's outer metal trim. The exterior silhouette is a
+ softly rolled black-leather edge; the leather terminates directly against the
+ continuous black glass through a narrow compressed dark seam, with no metal
+ between those materials.
+- Convert every remaining metal edge from chrome to warm brass. This includes
+ the bead encircling the lower faceplate and the hairline rounded VU aperture
+ edges. No silver/chrome edging remains anywhere.
+- The artwork, stacked Radio keys, and =PREV= transport key share one exact left
+ edge. Treat this as a single vertical construction datum; the seek assembly
+ begins there as well and remains below only the art/metadata block.
+- Make only the playlist/radio header row an analog instrument card: darker
+ coffee-and-cream stock behind glass with dark espresso lettering. Four hidden
+ warm lamps above it—one at each corner and two evenly spaced between—cast
+ broad subtle pools downward. Neither bulbs nor hotspots are visible, and all
+ list rows below remain black with white/selected-amber type.
+- Concept 27 demonstrates the leather-to-glass junction, brass-only edging, and
+ analog illuminated header in matched playlist/radio states. Its artwork is
+ visually close to the control datum; implementation must make the shared edge
+ mathematically exact.
+- Adopt Craig's GIMP faceplate composition as the control-layout authority.
+ Divide usable brass width into approximately 44/25/31 percent zones: dual VUs
+ over transport at left, Play Modes over a horizontal Name/Tag Radio pair at
+ center, and Volume vertically centered at right. Use shared baselines,
+ consistent label offsets, and even component gaps so the plate is full without
+ crowding.
+- The four transport keys span roughly the combined width of the two VUs and sit
+ directly beneath them. The center controls form two aligned modules: 2×2
+ direct-mounted brass modes above and two black radio search keys below.
+- Increase the Volume control to the diameter in Craig's GIMP arrangement, but
+ make it a low-profile frontal instrument: shallow knurled skirt, broad flush
+ machined brass cap, minimal shadow, and no domed, magnified, or fisheye look.
+ Center the complete knob/arc vertically and put =VOLUME= beneath it.
+- Volume lamps return to unmistakable warm amber/gold. Cumulative lit segments
+ must never wash out to white, cream, coral, or pink; unlit segments remain
+ dark.
+- Concept 28 demonstrates the proportional three-zone control layout and flatter
+ enlarged Volume dial in matched playlist and radio states.
+- Finalize the program-information construction line by moving the artwork and
+ all metadata right as one rigid group until the artwork's left edge is exactly
+ collinear with the left engraved boundary of the Radio group below. The local
+ seek treatment begins on that same datum: move its left time and left endpoint
+ with the information group, keep its right time and endpoint fixed, and shorten
+ the run accordingly.
+- The circular seek thumb is warm polished brass, not chrome or silver. Its
+ restrained highlight may distinguish it from the amber progress line without
+ introducing a cold-metal exception to the brass-only material language.
+- Treat Craig's manually refined 1916×821 playlist board, saved as
+ =concepts/30a-dupre-studios-user-refined-playlist.png=, as the near-final
+ visual authority. It resolves the upper-panel composition: the larger calm
+ =Dupre Studios= field, right-shifted artwork and separate metadata lines, a
+ contained shorter seek run with brass thumb, and a playlist-dominant right
+ side all coexist without crowding.
+- Preserve the board's proportional lower deck: paired illuminated VUs and one
+ coherent transport family at left, compact direct-mounted Play Modes and Radio
+ Search controls in the center, and the large vertically centered low-profile
+ Volume instrument at right.
+
+* Concept 14 Review Notes
+
+The four matching jeweled latching mode buttons, simplified two-step chrome
+bezel, fourteen-row playlist, explicit playlist name, one-line track rows, and
+clearly labeled action keys move the design toward the selected language. The
+board still contains three image-generation errors that are not design changes:
+
+- The player remains wider than the playlist instead of the required 42/58
+ allocation.
+- The volume arc stays illuminated beyond the knob pointer instead of ending at
+ and including the pointer-aligned segment.
+- =COLUMBIA= and =1959= share a line instead of occupying separate conditional
+ metadata lines.
diff --git a/working/music-svg-app-design/assets/vinyl-placeholder.svg b/working/music-svg-app-design/assets/vinyl-placeholder.svg
new file mode 100644
index 00000000..cf01519f
--- /dev/null
+++ b/working/music-svg-app-design/assets/vinyl-placeholder.svg
@@ -0,0 +1,20 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300" role="img" aria-label="Vinyl record placeholder">
+ <defs>
+ <radialGradient id="disc" cx="50%" cy="50%" r="50%">
+ <stop offset="0%" stop-color="#1a1a1a"/>
+ <stop offset="100%" stop-color="#000000"/>
+ </radialGradient>
+ </defs>
+ <circle cx="150" cy="150" r="148" fill="url(#disc)"/>
+ <g fill="none" stroke="#2b2b2b" stroke-width="1">
+ <circle cx="150" cy="150" r="138"/>
+ <circle cx="150" cy="150" r="126"/>
+ <circle cx="150" cy="150" r="114"/>
+ <circle cx="150" cy="150" r="102"/>
+ <circle cx="150" cy="150" r="90"/>
+ <circle cx="150" cy="150" r="78"/>
+ </g>
+ <circle cx="150" cy="150" r="52" fill="#d99a2b"/>
+ <circle cx="150" cy="150" r="52" fill="none" stroke="#a8741a" stroke-width="2"/>
+ <circle cx="150" cy="150" r="7" fill="#111111"/>
+</svg>
diff --git a/working/music-svg-app-design/concept-board.html b/working/music-svg-app-design/concept-board.html
new file mode 100644
index 00000000..378d13ea
--- /dev/null
+++ b/working/music-svg-app-design/concept-board.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Music SVG application — high-fidelity concept boards</title>
+<style>
+:root{color-scheme:dark;--bg:#0c0b0a;--panel:#151310;--ink:#eee3c8;--dim:#97938a;--gold:#e7aa3f;--line:#40351f}
+*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at top,#282014 0,transparent 38%),var(--bg);color:var(--ink);font:14px/1.5 "BerkeleyMono Nerd Font","Berkeley Mono",monospace}
+header{max-width:1500px;margin:auto;padding:28px 24px 18px}h1{margin:4px 0 8px;color:var(--gold);font:500 clamp(24px,4vw,44px) Georgia,serif}.eyebrow{color:var(--dim);font-size:11px;letter-spacing:.22em;text-transform:uppercase}p{max-width:880px;color:var(--dim)}
+main{max-width:1500px;margin:auto;padding:0 24px 70px;display:grid;gap:28px}.board{border:1px solid var(--line);border-radius:14px;overflow:hidden;background:var(--panel);box-shadow:0 18px 50px #0009}.board img{display:block;width:100%;height:auto}.caption{display:flex;gap:18px;align-items:baseline;padding:13px 17px;border-top:1px solid var(--line)}.num{color:var(--gold)}h2{font-size:15px;letter-spacing:.08em;margin:0;font-weight:500}.hint{margin-left:auto;color:var(--dim);font-size:11px}@media(max-width:700px){header,main{padding-left:12px;padding-right:12px}.caption{align-items:flex-start;flex-wrap:wrap}.hint{width:100%;margin:0}}
+</style>
+</head>
+<body>
+<header><div class="eyebrow">Music SVG application · material-design pass</div><h1>Six instruments worth living with</h1><p>Judge the silhouette, material palette, visual hierarchy, and behavior metaphors. Typography is illustrative; implementation will use the actual Dupre kit and SVG-safe primitives.</p></header>
+<main>
+<article class="board"><img src="concepts/01-champagne-receiver.png" alt="Champagne Receiver music application"><div class="caption"><span class="num">01</span><h2>Champagne Receiver</h2><span class="hint">hi-fi receiver · blue glass · walnut · moving-coil VU</span></div></article>
+<article class="board"><img src="concepts/02-le-mans-night-cluster.png" alt="Le Mans Night Cluster music application"><div class="caption"><span class="num">02</span><h2>Le Mans Night Cluster</h2><span class="hint">grand tourer · leather · chrome gauges · tell-tales</span></div></article>
+<article class="board"><img src="concepts/03-geneva-playback-chronograph.png" alt="Geneva Playback Chronograph music application"><div class="caption"><span class="num">03</span><h2>Geneva Playback Chronograph</h2><span class="hint">guilloché · sapphire chapter ring · jeweled complications</span></div></article>
+<article class="board"><img src="concepts/04-mastering-room-reel-console.png" alt="Mastering Room Reel Console music application"><div class="caption"><span class="num">04</span><h2>Mastering Room Reel Console</h2><span class="hint">reel-to-reel · brushed aluminum · VU glass · walnut rack</span></div></article>
+<article class="board"><img src="concepts/05-perpetual-calendar-salon.png" alt="Perpetual Calendar Salon music application"><div class="caption"><span class="num">05</span><h2>Perpetual Calendar Salon</h2><span class="hint">rosewood · ivory enamel · apertures · moonphase record</span></div></article>
+<article class="board"><img src="concepts/06-transatlantic-broadcast-navigator.png" alt="Transatlantic Broadcast Navigator music application"><div class="caption"><span class="num">06</span><h2>Transatlantic Broadcast Navigator</h2><span class="hint">black glass · chrome · frequency scale · radio radar</span></div></article>
+</main>
+</body>
+</html>
diff --git a/working/music-svg-app-design/concepts/01-champagne-receiver.png b/working/music-svg-app-design/concepts/01-champagne-receiver.png
new file mode 100644
index 00000000..453ca9e3
--- /dev/null
+++ b/working/music-svg-app-design/concepts/01-champagne-receiver.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/02-le-mans-night-cluster.png b/working/music-svg-app-design/concepts/02-le-mans-night-cluster.png
new file mode 100644
index 00000000..f967f7f6
--- /dev/null
+++ b/working/music-svg-app-design/concepts/02-le-mans-night-cluster.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/03-geneva-playback-chronograph.png b/working/music-svg-app-design/concepts/03-geneva-playback-chronograph.png
new file mode 100644
index 00000000..3990e075
--- /dev/null
+++ b/working/music-svg-app-design/concepts/03-geneva-playback-chronograph.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/04-mastering-room-reel-console.png b/working/music-svg-app-design/concepts/04-mastering-room-reel-console.png
new file mode 100644
index 00000000..3d9ced49
--- /dev/null
+++ b/working/music-svg-app-design/concepts/04-mastering-room-reel-console.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/05-perpetual-calendar-salon.png b/working/music-svg-app-design/concepts/05-perpetual-calendar-salon.png
new file mode 100644
index 00000000..301bec21
--- /dev/null
+++ b/working/music-svg-app-design/concepts/05-perpetual-calendar-salon.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.png b/working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.png
new file mode 100644
index 00000000..148f573f
--- /dev/null
+++ b/working/music-svg-app-design/concepts/06-transatlantic-broadcast-navigator.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/07-functional-black-glass-receiver.png b/working/music-svg-app-design/concepts/07-functional-black-glass-receiver.png
new file mode 100644
index 00000000..8be7cc89
--- /dev/null
+++ b/working/music-svg-app-design/concepts/07-functional-black-glass-receiver.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/08-functional-black-glass-flipped.png b/working/music-svg-app-design/concepts/08-functional-black-glass-flipped.png
new file mode 100644
index 00000000..404c7493
--- /dev/null
+++ b/working/music-svg-app-design/concepts/08-functional-black-glass-flipped.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/09-functional-black-glass-retro.png b/working/music-svg-app-design/concepts/09-functional-black-glass-retro.png
new file mode 100644
index 00000000..12893867
--- /dev/null
+++ b/working/music-svg-app-design/concepts/09-functional-black-glass-retro.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.png b/working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.png
new file mode 100644
index 00000000..ae1ed9f7
--- /dev/null
+++ b/working/music-svg-app-design/concepts/10-warm-black-glass-player-volume.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.png b/working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.png
new file mode 100644
index 00000000..df8ce658
--- /dev/null
+++ b/working/music-svg-app-design/concepts/11-illuminated-black-glass-controls.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/12-consolidated-functional-receiver.png b/working/music-svg-app-design/concepts/12-consolidated-functional-receiver.png
new file mode 100644
index 00000000..dc2ccab4
--- /dev/null
+++ b/working/music-svg-app-design/concepts/12-consolidated-functional-receiver.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.png b/working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.png
new file mode 100644
index 00000000..2fe1af0e
--- /dev/null
+++ b/working/music-svg-app-design/concepts/13-black-silver-chronograph-receiver.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.png b/working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.png
new file mode 100644
index 00000000..c238a8f6
--- /dev/null
+++ b/working/music-svg-app-design/concepts/14-corrected-luxury-chronograph-receiver.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.png b/working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.png
new file mode 100644
index 00000000..7469c348
--- /dev/null
+++ b/working/music-svg-app-design/concepts/15-champagne-brass-digital-hifi.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.png b/working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.png
new file mode 100644
index 00000000..e15bb4c5
--- /dev/null
+++ b/working/music-svg-app-design/concepts/16-champagne-aluminum-radio-groups.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.png b/working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.png
new file mode 100644
index 00000000..9e9e948e
--- /dev/null
+++ b/working/music-svg-app-design/concepts/17-dupre-coltrane-scrollbar.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/18-dupre-branding-control-study.png b/working/music-svg-app-design/concepts/18-dupre-branding-control-study.png
new file mode 100644
index 00000000..6826f613
--- /dev/null
+++ b/working/music-svg-app-design/concepts/18-dupre-branding-control-study.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/19a-dupre-playlist-state.png b/working/music-svg-app-design/concepts/19a-dupre-playlist-state.png
new file mode 100644
index 00000000..cb08988d
--- /dev/null
+++ b/working/music-svg-app-design/concepts/19a-dupre-playlist-state.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/19b-dupre-radio-state.png b/working/music-svg-app-design/concepts/19b-dupre-radio-state.png
new file mode 100644
index 00000000..26f0ff9a
--- /dev/null
+++ b/working/music-svg-app-design/concepts/19b-dupre-radio-state.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.png b/working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.png
new file mode 100644
index 00000000..60648503
--- /dev/null
+++ b/working/music-svg-app-design/concepts/20-dupre-playlist-radio-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/20a-dupre-long-playlist.png b/working/music-svg-app-design/concepts/20a-dupre-long-playlist.png
new file mode 100644
index 00000000..b4fb84a2
--- /dev/null
+++ b/working/music-svg-app-design/concepts/20a-dupre-long-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/20b-dupre-compact-radio.png b/working/music-svg-app-design/concepts/20b-dupre-compact-radio.png
new file mode 100644
index 00000000..42992161
--- /dev/null
+++ b/working/music-svg-app-design/concepts/20b-dupre-compact-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.png b/working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.png
new file mode 100644
index 00000000..c86f1f59
--- /dev/null
+++ b/working/music-svg-app-design/concepts/21-dupre-semantic-controls-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.png b/working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.png
new file mode 100644
index 00000000..a18fdc8b
--- /dev/null
+++ b/working/music-svg-app-design/concepts/21a-dupre-mode-lamps-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.png b/working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.png
new file mode 100644
index 00000000..70303bc2
--- /dev/null
+++ b/working/music-svg-app-design/concepts/21b-dupre-two-on-air-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.png b/working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.png
new file mode 100644
index 00000000..c0e7d59a
--- /dev/null
+++ b/working/music-svg-app-design/concepts/22-dupre-identity-bay-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.png b/working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.png
new file mode 100644
index 00000000..1ab38a42
--- /dev/null
+++ b/working/music-svg-app-design/concepts/22a-dupre-identity-bay-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.png b/working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.png
new file mode 100644
index 00000000..f752ed17
--- /dev/null
+++ b/working/music-svg-app-design/concepts/22b-dupre-identity-bay-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.png b/working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.png
new file mode 100644
index 00000000..252ad160
--- /dev/null
+++ b/working/music-svg-app-design/concepts/23-dupre-swapped-info-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.png b/working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.png
new file mode 100644
index 00000000..f9df6b85
--- /dev/null
+++ b/working/music-svg-app-design/concepts/23a-dupre-swapped-info-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.png b/working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.png
new file mode 100644
index 00000000..e532e98e
--- /dev/null
+++ b/working/music-svg-app-design/concepts/23b-dupre-swapped-info-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.png b/working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.png
new file mode 100644
index 00000000..285ec95f
--- /dev/null
+++ b/working/music-svg-app-design/concepts/24-dupre-full-brass-vu-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.png b/working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.png
new file mode 100644
index 00000000..59dd8109
--- /dev/null
+++ b/working/music-svg-app-design/concepts/24a-dupre-full-brass-vu-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.png b/working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.png
new file mode 100644
index 00000000..9a0bde44
--- /dev/null
+++ b/working/music-svg-app-design/concepts/24b-dupre-full-brass-vu-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.png b/working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.png
new file mode 100644
index 00000000..049d1dd3
--- /dev/null
+++ b/working/music-svg-app-design/concepts/25-dupre-seamless-flush-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.png b/working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.png
new file mode 100644
index 00000000..16063222
--- /dev/null
+++ b/working/music-svg-app-design/concepts/25a-dupre-seamless-flush-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.png b/working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.png
new file mode 100644
index 00000000..4fdbbbb8
--- /dev/null
+++ b/working/music-svg-app-design/concepts/25b-dupre-seamless-flush-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.png b/working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.png
new file mode 100644
index 00000000..ed067d99
--- /dev/null
+++ b/working/music-svg-app-design/concepts/26-dupre-studios-rounded-chrome-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.png b/working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.png
new file mode 100644
index 00000000..b086886d
--- /dev/null
+++ b/working/music-svg-app-design/concepts/26a-dupre-studios-rounded-chrome-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.png b/working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.png
new file mode 100644
index 00000000..dce2f048
--- /dev/null
+++ b/working/music-svg-app-design/concepts/26b-dupre-studios-rounded-chrome-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.png b/working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.png
new file mode 100644
index 00000000..603aa5a0
--- /dev/null
+++ b/working/music-svg-app-design/concepts/27-dupre-studios-leather-analog-header-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.png b/working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.png
new file mode 100644
index 00000000..288ad19b
--- /dev/null
+++ b/working/music-svg-app-design/concepts/27a-dupre-studios-leather-analog-header-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.png b/working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.png
new file mode 100644
index 00000000..c02286a2
--- /dev/null
+++ b/working/music-svg-app-design/concepts/27b-dupre-studios-leather-analog-header-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.png b/working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.png
new file mode 100644
index 00000000..1e8199f9
--- /dev/null
+++ b/working/music-svg-app-design/concepts/28-dupre-studios-balanced-controls-comparison.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.png b/working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.png
new file mode 100644
index 00000000..ee329bea
--- /dev/null
+++ b/working/music-svg-app-design/concepts/28a-dupre-studios-balanced-controls-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.png b/working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.png
new file mode 100644
index 00000000..516f555f
--- /dev/null
+++ b/working/music-svg-app-design/concepts/28b-dupre-studios-balanced-controls-radio.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.png b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.png
new file mode 100644
index 00000000..20871536
--- /dev/null
+++ b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcf b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcf
new file mode 100644
index 00000000..9691728b
--- /dev/null
+++ b/working/music-svg-app-design/concepts/29a-dupre-studios-brass-seek-playlist.xcf
Binary files differ
diff --git a/working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.png b/working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.png
new file mode 100644
index 00000000..50dae41b
--- /dev/null
+++ b/working/music-svg-app-design/concepts/30a-dupre-studios-user-refined-playlist.png
Binary files differ
diff --git a/working/music-svg-app-design/music-svg-directions.html b/working/music-svg-app-design/music-svg-directions.html
new file mode 100644
index 00000000..07b0456e
--- /dev/null
+++ b/working/music-svg-app-design/music-svg-directions.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Music SVG application — six directions</title>
+<style>
+:root{--ground:#151311;--panel:#100f0f;--raise:#1a1917;--well:#090a0b;--cream:#f3e7c5;--steel:#969385;--dim:#6f7478;--gold:#e2a038;--goldhi:#ffbe54;--amber:#f0b552;--green:#7fe0a0;--red:#e2543f;--line:#393226;--mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% -20%,#302818 0,transparent 55%),var(--ground);color:var(--cream);font:14px/1.35 var(--mono)}header{padding:20px max(20px,calc((100vw - 1320px)/2));border-bottom:1px solid var(--line);background:#100f0fdd;position:sticky;top:0;z-index:4;backdrop-filter:blur(12px)}.eyebrow{color:var(--steel);font-size:10px;letter-spacing:.22em;text-transform:uppercase}.head{display:flex;gap:20px;align-items:end;justify-content:space-between;flex-wrap:wrap}.head h1{font-size:22px;color:var(--goldhi);margin:3px 0}.head p{max-width:640px;margin:0;color:var(--steel)}.tabs{display:flex;gap:6px;flex-wrap:wrap}.tabs button,.button{font:inherit;color:var(--steel);background:var(--raise);border:1px solid var(--line);border-radius:5px;padding:7px 9px;cursor:pointer}.tabs button:hover,.tabs button.on,.button:hover{color:var(--panel);background:var(--gold);border-color:var(--goldhi)}main{max-width:1320px;margin:auto;padding:28px 20px 60px}.note{color:var(--steel);margin:0 0 14px}.concept{display:none;min-height:650px}.concept.on{display:block}.face{min-height:640px;padding:20px;border:1px solid #5d471d;border-radius:16px;background:linear-gradient(135deg,#211b12,#0b0b0b 45%,#19150f);box-shadow:inset 0 1px #ffffff13,0 18px 55px #0008;position:relative;overflow:hidden}.face:before,.face:after{content:"";position:absolute;left:16px;right:16px;height:2px;background:linear-gradient(90deg,transparent,var(--gold),var(--goldhi),var(--gold),transparent);opacity:.9}.face:before{top:11px}.face:after{bottom:11px}.label{color:var(--gold);font-size:10px;letter-spacing:.18em;text-transform:uppercase}.well{background:linear-gradient(#060707,#121313);border:1px solid #342b1b;border-radius:8px;box-shadow:inset 0 2px 12px #000;padding:13px}.screen{color:var(--green);text-shadow:0 0 8px #7fe0a077}.title{font:600 clamp(20px,3vw,38px) Georgia,serif;color:var(--cream)}.sub{color:var(--steel)}.gold{color:var(--goldhi)}.lamp{display:inline-block;width:9px;height:9px;border-radius:100%;background:#312818;border:1px solid #6b5223}.lamp.on{background:var(--goldhi);box-shadow:0 0 12px #ffbe54}.lamp.live{background:var(--green);box-shadow:0 0 12px #7fe0a0}.lamp.warn{background:var(--red);box-shadow:0 0 12px #e2543f}.transport{display:flex;gap:8px;align-items:center;justify-content:center}.transport button{width:46px;height:39px;border-radius:6px;border:1px solid #685025;background:linear-gradient(#3a2e1c,#17130d);color:var(--goldhi);font:18px var(--mono);cursor:pointer}.transport button.play{background:linear-gradient(#f0c46c,#8f671f);color:#181109}.transport button:active{transform:translateY(1px);filter:brightness(1.2)}.queue{display:grid;gap:4px}.track{display:grid;grid-template-columns:30px 1fr auto;gap:10px;align-items:center;padding:7px 8px;border-bottom:1px solid #ffffff0b;cursor:pointer}.track:hover,.track.current{background:#e2a03818}.track.current{border-left:3px solid var(--goldhi)}.track .num{color:var(--gold)}.track .meta{color:var(--steel);font-size:11px}.progress{height:7px;background:#28221a;border:1px solid #554321;border-radius:8px;overflow:hidden}.progress i{display:block;height:100%;width:42%;background:linear-gradient(90deg,#8f671f,var(--goldhi),#fff0af);box-shadow:0 0 8px var(--goldhi)}.presets{display:flex;gap:6px;flex-wrap:wrap}.preset{padding:5px 8px;border:1px solid #49391e;border-radius:20px;background:#17130e;color:var(--steel);cursor:pointer}.preset.on{color:var(--panel);background:var(--goldhi)}.knob{width:84px;height:84px;border:8px solid #5c4319;border-radius:50%;background:radial-gradient(circle at 38% 30%,#ffe0a0,#b77b27 45%,#3d2b13 68%);position:relative;box-shadow:0 3px 8px #000}.knob:after{content:"";position:absolute;width:3px;height:27px;background:#1b1308;top:8px;left:38px;border-radius:2px;transform:rotate(32deg);transform-origin:50% 34px}.gauge{width:230px;height:126px;overflow:hidden;position:relative}.gauge svg{width:230px;height:230px;position:absolute;bottom:-104px}.gauge b{position:absolute;bottom:7px;left:0;right:0;text-align:center;color:var(--goldhi);font-size:24px}.svg{width:100%;height:100%}.caption{margin-top:13px;color:var(--steel);font-size:12px}.pinstripe{display:grid;grid-template-columns:minmax(240px,1.1fr) minmax(360px,1.8fr) minmax(240px,1fr);gap:16px;align-items:stretch}.odometer{font-size:clamp(27px,5vw,58px);letter-spacing:.12em;color:#17120a;background:var(--cream);border:3px solid var(--gold);padding:10px 14px;border-radius:6px;box-shadow:inset 0 -5px #b9a988}.dashboard{display:grid;grid-template-columns:1fr minmax(340px,1.45fr) 1fr;gap:18px;align-items:center}.dash-center{text-align:center}.dial{width:min(100%,310px);aspect-ratio:1;margin:auto;border:10px solid #30343a;border-radius:50%;background:radial-gradient(circle,#171611 0 52%,transparent 53%),repeating-conic-gradient(from 225deg,#d59a36 0 1deg,transparent 1deg 7deg);position:relative}.dial:after{content:"";position:absolute;left:50%;bottom:50%;width:3px;height:36%;background:var(--red);transform-origin:bottom;transform:rotate(42deg);box-shadow:0 0 8px #e2543f}.dial .dialtext{position:absolute;inset:0;display:grid;place-content:center;text-align:center;color:var(--goldhi)}.chronometer{display:grid;grid-template-columns:minmax(260px,1fr) minmax(280px,1fr);gap:22px}.chrono{border-radius:50%;aspect-ratio:1;max-width:440px;margin:auto;border:12px double var(--gold);background:repeating-radial-gradient(circle,#15110c 0 5px,#1f1910 6px 7px);position:relative;display:grid;place-content:center;text-align:center}.chrono:before{content:"";position:absolute;inset:13px;border:1px solid #c28b2c;border-radius:50%;background:repeating-conic-gradient(#d69b38 0 1deg,transparent 1deg 6deg)}.chrono>*{position:relative}.time{font-size:clamp(34px,6vw,66px);letter-spacing:.05em;color:var(--cream)}.rack{display:grid;grid-template-columns:190px 1fr 210px;gap:14px}.vu{display:flex;gap:7px;align-items:end;height:110px}.vu i{width:12px;background:linear-gradient(var(--red) 0 18%,var(--goldhi) 18% 50%,var(--green) 50%);border-radius:2px;box-shadow:0 0 6px #7fe0a066}.vu i:nth-child(1){height:28%}.vu i:nth-child(2){height:55%}.vu i:nth-child(3){height:38%}.vu i:nth-child(4){height:76%}.vu i:nth-child(5){height:92%}.vu i:nth-child(6){height:64%}.vu i:nth-child(7){height:48%}.calendar{display:grid;grid-template-columns:1fr 1.35fr 1fr;gap:16px}.apertures{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.aperture{padding:11px 5px;text-align:center;background:#080706;border:2px solid var(--gold);border-radius:5px;color:var(--cream);font-size:20px;box-shadow:inset 0 3px 10px #000}.radio{display:grid;grid-template-columns:1.1fr 1.5fr;gap:18px}.radar{aspect-ratio:1;max-width:370px;margin:auto;border:2px solid #4b742d;border-radius:50%;background:repeating-radial-gradient(circle,#102012 0 1px,transparent 2px 58px),linear-gradient(135deg,#07100a,#122d16);position:relative;overflow:hidden}.radar:before{content:"";position:absolute;inset:50% 0 0 50%;background:conic-gradient(from 15deg,#7fe0a088,transparent 55deg);transform-origin:0 0;animation:sweep 3s linear infinite}.radar:after{content:"• 104.9\A\A ◉ \A\A •";white-space:pre;position:absolute;inset:30px;color:var(--green);font-size:20px;text-shadow:0 0 8px var(--green)}@keyframes sweep{to{transform:rotate(360deg)}}@media(max-width:900px){.pinstripe,.dashboard,.rack,.calendar,.chronometer,.radio{grid-template-columns:1fr}.face{min-height:auto}.gauge{margin:auto}}
+</style>
+</head>
+<body>
+<header><div class="head"><div><div class="eyebrow">Music SVG application · six functional directions</div><h1>Which instrument do you want to live with?</h1><p>Every direction drives the same player state: transport, queue selection, radio preset, playback mode, and volume all respond.</p></div><nav class="tabs" id="tabs"></nav></div></header>
+<main><p class="note" id="note"></p><div id="stage"></div></main>
+<script>
+const state={playing:true,track:0,volume:68,preset:'Groove Salad',modes:{repeat:false,random:false,consume:false}};
+const tracks=[['Groove Salad','SomaFM · 256k','◉'],['Blue in Green','Miles Davis · 5:37','♪'],['Dawn Chorus','Tycho · 4:21','♪'],['Drone Zone','SomaFM · 128k','◉'],['Night Drive','Chromatics · 4:06','♪']];
+const names=['Gold-pinstripe tuner','Touring-car dashboard','Chronometer record deck','Studio rack','Perpetual-calendar salon','Radio navigator'];
+const notes=['One machined faceplate: the clearest bridge to Waybar direction 1. A tape-counter queue and a brass volume knob make library work feel deliberate.','A driving instrument cluster: progress becomes speed, queue position becomes odometer, and state becomes a small set of unmistakable tell-tales.','A timepiece that treats playback as elapsed time. Best for focused listening: transport is controlled from the bezel and the queue becomes a program card.','A tactile late-70s receiver: VU meters make audio activity visible, while a rack layout gives playlist and library controls equal status.','A restrained horological receiver: album/stream art sits beside calendar-like status apertures. Best if the music panel should feel like a dressed object, not a machine.','A green-phosphor tuner/navigation station: radio discovery takes center stage, with stations as signals and playlists as routes.'];
+function controls(){return `<div class="transport"><button data-action="prev">‹‹</button><button class="play" data-action="play">${state.playing?'Ⅱ':'▶'}</button><button data-action="next">››</button><button data-action="seek">↶</button><button data-action="library">☰</button></div>`}
+function queue(){return `<div class="queue">${tracks.map((t,i)=>`<div class="track ${i===state.track?'current':''}" data-track="${i}"><span class="num">${String(i+1).padStart(2,'0')}</span><span>${t[2]} ${t[0]}<br><span class="meta">${t[1]}</span></span><span class="lamp ${i===state.track?'live':''}"></span></div>`).join('')}</div>`}
+function modes(){return `<div class="presets"><span class="preset ${state.modes.repeat?'on':''}" data-mode="repeat">repeat</span><span class="preset ${state.modes.random?'on':''}" data-mode="random">random</span><span class="preset ${state.modes.consume?'on':''}" data-mode="consume">consume</span></div>`}
+function radioPresets(){return `<div class="presets">${['Groove Salad','Drone Zone','FIP','NTS 1'].map(x=>`<span class="preset ${x===state.preset?'on':''}" data-preset="${x}">${x}</span>`).join('')}</div>`}
+function now(){let t=tracks[state.track];return `<div class="label">Now playing <span class="lamp ${state.playing?'live':''}"></span></div><div class="title">${t[0]}</div><div class="sub">${t[1]} · ${state.playing?'playing':'paused'}</div><div class="progress"><i style="width:${state.playing?42:13}%"></i></div>`}
+function pinstripe(){return `<section class="concept pinstripe on"><div class="face"><div class="pinstripe"><div class="well"><div class="label">Program counter</div><div class="odometer">0${state.track+1}·24</div><p class="caption">Playlist position is a physical counter. The control bank below opens load, save, edit, and delete flows.</p>${modes()}</div><div class="well">${now()}<div style="height:12px"></div>${controls()}<div style="height:16px"></div><div class="label">Live queue</div>${queue()}</div><div class="well" style="display:grid;place-items:center;align-content:center;gap:12px"><div class="label">Volume</div><div class="knob" data-action="volume"></div><b class="gold">${state.volume}%</b><div class="label">Radio presets</div>${radioPresets()}</div></div><p class="caption">Direction 1 — gold pinstripe tuner console. Use as the base language if the player should visibly belong beside the Waybar odometer console.</p></div></section>`}
+function dashboard(){return `<section class="concept dashboard"><div class="face"><div class="dashboard"><div class="well"><div class="label">Source / modes</div>${radioPresets()}<div style="height:18px"></div>${modes()}<div style="height:18px"></div><div class="label">Queue</div>${queue()}</div><div class="dash-center"><div class="dial"><div class="dialtext"><div class="label">elapsed</div><div style="font-size:42px">02:18</div><div class="gold">${state.track+1} / ${tracks.length}</div></div></div><div style="height:12px"></div>${controls()}<p class="caption">Needle position means playback progress. Lamps mean real state: green live, amber engaged, red fault or missing media.</p></div><div class="well"><div class="label">Signal and volume</div><div class="gauge"><svg viewBox="0 0 230 230"><path d="M28 172 A95 95 0 0 1 202 172" fill="none" stroke="#5d451e" stroke-width="16"/><path d="M28 172 A95 95 0 0 1 164 83" fill="none" stroke="#e2a038" stroke-width="10"/><line x1="115" y1="172" x2="153" y2="95" stroke="#ffbe54" stroke-width="4"/></svg><b>${state.volume}<small>%</small></b></div><div class="label">Tell-tales</div><p><span class="lamp live"></span> playing &nbsp; <span class="lamp on"></span> repeat &nbsp; <span class="lamp"></span> queue saved</p></div></div><p class="caption">Direction 2 — touring-car dashboard. Strongest one-glance state model; it turns progress, volume, queue position, and errors into instruments rather than labels.</p></div></section>`}
+function chronometer(){return `<section class="concept chronometer"><div class="face"><div class="chronometer"><div class="chrono"><div class="label">${state.playing?'running':'held'} chronometer</div><div class="time">02:18</div><div class="sub">of 05:37</div><div style="margin-top:18px">${controls()}</div></div><div class="well">${now()}<div style="height:16px"></div><div class="label">Program card</div>${queue()}<div style="height:15px"></div><div class="label">Playback modes</div>${modes()}<div style="height:15px"></div><div class="label">Station bank</div>${radioPresets()}</div></div><p class="caption">Direction 3 — chronometer record deck. Playback time is the hero; a physical bezel is a compelling seek surface in the SVG port.</p></div></section>`}
+function rack(){return `<section class="concept rack"><div class="face"><div class="rack"><div class="well"><div class="label">Input / source</div>${radioPresets()}<div style="height:15px"></div><div class="label">Program modes</div>${modes()}<div style="height:20px"></div><div class="label">Master</div><div class="knob" data-action="volume"></div><p class="gold">${state.volume}%</p></div><div class="well">${now()}<div style="height:18px"></div><div class="label">Stereo program</div><div class="vu">${Array.from({length:7},()=>'<i></i>').join('')}<span style="width:18px"></span>${Array.from({length:7},()=>'<i></i>').join('')}</div><div class="screen" style="margin:12px 0;padding:8px">L −8.1 dB&nbsp;&nbsp; R −7.8 dB&nbsp;&nbsp; ${state.playing?'SIGNAL':'HOLD'}</div>${controls()}</div><div class="well"><div class="label">Tape library / queue</div>${queue()}</div></div><p class="caption">Direction 4 — studio rack and tape machine. The VU meters are alive only while a track is playing; flashing or red meter states become a useful failure vocabulary.</p></div></section>`}
+function calendar(){return `<section class="concept calendar"><div class="face"><div class="calendar"><div class="well"><div class="label">Library cabinet</div>${queue()}<div style="height:12px"></div><div class="label">File operations</div><div class="presets"><span class="preset">load</span><span class="preset">save</span><span class="preset">edit</span><span class="preset">delete</span></div></div><div class="well"><div class="label">Music perpetual calendar</div><div class="apertures"><div class="aperture">JAZZ</div><div class="aperture">SIDE A</div><div class="aperture">LIVE</div></div><div style="height:22px"></div>${now()}<div style="height:18px"></div>${controls()}<div style="height:18px"></div>${modes()}</div><div class="well"><div class="label">Station complications</div>${radioPresets()}<div style="height:18px"></div><div class="label">Volume winding</div><div class="knob" data-action="volume"></div><p class="gold">${state.volume}%</p></div></div><p class="caption">Direction 5 — perpetual-calendar dress. Gold-framed apertures make source, mode, and queue identity feel calm and ceremonial; reserve flashing for actual attention, never decoration.</p></div></section>`}
+function navigator(){return `<section class="concept radio"><div class="face"><div class="radio"><div><div class="radar"></div><div class="caption" style="text-align:center">The sweep means an active radio discovery request. A fixed blip means a saved station; a pulsing blip means buffering or reconnecting.</div></div><div class="well"><div class="label">Tuner navigator</div>${now()}<div style="height:16px"></div>${controls()}<div style="height:16px"></div><div class="label">Stations</div>${radioPresets()}<div style="height:16px"></div><div class="label">Route / playlist</div>${queue()}<div style="height:16px"></div>${modes()}</div></div><p class="caption">Direction 6 — mission-control radio navigator. Best for elevating radio search, tag browsing, and saved stations to first-class interaction rather than hiding them behind a command row.</p></div></section>`}
+const renders=[pinstripe,dashboard,chronometer,rack,calendar,navigator];let active=Math.max(0,Math.min(renders.length-1,Number(new URLSearchParams(location.search).get('direction'))||0));
+function render(){document.querySelector('#tabs').innerHTML=names.map((n,i)=>`<button class="${i===active?'on':''}" data-tab="${i}">${i+1}. ${n}</button>`).join('');document.querySelector('#stage').innerHTML=renders[active]();document.querySelector('#stage .concept').classList.add('on');document.querySelector('#note').textContent=notes[active];bind()}
+function bind(){document.querySelectorAll('[data-tab]').forEach(e=>e.onclick=()=>{active=+e.dataset.tab;render()});document.querySelectorAll('[data-track]').forEach(e=>e.onclick=()=>{state.track=+e.dataset.track;render()});document.querySelectorAll('[data-preset]').forEach(e=>e.onclick=()=>{state.preset=e.dataset.preset;state.track=0;render()});document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{state.modes[e.dataset.mode]=!state.modes[e.dataset.mode];render()});document.querySelectorAll('[data-action]').forEach(e=>e.onclick=()=>{let a=e.dataset.action;if(a==='play')state.playing=!state.playing;if(a==='next')state.track=(state.track+1)%tracks.length;if(a==='prev')state.track=(state.track+tracks.length-1)%tracks.length;if(a==='volume')state.volume=state.volume>82?35:state.volume+8;render()})}render();
+</script>
+</body>
+</html>
diff --git a/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org
new file mode 100644
index 00000000..7243ad5f
--- /dev/null
+++ b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-benchmark.org
@@ -0,0 +1,61 @@
+#+TITLE: Music Config UI Remodel Prototype Benchmark
+#+DATE: 2026-07-19
+
+* Purpose
+
+Measure the rasterization cost that matters for the proposed Emacs SVG port.
+The interactive browser prototype proves behavior, but its DOM timings do not
+predict librsvg cost. These trials use =rsvg-convert= 2.62.3 against embedded
+raster/SVG fixtures at the intended roughly 958×411 dock size.
+
+The shell loop launched a fresh =rsvg-convert= process for each sample. Process
+startup is therefore included, making these results a conservative bound for an
+in-process Emacs/librsvg update.
+
+* Full instrument trial
+
+- Fixture: concept 30 embedded as a 3.0 MiB data URI, plus live metadata,
+ playlist rows, progress, VU needles, and volume segments.
+- Output: 958×411.
+- Duration: 60 seconds.
+- Samples: 448.
+- Mean: 134.074 ms.
+- Median: 133.616 ms.
+- 95th percentile: 139.822 ms.
+- Range: 125.105–163.231 ms.
+- Verdict: fail. A full receiver rasterization cannot drive ten-Hz VU motion.
+
+* Lower-deck tile trial
+
+- Fixture: the opaque lower control deck exported at its 585×226 display size
+ as JPEG quality 88, embedded as a 52 KiB data URI, plus two dynamic needles
+ and twenty-one dynamic volume segments.
+- Output: 585×226.
+- Duration: 60 seconds.
+- Samples: 1,677.
+- Mean: 35.694 ms.
+- Median: 35.513 ms.
+- 95th percentile: 38.221 ms.
+- Range: 31.719–62.626 ms.
+- Verdict: pass. Both the under-50-ms median and under-100-ms p95 budgets pass.
+
+* Resulting implementation constraint
+
+The production view uses three aligned image regions from one coordinate
+manifest:
+
+1. Upper player glass, redrawn for track, art, seek, and radio-state changes.
+2. Lower control deck, redrawn at meter cadence while visible and playing.
+3. Playlist, redrawn for queue, selection, and scroll changes.
+
+The editable neutral source remains lossless and full resolution. The opaque
+runtime deck export is sized for the normal dock and may use a high-quality JPEG
+after visual comparison with PNG; it is not the editable source. A wider dock
+may select a larger cached export, but V1 never returns to full-instrument
+meter-rate rasterization.
+
+* Related artifacts
+
+- [[file:2026-07-19-music-config-ui-remodel-prototype-1.html][Functional browser prototype]]
+- [[file:../2026-07-19-music-config-ui-remodel-render-fixture.svg][Full-render SVG fixture]]
+- [[file:../2026-07-19-music-config-ui-remodel-spec.org][Music Config UI Remodel spec]]
diff --git a/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html
new file mode 100644
index 00000000..c05a6947
--- /dev/null
+++ b/working/music-svg-app-design/prototypes/2026-07-19-music-config-ui-remodel-prototype-1.html
@@ -0,0 +1,1450 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Dupre Studios Music Config UI Remodel</title>
+<style>
+ :root {
+ --black: #050605;
+ --glass: #090a09;
+ --soft-white: #e9e2d3;
+ --dim-white: #a9a59c;
+ --amber: #f2a900;
+ --amber-hot: #ffc34f;
+ --green: #9dda59;
+ --red: #a73a29;
+ --brass: #c9a574;
+ --coffee: #b69c7d;
+ --ink: #201810;
+ --mono: "Berkeley Mono", "BerkeleyMono Nerd Font", ui-monospace, monospace;
+ }
+
+ * { box-sizing: border-box; }
+
+ [hidden] { display: none !important; }
+
+ html, body { min-height: 100%; }
+
+ body {
+ margin: 0;
+ color: var(--soft-white);
+ background:
+ radial-gradient(circle at 50% -20%, #2c261e 0, transparent 42%),
+ linear-gradient(#0e0e0d, #050505 70%);
+ font-family: var(--mono);
+ }
+
+ button, input { font: inherit; }
+
+ .workbench {
+ width: min(100%, 1960px);
+ margin: 0 auto;
+ padding: 16px 20px 28px;
+ }
+
+ .prototype-bar {
+ min-height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+ margin-bottom: 12px;
+ color: #bbb3a3;
+ font-size: 12px;
+ letter-spacing: .04em;
+ }
+
+ .prototype-bar h1 {
+ margin: 0 0 3px;
+ color: #e8d6b7;
+ font-size: 14px;
+ font-weight: 500;
+ letter-spacing: .13em;
+ text-transform: uppercase;
+ }
+
+ .prototype-bar p { margin: 0; }
+
+ .fixture-switch, .bench-controls {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ }
+
+ .lab-button {
+ min-height: 31px;
+ padding: 5px 10px;
+ color: #bbb3a3;
+ border: 1px solid #554b3e;
+ border-radius: 3px;
+ background: linear-gradient(#26231f, #141311);
+ cursor: pointer;
+ }
+
+ .lab-button:hover, .lab-button[aria-pressed="true"] {
+ color: #17130e;
+ border-color: #b89765;
+ background: linear-gradient(#e0c292, #a98857);
+ }
+
+ .bench-readout {
+ min-width: 242px;
+ color: #9b9487;
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+ }
+
+ .receiver-wrap {
+ width: 100%;
+ overflow: hidden;
+ border-radius: 0 0 12px 12px;
+ box-shadow: 0 28px 60px #000c;
+ }
+
+ .receiver {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1916 / 821;
+ overflow: hidden;
+ background: #060606 url("../concepts/30a-dupre-studios-user-refined-playlist.png") center / 100% 100% no-repeat;
+ user-select: none;
+ touch-action: none;
+ }
+
+ .dynamic-svg, .hit-layer {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ .dynamic-svg { pointer-events: none; }
+
+ .upper-live {
+ position: absolute;
+ left: 31.55%;
+ top: 11.6%;
+ width: 27.9%;
+ height: 31.4%;
+ overflow: hidden;
+ background:
+ radial-gradient(ellipse at 36% 10%, #25242140, transparent 46%),
+ linear-gradient(100deg, #090a09 0, #060706 72%, #080908 100%);
+ box-shadow: inset 0 0 34px #000;
+ }
+
+ .art {
+ position: absolute;
+ left: 1.4%;
+ top: 1.5%;
+ width: 28%;
+ height: 69%;
+ overflow: hidden;
+ border: 1px solid #292724;
+ border-radius: 2px;
+ background: #171716;
+ box-shadow: 0 5px 15px #000b;
+ }
+
+ .art img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ }
+
+ .album-crop {
+ width: 100%;
+ height: 100%;
+ background-image: url("../concepts/30a-dupre-studios-user-refined-playlist.png");
+ background-size: 1312.33% 475.2%;
+ background-position: 34.42% 20.9%;
+ }
+
+ .metadata {
+ position: absolute;
+ left: 33.1%;
+ right: 1.5%;
+ top: 0;
+ height: 72%;
+ display: grid;
+ align-content: start;
+ gap: 4.6%;
+ padding-top: 1.2%;
+ color: var(--soft-white);
+ font-size: clamp(7px, 1.04vw, 20px);
+ line-height: 1.07;
+ letter-spacing: .08em;
+ text-shadow: 0 0 6px #fff2;
+ text-transform: uppercase;
+ }
+
+ .metadata-line {
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .metadata-line.dim { color: #d5cfc2; }
+
+ .on-air-under-art {
+ position: absolute;
+ left: 1.6%;
+ top: 73.5%;
+ width: 27.5%;
+ color: #e9543f;
+ font-size: clamp(8px, 1vw, 18px);
+ letter-spacing: .18em;
+ text-align: center;
+ text-shadow: 0 0 9px #e9543f99;
+ }
+
+ .seek-line {
+ position: absolute;
+ left: 1.4%;
+ right: 1.5%;
+ bottom: 2.5%;
+ height: 20%;
+ display: grid;
+ grid-template-columns: 12% 1fr 12%;
+ align-items: center;
+ gap: 3%;
+ color: #d2cabd;
+ font-size: clamp(8px, .94vw, 18px);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .seek-time:last-child { text-align: right; }
+
+ .seek-track {
+ position: relative;
+ height: 12px;
+ cursor: ew-resize;
+ }
+
+ .seek-track::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 5px;
+ height: 2px;
+ background: #373735;
+ box-shadow: inset 0 1px #000;
+ }
+
+ .seek-fill {
+ position: absolute;
+ left: 0;
+ top: 5px;
+ width: calc(var(--progress) * 1%);
+ height: 2px;
+ background: var(--amber);
+ box-shadow: 0 0 4px #f2a90088;
+ }
+
+ .seek-thumb {
+ position: absolute;
+ top: 50%;
+ left: calc(var(--progress) * 1%);
+ width: 12px;
+ height: 21px;
+ border: 1px solid #ead0a4;
+ border-radius: 3px;
+ background: linear-gradient(90deg, #6f4c2d, #caa16a 22%, #f0d3a0 48%, #ad7f4a 74%, #654429);
+ box-shadow: 0 2px 4px #000d, inset 0 1px #fff4, inset 0 -1px #50351f;
+ transform: translate(-50%, -50%);
+ }
+
+ .seek-thumb::after {
+ content: "";
+ position: absolute;
+ left: 50%;
+ top: 3px;
+ bottom: 3px;
+ width: 1px;
+ background: #5f4026aa;
+ box-shadow: 1px 0 #f2d7aa66;
+ }
+
+ .playlist-header {
+ position: absolute;
+ left: 61.16%;
+ top: 6.35%;
+ width: 36%;
+ height: 7.8%;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 2.4%;
+ padding: 0 2.1%;
+ overflow: hidden;
+ color: #1f1811;
+ background:
+ radial-gradient(ellipse 10% 100% at 5% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%),
+ radial-gradient(ellipse 10% 100% at 35% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%),
+ radial-gradient(ellipse 10% 100% at 65% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%),
+ radial-gradient(ellipse 10% 100% at 95% -3%, #fff1d7 0, #eac184aa 35%, transparent 76%),
+ linear-gradient(90deg, #d5b78c22, transparent 25% 75%, #5c3c2233),
+ linear-gradient(180deg, #92795e, #877057);
+ box-shadow:
+ inset 0 1px #f7dfb6,
+ inset 0 5px 14px #fff2,
+ inset 0 -8px 15px #5a3d2433,
+ inset 0 0 0 1px #73583b66;
+ font-size: clamp(8px, 1.04vw, 20px);
+ letter-spacing: .075em;
+ text-transform: uppercase;
+ }
+
+ .header-name {
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .playlist-live {
+ position: absolute;
+ left: 61.05%;
+ top: 14.25%;
+ width: 36.12%;
+ height: 66.15%;
+ overflow: hidden;
+ background:
+ radial-gradient(ellipse at 48% 0, #20211e22, transparent 55%),
+ linear-gradient(90deg, #070807, #090a09 65%, #050605);
+ box-shadow: inset 0 0 26px #000;
+ }
+
+ .playlist-scroll {
+ position: absolute;
+ inset: 0 22px 0 0;
+ overflow-y: scroll;
+ scrollbar-width: none;
+ overscroll-behavior: contain;
+ }
+
+ .playlist-scroll::-webkit-scrollbar { display: none; }
+
+ .playlist-rows { min-height: 100%; }
+
+ .playlist-row {
+ height: var(--row-height, 37px);
+ min-height: var(--row-height, 37px);
+ display: grid;
+ grid-template-columns: 7.5% minmax(0, 1fr) 12%;
+ align-items: center;
+ gap: 1.5%;
+ padding: 0 1.2% 0 2.6%;
+ color: #dad6cd;
+ border-bottom: 1px solid #ffffff06;
+ font-size: clamp(7px, .91vw, 17px);
+ letter-spacing: .045em;
+ text-transform: uppercase;
+ cursor: pointer;
+ }
+
+ .playlist-row:hover { background: #f2a9000b; color: #fffaf0; }
+
+ .playlist-row.selected {
+ color: var(--amber);
+ background: linear-gradient(90deg, #f2a90012, transparent 72%);
+ text-shadow: 0 0 7px #f2a90044;
+ }
+
+ .row-number { position: relative; font-variant-numeric: tabular-nums; }
+
+ .playlist-row.selected .row-number::before {
+ content: "▶";
+ position: absolute;
+ right: calc(100% + 4px);
+ font-size: .78em;
+ }
+
+ .row-name {
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .row-meta { text-align: right; font-variant-numeric: tabular-nums; }
+
+ .scroll-rail {
+ position: absolute;
+ top: 1.2%;
+ right: 5px;
+ bottom: 1.2%;
+ width: 9px;
+ border: 1px solid #554b3d;
+ border-radius: 5px;
+ background: #191815;
+ box-shadow: inset 0 1px 3px #000;
+ }
+
+ .scroll-thumb {
+ position: absolute;
+ left: -1px;
+ top: 0;
+ width: 9px;
+ min-height: 38px;
+ border: 1px solid #aa9574;
+ border-radius: 5px;
+ background: linear-gradient(90deg, #7b6a53, #bca887 45%, #76634b);
+ box-shadow: 0 0 4px #000;
+ cursor: ns-resize;
+ }
+
+ .empty-row {
+ height: var(--row-height, 37px);
+ border-bottom: 1px solid #ffffff05;
+ }
+
+ .transport-bank {
+ position: absolute;
+ left: 5.95%;
+ top: 71.7%;
+ width: 24.5%;
+ height: 7.45%;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 5.4%;
+ }
+
+ .transport-button {
+ position: relative;
+ border: 1px solid #1d1b18;
+ border-radius: 4px;
+ color: #b6a687;
+ background:
+ linear-gradient(180deg, #292927 0 7%, #171716 15% 72%, #090a09 100%);
+ box-shadow:
+ inset 0 1px #5b5953,
+ inset 0 -3px #050505,
+ 0 5px 5px #0009;
+ font-size: clamp(10px, 1.4vw, 25px);
+ cursor: pointer;
+ transform: translateY(0);
+ }
+
+ .transport-button:hover { color: #e9ddc6; filter: brightness(1.12); }
+
+ .transport-button:active, .transport-button.pressed {
+ transform: translateY(3px);
+ box-shadow: inset 0 2px 4px #000, 0 1px 2px #000b;
+ }
+
+ .transport-button.playing {
+ color: #bdd27c;
+ text-shadow: 0 0 2px #a7c26c, 0 0 4px #70883e99;
+ transform: translateY(3px);
+ box-shadow: inset 0 2px 5px #000, 0 1px 2px #000b;
+ }
+
+ .mode-button,
+ .radio-button {
+ position: absolute;
+ width: 2.05%;
+ aspect-ratio: 1;
+ padding: 0;
+ border: 1px solid #6e5436;
+ border-radius: 50%;
+ color: #2a2118;
+ background:
+ radial-gradient(circle at 34% 25%, #fff0c5 0 2.5%, transparent 5%),
+ repeating-radial-gradient(circle at 50% 50%, #fff3d00b 0 1px, #4b2e160b 1px 2px),
+ radial-gradient(circle at 46% 42%, #e8cb94 0, #c79a60 52%, #8b6038 82%, #d3aa70 100%);
+ box-shadow:
+ 0 3px 3px #0009,
+ inset 0 1px #f2d39e,
+ 0 0 0 3px #342719,
+ 0 0 0 4px #d0ad78;
+ cursor: pointer;
+ transform: translateY(0);
+ }
+
+ .mode-button::after {
+ content: "";
+ position: absolute;
+ inset: -4px;
+ border: 1px solid transparent;
+ border-radius: 50%;
+ }
+
+ .mode-button[aria-pressed="true"]::after {
+ border-color: #829b55;
+ box-shadow: 0 0 2px #819a55aa, inset 0 0 2px #71864988;
+ }
+
+ .mode-button[aria-pressed="true"], .mode-button:active {
+ transform: translateY(2px);
+ box-shadow:
+ 0 1px 1px #0009,
+ inset 0 2px 3px #3c291777,
+ 0 0 0 3px #2c2117,
+ 0 0 0 4px #bb9766;
+ }
+
+ .radio-button {
+ box-shadow:
+ 0 3px 3px #0009,
+ inset 0 1px #f2d39e,
+ 0 0 0 3px #342719,
+ 0 0 0 4px #d0ad78;
+ }
+
+ .radio-button:active {
+ transform: translateY(2px);
+ box-shadow:
+ 0 1px 1px #0009,
+ inset 0 2px 3px #3c291777,
+ 0 0 0 3px #2c2117,
+ 0 0 0 4px #bb9766;
+ }
+
+ .mode-repeat { left: 34.68%; top: 52.75%; }
+ .mode-single { left: 39.18%; top: 52.75%; }
+ .mode-random { left: 34.68%; top: 61.25%; }
+ .mode-consume { left: 39.18%; top: 61.25%; }
+ .radio-name { left: 34.68%; top: 75.65%; }
+ .radio-tags { left: 39.18%; top: 75.65%; }
+
+ .volume-hit {
+ position: absolute;
+ left: 43.2%;
+ top: 49.6%;
+ width: 14.6%;
+ height: 34%;
+ border-radius: 50%;
+ cursor: grab;
+ }
+
+ .volume-hit:active { cursor: grabbing; }
+
+ .playlist-actions {
+ position: absolute;
+ left: 61.13%;
+ top: 81.65%;
+ width: 35.45%;
+ height: 7.45%;
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: .65%;
+ }
+
+ .playlist-action {
+ border: 1px solid #302b23;
+ border-radius: 2px;
+ color: #ccc5b8;
+ background: linear-gradient(#1d1c19, #090a09);
+ box-shadow: inset 0 1px #555147, 0 3px 5px #0007;
+ font-size: clamp(8px, 1vw, 19px);
+ letter-spacing: .05em;
+ cursor: pointer;
+ }
+
+ .playlist-action[data-action="add"],
+ .playlist-action[data-action="new"] {
+ background: linear-gradient(#29271d, #11120d);
+ }
+
+ .playlist-action[data-action="delete"] {
+ color: #ead3cc;
+ border-color: #572c24;
+ background: linear-gradient(#552019, #2e0f0b);
+ }
+
+ .playlist-action:hover { filter: brightness(1.2); }
+
+ .playlist-action:active { transform: translateY(2px); box-shadow: inset 0 2px 5px #000; }
+
+ .status-toast {
+ position: absolute;
+ left: 50%;
+ bottom: 2.7%;
+ z-index: 8;
+ max-width: 52%;
+ padding: .55% 1%;
+ color: #d7ccb8;
+ border: 1px solid #6d5b42;
+ border-radius: 3px;
+ background: #090907ed;
+ box-shadow: 0 5px 14px #000c;
+ font-size: clamp(7px, .78vw, 15px);
+ letter-spacing: .04em;
+ opacity: 0;
+ pointer-events: none;
+ transform: translate(-50%, 8px);
+ transition: opacity .15s, transform .15s;
+ }
+
+ .status-toast.show { opacity: 1; transform: translate(-50%, 0); }
+
+ dialog {
+ width: min(430px, calc(100vw - 40px));
+ color: #e6dccb;
+ border: 1px solid #9b7651;
+ border-radius: 6px;
+ background: linear-gradient(#1d1b18, #0b0b0a);
+ box-shadow: 0 24px 70px #000;
+ font-family: var(--mono);
+ }
+
+ dialog::backdrop { background: #000b; }
+ dialog h2 { color: #e0be87; font-size: 16px; font-weight: 500; }
+ dialog p { color: #bdb5a8; font-size: 13px; line-height: 1.45; }
+ dialog menu { display: flex; justify-content: flex-end; gap: 8px; padding: 0; }
+
+ .delete-confirm {
+ color: #f0dad3;
+ border-color: #7c3327;
+ background: #592018;
+ }
+
+ .inspection-note {
+ margin: 13px 2px 0;
+ display: flex;
+ justify-content: space-between;
+ gap: 18px;
+ color: #7f796f;
+ font-size: 11px;
+ line-height: 1.5;
+ }
+
+ .inspection-note span:last-child { text-align: right; }
+
+ @media (max-width: 980px) {
+ .workbench { padding: 10px 8px 20px; }
+ .prototype-bar { align-items: flex-start; flex-direction: column; }
+ .fixture-switch, .bench-controls { justify-content: flex-start; }
+ .bench-readout { text-align: left; }
+ }
+</style>
+</head>
+<body>
+<main class="workbench">
+ <header class="prototype-bar">
+ <div>
+ <h1>Music Config UI Remodel · Functional Prototype 1</h1>
+ <p>Drive the receiver. The controls outside it only switch fixtures and run the review benchmark.</p>
+ </div>
+ <div class="fixture-switch" aria-label="Prototype fixtures">
+ <button class="lab-button" id="localFixture" aria-pressed="true">Local playlist</button>
+ <button class="lab-button" id="radioFixture" aria-pressed="false">Radio station</button>
+ <button class="lab-button" id="restoreFixture">Restore fixture</button>
+ </div>
+ <div class="bench-controls">
+ <button class="lab-button" id="benchmarkButton">Run 60s benchmark</button>
+ <span class="bench-readout" id="benchmarkReadout">Dynamic updates: awaiting benchmark</span>
+ </div>
+ </header>
+
+ <section class="receiver-wrap" aria-label="Dupre Studios receiver prototype">
+ <div class="receiver" id="receiver">
+ <div class="upper-live" id="upperLive">
+ <div class="art" id="art"><div class="album-crop" aria-label="69 Love Songs album art"></div></div>
+ <div class="metadata" id="metadata"></div>
+ <div class="on-air-under-art" id="onAirUnderArt" hidden>ON AIR</div>
+ <div class="seek-line" id="seekLine">
+ <span class="seek-time" id="elapsed">01:30</span>
+ <div class="seek-track" id="seekTrack" role="slider" tabindex="0" aria-label="Track position" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
+ <div class="seek-fill"></div>
+ <div class="seek-thumb"></div>
+ </div>
+ <span class="seek-time" id="duration">02:59</span>
+ </div>
+ </div>
+
+ <div class="playlist-header" id="playlistHeader">
+ <span class="header-name" id="headerName">69 LOVE SONGS — COMPLETE</span>
+ <span id="headerCount">69 TRACKS</span>
+ </div>
+
+ <div class="playlist-live">
+ <div class="playlist-scroll" id="playlistScroll" tabindex="0" aria-label="Playlist rows">
+ <div class="playlist-rows" id="playlistRows"></div>
+ </div>
+ <div class="scroll-rail" id="scrollRail" hidden><div class="scroll-thumb" id="scrollThumb"></div></div>
+ </div>
+
+ <svg class="dynamic-svg" viewBox="0 0 1916 821" aria-hidden="true">
+ <defs>
+ <radialGradient id="meterGlow" cx="50%" cy="100%" r="65%">
+ <stop offset="0" stop-color="#ffc55b" stop-opacity=".75"/>
+ <stop offset=".32" stop-color="#8c5b16" stop-opacity=".18"/>
+ <stop offset="1" stop-color="#000" stop-opacity="0"/>
+ </radialGradient>
+ <linearGradient id="meterFrame" x1="0" x2="1">
+ <stop offset="0" stop-color="#5b4325"/>
+ <stop offset=".18" stop-color="#e2c08a"/>
+ <stop offset=".5" stop-color="#7c5a32"/>
+ <stop offset=".82" stop-color="#ddba82"/>
+ <stop offset="1" stop-color="#4a321c"/>
+ </linearGradient>
+ <linearGradient id="faceplateMask" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0" stop-color="#c3a477"/>
+ <stop offset=".52" stop-color="#b9996d"/>
+ <stop offset="1" stop-color="#c5a77b"/>
+ </linearGradient>
+ <linearGradient id="champagneFace" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0" stop-color="#c8b89f"/>
+ <stop offset=".18" stop-color="#b9a489"/>
+ <stop offset=".56" stop-color="#b19a7c"/>
+ <stop offset="1" stop-color="#a68e71"/>
+ </linearGradient>
+ <linearGradient id="glassLip" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0" stop-color="#050606" stop-opacity=".96"/>
+ <stop offset=".45" stop-color="#111311"/>
+ <stop offset=".76" stop-color="#030404"/>
+ <stop offset="1" stop-color="#2a2118"/>
+ </linearGradient>
+ <pattern id="microBrush" width="5" height="4" patternUnits="userSpaceOnUse">
+ <path d="M0 .5 H5" stroke="#fff5df" stroke-opacity=".1" stroke-width=".55"/>
+ <path d="M0 3.5 H5" stroke="#57452f" stroke-opacity=".08" stroke-width=".45"/>
+ </pattern>
+ <clipPath id="volumeKnobClip">
+ <circle cx="972" cy="551" r="98"/>
+ </clipPath>
+ <radialGradient id="volumeLamp" cx="36%" cy="28%" r="70%">
+ <stop offset="0" stop-color="#fff8d7"/>
+ <stop offset=".24" stop-color="#ffd27d"/>
+ <stop offset=".72" stop-color="#f1a02e"/>
+ <stop offset="1" stop-color="#b96513"/>
+ </radialGradient>
+ </defs>
+ <g id="faceplateSurface">
+ <rect x="69" y="366" width="1078" height="31" rx="19" fill="url(#glassLip)"/>
+ <path d="M88 369 H1128 Q1143 369 1147 385" fill="none" stroke="#353735" stroke-opacity=".8" stroke-width="2"/>
+ <rect x="69" y="379" width="1078" height="367" rx="19" fill="url(#champagneFace)"/>
+ <rect x="69" y="379" width="1078" height="367" rx="19" fill="url(#microBrush)"/>
+ <path d="M88 379 H1128 Q1142 379 1147 395" fill="none" stroke="#f0d09c" stroke-width="2.2"/>
+ <rect x="73" y="383" width="1070" height="359" rx="16" fill="none" stroke="#d2b584" stroke-width="1.5"/>
+ <rect x="77" y="387" width="1062" height="351" rx="14" fill="none" stroke="#715438" stroke-opacity=".72" stroke-width="1.4"/>
+ <line x1="641" y1="578" x2="817" y2="578" stroke="#705438" stroke-width="1.2"/>
+ <g fill="#261d16" font-family="Berkeley Mono, monospace" text-anchor="middle" letter-spacing=".5">
+ <text x="729" y="422" font-size="17">PLAY MODES</text>
+ <text x="684" y="492" font-size="15">REPEAT</text>
+ <text x="770" y="492" font-size="15">SINGLE</text>
+ <text x="684" y="563" font-size="15">RANDOM</text>
+ <text x="770" y="563" font-size="15">CONSUME</text>
+ <text x="729" y="608" font-size="16">RADIO SEARCH</text>
+ <text x="684" y="686" font-size="15">NAME</text>
+ <text x="770" y="686" font-size="15">TAGS</text>
+ <text x="162" y="690" font-size="16">PREV</text>
+ <text x="285" y="690" font-size="16">PLAY/PAUSE</text>
+ <text x="408" y="690" font-size="16">STOP</text>
+ <text x="531" y="690" font-size="16">NEXT</text>
+ <text x="972" y="691" font-size="17">VOLUME</text>
+ </g>
+ <image href="../concepts/30a-dupre-studios-user-refined-playlist.png"
+ x="0" y="0" width="1916" height="821" clip-path="url(#volumeKnobClip)"/>
+ </g>
+ <g id="meters"></g>
+ <g id="volumeMask"></g>
+ <g id="volumeSegments"></g>
+ </svg>
+
+ <div class="transport-bank" aria-label="Transport controls">
+ <button class="transport-button" data-transport="prev" aria-label="Previous">|◀</button>
+ <button class="transport-button" data-transport="play" aria-label="Play or pause">▶Ⅱ</button>
+ <button class="transport-button" data-transport="stop" aria-label="Stop">■</button>
+ <button class="transport-button" data-transport="next" aria-label="Next">▶|</button>
+ </div>
+
+ <button class="mode-button mode-repeat" data-mode="repeat" aria-label="Repeat playlist" aria-pressed="false"></button>
+ <button class="mode-button mode-single" data-mode="single" aria-label="Repeat one track" aria-pressed="false"></button>
+ <button class="mode-button mode-random" data-mode="random" aria-label="Random playback" aria-pressed="false"></button>
+ <button class="mode-button mode-consume" data-mode="consume" aria-label="Consume tracks" aria-pressed="false"></button>
+ <button class="radio-button radio-name" data-radio="name" aria-label="Search radio stations by name"></button>
+ <button class="radio-button radio-tags" data-radio="tags" aria-label="Search radio stations by tag"></button>
+ <div class="volume-hit" id="volumeHit" role="slider" tabindex="0" aria-label="Player volume" aria-valuemin="0" aria-valuemax="100" aria-valuenow="68"></div>
+
+ <div class="playlist-actions" aria-label="Playlist actions">
+ <button class="playlist-action" data-action="add">ADD</button>
+ <button class="playlist-action" data-action="new">NEW</button>
+ <button class="playlist-action" data-action="load">LOAD</button>
+ <button class="playlist-action" data-action="save">SAVE</button>
+ <button class="playlist-action" data-action="delete">DELETE</button>
+ </div>
+
+ <div class="status-toast" id="statusToast" role="status" aria-live="polite"></div>
+ </div>
+ </section>
+
+ <div class="inspection-note">
+ <span>Keyboard: Space play/pause · arrows select · Page Up/Down browse · Home/End · R/S/X/C modes. Prototype VU motion is synthetic; production reads mpv RMS.</span>
+ <span id="interactionStatus">Local fixture · playing · volume 68</span>
+ </div>
+</main>
+
+<dialog id="deleteDialog">
+ <form method="dialog">
+ <h2>Delete this playlist?</h2>
+ <p id="deletePrompt">This removes the saved playlist. Playback stops and the receiver returns to an untitled empty queue.</p>
+ <menu>
+ <button class="lab-button" value="cancel">Cancel</button>
+ <button class="lab-button delete-confirm" value="confirm">Delete playlist</button>
+ </menu>
+ </form>
+</dialog>
+
+<script>
+(() => {
+ "use strict";
+
+ const BASE_ROW_HEIGHT = 37;
+ const VISIBLE_ROWS = 14;
+ const localSeed = [
+ ["Absolutely Cuckoo", "The Magnetic Fields", "02:02"],
+ ["I Don’t Believe in the Sun", "The Magnetic Fields", "02:50"],
+ ["All My Little Words", "The Magnetic Fields", "02:03"],
+ ["A Chicken With Its Head Cut Off — Remastered Anniversary Edition", "The Magnetic Fields and the Long-Lost Orchestra", "02:59"],
+ ["(I’m) Dazota", "The Magnetic Fields", "02:38"],
+ ["I Don’t Want to Get Over You", "The Magnetic Fields", "02:50"],
+ ["Come Back From San Francisco", "The Magnetic Fields", "02:37"],
+ ["The Luckiest Guy on the Lower East Side", "The Magnetic Fields", "01:32"],
+ ["Let’s Pretend We’re Bunny Rabbits", "The Magnetic Fields", "02:10"],
+ ["The Cactus Where Your Heart Should Be", "The Magnetic Fields", "02:38"],
+ ["I Think I Need a New Heart", "The Magnetic Fields", "02:58"],
+ ["The Book of Love", "The Magnetic Fields", "02:56"],
+ ["Fido, Your Leash Is Too Long", "The Magnetic Fields", "02:13"],
+ ["How Fucking Romantic", "The Magnetic Fields", "00:58"],
+ ["The One You Really Love", "The Magnetic Fields", "02:53"],
+ ["Punk Love", "The Magnetic Fields", "00:58"],
+ ["Parades Go By", "The Magnetic Fields", "02:56"],
+ ["Boa Constrictor", "The Magnetic Fields", "00:58"],
+ ["A Pretty Girl Is Like…", "The Magnetic Fields", "01:50"],
+ ["My Sentimental Melody", "The Magnetic Fields", "03:07"]
+ ];
+
+ const fillerTitles = [
+ "Nothing Matters When We’re Dancing", "Sweet-Lovin’ Man", "The Things We Did and Didn’t Do",
+ "Roses", "Love Is Like Jazz", "When My Boy Walks Down the Street", "Time Enough for Rocking",
+ "Very Funny", "Grand Canyon", "No One Will Ever Love You", "If You Don’t Cry",
+ "You’re My Only Home", "Washington, D.C.", "Long-Forgotten Fairytale"
+ ];
+
+ const radioSeed = [
+ ["Groove Salad", "SomaFM · Ambient / Downtempo", "ON AIR"],
+ ["Drone Zone", "SomaFM · Atmospheric Textures", "256k"],
+ ["Secret Agent", "SomaFM · Cinematic", "256k"],
+ ["Illinois Street Lounge", "SomaFM · Lounge", "256k"],
+ ["Space Station Soma", "SomaFM · Space Music", "256k"],
+ ["Left Coast 70s", "SomaFM · Mellow Rock", "256k"],
+ ["The Trip", "SomaFM · Progressive House", "256k"],
+ ["Underground 80s", "SomaFM · Synthpop", "256k"],
+ ["Deep Space One", "SomaFM · Ambient", "256k"],
+ ["Black Rock FM", "SomaFM · Eclectic", "256k"],
+ ["Bossa Beyond", "SomaFM · Brazilian", "256k"],
+ ["Seven Inch Soul", "SomaFM · Vintage Soul", "256k"],
+ ["Suburbs of Goa", "SomaFM · South Asian", "256k"],
+ ["Boot Liquor", "SomaFM · Americana", "256k"],
+ ["Heavyweight Reggae", "SomaFM · Roots Reggae", "256k"],
+ ["Lush", "SomaFM · Female Vocals", "256k"],
+ ["Beat Blender", "SomaFM · Electronic", "256k"],
+ ["Digitalis", "SomaFM · Indie Electronic", "256k"]
+ ];
+
+ const makeLocalTracks = () => Array.from({length: 69}, (_, index) => {
+ const base = localSeed[index] || [
+ fillerTitles[index % fillerTitles.length],
+ index % 7 === 0 ? "The Magnetic Fields with an Improbably Long Guest Credit" : "The Magnetic Fields",
+ `${String(1 + (index % 3)).padStart(2, "0")}:${String((17 * index) % 60).padStart(2, "0")}`
+ ];
+ return {id: `local-${index}`, title: base[0], artist: base[1], meta: base[2]};
+ });
+
+ const makeRadioTracks = () => radioSeed.map((row, index) => ({
+ id: `radio-${index}`, title: row[0], artist: row[1], meta: row[2]
+ }));
+
+ const fixtures = {
+ local: {
+ name: "69 Love Songs — The Complete Three-Volume Collection",
+ kind: "PLAYLIST",
+ tracks: makeLocalTracks,
+ selected: 3,
+ position: 90,
+ duration: 179,
+ metadata: [
+ "A Chicken With Its Head Cut Off — Remastered Anniversary Edition",
+ "The Magnetic Fields and the Long-Lost Orchestra",
+ "69 Love Songs: The Complete Three-Volume Collection",
+ "Merge Records",
+ "1999"
+ ]
+ },
+ radio: {
+ name: "Name Search · Ambient",
+ kind: "RADIO",
+ tracks: makeRadioTracks,
+ selected: 0,
+ position: 0,
+ duration: 0,
+ metadata: ["Groove Salad", "SomaFM", "Ambient / Downtempo", "256 kbps"]
+ }
+ };
+
+ const state = {
+ fixture: "local",
+ playlistName: fixtures.local.name,
+ tracks: fixtures.local.tracks(),
+ selected: fixtures.local.selected,
+ position: fixtures.local.position,
+ duration: fixtures.local.duration,
+ playing: true,
+ volume: 68,
+ modes: {repeat: false, single: false, random: false, consume: false},
+ search: null,
+ vu: {left: -20, right: -20},
+ benchmark: null
+ };
+
+ const $ = selector => document.querySelector(selector);
+ const $$ = selector => [...document.querySelectorAll(selector)];
+ const receiver = $("#receiver");
+ const rows = $("#playlistRows");
+ const scroll = $("#playlistScroll");
+ const rail = $("#scrollRail");
+ const thumb = $("#scrollThumb");
+ const seekTrack = $("#seekTrack");
+ const volumeHit = $("#volumeHit");
+ const toast = $("#statusToast");
+ const deleteDialog = $("#deleteDialog");
+ let toastTimer = null;
+ let seekDragging = false;
+ let volumeDragging = false;
+ let thumbDragging = false;
+ let thumbGrabOffset = 0;
+ let lastMeterTime = performance.now();
+
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
+ const formatTime = seconds => {
+ const value = Math.max(0, Math.round(seconds));
+ return `${String(Math.floor(value / 60)).padStart(2, "0")}:${String(value % 60).padStart(2, "0")}`;
+ };
+
+ function announce(message) {
+ clearTimeout(toastTimer);
+ toast.textContent = message;
+ toast.classList.add("show");
+ toastTimer = setTimeout(() => toast.classList.remove("show"), 1800);
+ updateInspection(message);
+ }
+
+ function currentTrack() { return state.tracks[state.selected] || null; }
+
+ function switchFixture(name, message = null) {
+ const source = fixtures[name];
+ state.fixture = name;
+ state.playlistName = source.name;
+ state.tracks = source.tracks();
+ state.selected = source.selected;
+ state.position = source.position;
+ state.duration = source.duration;
+ state.playing = true;
+ state.search = null;
+ scroll.scrollTop = 0;
+ renderAll();
+ announce(message || `${name === "local" ? "Local playlist" : "Radio station"} fixture loaded`);
+ }
+
+ function renderInfo() {
+ const source = fixtures[state.fixture];
+ const track = currentTrack();
+ const metadata = track ? (state.fixture === "local" ? [
+ track.title,
+ track.artist,
+ source.metadata[2],
+ source.metadata[3],
+ source.metadata[4]
+ ] : [track.title, "SomaFM", track.artist.replace(/^SomaFM · /, ""), "256 kbps"]) : [];
+
+ $("#metadata").innerHTML = metadata.filter(Boolean).map((line, index) =>
+ `<div class="metadata-line ${index ? "dim" : ""}" title="${escapeHtml(line)}">${escapeHtml(line)}</div>`
+ ).join("");
+
+ $("#art").innerHTML = state.fixture === "radio"
+ ? '<img src="../../../assets/vinyl-placeholder.svg" alt="Radio station artwork unavailable; vinyl placeholder">'
+ : '<div class="album-crop" role="img" aria-label="69 Love Songs album art"></div>';
+
+ const onAir = state.fixture === "radio" && Boolean(track);
+ $("#onAirUnderArt").hidden = !onAir;
+ $("#seekLine").hidden = onAir || !track;
+ if (track && !onAir) updateSeekVisual();
+ }
+
+ function escapeHtml(value) {
+ return String(value).replace(/[&<>"']/g, character => ({
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
+ })[character]);
+ }
+
+ function renderHeader() {
+ $("#headerName").textContent = state.playlistName || "UNTITLED";
+ $("#headerName").title = state.playlistName || "UNTITLED";
+ const noun = state.fixture === "radio" ? (state.tracks.length === 1 ? "STATION" : "STATIONS") : (state.tracks.length === 1 ? "TRACK" : "TRACKS");
+ $("#headerCount").textContent = `${state.tracks.length} ${noun}`;
+ }
+
+ function renderRows() {
+ const rowHeight = scroll.clientHeight / VISIBLE_ROWS;
+ rows.style.setProperty("--row-height", `${rowHeight}px`);
+ if (!state.tracks.length) {
+ rows.innerHTML = Array.from({length: VISIBLE_ROWS}, () => '<div class="empty-row"></div>').join("");
+ rows.style.height = `${scroll.clientHeight}px`;
+ rail.hidden = true;
+ return;
+ }
+ rows.style.height = `${rowHeight * Math.max(VISIBLE_ROWS, state.tracks.length)}px`;
+ rows.innerHTML = state.tracks.map((track, index) => `
+ <div class="playlist-row ${index === state.selected ? "selected" : ""}" data-row="${index}" title="${escapeHtml(`${track.title} — ${track.artist}`)}">
+ <span class="row-number">${String(index + 1).padStart(2, "0")}</span>
+ <span class="row-name">${escapeHtml(track.title)} — ${escapeHtml(track.artist)}</span>
+ <span class="row-meta">${state.fixture === "radio" && index === state.selected && state.playing ? "ON AIR" : escapeHtml(track.meta)}</span>
+ </div>`).join("");
+ $$("[data-row]").forEach(row => row.addEventListener("click", () => selectTrack(Number(row.dataset.row), true)));
+ requestAnimationFrame(updateScrollbar);
+ }
+
+ function renderControls() {
+ const hasTrack = Boolean(currentTrack());
+ const play = $('[data-transport="play"]');
+ play.classList.toggle("playing", state.playing && hasTrack);
+ play.disabled = !hasTrack;
+ $('[data-transport="prev"]').disabled = !hasTrack;
+ $('[data-transport="next"]').disabled = !hasTrack;
+ $('[data-transport="stop"]').disabled = !hasTrack;
+ $$("[data-mode]").forEach(button => button.setAttribute("aria-pressed", String(state.modes[button.dataset.mode])));
+ $("#localFixture").setAttribute("aria-pressed", String(state.fixture === "local"));
+ $("#radioFixture").setAttribute("aria-pressed", String(state.fixture === "radio"));
+ volumeHit.setAttribute("aria-valuenow", String(state.volume));
+ updateVolumeSegments();
+ updateInspection();
+ }
+
+ function renderAll() {
+ const started = performance.now();
+ renderInfo();
+ renderHeader();
+ renderRows();
+ renderControls();
+ recordRender(performance.now() - started);
+ }
+
+ function updateSeekVisual() {
+ const percentage = state.duration ? clamp((state.position / state.duration) * 100, 0, 100) : 0;
+ seekTrack.style.setProperty("--progress", percentage.toFixed(3));
+ seekTrack.setAttribute("aria-valuenow", String(Math.round(percentage)));
+ $("#elapsed").textContent = formatTime(state.position);
+ $("#duration").textContent = formatTime(state.duration);
+ }
+
+ function seekFromPointer(event) {
+ if (state.fixture !== "local" || !currentTrack()) return;
+ const bounds = seekTrack.getBoundingClientRect();
+ const fraction = clamp((event.clientX - bounds.left) / bounds.width, 0, 1);
+ state.position = state.duration * fraction;
+ updateSeekVisual();
+ updateInspection(`Seek ${formatTime(state.position)} / ${formatTime(state.duration)}`);
+ }
+
+ function selectTrack(index, play = false) {
+ if (!state.tracks.length) return;
+ state.selected = clamp(index, 0, state.tracks.length - 1);
+ state.position = 0;
+ if (play) state.playing = true;
+ renderInfo();
+ renderRows();
+ renderControls();
+ ensureSelectedVisible();
+ }
+
+ function ensureSelectedVisible() {
+ const row = rows.children[state.selected];
+ if (row) row.scrollIntoView({block: "nearest"});
+ }
+
+ function moveTrack(delta) {
+ if (!state.tracks.length) return;
+ const next = (state.selected + delta + state.tracks.length) % state.tracks.length;
+ selectTrack(next, true);
+ announce(`${delta < 0 ? "Previous" : "Next"}: ${currentTrack().title}`);
+ }
+
+ function togglePlay() {
+ if (!currentTrack()) return;
+ state.playing = !state.playing;
+ renderRows();
+ renderControls();
+ announce(state.playing ? "Playback resumed" : "Playback paused");
+ }
+
+ function setVolume(value, message = true) {
+ state.volume = Math.round(clamp(value, 0, 100));
+ renderControls();
+ if (message) updateInspection(`Player volume ${state.volume}`);
+ }
+
+ function volumeFromPointer(event) {
+ const bounds = volumeHit.getBoundingClientRect();
+ const x = event.clientX - (bounds.left + bounds.width / 2);
+ const y = event.clientY - (bounds.top + bounds.height / 2);
+ let angle = Math.atan2(y, x) * 180 / Math.PI;
+ if (angle < 150) angle += 360;
+ setVolume(((clamp(angle, 150, 390) - 150) / 240) * 100, false);
+ }
+
+ function updateVolumeSegments() {
+ const points = [
+ [885, 630], [871, 608], [861, 583], [857, 557], [857, 531], [864, 507],
+ [875, 484], [890, 464], [906, 448], [924, 440], [947, 432], [972, 431],
+ [996, 433], [1018, 440], [1038, 452], [1055, 467], [1068, 486], [1079, 507],
+ [1086, 532], [1087, 557], [1085, 582], [1078, 608], [1059, 628]
+ ];
+ const count = points.length;
+ const lit = Math.round((state.volume / 100) * (count - 1));
+ $("#volumeMask").innerHTML = points.map(([x, y]) =>
+ `<circle cx="${x}" cy="${y}" r="5.3" fill="#0d0e0c" stroke="#655039" stroke-width=".8"/>`
+ ).join("");
+ const circles = points.map(([x, y], index) => {
+ const active = index <= lit;
+ if (active) {
+ return `<g><circle cx="${x}" cy="${y}" r="10" fill="#ff9d24" fill-opacity=".28"/><circle cx="${x}" cy="${y}" r="7" fill="#ffad32" fill-opacity=".22"/><circle cx="${x}" cy="${y}" r="4.8" fill="url(#volumeLamp)" stroke="#fff1c2" stroke-width=".85"/></g>`;
+ }
+ return "";
+ }).join("");
+ $("#volumeSegments").innerHTML = circles;
+ }
+
+ function meterMarkup(x, id) {
+ const labels = ["-40", "-20", "-10", "-6", "-3", "0", "+3"];
+ const ticks = Array.from({length: 19}, (_, index) => {
+ const angle = -56 + index * (112 / 18);
+ const major = index % 3 === 0;
+ const radians = angle * Math.PI / 180;
+ const x1 = 115 + Math.sin(radians) * (major ? 91 : 95);
+ const y1 = 131 - Math.cos(radians) * (major ? 91 : 95);
+ const x2 = 115 + Math.sin(radians) * 102;
+ const y2 = 131 - Math.cos(radians) * 102;
+ return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${index > 14 ? "#d43b25" : "#d7c48d"}" stroke-width="${major ? 1.7 : 1}"/>`;
+ }).join("");
+ const labelText = labels.map((label, index) => {
+ const angle = -52 + index * (104 / (labels.length - 1));
+ const radians = angle * Math.PI / 180;
+ const lx = 115 + Math.sin(radians) * 72;
+ const ly = 131 - Math.cos(radians) * 72;
+ return `<text x="${lx}" y="${ly}" text-anchor="middle" fill="${index > 4 ? "#de442f" : "#ddd0aa"}" font-family="Berkeley Mono,monospace" font-size="10">${label}</text>`;
+ }).join("");
+ return `<g transform="translate(${x} 432)">
+ <rect x="0" y="0" width="230" height="113" rx="7" fill="url(#meterFrame)"/>
+ <rect x="4" y="4" width="222" height="105" rx="5" fill="#050605" stroke="#1b1812" stroke-width="2"/>
+ <rect x="5" y="5" width="220" height="103" rx="5" fill="url(#meterGlow)"/>
+ ${ticks}${labelText}
+ <text x="115" y="91" text-anchor="middle" fill="#e7d7ae" font-family="Berkeley Mono,monospace" font-size="18">VU</text>
+ <circle cx="115" cy="111" r="17" fill="url(#meterGlow)" opacity=".9"/>
+ <line id="needle-${id}" x1="115" y1="105" x2="115" y2="27" stroke="#f3d59c" stroke-width="2.2" transform="rotate(0 115 105)"/>
+ <circle cx="115" cy="105" r="3.2" fill="#c7a56f"/>
+ </g>`;
+ }
+
+ function initMeters() {
+ $("#meters").innerHTML = meterMarkup(112, "left") + meterMarkup(365, "right");
+ }
+
+ function vuAngle(db) {
+ const normalized = clamp((db + 40) / 43, 0, 1);
+ return -54 + normalized * 108;
+ }
+
+ function updateMeters(timestamp) {
+ const started = performance.now();
+ const elapsed = Math.min(.05, (timestamp - lastMeterTime) / 1000);
+ lastMeterTime = timestamp;
+ const signal = state.playing && currentTrack();
+ const base = timestamp / 350;
+ const targets = signal ? {
+ left: -13 + Math.sin(base * 1.13) * 5 + Math.sin(base * 2.31) * 2,
+ right: -14 + Math.sin(base * .97 + 1.1) * 5.5 + Math.sin(base * 2.08) * 2
+ } : {left: -40, right: -40};
+ for (const side of ["left", "right"]) {
+ const target = targets[side];
+ const speed = target > state.vu[side] ? 7.2 : 2.4;
+ state.vu[side] += (target - state.vu[side]) * Math.min(1, elapsed * speed);
+ $(`#needle-${side}`).setAttribute("transform", `rotate(${vuAngle(state.vu[side]).toFixed(2)} 115 105)`);
+ }
+ recordRender(performance.now() - started);
+ requestAnimationFrame(updateMeters);
+ }
+
+ function updateScrollbar() {
+ const overflow = scroll.scrollHeight - scroll.clientHeight;
+ rail.hidden = overflow <= 1;
+ if (overflow <= 1) return;
+ const railHeight = rail.clientHeight;
+ const ratio = scroll.clientHeight / scroll.scrollHeight;
+ const thumbHeight = Math.max(38, railHeight * ratio);
+ const travel = railHeight - thumbHeight;
+ thumb.style.height = `${thumbHeight}px`;
+ thumb.style.top = `${travel * (scroll.scrollTop / overflow)}px`;
+ }
+
+ function updateInspection(message = null) {
+ const label = state.fixture === "radio" ? "Radio fixture" : "Local fixture";
+ const play = state.playing ? "playing" : "paused";
+ $("#interactionStatus").textContent = message || `${label} · ${play} · volume ${state.volume}`;
+ }
+
+ function handlePlaylistAction(action) {
+ if (action === "add") {
+ const number = state.tracks.length + 1;
+ state.tracks.push({id: `added-${Date.now()}`, title: "Newly Added Track With a Deliberately Long Display Name", artist: "Prototype Library", meta: "04:12"});
+ state.selected = number - 1;
+ renderAll();
+ ensureSelectedVisible();
+ announce(`Added track ${number}`);
+ } else if (action === "new") {
+ state.playlistName = "UNTITLED";
+ state.tracks = [];
+ state.selected = 0;
+ state.playing = false;
+ renderAll();
+ announce("New empty playlist");
+ } else if (action === "load") {
+ switchFixture(state.fixture, `Loaded ${fixtures[state.fixture].name}`);
+ } else if (action === "save") {
+ announce(`Saved ${state.playlistName || "UNTITLED"} · ${state.tracks.length} tracks`);
+ } else if (action === "delete") {
+ $("#deletePrompt").textContent = `Delete “${state.playlistName || "UNTITLED"}”? This removes the saved playlist and leaves an untitled empty queue.`;
+ deleteDialog.showModal();
+ }
+ }
+
+ function handleTransport(action) {
+ if (action === "prev") moveTrack(-1);
+ if (action === "next") moveTrack(1);
+ if (action === "play") togglePlay();
+ if (action === "stop") {
+ state.playing = false;
+ state.position = 0;
+ renderRows();
+ renderControls();
+ if (state.fixture === "local") updateSeekVisual();
+ announce("Playback stopped");
+ }
+ }
+
+ function radioSearch(kind) {
+ const label = kind === "name" ? "Name search · Groove" : "Tag search · Ambient";
+ if (state.fixture !== "radio") switchFixture("radio", label);
+ state.search = kind;
+ state.playlistName = label;
+ renderHeader();
+ announce(label);
+ }
+
+ function recordRender(value) {
+ if (!state.benchmark) return;
+ state.benchmark.samples.push(value);
+ }
+
+ function percentile(sorted, fraction) {
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * fraction))] || 0;
+ }
+
+ function updateBenchmarkReadout() {
+ if (!state.benchmark) return;
+ const elapsed = performance.now() - state.benchmark.started;
+ const remaining = Math.max(0, state.benchmark.duration - elapsed);
+ const sorted = [...state.benchmark.samples].sort((a, b) => a - b);
+ const median = percentile(sorted, .5);
+ const p95 = percentile(sorted, .95);
+ $("#benchmarkReadout").textContent = remaining > 0
+ ? `Benchmark ${Math.ceil(remaining / 1000)}s · median ${median.toFixed(2)} ms · p95 ${p95.toFixed(2)} ms`
+ : `Complete · ${sorted.length} updates · median ${median.toFixed(2)} ms · p95 ${p95.toFixed(2)} ms`;
+ if (remaining > 0) setTimeout(updateBenchmarkReadout, 250);
+ else {
+ state.benchmark.complete = true;
+ $("#benchmarkButton").disabled = false;
+ document.documentElement.dataset.benchmark = JSON.stringify({
+ samples: sorted.length,
+ medianMs: Number(median.toFixed(3)),
+ p95Ms: Number(p95.toFixed(3)),
+ durationMs: state.benchmark.duration,
+ viewport: `${Math.round(receiver.getBoundingClientRect().width)}x${Math.round(receiver.getBoundingClientRect().height)}`
+ });
+ announce("60-second dynamic-update benchmark complete");
+ }
+ }
+
+ function runBenchmark(duration = 60000) {
+ if (state.benchmark && !state.benchmark.complete) return;
+ state.benchmark = {started: performance.now(), duration, samples: [], complete: false};
+ $("#benchmarkButton").disabled = true;
+ updateBenchmarkReadout();
+ }
+
+ function bindEvents() {
+ $("#localFixture").addEventListener("click", () => switchFixture("local"));
+ $("#radioFixture").addEventListener("click", () => switchFixture("radio"));
+ $("#restoreFixture").addEventListener("click", () => switchFixture(state.fixture));
+ $("#benchmarkButton").addEventListener("click", () => runBenchmark());
+
+ $$("[data-transport]").forEach(button => button.addEventListener("click", () => handleTransport(button.dataset.transport)));
+ $$("[data-mode]").forEach(button => button.addEventListener("click", () => {
+ const mode = button.dataset.mode;
+ state.modes[mode] = !state.modes[mode];
+ renderControls();
+ announce(`${mode.toUpperCase()} ${state.modes[mode] ? "enabled" : "disabled"}`);
+ }));
+ $$("[data-radio]").forEach(button => button.addEventListener("click", () => radioSearch(button.dataset.radio)));
+ $$("[data-action]").forEach(button => button.addEventListener("click", () => handlePlaylistAction(button.dataset.action)));
+
+ seekTrack.addEventListener("pointerdown", event => {
+ seekDragging = true;
+ seekTrack.setPointerCapture(event.pointerId);
+ seekFromPointer(event);
+ });
+ seekTrack.addEventListener("pointermove", event => { if (seekDragging) seekFromPointer(event); });
+ seekTrack.addEventListener("pointerup", event => {
+ seekDragging = false;
+ seekTrack.releasePointerCapture(event.pointerId);
+ announce(`Seeked to ${formatTime(state.position)}`);
+ });
+ seekTrack.addEventListener("keydown", event => {
+ if (!["ArrowLeft", "ArrowRight"].includes(event.key)) return;
+ event.preventDefault();
+ state.position = clamp(state.position + (event.key === "ArrowRight" ? 5 : -5), 0, state.duration);
+ updateSeekVisual();
+ });
+
+ volumeHit.addEventListener("pointerdown", event => {
+ volumeDragging = true;
+ volumeHit.setPointerCapture(event.pointerId);
+ volumeFromPointer(event);
+ });
+ volumeHit.addEventListener("pointermove", event => { if (volumeDragging) volumeFromPointer(event); });
+ volumeHit.addEventListener("pointerup", event => {
+ volumeDragging = false;
+ volumeHit.releasePointerCapture(event.pointerId);
+ announce(`Player volume ${state.volume}`);
+ });
+ volumeHit.addEventListener("wheel", event => {
+ event.preventDefault();
+ setVolume(state.volume + (event.deltaY < 0 ? 5 : -5));
+ }, {passive: false});
+ volumeHit.addEventListener("keydown", event => {
+ if (!["ArrowLeft", "ArrowDown", "ArrowRight", "ArrowUp"].includes(event.key)) return;
+ event.preventDefault();
+ setVolume(state.volume + (["ArrowRight", "ArrowUp"].includes(event.key) ? 5 : -5));
+ });
+
+ scroll.addEventListener("scroll", updateScrollbar);
+ thumb.addEventListener("pointerdown", event => {
+ thumbDragging = true;
+ thumbGrabOffset = event.clientY - thumb.getBoundingClientRect().top;
+ thumb.setPointerCapture(event.pointerId);
+ });
+ thumb.addEventListener("pointermove", event => {
+ if (!thumbDragging) return;
+ const railBox = rail.getBoundingClientRect();
+ const thumbHeight = thumb.getBoundingClientRect().height;
+ const travel = railBox.height - thumbHeight;
+ const top = clamp(event.clientY - railBox.top - thumbGrabOffset, 0, travel);
+ scroll.scrollTop = (top / travel) * (scroll.scrollHeight - scroll.clientHeight);
+ });
+ thumb.addEventListener("pointerup", event => {
+ thumbDragging = false;
+ thumb.releasePointerCapture(event.pointerId);
+ });
+
+ deleteDialog.addEventListener("close", () => {
+ if (deleteDialog.returnValue !== "confirm") return;
+ const deleted = state.playlistName || "UNTITLED";
+ state.playlistName = "UNTITLED";
+ state.tracks = [];
+ state.selected = 0;
+ state.playing = false;
+ renderAll();
+ announce(`Deleted ${deleted}`);
+ });
+
+ document.addEventListener("keydown", event => {
+ if (deleteDialog.open || event.target.matches("button, input, [role=slider], .playlist-scroll")) return;
+ if (event.code === "Space") { event.preventDefault(); togglePlay(); }
+ else if (event.key === "ArrowDown") { event.preventDefault(); selectTrack(state.selected + 1); ensureSelectedVisible(); }
+ else if (event.key === "ArrowUp") { event.preventDefault(); selectTrack(state.selected - 1); ensureSelectedVisible(); }
+ else if (event.key === "PageDown") { event.preventDefault(); selectTrack(state.selected + VISIBLE_ROWS); ensureSelectedVisible(); }
+ else if (event.key === "PageUp") { event.preventDefault(); selectTrack(state.selected - VISIBLE_ROWS); ensureSelectedVisible(); }
+ else if (event.key === "Home") { event.preventDefault(); selectTrack(0); ensureSelectedVisible(); }
+ else if (event.key === "End") { event.preventDefault(); selectTrack(state.tracks.length - 1); ensureSelectedVisible(); }
+ else if (["r", "s", "x", "c"].includes(event.key.toLowerCase())) {
+ const mode = ({r: "repeat", s: "single", x: "random", c: "consume"})[event.key.toLowerCase()];
+ state.modes[mode] = !state.modes[mode];
+ renderControls();
+ }
+ });
+
+ window.addEventListener("resize", () => {
+ renderRows();
+ updateScrollbar();
+ });
+ }
+
+ function runSelfTest() {
+ const checks = [];
+ const check = (name, condition) => {
+ checks.push({name, passed: Boolean(condition)});
+ if (!condition) throw new Error(`Self-test failed: ${name}`);
+ };
+ try {
+ switchFixture("radio", "Self-test radio fixture");
+ check("radio hides seek", $("#seekLine").hidden);
+ check("radio has two ON AIR indications", !$("#onAirUnderArt").hidden && $(".playlist-row.selected .row-meta")?.textContent === "ON AIR");
+ $('[data-mode="repeat"]').click();
+ check("mode latches", state.modes.repeat);
+ $('[data-transport="next"]').click();
+ check("next selects a station", state.selected === 1);
+ $('[data-action="add"]').click();
+ check("add changes queue", state.tracks.length === 19);
+ $('[data-action="new"]').click();
+ check("new clears queue", state.tracks.length === 0 && state.playlistName === "UNTITLED");
+ $('[data-action="load"]').click();
+ check("load restores queue", state.tracks.length === 18);
+ switchFixture("local", "Self-test local fixture");
+ check("local shows seek", !$("#seekLine").hidden);
+ setVolume(35, false);
+ check("volume state changes", state.volume === 35);
+ state.position = state.duration * .72;
+ updateSeekVisual();
+ check("seek state changes", Math.round(Number(seekTrack.getAttribute("aria-valuenow"))) === 72);
+ scroll.scrollTop = scroll.scrollHeight;
+ updateScrollbar();
+ check("long playlist has scrollbar", !rail.hidden && scroll.scrollTop > 0);
+ check("fourteen rows fit viewport", Math.abs(rows.children[0].getBoundingClientRect().height * VISIBLE_ROWS - scroll.clientHeight) < 2);
+ switchFixture("local", "Self-test restored");
+ document.documentElement.dataset.selftest = JSON.stringify({passed: true, checks});
+ } catch (error) {
+ document.documentElement.dataset.selftest = JSON.stringify({passed: false, error: error.message, checks});
+ console.error(error);
+ }
+ }
+
+ initMeters();
+ bindEvents();
+ renderAll();
+ requestAnimationFrame(updateMeters);
+
+ const params = new URLSearchParams(location.search);
+ if (params.get("state") === "radio") switchFixture("radio");
+ if (params.has("benchmark")) runBenchmark(Number(params.get("benchmark")) || 60000);
+ if (params.has("selftest")) setTimeout(runSelfTest, 50);
+})();
+</script>
+</body>
+</html>
diff --git a/working/music-svg-app-design/references/29-alignment-guide.png b/working/music-svg-app-design/references/29-alignment-guide.png
new file mode 100644
index 00000000..d8da4ebf
--- /dev/null
+++ b/working/music-svg-app-design/references/29-alignment-guide.png
Binary files differ
diff --git a/working/music-svg-app-design/references/dupre-gallery-tall.png b/working/music-svg-app-design/references/dupre-gallery-tall.png
new file mode 100644
index 00000000..0f42c2ae
--- /dev/null
+++ b/working/music-svg-app-design/references/dupre-gallery-tall.png
Binary files differ
diff --git a/working/music-svg-app-design/references/dupre-gallery.png b/working/music-svg-app-design/references/dupre-gallery.png
new file mode 100644
index 00000000..1842fb5a
--- /dev/null
+++ b/working/music-svg-app-design/references/dupre-gallery.png
Binary files differ
diff --git a/working/music-svg-app-design/references/waybar-redesign-tall.png b/working/music-svg-app-design/references/waybar-redesign-tall.png
new file mode 100644
index 00000000..554dc041
--- /dev/null
+++ b/working/music-svg-app-design/references/waybar-redesign-tall.png
Binary files differ
diff --git a/working/music-svg-app-design/references/waybar-redesign.png b/working/music-svg-app-design/references/waybar-redesign.png
new file mode 100644
index 00000000..17e4c307
--- /dev/null
+++ b/working/music-svg-app-design/references/waybar-redesign.png
Binary files differ