feat(ui): live VU + audio-driven spectrum, editorial banner, subtler dynamic bg

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.
This commit is contained in:
2026-04-25 02:03:15 +03:00
parent d157388a94
commit d937c1590c
4 changed files with 230 additions and 72 deletions
+122 -61
View File
@@ -216,7 +216,7 @@
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
/* Dynamic Background Canvas */
/* Dynamic Background Canvas — editorial-toned (warm, subtle) */
.bg-shader-canvas {
position: fixed;
top: 0;
@@ -227,10 +227,23 @@
pointer-events: none;
opacity: 0;
transition: opacity 0.6s ease;
/* Sepia + slight desaturation keeps the shader colors palette-aligned */
filter: sepia(0.4) saturate(0.75) contrast(0.95) brightness(0.9) hue-rotate(-8deg);
/* Multiply blends the bright shader into the warm dark page background */
mix-blend-mode: screen;
}
.bg-shader-canvas.visible {
opacity: 1;
/* Lower max opacity so it reads as atmosphere, not foreground */
opacity: 0.45;
}
:root[data-theme="light"] .bg-shader-canvas {
filter: sepia(0.35) saturate(0.7) contrast(1.05) brightness(1.05) hue-rotate(-12deg);
mix-blend-mode: multiply;
}
:root[data-theme="light"] .bg-shader-canvas.visible {
opacity: 0.35;
}
body.dynamic-bg-active {
@@ -4368,31 +4381,101 @@ header .brand-sub {
box-shadow: 0 0 12px var(--copper-glow);
}
/* ─── Update + connection banners ───────────────────────────── */
/* ─── Update + connection banners (editorial) ────────────────── */
.update-banner,
.connection-banner {
background: var(--bg-card);
border: 1px solid var(--copper);
border-radius: 0;
color: var(--ink-soft);
box-shadow: 0 0 24px var(--copper-glow);
font-family: var(--sans);
font-size: 13px;
/* Override legacy fixed-top + accent background */
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
z-index: 1001;
background: rgba(33, 30, 24, 0.94) !important;
color: var(--ink-soft) !important;
border: 0 !important;
border-bottom: 1px solid var(--copper) !important;
border-radius: 0 !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4) !important;
padding: 12px 32px !important;
font-family: var(--mono) !important;
font-size: 11px !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
font-weight: 400 !important;
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
}
.update-banner a,
.connection-banner-btn {
/* Tiny copper hairline accent on the bottom edge */
.update-banner::before,
.connection-banner::before {
content: "";
position: absolute;
bottom: -1px;
left: 32px;
right: 32px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--copper), transparent);
opacity: 0.7;
}
/* Brand prefix */
.update-banner > span:first-child::before,
.connection-banner > span:first-child::before {
content: "● ";
color: var(--copper);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
border-color: var(--copper);
border-radius: 0;
background: transparent;
margin-right: 6px;
}
.update-banner a {
color: var(--copper) !important;
text-decoration: none !important;
font-family: var(--mono) !important;
font-size: 11px !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
font-weight: 500 !important;
border: 1px solid var(--copper);
padding: 6px 14px;
transition: all 180ms var(--ease);
}
.update-banner a:hover {
background: var(--copper);
color: var(--bg-deep) !important;
opacity: 1 !important;
}
.update-banner-close {
background: transparent !important;
color: var(--ink-mute) !important;
border: 0 !important;
font-size: 18px !important;
width: 24px;
height: 24px;
padding: 0 !important;
margin-left: 8px;
cursor: pointer;
transition: color 180ms var(--ease);
opacity: 1 !important;
}
.update-banner-close:hover {
color: var(--copper) !important;
}
.connection-banner-btn {
color: var(--copper) !important;
font-family: var(--mono) !important;
font-size: 11px !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
border: 1px solid var(--copper) !important;
border-radius: 0 !important;
background: transparent !important;
padding: 6px 14px !important;
transition: all 180ms var(--ease);
}
.connection-banner-btn:hover {
background: var(--copper);
color: var(--bg-deep);
background: var(--copper) !important;
color: var(--bg-deep) !important;
}
/* ═══════════════════════════════════════════════════════════════
@@ -6753,61 +6836,39 @@ body.visualizer-active .now-playing .spectrogram-canvas {
fill: currentColor;
}
/* ─── Spectrum bars ───────────────────────────────────────── */
/* ─── Spectrum bars (JS-injected; real audio when available) ─── */
.now-playing .spectrum {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 3px;
height: 38px;
margin: 36px auto 24px;
max-width: 480px;
justify-content: stretch;
gap: 2px;
height: 56px;
margin: 36px 0 24px;
width: 100%;
}
.now-playing .spectrum span {
display: block;
width: 3px;
flex: 0 0 3px;
flex: 1 1 0;
min-width: 2px;
background: linear-gradient(to top, var(--copper-lo), var(--copper-hi));
opacity: 0.85;
opacity: 0.9;
transform-origin: bottom;
border-radius: 99px 99px 0 0;
height: var(--bar-h, 40%);
animation: sr-snap-bar 1.1s ease-in-out infinite;
animation-delay: var(--bar-delay, 0s);
animation-play-state: paused;
transition: height 80ms linear;
}
:root[data-playstate="playing"] .now-playing .spectrum span {
animation-play-state: running;
}
.now-playing .spectrum span:nth-child(1) { animation-delay: -0.10s; height: 30%; }
.now-playing .spectrum span:nth-child(2) { animation-delay: -0.45s; height: 60%; }
.now-playing .spectrum span:nth-child(3) { animation-delay: -0.20s; height: 80%; }
.now-playing .spectrum span:nth-child(4) { animation-delay: -0.55s; height: 50%; }
.now-playing .spectrum span:nth-child(5) { animation-delay: -0.30s; height: 95%; }
.now-playing .spectrum span:nth-child(6) { animation-delay: -0.05s; height: 70%; }
.now-playing .spectrum span:nth-child(7) { animation-delay: -0.65s; height: 40%; }
.now-playing .spectrum span:nth-child(8) { animation-delay: -0.25s; height: 85%; }
.now-playing .spectrum span:nth-child(9) { animation-delay: -0.40s; height: 55%; }
.now-playing .spectrum span:nth-child(10) { animation-delay: -0.10s; height: 75%; }
.now-playing .spectrum span:nth-child(11) { animation-delay: -0.50s; height: 35%; }
.now-playing .spectrum span:nth-child(12) { animation-delay: -0.15s; height: 90%; }
.now-playing .spectrum span:nth-child(13) { animation-delay: -0.60s; height: 45%; }
.now-playing .spectrum span:nth-child(14) { animation-delay: -0.30s; height: 65%; }
.now-playing .spectrum span:nth-child(15) { animation-delay: -0.45s; height: 85%; }
.now-playing .spectrum span:nth-child(16) { animation-delay: -0.20s; height: 55%; }
.now-playing .spectrum span:nth-child(17) { animation-delay: -0.55s; height: 70%; }
.now-playing .spectrum span:nth-child(18) { animation-delay: -0.10s; height: 30%; }
.now-playing .spectrum span:nth-child(19) { animation-delay: -0.40s; height: 80%; }
.now-playing .spectrum span:nth-child(20) { animation-delay: -0.25s; height: 60%; }
.now-playing .spectrum span:nth-child(21) { animation-delay: -0.50s; height: 90%; }
.now-playing .spectrum span:nth-child(22) { animation-delay: -0.15s; height: 50%; }
.now-playing .spectrum span:nth-child(23) { animation-delay: -0.60s; height: 70%; }
.now-playing .spectrum span:nth-child(24) { animation-delay: -0.30s; height: 40%; }
.now-playing .spectrum span:nth-child(25) { animation-delay: -0.45s; height: 85%; }
.now-playing .spectrum span:nth-child(26) { animation-delay: -0.20s; height: 55%; }
.now-playing .spectrum span:nth-child(27) { animation-delay: -0.55s; height: 75%; }
.now-playing .spectrum span:nth-child(28) { animation-delay: -0.10s; height: 35%; }
.now-playing .spectrum span:nth-child(29) { animation-delay: -0.40s; height: 65%; }
.now-playing .spectrum span:nth-child(30) { animation-delay: -0.25s; height: 95%; }
/* When real audio data is driving heights, freeze the CSS animation
so JS-set heights aren't overridden by the keyframe. */
body.audio-spectrum-live .now-playing .spectrum span {
animation: none !important;
transition: height 50ms linear;
}
@keyframes sr-snap-bar {
0%, 100% { transform: scaleY(0.4); }
50% { transform: scaleY(1); }