Wrapping overflow:hidden + ellipsis on the parent flex container
clipped the favourite star whenever the monitor name was long enough
to truncate. Move the truncation rules onto a new inner span around
the name text only, and add flex-shrink:0 to the badge so it always
renders in full.
- Restore 1:1 aspect-ratio on .vinyl-stage; the previous 1:0.85
override created an inconsistent crop on resize. Replace the
tonearm sibling's aspect-ratio with explicit height:36% so its
vertical span tracks the stage instead of its own width.
- Brighten the tonearm SVG: lighter pivot/arm gradient stops,
thicker stroke widths, stronger cartridge highlight.
- Add a subtle -2deg tilt to the sleeve so it reads as physically
resting on the disc rather than rectilinearly composed.
- Add .mcp.json registering code-review-graph (uvx, stdio)
- Document the MCP tools in CLAUDE.md so the assistant prefers
graph queries over Grep/Glob/Read for structural exploration
- Ignore .code-review-graph/ index directory
- Move colophon (credit/email/source link) from sticky footer into
a dedicated About dialog, opened from a new header button
- Drop ~64px of bottom container padding now that the footer is gone
- Loosen vinyl-stage aspect-ratio (1:1 -> 1:0.85) so the disc no
longer leaves a tall empty band below the sleeve
- Switch tonearm height: 36% to aspect-ratio: 1 to keep proportions
consistent across the new stage ratio
- Add about.* / dialog.close i18n keys for EN and RU
- Add vinyl-variants-mockup.html as next design reference target
The 720–1240 px viewport range was a "strange zone": below 1240 the layout
is single-column, but above 720 none of the Pocket Edition rules fire, so
the vinyl stage stretched to full content width (~1100 px) and the masthead
ran to a 1000 px+ measure on a small-desktop window.
Caps now degrade in three steps:
- ≤ 720 px: vinyl 460 px / 92% width (mobile hero unchanged)
- 721–1240 px: vinyl 480 px, masthead 640 px, both centered
- ≥ 1241 px: two-column layout (no caps needed; grid does it)
Also reduce the bottom dead space:
- footer margin-top 80 → 36, inner top padding 28 → 20
- .container bottom padding 140 → 64 (desktop) / 56 (mobile)
And a small mobile-volume fix in the same range:
- .controls flex-wrap nowrap → wrap so the vu-cluster can take its own row
- vu-cluster gets flex-basis 100 % (forces own row in the wrapping flexbox)
- volume-slider drops the max-width: 200 cap so it fills the row width
- vinyl-stage on mobile bumped 320 → 460 px / 78% → 92% width
The accent picker only mutated --accent / --accent-hover, but the redesign
reads everything off --copper, --copper-hi, --copper-lo, --copper-glow.
--accent was a one-way alias of --copper, so picking a color did nothing.
Frontend (player.js):
- applyAccentColor now drives --copper, --copper-hi, --copper-lo, and a
new --copper-rgb triplet (used by every soft tint / glow on both themes)
- darkenColor / hexToRgbTriple helpers added beside lightenColor
CSS (styles.css):
- introduce --copper-rgb tokens for both themes; --copper-glow now derives
from rgba(var(--copper-rgb), 0.35) so it follows the picker
- replace 21 hardcoded rgba(224,128,56,...) / rgba(31,78,61,...) literals
across hover bgs, focus halos, glows, vinyl-label gradients with
rgba(var(--copper-rgb), ...)
- replace the light-theme vinyl-label gradient hexes with
var(--copper) / var(--copper-lo)
Other player polish in this changeset:
- track-masthead: padding-right clamp(12px, 1.5vw, 24px) so VU meter,
spectrum tail, end timecode and controls sit inset from the panel edge
(zeroed on the single-column mobile breakpoint to keep symmetry)
- VU meter 140→120 px, volume slider 80→64 px to free up row width so
the cluster stays inline with prev/play/next instead of wrapping
- light-theme VU meter override: cream gauge face, dark-ink scale ticks,
hunter-emerald needle (replaces the hardcoded black gauge)
- fullscreen meta-cell labels: var(--ink-faint) → var(--copper) so STATE
and SOURCE read as part of the same editorial system as the kicker
Frontend hot path (player.js, background.js):
- visualizer rAF: drop per-frame getComputedStyle('--accent') (cached on
applyAccentColor), build canvas LinearGradient once per accent change
instead of 32× per frame, batch all bars into a single beginPath/fill
- FPS-gate canvas redraw via frequencyDataVersion so 60-144 Hz monitors
stop re-rendering identical frames produced at 30 Hz on the backend
- editorial spectrum bars: replace style.height (layout) with
transform: scaleY (compositor-only); cache bar refs, pre-compute
per-bar gain/range, dedup writes at 1/1000 quantization
- coalesce VU needle into the visualizer rAF; cache vuNeedle ref;
dedup angle writes at 0.1°
- updateUI: status-payload fingerprint short-circuits the redundant
status_update broadcasts that fire during a track change
- swapArtworkSrc: only force layout reflow when keyframe is in flight;
drop the ?_=Date.now() cache-buster so identical artwork URLs reuse
the decoded bitmap; mini/glow imgs only re-set src when changed
- drop the fullscreen MutationObserver — fs-bloom-art is mirrored
directly from the artwork-swap path, eliminating the second blur paint
- updateProgress: skip text writes when the rounded second hasn't moved;
POSITION_INTERPOLATION_MS 100 → 250
- background.js: lift resizeBackgroundCanvas out of the rAF body, cache
step, accept new int-scaled wire format
CSS:
- spectrum bars use transform: scaleY(var(--bar-h-scale)) + transition
on transform; will-change updated to transform
- album-art-glow and fs-bloom-art switched to small-source-blur trick
(render at 20-25% size, scale 4-6×, lower blur radius) — visually
equivalent, ~10-25× cheaper repaint on track change
- drop unused transition: filter on .vinyl-stage #album-art
Backend (audio_analyzer.py, websocket_manager.py):
- pre-allocate windowed and cumsum buffers; replace
np.concatenate(([0.0], np.cumsum(...))) with cumsum[0]=0 +
np.cumsum(out=cumsum[1:]); float32 hanning window
- RMS via np.dot(mono, mono) — no astype copy, no ** temp
- int16 wire format (scale=1000) — smaller JSON, no Python float boxing
- versioned data + threading.Event so _audio_broadcast_loop is event-
driven (ev.wait + monotonic seq dedup) instead of polling on a timer
with the always-false `data is _last_data` identity check
ruff clean, pytest 7 passed / 3 numpy-skipped, esbuild bundle 113.6 kB.
- vinyl-stage background fades to transparent (matches fullscreen Listening Room) — no more rectangular dark card around the sleeve+disc composition
- album-art placeholder SVG drops the opaque #282828 backdrop in favour of a translucent disc glyph so the sleeve cardstock shows through before the first artwork load
- new swapArtworkSrc helper retriggers the .is-swapping keyframes so artwork changes crossfade instead of popping
Toggleable theater-scale player view that takes over the viewport
and amplifies the existing Studio Reference aesthetic — same fonts,
same copper/ink palette, just dialed up for immersive listening.
Layout & typography:
- Two-column centerfold: massive vinyl stage left (clamp(., 72vh, 720px)),
editorial column right with Fraunces italic title at clamp(48px, 6.4vw,
112px), Geist Mono console-style metadata strip, oversized timecodes,
full-width amplitude spectrum.
- Mobile / portrait flips to vertical theater (vinyl top, masthead+
transport below) at <=900px or any portrait orientation.
Ambient bloom:
- Duplicate of #album-art rendered behind everything at blur(110px)
saturate(1.6) opacity(0.42) — paints the room in the record's color.
Slow 28s drift animation. Light-theme variant at lower opacity.
- MutationObserver keeps bloom art in sync as tracks change.
- Vignette + edge darkening + subtle paper-grain veil frame the stage.
Interaction:
- Header button (corner-arrows-out icon) toggles; pressing 'F' anywhere
outside text inputs also toggles; ESC exits.
- Native Fullscreen API requested as best-effort sugar on top of the
CSS overlay (works on TV / tablet); CSS overlay alone covers the
CSS-only fallback case (iOS Safari, embedded webviews).
- fullscreenchange listener mirrors OS-level exit back into the overlay.
- Auto-hide chrome + cursor after 2.5s idle, restored on mousemove.
- Focus moves to play/pause on enter; restored to invoking element on
exit.
- Hides mini-player, tab bar, header, folio marks, and other tabs while
active.
Motion:
- 320ms fade-in for the stage, 600ms vinyl rise, 1.4s bloom-in,
staggered 80ms ladder for kicker -> title -> byline -> album -> meta
-> spectrum -> transport. prefers-reduced-motion disables all.
i18n:
- player.fullscreen / player.fullscreen.exit / player.fullscreen.exit_short
added to en.json and ru.json.
Files: index.html (header button + fs-chrome strip + fs-bloom layer),
styles.css (~360-line fullscreen block at end, scoped to body.is-
fullscreen-player), player.js (toggle + init + idle/key/observer
plumbing, ~170 lines), app.js (import + window export + init call).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Production-readiness pass before merging the Studio Reference redesign
to master.
Audio (backend):
- Reset AGC `_spectrum_ref` envelope on `start()` so a long silent gap
between sessions doesn't make the first new transients clip at the
ceiling. Annotated the trade-off (loud transient lifts reference for
a few seconds afterwards — the price of real loudness).
- Add `tests/test_audio_analyzer.py` with 10 cases: bin-edge layout,
AGC attack/release asymmetry, lifecycle reset. Skips numpy-dependent
cases when numpy isn't installed; CI has it.
Vinyl mode dead code removed:
- The toggle button was dropped during the sleeve refactor but the JS
state, 2 s `setInterval`, `beforeunload` handler, and `applyVinylMode`
call (commented out in app.js) all stayed. Now properly excised from
player.js + app.js + window.* exports.
- Stripped the matching `.album-art-container.vinyl*` CSS block and its
`vinylSpin` keyframes (~95 LoC).
Sleeve + tonearm fixes:
- Removed the duplicate `.now-playing .vinyl-stage` / `.vinyl-label` /
`.tonearm` block that was overriding the new `.vinyl-stage` rules by
source order — the uncommitted tonearm geometry never took effect
because the stale clone won the cascade.
- Tightened tonearm to 36% × 36% at right:-6%, top:26% so the SVG
bounding box stays right of the sleeve (sleeve right edge ~68%).
Needle now lands on the visible disc grooves at both rest and
playing rotations and never overlaps the cover.
- Removed sleeve `transform: rotate(-2.5deg)` + the matching mobile
`-1.8deg` override; sleeve now sits flat and squared-off.
- Removed the 1px inset hairline on the sleeve and the 1px outline +
inset highlight on the album art — cleaner, no semitransparent
border noise.
- Album art inset 5% to expose a cardstock margin around the print
(using explicit width/height — `inset` shorthand triggered the CSS
replaced-element rule that uses the image's intrinsic size and blew
out the grid track).
Mobile + misc:
- Removed mobile tonearm overrides at 720px and 420px — they were
calibrated for the pre-sleeve geometry and put the needle back over
the cover on phones; desktop geometry is proportional and works.
- Added `<meta name="mobile-web-app-capable">` alongside the legacy
Apple variant to silence the deprecation warning in Chromium.
- Replaced the "PRIMARY" badge on display cards with a copper star
icon (translation key still drives title + aria-label).
- `.gitattributes` with `* text=auto eol=lf` so Windows checkouts stop
nagging "LF will be replaced by CRLF".
Annotations:
- "REF · 24" record-label catalogue mark marked as intentional non-i18n
decoration in index.html.
CI: ruff clean, pytest 7 passed + 3 numpy-skipped (all 10 run on CI).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Pocket Edition (<=720px): bottom-fixed tab nav with frosted
backdrop, copper hairline + glow on active, mono numerals +
abbreviated labels (legacy rule was hiding labels under 600px).
- Single-column hero player tab: centered vinyl + tonearm,
clamp(28..38px) title, full-width volume row replacing the desktop
VU cluster (the analog meter is decorative on a phone).
- Mini-player floats above the bottom nav, condensed to art + title +
play/pause with the hairline progress on the top edge.
- Library pagination stacks to full-width buttons; settings tables
reflow to card-style rows so no columns drop off-screen.
- Tablet 721-980px: editorial top tabs but with compressed padding so
all five labels fit without clipping.
- Side-by-side player threshold bumped 980 -> 1240 so the right
column has genuine room for controls + VU cluster (was clipping
off the edge at 981-1240px). flex-wrap on .controls as safety net.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Backend computes time-domain RMS, maps -60..-6 dB to 0..1, sends as
`level` alongside the per-frame-normalized frequency bins.
- Frontend prefers `level` directly; drops the peak-of-bins fallback
and the redundant volume-slider attenuation (loopback capture is
already post-volume on Windows/macOS).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- VU needle swings -22..+22deg instead of -45..+45 for a more realistic VU look
- Switch from RMS to peak frequency reading so the needle catches musical hits
- Faster attack (0.7) and release (0.25) so it swings rather than pinning
- Replace explicit grid lines with subtle repeating-conic-gradient ticks
- Scope mini progress bar styles to .mini-player; taller (3px), clickable
Library
- Search icon overlapped placeholder text — legacy CSS positioned
the icon absolutely (left: 0.6rem) inside a position: relative
wrapper. Override now resets position: static (with !important)
on icon, clear button, and wrapper, lets the flexbox order them
naturally with gap: 10px, and zeroes the input's legacy
padding: 0.4rem 2rem 0.4rem 2rem.
- Compact view now visually distinct from grid view: tighter
grid (minmax(120px, 1fr) vs 200px), 18px gap, smaller sans-
font name (13px sans 500 weight) instead of serif, smaller
meta (9px), smaller browser-icon. The legacy
.browser-grid-compact class was being applied but my
.now-playing-styled rules ignored it.
Display tab — full card styling
- Cards: 360px min width (was 280px), serif name (17px) with
copper monitor icon, mono uppercase resolution + manufacturer,
copper-bordered "PRIMARY" badge.
- Power button: 38px circle, jade when ON (with copper-glow
shadow), faint ink when OFF, copper on hover. Was previously
unstyled / invisible.
- Brightness control: hairline divider above, copper hairline
slider with copper handle and copper-glow, mono tabular-num
percentage in copper.
Native form widgets readable on dark theme
- color-scheme: dark on :root (light on light theme) so native
controls (select dropdowns, scrollbars) inherit dark colors.
- select option { background-color, color } so the popup list
paints dark text on dark background instead of system white.
The mask + repeating-conic-gradient combo was rendering the visible
arc shifted to the right (mask appeared centred on 12-3 o'clock
instead of 10:30-1:30). Cause unclear — likely a browser quirk
around `from -45deg` interaction between the two conic-gradients.
Replaced with a single non-repeating conic-gradient using `from 315deg`
(the positive equivalent of -45deg). Lines are drawn explicitly at
every 9° from 0° to 90° in the gradient (-45° to +45° in standard
orientation) — 11 lines total, with the centre line (at 0°) slightly
brighter so it reads as the meter's "0 VU" mark. Outside the 90°
active wedge the gradient is transparent, so no mask is needed.
Result: the leftmost gridline now sits exactly where the needle rests
at -45°, like a real VU meter at silence.
The grid pattern was a 360° repeating conic gradient (visible across
the full upper 180° arc), while the needle only swings -45..+45.
That made the rest position at -45 sit *inside* the visible arc
rather than at the leftmost gridline — looked wrong.
Now:
- Grid lines start at the -45 angle (matches needle origin)
- Conic-gradient mask clips visibility to the 90° active wedge
- Leftmost gridline now coincides with the needle's rest rotation
→ looks like a real VU meter at zero (silence)
Toolbar icons (off-centre inside hit box)
- Legacy .header-btn had padding: 4px 6px and inline-flex with no
justify-content. With box-sizing: content-box and width: 36px, the
asymmetric horizontal padding shifted icons to the upper-left of
the 36×36 hit box.
- Override now sets display: inline-flex; align-items: center;
justify-content: center; padding: 0; box-sizing: border-box and
forces SVG children to display: block 16x16 so icons sit dead-
centre.
Spectrum width — root cause finally found and removed
- An old override block (lines 4821–4878) was still in the file:
.spectrum { max-width: 360px } + .spectrum span { width: 3px;
flex: 0 0 3px } + 30 nth-child rules. They had equal/higher
specificity for some props than the .now-playing-scoped rules
and were declared first, so width was capping the row.
- Deleted that whole block. .now-playing .spectrum is now the
single source of truth. Combined with the explicit
grid-template-columns: repeat(40, minmax(0, 1fr)) (set both in
CSS literal and from JS via gridTemplateColumns), the row
reliably fills the column.
VU needle resting position
- CSS default rotation changed from -22deg (pointing upper-left)
to -45deg — the conventional VU meter rest at silence (-∞ dB).
Matches stopVuWobble() which also settles the needle there.
Dynamic background — removed
- .bg-shader-canvas hidden via display: none.
- Toggle button (#bgToggle) hidden so the toolbar is cleaner.
- Canvas + JS module stay in DOM so the existing JS doesn't crash.
VU needle reflects actual audio output
- Was just a synthetic wobble bounded by the volume slider value.
Now reads RMS of the FFT bins (skipping bin 0 / DC) the visualizer
feeds in, multiplies by current volume, and applies attack/release
smoothing for analog-feeling ballistics.
- Falls back to the synthetic wobble when audio capture isn't
running so the needle still looks alive on the static fallback.
- When playback stops, needle settles to the bottom of the swing
(-45deg) instead of holding the volume position.
Spectrum width — actually fixed this time
- Root cause: CSS repeat() does NOT accept a CSS variable for its
count argument, so my `repeat(var(--spectrum-bars), 1fr)` rule
was invalid and silently dropped, leaving the legacy/auto sizing
behavior. Set grid-template-columns directly from JS to
`repeat(40, minmax(0, 1fr))`.
- CSS retains a `repeat(40, minmax(0, 1fr))` literal as a default
so the row renders sane even before JS executes.
Spectrogram canvas under vinyl
- Hidden via display: none — the editorial .spectrum row already
shows the audio spectrum; the canvas was redundant and ugly.
Element stays in DOM so the visualizer JS keeps rendering (drives
album-art bass-pulse + dynamic background bands).
Spectrum width
- grid-auto-flow: column with implicit columns wasn't reliably
stretching to fill the parent. Switch to explicit
grid-template-columns: repeat(var(--spectrum-bars), minmax(0, 1fr))
with the bar count exposed as a CSS variable from JS so the
column count and the actual bar count stay in sync.
- !important on display/grid-template-columns/width to defeat any
legacy descendant rules.
Device selection
- Picking a device in the audio-device dropdown is an explicit
signal that the user wants capture. Auto-enable the visualizer
if it isn't already on, then call applyVisualizerMode so the
WS subscription happens and the badge flips from 'Available' to
'Active'. Was only doing this when visualizer was already on,
which is why the user kept seeing 'Available, not capturing'.
Auto-enable was a no-op
- Writing 'visualizerEnabled'='true' to localStorage from app.js did
not update the exported `let visualizerEnabled` in player.js. So
applyVisualizerMode() saw the stale `false` and went into the
DISABLE branch — leaving the device 'available, not capturing'.
- Add a setVisualizerEnabled() setter exported from player.js and
call it before applyVisualizerMode() during boot.
Audio device persistence
- Save the selected device name to localStorage on change.
- On loadAudioDevices(), prefer status.current_device (server's
current state) but fall back to the localStorage value if the
server doesn't know one (e.g. after a server restart).
- If the saved device wasn't recognized by the server, push it back
via POST /api/media/visualizer/device so capture lands on it
immediately. Best-effort; no toast on failure.
- Spectrum: switch from flex to grid auto-flow so each bar gets an
equal-width column slot regardless of bar count. Fewer (40) but
fatter bars now genuinely span the full grid column.
- Spectrum height bumped to 70px, gap 4px.
- VU cluster: flex-direction: row-reverse so volume controls sit on
the LEFT, readout in the middle, VU meter on the RIGHT.
- Volume hairline divider switched from border-left to border-right
so it stays between the volume controls and the readout.
Spectrum
- Logarithmic frequency-to-bar mapping (squared time so bins
stretch toward the highs). Per-bar high-end gain ramps from
1.0x at the lowest bar to 3.0x at the highest, so the right
half of the spectrum no longer reads as dead air.
- Floor bumped from 6% to 12% so silent bars stay visible.
- Skip bin 0 (DC + sub-rumble) which was overwhelming the lows.
- Use peak (not average) within each band — punchier visual.
- Container height 56→64, gradient now copper-lo → copper →
copper-hi for more visible top tips. min-width: 0 / box-sizing
border-box ensures the row truly claims the full grid column.
- Backend FFT path is unchanged: WS audio_data → setFrequencyData
→ renderVisualizerFrame → updateEditorialSpectrum. No
client-side analyzer added.
Album art (vinyl label)
- Deeper sepia (0.35→0.6) and lower saturate (0.85→0.7) so
vibrant covers blend into the copper grooves.
- Soft radial mask: outer ~22% of the disc fades toward the
vinyl black so the album art dissolves into the surface
rather than terminating at a hard clip edge.
- Hover state pulls the fade inward and eases sepia back so
the user can still see the real cover at near-natural color.
- Glow tint matches the new sepia depth.
VU needle (animated)
- Synthetic wobble bounded by current volume runs only while
state==='playing'. Two combined sines + jitter make it look
like a real analog needle reacting to peaks.
- Settles back to the static volume-mapped position when paused.
Spectrum (real audio)
- Now driven by the same frequencyData the visualizer canvas
uses. Each visual bar averages a chunk of frequency bins.
- Spans are now JS-injected (60 bars) instead of hardcoded so
the bar count is no longer baked in.
- Spectrum spans full width of the masthead column, height
bumped to 56px for presence.
- CSS animation pauses (sets via `body.audio-spectrum-live`)
when JS is driving heights so the keyframes don't fight.
- Synthetic CSS animation remains as the fallback when audio
capture isn't available.
Visualizer auto-enable
- On first install with loopback support, visualizer is
enabled so the spectrum is alive out of the box.
Dynamic background
- Lower max opacity (1 → 0.45 dark, 0.35 light)
- sepia + saturate filter + hue-rotate keep it palette-aligned
with the copper editorial tones instead of fighting them
- mix-blend-mode screen (dark) / multiply (light) blends into
the page background instead of overlaying
Update + connection banners
- Fully restyled: glassy card with copper hairline accent,
mono uppercase text, copper hairline-border CTA buttons,
minimal close button. Matches the rest of the editorial
palette instead of the old solid-green-bar look.
Toolbar
- Strip the legacy dark capsule from .header-toolbar (background,
border, border-radius, padding) — buttons now sit as individual
ghost icons matching the mockup, with the per-button transparent
border + copper hover that was already in place.
Album art
- Apply a warm sepia/saturate/contrast filter to #album-art inside
the vinyl label so vibrant covers (e.g. Rammstein red) don't
fight the copper palette and read consistent with the vinyl
groove rendering. Eases back to near-natural color on .now-playing
hover so the user can still see the real cover.
- Match the album-art-glow tint with the same hue-rotate so the
bloom around the spinning label stays palette-aligned.
Tabs
- Replace SVG icons with mockup-style numeric badges (01..05)
- Hide the legacy .tab-indicator (was rendering as a long copper
bar above the strip); active tab gets its own copper underline
via .tab-btn.active::after
- Numbers turn copper on the active tab
Player view
- Volume control restored: mute button + slim copper slider live
inside the VU cluster, on the right of the readout, separated
by a hairline. Slider is the existing #volume-slider so all JS
hooks (bidirectional sync, drag, etc.) keep working.
- Track title font scaled down (clamp 34..64) and clamped to 3
lines with ellipsis so long YouTube-style titles don't dominate
the masthead. Adds word-break + overflow-wrap.
- #artist:empty and #album:empty are now display:none so blank
rows don't leave a gap when the source provides no metadata.
Mini player
- Forced display: grid with 4 columns: track / controls / progress
/ volume. Was inheriting legacy display:flex which pushed elements
into a single non-aligned row.
- Position locked: position: fixed, bottom: 0, left/right: 0 with
!important. The strip is firmly anchored to the viewport bottom.
- Top edge progress (::before) painted in copper with glow.
- Responsive collapse: hide progress at <=880px, hide volume at
<=540px, leaving track + controls on phones.
i18n
- tab.player default text aligned to "Now Spinning" (matches
existing en/ru values added earlier).
Wholesale replacement of the player view markup + a verbatim mockup
CSS block scoped to .now-playing. Previous approach kept restyling
legacy classes which left a layered, inconsistent result. This is a
clean snap: same DOM structure as the mockup, same CSS rules, mapped
onto existing JS-touched IDs.
Markup
- One <section class="now-playing"> with vinyl-stage on left and
track-masthead on right
- Vinyl spins via data-playstate and contains album art as the
circular center label (existing #album-art img preserved)
- SVG tonearm pivots in/out by data-playstate
- Track masthead: .kicker (copper italic mono), large italic-serif
.track-title, italic .track-byline, mono .track-album
- Meta grid: 2 cells (State / Source) with mono labels + italic
serif values
- 30 .spectrum bars between metadata and transport
- .transport: progress-row (timecode + .progress-track + timecode)
and .controls (3 .btn-trans buttons + .vu-cluster)
- Volume slider, mute button, visualizer toggle moved to a
.visually-hidden block — they remain functional for JS / a11y
but no longer compete for visual real estate. Volume control
happens via the always-visible mini player.
VU cluster (mockup-faithful)
- 140x60 VU meter with conic-gradient grid background, copper
needle, "VU" label
- Stacked readout: "OUT <strong>SYS</strong>" / "VOL <strong>72%</strong>"
- Click anywhere on cluster toggles mute (calls toggleMute())
- When muted: needle turns rust, readout strong turns rust,
OUT label switches to MUTE
JS hooks
- updatePlaybackState already sets :root[data-playstate] (drives
spin + tonearm)
- Volume tick now updates #vu-vol and #vu-out
- updateMuteIcon updates #vu-out + .vu-cluster.muted class
Scoping
- All new CSS is .now-playing-prefixed so other tabs and dialogs
are untouched
- Legacy .progress-bar:hover scaleY and ::after scale(0) are
defeated with !important inside .now-playing
- Meta-grid reduced from 4 cells to 2 (State / Source). Elapsed and
Length were duplicates of the timecodes already flanking the
timeline. Added .meta-grid-2 modifier with a 2-column layout.
- Timeline was distorted by legacy rules: .progress-bar:hover did
scaleY(1.4) (inflating the 2px hairline on hover) and the
progress-fill::after handle defaulted to scale(0). Both are now
forced via !important to keep the hairline flat and the copper
handle always visible.
- progress-row uses minmax(0, 1fr) for the bar cell so it shrinks
cleanly inside the grid.
- Removed unused meta.elapsed / meta.length keys from en.json + ru.json.
- Dead JS lookups for #meta-elapsed / #meta-length stay (cheap if-checks,
no-op when DOM elements gone).
Side-by-side comparison surfaced several layout regressions vs. the
mockup. This commit lands all of them at once.
Header
- Restore centered "Media Server / Studio Reference Edition" wordmark
in italic Fraunces
- Move folio marks to fixed page corners (visible on every tab):
TL = green pulse + "Connected · Local 8765"
TR = "Vol. I — Studio Reference · v0.x.x"
- Replace boxed version-label badge with copper mono inline in folio.tr
- Reduce header-to-content gap (container padding-top 28→56 with the
folio now anchored above)
Player view
- Spectrum bars: smaller height (32px), centered with max-width so
they don't span the whole right column
- Spectrogram canvas: hidden by default (opacity 0); reveals only when
visualizer toggle is active. No more leaking into bottom-left.
- VU cluster volume controls: strip legacy box (background, padding,
border-radius); compact stacked layout with thin slider, small mute
button, mono "VOL · XX%" readout
- Disable legacy applyVinylMode() — the .vinyl class added a SECOND
rotation animation on top of the structural .vinyl-stage spin,
causing visible compounding. Vinyl is now purely structural.
Toggles
- Remove vinyl mode toggle button (vinyl is always on)
- Keep audio visualizer (spectrum vis) toggle — still shown by JS
when supported
Mini player
- Force always-visible on non-player tabs regardless of scroll, by
short-circuiting setMiniPlayerVisible when activeTab !== 'player'
i18n
- New keys: header.connected, header.volume, header.edition,
header.edition_sub
- Removed unused: player.folio_left, player.folio_right
- en.json + ru.json updated
Restructures the player tab DOM to actually look like the editorial
mockup, not just inherit new fonts. The previous commit only swapped
tokens & typography on the legacy Spotify-clone layout.
DOM additions (all preserve existing JS-touched IDs):
- Vinyl stage: rotating vinyl wrapping the existing #album-art as a
circular center label; spins only when state=playing via CSS hook
- SVG tonearm: pivots in/out based on data-playstate
- Kicker line: copper italic mono header above the track title
- Editorial 4-cell metadata grid: State / Source / Elapsed / Length
- Decorative spectrum bars (30, CSS-only animation, paused when idle)
- VU meter cluster: needle visual driven by volume %, alongside the
preserved volume slider for a11y
- Folio marks: top-left and top-right of the player container
JS hooks (small, additive):
- updatePlaybackState now sets :root[data-playstate] for CSS
- progress tick mirrors timecode into meta-grid cells
- volume update rotates the VU needle
- folio-version mirrors the version label
i18n:
- new keys: player.kicker, player.modes, player.folio_*, meta.*
- added to both en.json and ru.json
Restored: media_server/static/redesign-mockup.html (Studio Reference
visual reference; deleting it in the prior commit was a mistake).
Replaces the Spotify-clone dark theme with a warm editorial design
language inspired by hi-fi audio mastering and magazine layouts.
- Self-host Fraunces, Geist, and Geist Mono as variable WOFF2 files
(Latin + Latin-ext + Cyrillic subsets, OFL-licensed)
- New design tokens: warm charcoal + copper accent (dark) /
cream paper + hunter emerald (light)
- Editorial typography: Fraunces serif for display + masthead,
Geist for UI, Geist Mono for technical readouts (timecodes, bitrates)
- Player view restyled as magazine spread with framed album art
- Mini player as glassy console strip with copper hairline glow
- Tabs as italic editorial nav with copper underline
- Browser items as gallery cards with editorial typography
- Settings as numbered sections with refined tables
- Quick Access as console rail
- Dialogs and auth modal as paper cards with mono kickers
- Subtle film-grain overlay for analog warmth
- Localized tab labels: Player → Now Spinning, Browser → Library
- Generate numpy/_distributor_init_local.py during build so libopenblas
can be located when running from the Windows installer
- Add os.add_dll_directory() call at runtime as a fallback for embedded Python
- Broaden audio import errors from ImportError to Exception, log at warning
- Move visualizer WS re-subscription into loadAudioDevices() so it runs
after availability is confirmed from the API
- Show/hide the visualizer toggle button based on fetched availability
Instead of waiting for the next poll cycle, new clients now get the
current playback status immediately on connect by calling get_status_func
if no cached status is available yet.
Two root causes for the 'imaging extension was built for another version
of Pillow' error users hit after install:
1) cleanup_site_packages ran 'find ... -name "*.py" ! -name "__init__.py"
-delete' with a comment claiming 'keep .pyc only' — but no compileall
step exists. Result: the dist shipped __init__.py + .pyd only, missing
every submodule (Image.py, ImageDraw.py, _version.py, ...). Fresh
installs were broken; in-place upgrades produced a half-old/half-new
site-packages. Removed the deletion entirely.
2) NSIS installer extracted over the previous install without cleaning
python/, app/, scripts/. Upgrades left stale files (old PIL/_version.py
next to new PIL/_imaging.pyd) which raised the Pillow ABI mismatch.
Wipe those subtrees before File /r, preserving config.yaml at the
install root.
pystray in WIN_DEPS (per-dep loop) downloaded its own Pillow version,
which overwrote the one resolved alongside CORE_DEPS during unzip.
Result at runtime: '_imaging extension was built for another version
of Pillow'.
Move pystray into VIS_DEPS so it's resolved in the single cross-deps
pip-download call and shares one consistent Pillow version.