fix(player): redesign cleanup pass — sleeve, tonearm, AGC, dead code

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>
This commit is contained in:
2026-04-25 14:39:20 +03:00
parent f85ce77f14
commit 2a474ea52c
9 changed files with 424 additions and 442 deletions
+204 -335
View File
@@ -829,97 +829,10 @@ h1 {
filter: blur(50px) saturate(1.8);
}
/* Vinyl Record Mode */
.album-art-container.vinyl #album-art {
border-radius: 50%;
width: 210px;
height: 210px;
margin: 45px;
filter: saturate(0.55) sepia(0.12) brightness(0.92) contrast(1.08);
box-shadow:
0 0 0 3px var(--vinyl-groove),
0 0 0 5px var(--vinyl-ring),
0 0 0 6px var(--vinyl-highlight),
0 0 0 12px var(--vinyl-ring),
0 0 0 13px var(--vinyl-highlight-dim),
0 0 0 20px var(--vinyl-ring),
0 0 0 21px var(--vinyl-highlight),
0 0 0 28px var(--vinyl-ring),
0 0 0 29px var(--vinyl-highlight-dim),
0 0 0 36px var(--vinyl-ring),
0 0 0 37px var(--vinyl-highlight),
0 0 0 42px var(--vinyl-ring),
0 0 0 43px var(--vinyl-groove),
0 0 0 45px var(--vinyl-edge),
0 4px 15px 45px var(--shadow-elevation);
}
/* Vinyl label vignette overlay */
.album-art-container.vinyl::before {
content: '';
position: absolute;
width: 210px;
height: 210px;
border-radius: 50%;
background: radial-gradient(
circle,
transparent 50%,
rgba(0,0,0,0.25) 100%
);
z-index: 2;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
opacity: 0;
transition: opacity 0.6s ease;
}
.album-art-container.vinyl.spinning::before,
.album-art-container.vinyl.paused::before {
opacity: 1;
}
.album-art-container.vinyl .album-art-glow {
border-radius: 50%;
}
/* Center spindle hole */
.album-art-container::after {
content: '';
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--vinyl-spindle);
border: 2px solid var(--border);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
z-index: 3;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
opacity: 0;
transition: opacity 0.4s ease 0.3s;
}
.album-art-container.vinyl::after {
opacity: 1;
}
.album-art-container.vinyl.spinning #album-art {
animation: vinylSpin 12s linear infinite;
}
.album-art-container.vinyl.paused #album-art {
animation: vinylSpin 12s linear infinite;
animation-play-state: paused;
}
@keyframes vinylSpin {
from { transform: rotate(var(--vinyl-offset, 0deg)) scale(var(--vinyl-scale, 1)); }
to { transform: rotate(calc(var(--vinyl-offset, 0deg) + 360deg)) scale(var(--vinyl-scale, 1)); }
}
/* Legacy "vinyl mode" toggle (.album-art-container.vinyl + JS-driven
spinning class) was removed alongside the sleeve+disc redesign — the
disc now spins structurally via .vinyl-stage .vinyl when playstate
is "playing" (see SLEEVE FRAME section below). */
/* Audio Spectrogram Visualization */
.spectrogram-canvas {
@@ -946,12 +859,6 @@ h1 {
transition: opacity 0.08s ease-out;
}
/* Adapt spectrogram for vinyl mode */
.album-art-container.vinyl .spectrogram-canvas {
bottom: -10px;
border-radius: 0 0 50% 50%;
}
.track-info {
text-align: center;
margin-bottom: 2rem;
@@ -2854,27 +2761,6 @@ button.primary svg {
height: 250px;
}
.album-art-container.vinyl #album-art {
width: 170px;
height: 170px;
margin: 40px;
box-shadow:
0 0 0 3px #2a2a2a,
0 0 0 5px #1a1a1a,
0 0 0 6px rgba(255,255,255,0.05),
0 0 0 12px #1a1a1a,
0 0 0 13px rgba(255,255,255,0.03),
0 0 0 20px #1a1a1a,
0 0 0 21px rgba(255,255,255,0.05),
0 0 0 28px #1a1a1a,
0 0 0 29px rgba(255,255,255,0.03),
0 0 0 36px #1a1a1a,
0 0 0 37px rgba(255,255,255,0.04),
0 0 0 38px #2a2a2a,
0 0 0 40px #111,
0 4px 12px 40px rgba(0,0,0,0.4);
}
#track-title {
font-size: 1.5rem;
}
@@ -4656,12 +4542,17 @@ header .brand-sub {
}
/* ─── Tonearm SVG ──────────────────────────────────────────── */
/* Geometry chosen so the SVG bounding box stays right of the sleeve
(sleeve right edge ≈ 68%). Pivot floats just past the stage's right
edge; needle lands on the visible disc grooves at playing rotation
(0deg) and on the outer rest position at -22deg. Never overlaps
the sleeve cover. */
.vinyl-stage .tonearm {
position: absolute;
top: -6%;
right: -4%;
width: 56%;
height: 56%;
top: 26%;
right: -6%;
width: 36%;
height: 36%;
pointer-events: none;
transform-origin: 88% 12%;
transform: rotate(-22deg);
@@ -4694,6 +4585,162 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
opacity: 0.6;
}
/* ════════════════════════════════════════════════════════════════
SLEEVE FRAME — production layout
The album cover prints on a cardstock sleeve at left; the disc
sits to the right and peeks out, spinning while the tonearm
rests on it. The vinyl label is now a typographic plate; track
metadata lives in the masthead beside the stage.
════════════════════════════════════════════════════════════════ */
/* Glow: soft ambient halo behind the sleeve */
.vinyl-stage > #album-art-glow {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: 0;
object-fit: cover;
filter: blur(34px) saturate(1.6);
opacity: 0.45;
z-index: 0;
pointer-events: none;
transform: scale(1.05);
}
:root[data-theme="light"] .vinyl-stage > #album-art-glow {
opacity: 0.26;
filter: blur(40px) saturate(1.8);
}
/* Stage gets a warm ambient gradient (matches mockup) */
.album-art-container.vinyl-stage {
background:
radial-gradient(ellipse at center, #1a1611 0%, var(--bg-deep) 80%);
}
:root[data-theme="light"] .album-art-container.vinyl-stage {
background:
radial-gradient(ellipse at center, var(--bg-card-2) 0%, var(--bg-deep) 80%);
}
/* Sleeve: cardstock card with album cover printed on its face.
Geometry mirrors the mockup — sleeve+disc occupy a centered 90%
inner square so the arrangement breathes inside the stage. */
.vinyl-stage .sleeve {
position: absolute;
left: 5%;
top: 10.4%;
width: 63%;
aspect-ratio: 1;
z-index: 3;
background: var(--bg-card-2);
box-shadow:
inset 4px 4px 24px rgba(0, 0, 0, 0.35),
-2px 8px 24px rgba(0, 0, 0, 0.5),
-4px 18px 44px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
:root[data-theme="light"] .vinyl-stage .sleeve {
background: var(--bg-card);
box-shadow:
inset 4px 4px 18px rgba(0, 0, 0, 0.10),
-2px 8px 22px rgba(0, 0, 0, 0.20),
-4px 18px 36px rgba(0, 0, 0, 0.12);
}
/* Album art is the printed cover tipped onto the cardstock sleeve.
A 5% inset reveals the cardstock as a visible border around the
print; outline + inset shadow define the printed-cover edge.
Explicit width/height (not `inset` shorthand) — img is a replaced
element and would otherwise fall back to its intrinsic pixel
size, blowing out the grid track. */
.vinyl-stage .sleeve #album-art {
position: absolute;
top: 5%;
left: 5%;
width: 90%;
height: 90%;
object-fit: cover;
border-radius: 0;
z-index: 1;
box-shadow:
inset 0 0 18px rgba(0, 0, 0, 0.35),
0 2px 6px rgba(0, 0, 0, 0.35);
margin: 0;
background: var(--bg-card);
filter: contrast(0.96) saturate(0.92);
transition: filter 0.6s ease;
}
/* Cardstock paper grain over the print — multiplies into the image */
.vinyl-stage .sleeve-grain {
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.2' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.10 0 0 0 0 0.08 0 0 0 0 0.06 0 0 0 0.55 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
mix-blend-mode: multiply;
pointer-events: none;
z-index: 2;
opacity: 0.7;
}
:root[data-theme="light"] .vinyl-stage .sleeve-grain { opacity: 0.32; }
/* Worn corner notch — tiny triangular wear on bottom-right */
.vinyl-stage .sleeve-corner {
position: absolute;
width: 13%;
height: 13%;
bottom: -1px;
right: -1px;
background: var(--bg-deep);
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0.6;
z-index: 4;
}
/* Disc wrap — sits behind the sleeve, peeks out the right edge */
.vinyl-stage .vinyl-wrap {
position: absolute;
right: 3.2%;
top: 19.4%;
width: 63%;
aspect-ratio: 1;
z-index: 2;
}
.vinyl-stage .vinyl-wrap .vinyl {
width: 100%;
}
/* Vinyl label = typographic plate (no album art, lives on the sleeve now) */
.vinyl-stage .vinyl-wrap .vinyl-label {
background: linear-gradient(135deg, #2E2820 0%, #1f1a13 100%);
box-shadow:
inset 0 0 18px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 0 0 3px var(--bg-deep),
0 0 0 4px var(--copper-lo);
display: flex;
align-items: center;
justify-content: center;
}
:root[data-theme="light"] .vinyl-stage .vinyl-wrap .vinyl-label {
background: linear-gradient(135deg, #1F4E3D 0%, #143E2F 100%);
box-shadow:
inset 0 0 18px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 0 0 3px var(--bg-paper),
0 0 0 4px var(--copper-lo);
}
.vinyl-label-text {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.3em;
color: var(--copper);
text-transform: uppercase;
z-index: 2;
font-weight: 500;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
user-select: none;
}
/* ─── Player details (right column / masthead) ──────────────── */
.player-details,
.track-masthead {
@@ -6511,196 +6558,16 @@ footer .separator { color: var(--ink-ghost); margin: 0 8px; }
.now-playing { grid-template-columns: 1fr; gap: 40px; }
}
/* ─── Vinyl + tonearm ─────────────────────────────────────── */
.now-playing .vinyl-stage {
position: relative;
aspect-ratio: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
box-shadow: none;
padding: 0;
overflow: visible;
transform: none !important;
}
.now-playing .vinyl {
position: relative;
width: 86%;
aspect-ratio: 1;
border-radius: 50%;
background:
radial-gradient(circle at 50% 50%,
#0a0907 0%,
#0a0907 18%,
#1a1611 18.3%,
#0a0907 18.6%,
#14110c 22%,
#0a0907 22.3%,
#14110c 26%,
#0a0907 26.3%,
#14110c 30%,
#0a0907 30.3%,
#14110c 34%,
#0a0907 34.3%,
#14110c 38%,
#0a0907 38.3%,
#14110c 42%,
#0a0907 42.3%,
#14110c 46%,
#0a0907 46.3%,
#1c1812 47%,
#0a0907 100%);
box-shadow:
inset 0 0 60px rgba(0,0,0,0.7),
0 30px 80px rgba(0,0,0,0.6),
0 6px 20px rgba(0,0,0,0.5);
animation: sr-snap-spin 14s linear infinite;
animation-play-state: paused;
}
:root[data-playstate="playing"] .now-playing .vinyl {
animation-play-state: running;
}
.now-playing .vinyl::before {
content: "";
position: absolute; inset: 12%;
border-radius: 50%;
background:
conic-gradient(from 0deg,
rgba(255,255,255,0.04) 0deg,
transparent 30deg,
rgba(255,255,255,0.06) 90deg,
transparent 150deg,
rgba(255,255,255,0.03) 210deg,
transparent 270deg,
rgba(255,255,255,0.05) 330deg,
transparent 360deg);
mix-blend-mode: screen;
pointer-events: none;
}
/* Vinyl label = circular clip holding the actual album art */
.now-playing .vinyl-label {
position: absolute;
inset: 28%;
border-radius: 50%;
overflow: hidden;
background: var(--bg-card);
box-shadow:
inset 0 0 24px rgba(0,0,0,0.4),
0 0 0 4px var(--bg-deep),
0 0 0 5px var(--copper-lo);
z-index: 1;
}
.now-playing .vinyl-label::before {
/* Spindle hole */
content: "";
position: absolute;
width: 8%; height: 8%;
top: 46%; left: 46%;
border-radius: 50%;
background: var(--bg-deep);
box-shadow: inset 0 1px 2px rgba(255,255,255,0.1);
z-index: 3;
}
.now-playing .vinyl-label #album-art-glow {
position: absolute;
inset: -10%;
width: 120%;
height: 120%;
border-radius: 50%;
filter: blur(22px) saturate(1.4);
opacity: 0.5;
z-index: 0;
object-fit: cover;
}
.now-playing .vinyl-label #album-art {
position: relative;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 50%;
z-index: 2;
/* Heavy vinyl-consistent tint: deeper sepia + lower saturation
so vibrant covers blend with the copper grooves. */
filter:
sepia(0.6)
saturate(0.7)
contrast(1.12)
brightness(0.88)
hue-rotate(-8deg);
transition: filter 480ms var(--ease), -webkit-mask-image 480ms var(--ease);
/* Soft radial fade — the outer ~12% of the art fades to black so
the album image dissolves into the vinyl surface rather than
cutting hard at the circular clip edge. */
-webkit-mask-image: radial-gradient(circle at 50% 50%,
black 0%,
black 78%,
rgba(0,0,0,0.85) 88%,
rgba(0,0,0,0.4) 96%,
transparent 100%);
mask-image: radial-gradient(circle at 50% 50%,
black 0%,
black 78%,
rgba(0,0,0,0.85) 88%,
rgba(0,0,0,0.4) 96%,
transparent 100%);
}
.now-playing:hover .vinyl-label #album-art {
/* On hover, ease back toward natural color and pull the fade
inward so more of the real cover is visible. */
filter:
sepia(0.25)
saturate(0.92)
contrast(1.05)
brightness(0.98)
hue-rotate(-4deg);
-webkit-mask-image: radial-gradient(circle at 50% 50%,
black 0%,
black 88%,
rgba(0,0,0,0.9) 95%,
rgba(0,0,0,0.5) 99%,
transparent 100%);
mask-image: radial-gradient(circle at 50% 50%,
black 0%,
black 88%,
rgba(0,0,0,0.9) 95%,
rgba(0,0,0,0.5) 99%,
transparent 100%);
}
/* Match the glow tint and soft edge to the album art treatment */
.now-playing .vinyl-label #album-art-glow {
filter: blur(22px) saturate(1.1) sepia(0.5) hue-rotate(-8deg);
opacity: 0.4;
}
/* Tonearm */
.now-playing .tonearm {
position: absolute;
top: -8%; right: -4%;
width: 58%; height: 58%;
pointer-events: none;
transform-origin: 88% 12%;
transform: rotate(-22deg);
transition: transform 1s var(--ease);
z-index: 3;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.5));
}
:root[data-playstate="playing"] .now-playing .tonearm {
transform: rotate(0deg);
}
@keyframes sr-snap-spin { to { transform: rotate(360deg); } }
/* Vinyl/disc/tonearm rules live in the SLEEVE FRAME section under
.vinyl-stage selectors above. The earlier .now-playing duplicates
were stale clones from the pre-sleeve mockup snap and overrode the
new geometry by source order — removed to let .vinyl-stage rules
take effect. */
/* Spectrogram canvas: hidden — the editorial .spectrum row in the
track-masthead already shows the audio spectrum. The canvas
element stays in the DOM so the visualizer JS keeps rendering
(drives the album-art bass-pulse + dynamic background). */
track-masthead is the visible spectrum. The canvas stays in the
DOM so the visualizer render loop keeps emitting frequencyData
for the dynamic background to consume. */
.now-playing .spectrogram-canvas {
display: none !important;
}
@@ -7998,7 +7865,7 @@ select option {
letter-spacing: -0.005em;
line-height: 1.2;
display: inline-flex;
align-items: baseline;
align-items: center;
gap: 10px;
/* Allow text to wrap so we don't ellipsis-truncate the model name */
overflow: hidden;
@@ -8013,13 +7880,20 @@ select option {
color: var(--ink-mute);
}
.display-container .display-primary-badge {
font-family: var(--mono);
font-size: 8px;
letter-spacing: 0.18em;
text-transform: uppercase;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--copper);
border: 1px solid var(--copper);
padding: 2px 6px;
background: none;
border: 0;
padding: 0;
margin: 0;
line-height: 0;
vertical-align: middle;
filter: drop-shadow(0 0 4px var(--copper-glow));
}
.display-container .display-primary-badge svg {
display: block;
}
/* Power button — visible & state-coloured (on = jade, off = ink-mute) */
@@ -8486,19 +8360,19 @@ select option {
width: 78%;
margin: 0 auto !important;
}
.vinyl-stage .vinyl { width: 92%; }
.vinyl-stage .vinyl-label {
/* Lighter sleeve grain on phones so the printed art reads
cleanly at small size. */
.vinyl-stage .sleeve-grain { opacity: 0.55; }
.vinyl-stage .vinyl-wrap .vinyl-label {
box-shadow:
inset 0 0 24px rgba(0, 0, 0, 0.4),
0 0 0 3px var(--bg-deep),
0 0 0 4px var(--copper-lo);
}
.vinyl-stage .tonearm {
top: -8% !important;
right: -6% !important;
width: 60% !important;
height: 60% !important;
inset 0 0 16px rgba(0, 0, 0, 0.5),
0 0 0 2px var(--bg-deep),
0 0 0 3px var(--copper-lo);
}
.vinyl-label-text { font-size: 9px; letter-spacing: 0.24em; }
/* Tonearm geometry inherits the desktop .vinyl-stage .tonearm
values — sleeve + disc proportions are identical on mobile,
so the needle still lands on the visible disc grooves. */
/* Track masthead text: centered, condensed cadence */
.track-masthead { text-align: center; }
@@ -8767,14 +8641,9 @@ select option {
display: none !important;
}
/* Tonearm + vinyl tighter still */
/* Vinyl stage tighter still on small phones; tonearm inherits
desktop geometry which is proportional to the stage. */
.album-art-container.vinyl-stage { width: 84%; }
.vinyl-stage .tonearm {
top: -10% !important;
right: -8% !important;
width: 64% !important;
height: 64% !important;
}
.now-playing #track-title,
.player-layout #track-title {