diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor
index 191045e..15d4b35 100644
--- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor
+++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor
@@ -9,20 +9,25 @@
+
+@* Viewing control only — never a seek surface. Hidden until a datum is present.
+ Deliberately a SIBLING of .mix-waveform-bg, not a child: the backdrop is position:fixed and so
+ forms its own stacking context, which would trap any descendant below the page's z-index:1
+ foreground (.mix-detail-foreground) and let that foreground swallow the slider's pointer events.
+ As a top-level sibling with its own z-index, the slider stacks above the foreground and stays
+ draggable. *@
+@if (_hasDatum)
+{
+
+
+
+}
diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs
index e7507fb..05cd367 100644
--- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs
+++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs
@@ -180,6 +180,7 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
private async Task OnZoomFractionChanged(double fraction)
{
ZoomState.VisibleSeconds = MixZoomMapping.FractionToSeconds(fraction);
+ DebugLog($"zoom slider changed — raw fraction={fraction:F3} → visibleSeconds={ZoomState.VisibleSeconds:F3}s; pushing setZoom (handle={(_handle is null ? "null" : "ready")}).");
await PushZoomAsync();
StateHasChanged();
}
diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css
index 4113ef7..97529cc 100644
--- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css
+++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css
@@ -21,13 +21,18 @@
}
/* Zoom slider — a small viewing control pinned to the top-right, clear of the player bar at
- the bottom and the nav bar at the top. Pointer events are re-enabled here only (the backdrop
- stays inert), and it is never a seek surface. top: 5rem sits just below the fixed nav bar
- (~4.5rem tall) so neither the expanded player bar nor the nav occludes it. */
+ the bottom and the nav bar at the top. It is never a seek surface. top: 5rem sits just below the
+ fixed nav bar (~4.5rem tall) so neither the expanded player bar nor the nav occludes it.
+
+ position: fixed (not absolute) because the slider is now a top-level sibling of the backdrop, not
+ a child of it — see the comment in the .razor. z-index: 10 lifts it above the page foreground
+ (.mix-detail-foreground, z-index: 1) so the foreground can't intercept its pointer events; that
+ occlusion was the resolution-slider regression. */
.mix-waveform-zoom {
- position: absolute;
+ position: fixed;
right: 1.5rem;
top: 5rem;
+ z-index: 10;
width: 180px;
max-width: 40vw;
pointer-events: auto;
diff --git a/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts b/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts
index ef66701..7e8fc73 100644
--- a/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts
+++ b/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts
@@ -795,7 +795,12 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle {
setZoom(seconds: number): void {
// Clamp into the supported span so a stray value can't break the math.
visibleSeconds = Math.min(MAX_VISIBLE_SECONDS, Math.max(MIN_VISIBLE_SECONDS, seconds));
- if (!playback.isPlaying) redrawOnce();
+ // While playing, the running rAF loop uploads uVisibleSeconds next frame; while idle the
+ // loop is stopped (spec §E), so a zoom change must force one still frame here or the new
+ // span is uploaded only on the next unrelated redraw (theme/datum/resize) — i.e. never.
+ const idleRedraw = !playback.isPlaying;
+ debugLog(`setZoom — requested ${seconds.toFixed(3)}s, clamped ${visibleSeconds.toFixed(3)}s; idleRedraw=${idleRedraw} (isPlaying=${playback.isPlaying}).`);
+ if (idleRedraw) redrawOnce();
},
refreshTheme(): void {