ChatGPT commited on
Commit
3e9170f
·
1 Parent(s): f08496f

feat: align default ui with reference layout

Browse files
Files changed (9) hide show
  1. README.md +3 -3
  2. docs/FEATURES.md +9 -8
  3. docs/PROGRESS.md +30 -0
  4. docs/REMAINING_WORK.md +7 -8
  5. docs/TASKS.md +3 -4
  6. docs/UI_REPLACEMENT.md +29 -22
  7. web/app.js +71 -1
  8. web/index.html +142 -132
  9. web/styles.css +349 -283
README.md CHANGED
@@ -39,7 +39,7 @@ Implemented:
39
  - event log,
40
  - suggestions,
41
  - undo stack.
42
- - Minimal waveform-first UI inspired by dedicated sample-extractor hardware/software: compact top bar, large waveform workspace, right-side core controls, sample-card grid, and collapsible advanced panels.
43
  - Supervision UI:
44
  - selected-hit actions,
45
  - move hit to cluster,
@@ -84,7 +84,7 @@ uvicorn app:app --host 0.0.0.0 --port 7860
84
 
85
  Open `http://127.0.0.1:7860`.
86
 
87
- For fast iteration, set:
88
 
89
  - `Stem = all`
90
  - `Clustering mode = online_preview`
@@ -154,7 +154,7 @@ curl http://127.0.0.1:7860/api/jobs
154
  | `sample_extractor.py` | Core DSP/sample extraction implementation |
155
  | `supervised_state.py` | Persistent semantic state, confidence, constraints, events, suggestions, force-onset, restore, undo |
156
  | `supervised_export.py` | Renders edited semantic state into supervised WAV/MIDI/reconstruction/ZIP artifacts |
157
- | `web/` | Custom no-build browser frontend with the light waveform-first UI, sample-card grid, hit review, add-onset mode, edited export, and supervision panel |
158
  | `scripts/benchmark_subprocesses.py` | Synthetic benchmark runner for stage timings |
159
  | `scripts/test_interactive_supervision.py` | Smoke test for supervised state endpoints |
160
  | `scripts/test_supervised_export_and_force_onset.py` | Smoke test for force-onset, restore, suggestion diffs, and edited exports |
 
39
  - event log,
40
  - suggestions,
41
  - undo stack.
42
+ - Reference-aligned minimal waveform-first UI: compact file/action bar, quiet large waveform card, one custom transport row, right-side core controls, sample-card grid, and one collapsed review/edit workbench for power features.
43
  - Supervision UI:
44
  - selected-hit actions,
45
  - move hit to cluster,
 
84
 
85
  Open `http://127.0.0.1:7860`.
86
 
87
+ For fast iteration, open `Advanced`, then use `Fast full-mix mode` or set:
88
 
89
  - `Stem = all`
90
  - `Clustering mode = online_preview`
 
154
  | `sample_extractor.py` | Core DSP/sample extraction implementation |
155
  | `supervised_state.py` | Persistent semantic state, confidence, constraints, events, suggestions, force-onset, restore, undo |
156
  | `supervised_export.py` | Renders edited semantic state into supervised WAV/MIDI/reconstruction/ZIP artifacts |
157
+ | `web/` | Custom no-build browser frontend with the reference-aligned light waveform-first UI, sample-card grid, hidden-audio audition, add-onset mode, edited export, and collapsed supervision workbench |
158
  | `scripts/benchmark_subprocesses.py` | Synthetic benchmark runner for stage timings |
159
  | `scripts/test_interactive_supervision.py` | Smoke test for supervised state endpoints |
160
  | `scripts/test_supervised_export_and_force_onset.py` | Smoke test for force-onset, restore, suggestion diffs, and edited exports |
docs/FEATURES.md CHANGED
@@ -12,13 +12,13 @@ Turn an input audio file into a practical drum sample pack: detected hits, group
12
  |---|---|---:|---|
13
  | UI | Custom browser frontend | Implemented | `web/index.html`, `web/styles.css`, `web/app.js`; no Gradio dependency in active app. |
14
  | UI | Drag/drop audio upload | Implemented | Uses multipart upload to `POST /api/jobs`. |
15
- | UI | Source preview | Implemented | Browser `<audio>` preview before extraction. |
16
  | UI | Pipeline controls | Implemented | Stem/model/onset/clustering/MIDI/synthesis/cache controls. |
17
  | UI | Streaming progress | Implemented | Uses `EventSource` over `GET /api/jobs/{id}/events`, with polling fallback. |
18
  | UI | Waveform/onset overview | Implemented | Canvas envelope plus clickable onset markers from `manifest.json`. |
19
  | UI | Result downloads | Implemented | ZIP, MIDI, stem WAV, reconstruction WAV, individual sample WAVs, and per-hit review WAVs. |
20
  | UI | Run history browser | Implemented | Lists completed `.runs/*/output/manifest.json` entries and reloads results. |
21
- | UI | Hit and sample audition | Implemented | Dedicated players for selected hit slices and representative sample WAVs. |
22
  | API | Health/config | Implemented | `GET /api/health`, `GET /api/config`. |
23
  | API | Job creation/status | Implemented | `POST /api/jobs`, `GET /api/jobs/{id}`. |
24
  | API | SSE job events | Implemented | `GET /api/jobs/{id}/events` streams job snapshots until complete/error. |
@@ -92,9 +92,10 @@ Turn an input audio file into a practical drum sample pack: detected hits, group
92
 
93
  Status: implemented.
94
 
95
- - Light, minimal, waveform-first interface matching the supplied reference direction.
96
- - Compact top bar with file selector, backend status, and primary extract action.
97
- - Main waveform card with colored onset markers and click-to-select / force-onset behavior.
98
- - Right-side core-control card for stem, sensitivity, cluster count, edited export, and fast modes.
99
- - Collapsible advanced controls keep detailed DSP/model parameters available without cluttering the main flow.
100
- - Extracted samples render as auditionable cards with waveform thumbnails; raw tables remain available in a collapsible detail panel.
 
 
12
  |---|---|---:|---|
13
  | UI | Custom browser frontend | Implemented | `web/index.html`, `web/styles.css`, `web/app.js`; no Gradio dependency in active app. |
14
  | UI | Drag/drop audio upload | Implemented | Uses multipart upload to `POST /api/jobs`. |
15
+ | UI | Minimal custom transport | Implemented | One image-aligned play/time/progress row drives source preview before extraction and stem preview after extraction. |
16
  | UI | Pipeline controls | Implemented | Stem/model/onset/clustering/MIDI/synthesis/cache controls. |
17
  | UI | Streaming progress | Implemented | Uses `EventSource` over `GET /api/jobs/{id}/events`, with polling fallback. |
18
  | UI | Waveform/onset overview | Implemented | Canvas envelope plus clickable onset markers from `manifest.json`. |
19
  | UI | Result downloads | Implemented | ZIP, MIDI, stem WAV, reconstruction WAV, individual sample WAVs, and per-hit review WAVs. |
20
  | UI | Run history browser | Implemented | Lists completed `.runs/*/output/manifest.json` entries and reloads results. |
21
+ | UI | Hit and sample audition | Implemented | Click-to-audition via hidden audio elements keeps the default screen clean. |
22
  | API | Health/config | Implemented | `GET /api/health`, `GET /api/config`. |
23
  | API | Job creation/status | Implemented | `POST /api/jobs`, `GET /api/jobs/{id}`. |
24
  | API | SSE job events | Implemented | `GET /api/jobs/{id}/events` streams job snapshots until complete/error. |
 
92
 
93
  Status: implemented.
94
 
95
+ - Light, minimal, waveform-first interface closely aligned with the supplied reference composition.
96
+ - Compact top file selector plus one primary purple extract action.
97
+ - Quiet main waveform card with colored onset markers and click-to-select / force-onset behavior.
98
+ - One custom preview transport row instead of multiple native audio players.
99
+ - Right-side core-control card now exposes only stem, sensitivity, cluster count, and export samples in the default view.
100
+ - Fast modes, DSP/model parameters, pipeline logs, history, supervision, and raw tables are hidden behind `Advanced` / `Review & edit`.
101
+ - Extracted samples render as auditionable cards with waveform thumbnails and minimal labels.
docs/PROGRESS.md CHANGED
@@ -210,3 +210,33 @@ Validation performed in this pass:
210
  Outcome:
211
 
212
  The app now looks and behaves more like a focused sample-extraction tool instead of a generic control dashboard while keeping the advanced review/supervision functionality available below the main workflow.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  Outcome:
211
 
212
  The app now looks and behaves more like a focused sample-extraction tool instead of a generic control dashboard while keeping the advanced review/supervision functionality available below the main workflow.
213
+
214
+
215
+ ## Pass 6: minimal waveform-first UI
216
+
217
+ Completed in this pass:
218
+
219
+ 1. Reworked the frontend visual language from a dense workstation dashboard into a light waveform-first interface.
220
+ 2. Added the top file identity bar and primary purple extraction action.
221
+ 3. Promoted the waveform and extracted sample cards as the default workflow.
222
+ 4. Moved advanced controls and power-user tools into collapsible areas.
223
+
224
+ Outcome:
225
+
226
+ The app matched the general direction of the supplied reference image, but still had visible complexity: separate audio players, a waveform header, fast-mode buttons in the right card, and multiple utility panels visible in the main page flow.
227
+
228
+ ## Pass 7: reference-alignment hardening
229
+
230
+ Completed in this pass:
231
+
232
+ 1. Removed the visible waveform header and made the waveform card visually quiet.
233
+ 2. Added a single custom transport row with play, elapsed/total time, and seek line.
234
+ 3. Hid native source/stem/reconstruction/hit/sample audio elements from the default layout while preserving playback behavior.
235
+ 4. Moved `Online preview mode` and `Fast full-mix mode` into the collapsed `Advanced` panel.
236
+ 5. Collapsed pipeline, run history, supervision, and detailed tables into one `Review & edit` workbench below the sample cards.
237
+ 6. Simplified sample cards to thumbnail waveform, play affordance, and label-first presentation.
238
+ 7. Updated docs to distinguish visual-reference work from remaining interaction-depth work.
239
+
240
+ Outcome:
241
+
242
+ The default UI is now aligned with the supplied image: file/action bar, large waveform card, compact right controls, one transport row, and sample cards. Advanced extraction and semantic editing remain available but no longer dominate the first screen.
docs/REMAINING_WORK.md CHANGED
@@ -8,13 +8,12 @@ The project is now a usable extraction workstation, not a complete interactive s
8
 
9
  ## Highest-priority remaining gaps
10
 
11
- 1. **Waveform editing**: add onset adjustment, delete/add hit, and rerun-from-edited-onsets without redoing Demucs.
12
- 2. **Cluster editing**: allow merge, split, relabel, and manual reassignment of hits.
13
- 3. **Edited re-export**: regenerate samples/MIDI/ZIP from edited hit/cluster state without rerunning Demucs or onset detection.
14
- 4. **Run comparison**: compare two manifests side-by-side for parameter tuning.
15
- 5. **Lower-level progress**: expose internal Demucs/clustering progress where libraries make that possible.
16
- 6. **Frontend engineering hardening**: migrate the frontend to TypeScript after the UX stabilizes and add browser-level tests.
17
- 7. **Benchmark panel**: add an in-app benchmark view that can run synthetic fixtures and compare parameter profiles.
18
 
19
  ## Known constraints
20
 
@@ -75,4 +74,4 @@ Highest-priority remaining work now:
75
 
76
  ## UI status note
77
 
78
- The minimal waveform-first restyle is complete. Remaining UI work is now interaction depth and engineering hardening, not matching the supplied visual direction: browser tests, TypeScript/Vite migration, edited-vs-original comparison, and richer cluster merge/split/relabel workflows.
 
8
 
9
  ## Highest-priority remaining gaps
10
 
11
+ 1. **Cluster editing**: allow merge, split, relabel, and manual reassignment of groups from the `Review & edit` workbench.
12
+ 2. **Waveform editing depth**: add onset drag/shift, hit trim boundaries, and rerun-from-edited-onsets without redoing Demucs.
13
+ 3. **Run comparison**: compare two manifests side-by-side for parameter tuning.
14
+ 4. **Lower-level progress**: expose internal Demucs/clustering progress where libraries make that possible.
15
+ 5. **Frontend engineering hardening**: migrate the frontend to TypeScript after the UX stabilizes and add browser-level tests.
16
+ 6. **Benchmark panel**: add an in-app benchmark view that can run synthetic fixtures and compare parameter profiles.
 
17
 
18
  ## Known constraints
19
 
 
74
 
75
  ## UI status note
76
 
77
+ The reference-aligned default UI is complete for the current no-build frontend. The remaining UI work is interaction depth and engineering hardening, not matching the supplied visual direction: browser screenshot regression tests, TypeScript/Vite migration, edited-vs-original comparison, waveform zoom/pan, and richer cluster merge/split/relabel workflows.
docs/TASKS.md CHANGED
@@ -87,11 +87,10 @@ Last updated: 2026-05-12
87
  | Task | Status | Notes |
88
  |---|---|---|
89
  | Restyle web UI to supplied minimal waveform-first reference | Done | Light theme, top-bar primary action, large waveform, compact right controls, sample-card grid, collapsible advanced/power panels. |
 
90
 
91
  ## Latest validation tasks
92
 
93
- - [x] `python3 -m py_compile app.py pipeline_runner.py sample_extractor.py supervised_state.py scripts/*.py`
94
  - [x] `node --check web/app.js`
95
- - [x] `python3 scripts/test_sse_and_review_hits.py`
96
- - [x] `python3 scripts/test_interactive_supervision.py`
97
- - [x] `python3 scripts/test_supervised_export_and_force_onset.py`
 
87
  | Task | Status | Notes |
88
  |---|---|---|
89
  | Restyle web UI to supplied minimal waveform-first reference | Done | Light theme, top-bar primary action, large waveform, compact right controls, sample-card grid, collapsible advanced/power panels. |
90
+ | Close remaining reference-alignment gaps | Done | Removed waveform header, added one custom transport row, moved fast modes into Advanced, collapsed power tools into Review & edit, hid visible audition players. |
91
 
92
  ## Latest validation tasks
93
 
94
+ - [x] `python3 -m py_compile app.py pipeline_runner.py sample_extractor.py supervised_state.py supervised_export.py scripts/*.py`
95
  - [x] `node --check web/app.js`
96
+ - [x] `python3 scripts/test_api_job.py`
 
 
docs/UI_REPLACEMENT.md CHANGED
@@ -8,41 +8,48 @@ The active interface is a custom browser UI served from `web/` by the FastAPI ap
8
 
9
  ## UX goals
10
 
11
- 1. Make the process feel like a sample-extraction workstation, not a generic notebook form.
12
- 2. Keep upload, controls, pipeline status, logs, waveform review, audio previews, downloads, run history, and sample rows visible without tab hunting.
13
- 3. Show stage timing as a first-class result, because extraction quality and speed tradeoffs matter.
14
- 4. Make `stem=all` obvious for fast iteration when Demucs is unnecessary.
15
- 5. Make `online_preview` obvious as the near-realtime clustering path.
16
  6. Keep the frontend deployable without a JavaScript build step until the interaction model stabilizes.
17
 
18
 
19
  ## Pass 6 visual simplification
20
 
21
- The UI has been restyled to match the supplied minimal reference direction:
22
 
23
  - light native-feeling canvas instead of the previous dark dashboard;
24
  - compact top bar with file selector/status and one primary purple `Extract Samples` action;
25
  - large waveform-first workspace with colored lollipop onset markers;
26
- - right-side control card for the primary extraction decisions: stem, sensitivity, cluster count, edited export, fast modes;
27
  - advanced DSP/model parameters moved into a collapsible panel;
28
- - representative samples rendered as auditionable cards with waveform thumbnails instead of forcing the table to be the primary view;
29
- - pipeline, run history, supervision, and raw tables kept available as collapsible utility panels so power features remain reachable without dominating the first screen.
30
 
31
- This pass intentionally keeps the no-build frontend architecture. The goal was to simplify the product surface without destabilizing the backend/API or the supervised-editing flow.
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  ## UI structure
34
 
35
  | Area | Purpose |
36
  |---|---|
37
- | Hero/status | Backend readiness and product framing. |
38
- | Source panel | Drag/drop upload and source audio preview. |
39
- | Controls panel | Stem, onset, clustering, MIDI, synthesis, and disk-cache parameters. |
40
- | Pipeline panel | Stage statuses, durations, and logs. |
41
- | Run history panel | Loads completed manifests from `.runs/`. |
42
- | Waveform workspace | Summary, waveform/onsets, stem/reconstruction audio, and downloads. |
43
- | Core control card | Stem, sensitivity, cluster count, fast modes, edited export, and collapsible advanced settings. |
44
  | Sample card grid | Primary representative-sample browsing and audition surface. |
45
- | Utility panels | Pipeline, run history, supervision, and raw detail tables. |
46
 
47
  ## Frontend implementation
48
 
@@ -89,8 +96,8 @@ The active UI uses Server-Sent Events through `GET /api/jobs/{job_id}/events` fo
89
 
90
  ## Remaining UI improvements
91
 
92
- - Add waveform zoom and click-to-audition individual detected hits.
93
- - Add inline controls for reassigning sample labels and merging/splitting clusters.
94
  - Add A/B comparison between parameter runs.
95
  - Add downloadable timing report per job.
96
  - Add filters/search to the run history browser.
@@ -100,8 +107,8 @@ The active UI uses Server-Sent Events through `GET /api/jobs/{job_id}/events` fo
100
 
101
  The current UI now includes:
102
 
103
- - Dedicated selected-hit and selected-sample audio players.
104
- - Clickable waveform onset markers that select the nearest detected hit.
105
  - A detected-hit review table backed by `review/hits/*.wav` artifacts.
106
  - Audition buttons for representative sample rows.
107
  - Server-sent-events job progress via `GET /api/jobs/{job_id}/events`, with polling fallback.
 
8
 
9
  ## UX goals
10
 
11
+ 1. Match the supplied minimal reference image as the default view: file identity, one purple extract action, large waveform, compact right controls, and sample cards.
12
+ 2. Keep the first screen focused on extraction and audition, not pipeline/debug/editor internals.
13
+ 3. Preserve advanced workstation features behind a single secondary `Review & edit` workbench.
14
+ 4. Keep stage timing, logs, run history, and semantic supervision available without dominating the default layout.
15
+ 5. Make `stem=all` and `online_preview` available as advanced presets instead of primary controls.
16
  6. Keep the frontend deployable without a JavaScript build step until the interaction model stabilizes.
17
 
18
 
19
  ## Pass 6 visual simplification
20
 
21
+ The UI was first restyled to the supplied minimal reference direction:
22
 
23
  - light native-feeling canvas instead of the previous dark dashboard;
24
  - compact top bar with file selector/status and one primary purple `Extract Samples` action;
25
  - large waveform-first workspace with colored lollipop onset markers;
26
+ - right-side control card for the primary extraction decisions;
27
  - advanced DSP/model parameters moved into a collapsible panel;
28
+ - representative samples rendered as auditionable cards with waveform thumbnails;
29
+ - pipeline, run history, supervision, and raw tables kept available as collapsible utility panels.
30
 
31
+ ## Pass 7 reference-alignment hardening
32
+
33
+ This pass closed the visual fidelity gaps from the previous approximation:
34
+
35
+ - removed the visible waveform header so the canvas is quiet like the reference image;
36
+ - replaced separate native stem/reconstruction audio controls with one minimal transport row: play button, time, and progress line;
37
+ - moved fast-mode buttons into `Advanced` so the right card now exposes only `Stem`, `Sensitivity`, `Cluster Count`, and `Export Samples`;
38
+ - collapsed pipeline/history/supervision/tables into one `Review & edit` workbench below the sample cards;
39
+ - hid selected-hit/sample audio elements from the default layout while preserving click-to-audition behavior;
40
+ - tightened card spacing, border radii, font scale, waveform height, and sample-card proportions to better match the supplied image.
41
+
42
+ The UI is still not a pixel-for-pixel clone because it must remain functional across arbitrary audio files and preserve the project’s editing tools, but the default screen is now intentionally aligned with the reference composition.
43
 
44
  ## UI structure
45
 
46
  | Area | Purpose |
47
  |---|---|
48
+ | Top file bar | File identity/drop target and one primary purple `Extract Samples` action. |
49
+ | Waveform workspace | Quiet canvas with grey waveform, colored lollipop markers, one custom transport row, and compact downloads. |
50
+ | Core control card | Stem, sensitivity, cluster count, export samples, and collapsed advanced settings. |
 
 
 
 
51
  | Sample card grid | Primary representative-sample browsing and audition surface. |
52
+ | Review & edit workbench | Collapsed secondary area containing pipeline logs, run history, supervision tools, and raw tables. |
53
 
54
  ## Frontend implementation
55
 
 
96
 
97
  ## Remaining UI improvements
98
 
99
+ - Add waveform zoom/pan while keeping the default view uncluttered.
100
+ - Add inline cluster merge/split/relabel workflows inside `Review & edit`.
101
  - Add A/B comparison between parameter runs.
102
  - Add downloadable timing report per job.
103
  - Add filters/search to the run history browser.
 
107
 
108
  The current UI now includes:
109
 
110
+ - Hidden selected-hit and selected-sample audio elements for click-to-audition without visible player clutter.
111
+ - Clickable waveform onset markers that select and audition the nearest detected hit.
112
  - A detected-hit review table backed by `review/hits/*.wav` artifacts.
113
  - Audition buttons for representative sample rows.
114
  - Server-sent-events job progress via `GET /api/jobs/{job_id}/events`, with polling fallback.
web/app.js CHANGED
@@ -48,6 +48,62 @@ function fmtDate(epochSeconds) {
48
  return new Date(epochSeconds * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  function setHealth(ok, text, subtext) {
52
  $("healthDot").className = `status-dot ${ok ? "ok" : "bad"}`;
53
  $("healthText").textContent = text;
@@ -268,6 +324,8 @@ function drawWaveform(overview) {
268
 
269
  function playAudio(el, url) {
270
  if (!url) return;
 
 
271
  el.src = url;
272
  el.currentTime = 0;
273
  const promise = el.play();
@@ -675,6 +733,7 @@ function renderResult(job) {
675
  $("downloads").innerHTML = Object.entries(fileUrls).map(([key, url]) => `<a href="${esc(url)}" download>${esc(labels[key] ?? key)}</a>`).join("");
676
  $("stemAudio").src = fileUrls.stem ?? "";
677
  $("reconAudio").src = fileUrls.reconstruction ?? "";
 
678
 
679
  renderSamples(result);
680
  renderHits(result);
@@ -810,8 +869,9 @@ function setFile(file) {
810
  $("resultSummary").textContent = `${file.name} is ready. Extract samples to see waveform markers and sample cards.`;
811
  }
812
  if (file) {
813
- $("sourcePreview").hidden = false;
814
  $("sourcePreview").src = URL.createObjectURL(file);
 
815
  }
816
  }
817
 
@@ -901,6 +961,16 @@ $("lockClusterButton").addEventListener("click", () => toggleTargetClusterLock()
901
  $("explainClusterButton").addEventListener("click", () => explainTargetCluster().catch((error) => { $("clusterExplanation").textContent = error.message; }));
902
  $("targetClusterSelect").addEventListener("change", setActionButtons);
903
  $("waveform").addEventListener("click", selectNearestWaveformHit);
 
 
 
 
 
 
 
 
 
 
904
 
905
  const dropzone = $("dropzone");
906
  for (const eventName of ["dragenter", "dragover"]) {
 
48
  return new Date(epochSeconds * 1000).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
49
  }
50
 
51
+
52
+ function fmtClock(seconds) {
53
+ const value = Number(seconds);
54
+ if (!Number.isFinite(value) || value <= 0) return "0:00";
55
+ const mins = Math.floor(value / 60);
56
+ const secs = Math.floor(value % 60).toString().padStart(2, "0");
57
+ return `${mins}:${secs}`;
58
+ }
59
+
60
+ function transportAudio() {
61
+ const stem = $("stemAudio");
62
+ const source = $("sourcePreview");
63
+ return stem?.src ? stem : source;
64
+ }
65
+
66
+ function updateTransport() {
67
+ const audio = transportAudio();
68
+ const current = audio ? Number(audio.currentTime || 0) : 0;
69
+ const duration = audio && Number.isFinite(Number(audio.duration)) ? Number(audio.duration) : 0;
70
+ const time = $("transportTime");
71
+ if (time) time.textContent = `${fmtClock(current)} / ${fmtClock(duration)}`;
72
+ const seek = $("transportSeek");
73
+ if (seek && !seek.matches(":active")) {
74
+ seek.value = duration > 0 ? String(Math.max(0, Math.min(1000, Math.round((current / duration) * 1000)))) : "0";
75
+ }
76
+ const button = $("transportPlayButton");
77
+ if (button) button.textContent = audio && !audio.paused ? "❚❚" : "▶";
78
+ }
79
+
80
+ function pauseNonTransportAudio() {
81
+ for (const id of ["hitAudio", "sampleAudio", "reconAudio"]) {
82
+ const el = $(id);
83
+ if (el && !el.paused) el.pause();
84
+ }
85
+ }
86
+
87
+ async function toggleTransportPlayback() {
88
+ const audio = transportAudio();
89
+ if (!audio?.src) return;
90
+ if (audio.paused) {
91
+ pauseNonTransportAudio();
92
+ const promise = audio.play();
93
+ if (promise && typeof promise.catch === "function") await promise.catch(() => {});
94
+ } else {
95
+ audio.pause();
96
+ }
97
+ updateTransport();
98
+ }
99
+
100
+ function seekTransport(value) {
101
+ const audio = transportAudio();
102
+ if (!audio?.src || !Number.isFinite(Number(audio.duration)) || Number(audio.duration) <= 0) return;
103
+ audio.currentTime = (Number(value) / 1000) * Number(audio.duration);
104
+ updateTransport();
105
+ }
106
+
107
  function setHealth(ok, text, subtext) {
108
  $("healthDot").className = `status-dot ${ok ? "ok" : "bad"}`;
109
  $("healthText").textContent = text;
 
324
 
325
  function playAudio(el, url) {
326
  if (!url) return;
327
+ const currentTransport = transportAudio();
328
+ if (currentTransport && !currentTransport.paused) currentTransport.pause();
329
  el.src = url;
330
  el.currentTime = 0;
331
  const promise = el.play();
 
733
  $("downloads").innerHTML = Object.entries(fileUrls).map(([key, url]) => `<a href="${esc(url)}" download>${esc(labels[key] ?? key)}</a>`).join("");
734
  $("stemAudio").src = fileUrls.stem ?? "";
735
  $("reconAudio").src = fileUrls.reconstruction ?? "";
736
+ updateTransport();
737
 
738
  renderSamples(result);
739
  renderHits(result);
 
869
  $("resultSummary").textContent = `${file.name} is ready. Extract samples to see waveform markers and sample cards.`;
870
  }
871
  if (file) {
872
+ $("stemAudio").removeAttribute("src");
873
  $("sourcePreview").src = URL.createObjectURL(file);
874
+ updateTransport();
875
  }
876
  }
877
 
 
961
  $("explainClusterButton").addEventListener("click", () => explainTargetCluster().catch((error) => { $("clusterExplanation").textContent = error.message; }));
962
  $("targetClusterSelect").addEventListener("change", setActionButtons);
963
  $("waveform").addEventListener("click", selectNearestWaveformHit);
964
+ $("transportPlayButton").addEventListener("click", () => { toggleTransportPlayback().catch(() => {}); });
965
+ $("transportSeek").addEventListener("input", (event) => seekTransport(event.target.value));
966
+ for (const id of ["sourcePreview", "stemAudio"]) {
967
+ const audio = $(id);
968
+ audio.addEventListener("timeupdate", updateTransport);
969
+ audio.addEventListener("durationchange", updateTransport);
970
+ audio.addEventListener("play", updateTransport);
971
+ audio.addEventListener("pause", updateTransport);
972
+ audio.addEventListener("ended", updateTransport);
973
+ }
974
 
975
  const dropzone = $("dropzone");
976
  for (const eventName of ["dragenter", "dragover"]) {
web/index.html CHANGED
@@ -27,24 +27,28 @@
27
  </header>
28
 
29
  <main class="app-layout">
30
- <section class="wave-card panel">
31
- <div class="wave-header">
32
- <div>
33
- <p class="eyebrow">Sample extraction workstation</p>
34
- <h1 id="resultSummary">Load audio, tune sensitivity, extract clean drum hits.</h1>
35
- </div>
36
- <span class="job-pill" id="jobPill">idle</span>
 
 
 
37
  </div>
38
- <canvas id="waveform" class="waveform" height="360"></canvas>
39
- <div class="transport-row">
40
- <audio id="sourcePreview" controls hidden></audio>
41
- <label class="transport-audio">Stem audio<audio id="stemAudio" controls></audio></label>
42
- <label class="transport-audio">Reconstruction<audio id="reconAudio" controls></audio></label>
 
 
43
  </div>
44
- <div class="downloads" id="downloads"></div>
45
  </section>
46
 
47
- <aside class="control-card panel">
48
  <div class="control-group">
49
  <label>Stem
50
  <select id="stem"></select>
@@ -60,20 +64,22 @@
60
  <div class="control-group">
61
  <label>Cluster Count</label>
62
  <div class="stepper">
63
- <button id="clusterMinusButton" type="button" class="step-button">−</button>
64
- <input id="target_max" type="number" min="0" max="256" step="1" />
65
- <button id="clusterPlusButton" type="button" class="step-button">+</button>
66
  </div>
67
  </div>
68
 
69
  <div class="primary-actions-stack">
70
- <button id="exportStateButton" class="export-button" type="button" disabled>⇧ Export edited pack</button>
71
- <button id="usePreviewButton" class="ghost-button" type="button">Online preview mode</button>
72
- <button id="useFastButton" class="ghost-button" type="button">Fast full-mix mode</button>
73
  </div>
74
 
75
  <details class="advanced-controls">
76
- <summary>Advanced extraction settings</summary>
 
 
 
 
77
  <div class="control-grid compact-controls">
78
  <label>Demucs model
79
  <select id="demucs_model"></select>
@@ -150,131 +156,135 @@
150
  </details>
151
  </aside>
152
 
153
- <section class="samples-section">
154
  <div class="section-heading">
155
  <h2>Extracted Samples <span id="sampleCountLabel">(0)</span></h2>
156
- <p>Representative samples appear as auditionable cards. Use the supervision tools below for deeper correction.</p>
157
  </div>
158
  <div id="samplesGrid" class="sample-grid"></div>
159
  </section>
160
 
161
- <section class="review-strip">
162
- <article class="review-card">
163
- <strong>Selected hit</strong>
164
- <span id="selectedHitMeta">Click an onset marker or hit row to audition the detected slice.</span>
165
- <audio id="hitAudio" controls></audio>
166
- </article>
167
- <article class="review-card">
168
- <strong>Selected sample</strong>
169
- <span id="selectedSampleMeta">Click a sample card to hear the representative sample.</span>
170
- <audio id="sampleAudio" controls></audio>
171
- </article>
172
- </section>
173
-
174
- <details class="panel progress-panel utility-panel" open>
175
- <summary>
176
- <span>Pipeline</span>
177
- <small>Stage timings, logs, and streaming job progress</small>
178
- </summary>
179
- <div id="stageList" class="stage-list"></div>
180
- <pre id="logs" class="logs" aria-live="polite"></pre>
181
- </details>
182
-
183
- <details class="panel history-panel utility-panel">
184
  <summary>
185
- <span>Run history</span>
186
- <small>Completed manifests under .runs/</small>
187
  </summary>
188
- <button id="refreshHistoryButton" class="ghost-button" type="button">Refresh history</button>
189
- <div id="historyList" class="history-list"></div>
190
- </details>
191
 
192
- <details class="panel supervision-panel utility-panel">
193
- <summary>
194
- <span>Interactive supervision</span>
195
- <small>Move, suppress, restore, force-onset, explain, and export edited packs</small>
196
- </summary>
197
- <div class="supervision-header">
198
- <div>
199
- <h3>Semantic edit state</h3>
200
- <p class="subtle">Moves, locks, suppressions, favorites, and accepted suggestions are saved as replayable semantic state next to the run manifest.</p>
201
- </div>
202
- <div class="supervision-actions">
203
- <button id="refreshStateButton" class="ghost-button" type="button">Refresh state</button>
204
- <button id="forceOnsetButton" class="ghost-button" type="button" disabled>Add-onset mode off</button>
205
- <button id="undoButton" class="ghost-button" type="button" disabled>Undo edit</button>
206
- </div>
207
- </div>
208
- <div id="supervisionSummary" class="state-summary">No interactive state loaded.</div>
209
- <div id="editedDownloads" class="downloads edited-downloads"></div>
210
- <div class="supervision-tools">
211
- <label>Target cluster
212
- <select id="targetClusterSelect"></select>
213
- </label>
214
- <button id="moveHitButton" class="secondary-button" type="button" disabled>Move selected hit</button>
215
- <button id="pullHitButton" class="secondary-button" type="button" disabled>Pull into new cluster</button>
216
- <button id="acceptHitButton" class="secondary-button" type="button" disabled>Accept hit</button>
217
- <button id="favoriteHitButton" class="secondary-button" type="button" disabled>Favorite as representative</button>
218
- <button id="suppressHitButton" class="secondary-button danger-button" type="button" disabled>Suppress as bleed</button>
219
- <button id="restoreHitButton" class="secondary-button" type="button" disabled>Restore selected hit</button>
220
- <button id="lockClusterButton" class="secondary-button" type="button" disabled>Lock target cluster</button>
221
- <button id="explainClusterButton" class="secondary-button" type="button" disabled>Explain target cluster</button>
222
- </div>
223
- <div class="supervision-grid">
224
- <article>
225
- <h4>Outlier-first review queue</h4>
226
- <div id="reviewQueue" class="compact-list"></div>
227
- </article>
228
- <article>
229
- <h4>Cluster board</h4>
230
- <div id="clusterBoard" class="compact-list"></div>
231
  </article>
232
- <article>
233
- <h4>Suggestion inbox</h4>
234
- <div id="suggestionInbox" class="compact-list"></div>
235
  </article>
236
- <article>
237
- <h4>Constraint / event log</h4>
238
- <div id="stateLog" class="compact-list"></div>
239
- </article>
240
- </div>
241
- <pre id="clusterExplanation" class="explanation empty">Select a cluster and click Explain.</pre>
242
- </details>
243
 
244
- <details class="panel utility-panel data-panel">
245
- <summary>
246
- <span>Detailed tables</span>
247
- <small>Raw sample and detected-hit review rows</small>
248
- </summary>
249
- <div class="result-columns">
250
- <section>
251
- <h3>Representative samples</h3>
252
- <div class="table-wrap">
253
- <table id="samplesTable">
254
- <thead>
255
- <tr>
256
- <th>Audition</th><th>Sample</th><th>Class</th><th>Hits</th><th>Score</th><th>Duration</th><th>First hit</th><th>File</th>
257
- </tr>
258
- </thead>
259
- <tbody></tbody>
260
- </table>
 
 
 
 
 
 
 
 
 
 
261
  </div>
262
- </section>
263
- <section>
264
- <h3>Detected hit review</h3>
265
- <p class="subtle">Every detected slice is exported under <code>review/hits/</code>. Click rows or waveform markers to audition.</p>
266
- <div class="table-wrap hit-table-wrap">
267
- <table id="hitsTable">
268
- <thead>
269
- <tr>
270
- <th>Audition</th><th>#</th><th>Label</th><th>Cluster</th><th>Confidence</th><th>Flags</th><th>Onset</th><th>Duration</th><th>Energy</th><th>File</th>
271
- </tr>
272
- </thead>
273
- <tbody></tbody>
274
- </table>
275
  </div>
276
- </section>
277
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  </details>
279
  </main>
280
  </div>
 
27
  </header>
28
 
29
  <main class="app-layout">
30
+ <section class="wave-card panel" aria-label="Waveform overview">
31
+ <div class="wave-status-row">
32
+ <span id="jobPill" class="job-pill">idle</span>
33
+ <p id="resultSummary" class="status-text">Load audio, tune sensitivity, extract clean drum hits.</p>
34
+ </div>
35
+ <canvas id="waveform" class="waveform" height="420"></canvas>
36
+ <div class="transport-row" aria-label="Preview transport">
37
+ <button id="transportPlayButton" class="round-play" type="button" aria-label="Play preview">▶</button>
38
+ <span id="transportTime" class="transport-time">0:00 / 0:00</span>
39
+ <input id="transportSeek" class="transport-seek" type="range" min="0" max="1000" value="0" step="1" aria-label="Seek preview" />
40
  </div>
41
+ <div id="downloads" class="downloads compact-downloads"></div>
42
+ <div class="hidden-audio-bank" aria-hidden="true">
43
+ <audio id="sourcePreview"></audio>
44
+ <audio id="stemAudio"></audio>
45
+ <audio id="reconAudio"></audio>
46
+ <audio id="hitAudio"></audio>
47
+ <audio id="sampleAudio"></audio>
48
  </div>
 
49
  </section>
50
 
51
+ <aside class="control-card panel" aria-label="Extraction controls">
52
  <div class="control-group">
53
  <label>Stem
54
  <select id="stem"></select>
 
64
  <div class="control-group">
65
  <label>Cluster Count</label>
66
  <div class="stepper">
67
+ <button id="clusterMinusButton" type="button" class="step-button" aria-label="Decrease cluster count">−</button>
68
+ <input id="target_max" type="number" min="0" max="256" step="1" aria-label="Cluster count" />
69
+ <button id="clusterPlusButton" type="button" class="step-button" aria-label="Increase cluster count">+</button>
70
  </div>
71
  </div>
72
 
73
  <div class="primary-actions-stack">
74
+ <button id="exportStateButton" class="export-button" type="button" disabled>⇧ Export Samples</button>
 
 
75
  </div>
76
 
77
  <details class="advanced-controls">
78
+ <summary>Advanced</summary>
79
+ <div class="preset-row">
80
+ <button id="usePreviewButton" class="ghost-button" type="button">Online preview mode</button>
81
+ <button id="useFastButton" class="ghost-button" type="button">Fast full-mix mode</button>
82
+ </div>
83
  <div class="control-grid compact-controls">
84
  <label>Demucs model
85
  <select id="demucs_model"></select>
 
156
  </details>
157
  </aside>
158
 
159
+ <section class="samples-section" aria-label="Extracted samples">
160
  <div class="section-heading">
161
  <h2>Extracted Samples <span id="sampleCountLabel">(0)</span></h2>
 
162
  </div>
163
  <div id="samplesGrid" class="sample-grid"></div>
164
  </section>
165
 
166
+ <details class="review-workbench panel utility-panel">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  <summary>
168
+ <span>Review & edit</span>
169
+ <small>Pipeline progress, run history, detailed hit rows, semantic editing, force-onset, and edited export.</small>
170
  </summary>
 
 
 
171
 
172
+ <section class="review-strip">
173
+ <article class="review-card">
174
+ <strong>Selected hit</strong>
175
+ <span id="selectedHitMeta">Click an onset marker or hit row to audition the detected slice.</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  </article>
177
+ <article class="review-card">
178
+ <strong>Selected sample</strong>
179
+ <span id="selectedSampleMeta">Click a sample card to hear the representative sample.</span>
180
  </article>
181
+ </section>
 
 
 
 
 
 
182
 
183
+ <details class="panel progress-panel utility-panel" open>
184
+ <summary>
185
+ <span>Pipeline</span>
186
+ <small>Stage timings, logs, and streaming job progress</small>
187
+ </summary>
188
+ <div id="stageList" class="stage-list"></div>
189
+ <pre id="logs" class="logs" aria-live="polite"></pre>
190
+ </details>
191
+
192
+ <details class="panel history-panel utility-panel">
193
+ <summary>
194
+ <span>Run history</span>
195
+ <small>Completed manifests under .runs/</small>
196
+ </summary>
197
+ <button id="refreshHistoryButton" class="ghost-button" type="button">Refresh history</button>
198
+ <div id="historyList" class="history-list"></div>
199
+ </details>
200
+
201
+ <details class="panel supervision-panel utility-panel">
202
+ <summary>
203
+ <span>Interactive supervision</span>
204
+ <small>Move, suppress, restore, force-onset, explain, and export edited packs</small>
205
+ </summary>
206
+ <div class="supervision-header">
207
+ <div>
208
+ <h3>Semantic edit state</h3>
209
+ <p class="subtle">Moves, locks, suppressions, favorites, and accepted suggestions are saved as replayable semantic state next to the run manifest.</p>
210
  </div>
211
+ <div class="supervision-actions">
212
+ <button id="refreshStateButton" class="ghost-button" type="button">Refresh state</button>
213
+ <button id="forceOnsetButton" class="ghost-button" type="button" disabled>Add-onset mode off</button>
214
+ <button id="undoButton" class="ghost-button" type="button" disabled>Undo edit</button>
 
 
 
 
 
 
 
 
 
215
  </div>
216
+ </div>
217
+ <div id="supervisionSummary" class="state-summary">No interactive state loaded.</div>
218
+ <div id="editedDownloads" class="downloads edited-downloads"></div>
219
+ <div class="supervision-tools">
220
+ <label>Target cluster
221
+ <select id="targetClusterSelect"></select>
222
+ </label>
223
+ <button id="moveHitButton" class="secondary-button" type="button" disabled>Move selected hit</button>
224
+ <button id="pullHitButton" class="secondary-button" type="button" disabled>Pull into new cluster</button>
225
+ <button id="acceptHitButton" class="secondary-button" type="button" disabled>Accept hit</button>
226
+ <button id="favoriteHitButton" class="secondary-button" type="button" disabled>Favorite as representative</button>
227
+ <button id="suppressHitButton" class="secondary-button danger-button" type="button" disabled>Suppress as bleed</button>
228
+ <button id="restoreHitButton" class="secondary-button" type="button" disabled>Restore selected hit</button>
229
+ <button id="lockClusterButton" class="secondary-button" type="button" disabled>Lock target cluster</button>
230
+ <button id="explainClusterButton" class="secondary-button" type="button" disabled>Explain target cluster</button>
231
+ </div>
232
+ <div class="supervision-grid">
233
+ <article>
234
+ <h4>Outlier-first review queue</h4>
235
+ <div id="reviewQueue" class="compact-list"></div>
236
+ </article>
237
+ <article>
238
+ <h4>Cluster board</h4>
239
+ <div id="clusterBoard" class="compact-list"></div>
240
+ </article>
241
+ <article>
242
+ <h4>Suggestion inbox</h4>
243
+ <div id="suggestionInbox" class="compact-list"></div>
244
+ </article>
245
+ <article>
246
+ <h4>Constraint / event log</h4>
247
+ <div id="stateLog" class="compact-list"></div>
248
+ </article>
249
+ </div>
250
+ <pre id="clusterExplanation" class="explanation empty">Select a cluster and click Explain.</pre>
251
+ </details>
252
+
253
+ <details class="panel utility-panel data-panel">
254
+ <summary>
255
+ <span>Detailed tables</span>
256
+ <small>Raw sample and detected-hit review rows</small>
257
+ </summary>
258
+ <div class="result-columns">
259
+ <section>
260
+ <h3>Representative samples</h3>
261
+ <div class="table-wrap">
262
+ <table id="samplesTable">
263
+ <thead>
264
+ <tr>
265
+ <th>Audition</th><th>Sample</th><th>Class</th><th>Hits</th><th>Score</th><th>Duration</th><th>First hit</th><th>File</th>
266
+ </tr>
267
+ </thead>
268
+ <tbody></tbody>
269
+ </table>
270
+ </div>
271
+ </section>
272
+ <section>
273
+ <h3>Detected hit review</h3>
274
+ <p class="subtle">Every detected slice is exported under <code>review/hits/</code>. Click rows or waveform markers to audition.</p>
275
+ <div class="table-wrap hit-table-wrap">
276
+ <table id="hitsTable">
277
+ <thead>
278
+ <tr>
279
+ <th>Audition</th><th>#</th><th>Label</th><th>Cluster</th><th>Confidence</th><th>Flags</th><th>Onset</th><th>Duration</th><th>Energy</th><th>File</th>
280
+ </tr>
281
+ </thead>
282
+ <tbody></tbody>
283
+ </table>
284
+ </div>
285
+ </section>
286
+ </div>
287
+ </details>
288
  </details>
289
  </main>
290
  </div>
web/styles.css CHANGED
@@ -3,25 +3,20 @@
3
  --bg: #f7f7f8;
4
  --surface: #ffffff;
5
  --surface-soft: #fbfbfc;
6
- --line: #e3e4e8;
7
  --line-strong: #d4d6dd;
8
- --muted: #8b8f9a;
9
- --text: #26272b;
10
- --text-soft: #4a4d55;
11
- --accent: #6b3ff2;
12
- --accent-strong: #5129d9;
13
  --accent-soft: #eee9ff;
14
- --good: #64aa45;
15
- --bad: #d95767;
16
- --warn: #f08f3e;
17
- --cyan: #42b8b4;
18
- --blue: #4f7df2;
19
- --pink: #ea5ca9;
20
- --orange: #ef9343;
21
- --green: #8abc59;
22
- --shadow: 0 18px 50px rgba(20, 22, 30, .06);
23
- --radius-xl: 22px;
24
- --radius-lg: 16px;
25
  font-synthesis-weight: none;
26
  }
27
 
@@ -29,34 +24,33 @@
29
  html, body {
30
  margin: 0;
31
  min-height: 100%;
32
- background:
33
- radial-gradient(circle at 12% 0%, rgba(107, 63, 242, .05), transparent 28rem),
34
- radial-gradient(circle at 90% 12%, rgba(66, 184, 180, .04), transparent 30rem),
35
- var(--bg);
36
  color: var(--text);
37
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif;
38
  }
39
  button, input, select { font: inherit; }
 
 
40
  code { color: var(--accent-strong); }
41
 
42
  .shell {
43
- width: min(100% - 64px, 1760px);
44
  margin: 0 auto;
45
- padding: 34px 0 56px;
46
  }
47
 
48
  .topbar {
49
  display: flex;
50
  align-items: center;
51
  justify-content: space-between;
52
- gap: 24px;
53
  margin-bottom: 34px;
54
  }
55
  .file-chip {
56
  position: relative;
57
  display: inline-flex;
58
  align-items: center;
59
- gap: 28px;
60
  min-width: min(620px, 100%);
61
  cursor: pointer;
62
  color: var(--text);
@@ -70,8 +64,8 @@ code { color: var(--accent-strong); }
70
  .file-chip.dragging .brand-mark,
71
  .file-chip:hover .brand-mark { transform: translateY(-1px); }
72
  .brand-mark {
73
- width: 52px;
74
- height: 52px;
75
  display: inline-flex;
76
  align-items: center;
77
  justify-content: center;
@@ -82,181 +76,245 @@ code { color: var(--accent-strong); }
82
  .brand-mark i {
83
  width: 4px;
84
  border-radius: 999px;
85
- background: #1f2024;
86
  display: block;
87
  }
88
- .brand-mark i:nth-child(1) { height: 17px; }
89
- .brand-mark i:nth-child(2) { height: 29px; }
90
- .brand-mark i:nth-child(3) { height: 40px; }
91
- .brand-mark i:nth-child(4) { height: 25px; }
92
- .brand-mark i:nth-child(5) { height: 13px; }
93
  .file-copy strong {
94
  display: block;
95
- font-size: clamp(20px, 2vw, 27px);
 
 
 
 
96
  line-height: 1.1;
97
- font-weight: 640;
98
  letter-spacing: -.035em;
 
99
  }
100
  .file-copy small {
101
  display: block;
102
- margin-top: 5px;
103
  color: var(--muted);
104
- font-size: 13px;
105
  }
106
  .topbar-actions {
107
  display: inline-flex;
108
  align-items: center;
109
- justify-content: flex-end;
110
- gap: 14px;
111
- flex-wrap: wrap;
112
  }
113
  .backend-pill {
114
  display: inline-flex;
115
  align-items: center;
116
- gap: 9px;
117
- color: var(--text-soft);
118
- font-size: 12px;
119
- padding: 8px 11px;
120
- border: 1px solid var(--line);
121
- border-radius: 999px;
122
- background: rgba(255, 255, 255, .72);
123
  }
124
- .backend-pill strong { display: block; font-size: 12px; line-height: 1; }
125
- .backend-pill small { display: block; color: var(--muted); margin-top: 3px; }
126
  .status-dot {
127
- width: 9px;
128
- height: 9px;
129
  border-radius: 999px;
130
  background: var(--warn);
131
- box-shadow: 0 0 0 4px rgba(240, 143, 62, .13);
132
  }
133
- .status-dot.ok { background: var(--good); box-shadow: 0 0 0 4px rgba(100, 170, 69, .13); }
134
- .status-dot.bad { background: var(--bad); box-shadow: 0 0 0 4px rgba(217, 87, 103, .13); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  .app-layout {
137
  display: grid;
138
- grid-template-columns: minmax(0, 1fr) 360px;
139
  grid-template-areas:
140
  "wave controls"
141
  "samples samples"
142
- "review review"
143
- "progress progress"
144
- "history history"
145
- "supervision supervision"
146
- "data data";
147
  gap: 28px;
148
  align-items: start;
149
  }
150
  .panel {
151
  border: 1px solid var(--line);
152
  border-radius: var(--radius-xl);
153
- background: rgba(255, 255, 255, .82);
154
  box-shadow: var(--shadow);
155
  }
156
- .wave-card { grid-area: wave; padding: 0; overflow: hidden; min-height: 590px; }
157
- .control-card { grid-area: controls; padding: 30px; min-height: 590px; }
158
- .samples-section { grid-area: samples; }
159
- .review-strip { grid-area: review; }
160
- .progress-panel { grid-area: progress; }
161
- .history-panel { grid-area: history; }
162
- .supervision-panel { grid-area: supervision; }
163
- .data-panel { grid-area: data; }
 
 
 
 
 
164
 
165
- .wave-header {
 
 
 
 
 
166
  display: flex;
167
  justify-content: space-between;
168
- gap: 18px;
169
  align-items: flex-start;
170
- padding: 26px 30px 8px;
171
- }
172
- .eyebrow {
173
- margin: 0 0 8px;
174
- color: var(--muted);
175
- text-transform: uppercase;
176
- letter-spacing: .14em;
177
- font-size: 11px;
178
- font-weight: 780;
179
- }
180
- h1 {
181
- margin: 0;
182
- font-size: clamp(20px, 2.2vw, 30px);
183
- line-height: 1.16;
184
- letter-spacing: -.045em;
185
- font-weight: 620;
186
- color: var(--text);
187
  }
188
  .job-pill {
189
  display: inline-flex;
190
  align-items: center;
191
  white-space: nowrap;
192
- border: 1px solid var(--line);
193
  border-radius: 999px;
194
- padding: 8px 11px;
195
  color: var(--muted);
196
- background: #fff;
 
 
 
 
 
 
 
 
197
  font-size: 12px;
 
 
 
 
 
 
198
  }
199
  .waveform {
200
  display: block;
201
  width: 100%;
202
- height: 420px;
203
- min-height: 420px;
204
- margin: 0;
205
  background: linear-gradient(180deg, #fff, #fbfbfc);
206
  cursor: crosshair;
207
  }
208
  .transport-row {
209
  display: grid;
210
- grid-template-columns: repeat(2, minmax(0, 1fr));
211
- gap: 16px;
212
  align-items: center;
213
- padding: 0 30px 26px;
214
  }
215
- audio { width: 100%; margin-top: 8px; height: 38px; }
216
- .transport-audio {
217
- color: var(--muted);
218
- font-size: 11px;
219
- font-weight: 680;
220
- letter-spacing: .02em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
 
 
222
  .downloads {
223
  display: flex;
224
  flex-wrap: wrap;
225
- gap: 10px;
226
- padding: 0 30px 28px;
 
 
 
 
227
  }
 
228
  .downloads a, .table-wrap a {
229
  color: var(--accent-strong);
230
  text-decoration: none;
231
- font-weight: 760;
232
  background: var(--accent-soft);
233
- border: 1px solid rgba(107, 63, 242, .18);
234
  border-radius: 999px;
235
- padding: 8px 12px;
 
236
  }
237
 
238
  label {
239
  display: block;
240
  color: var(--text-soft);
241
  font-size: 20px;
242
- font-weight: 520;
243
- letter-spacing: -.025em;
244
  }
245
- .control-group { margin-bottom: 34px; }
246
  input, select {
247
  width: 100%;
248
- margin-top: 12px;
249
  border: 1px solid var(--line);
250
- border-radius: 10px;
251
  padding: 15px 18px;
252
  color: var(--text);
253
  background: #fff;
254
  outline: none;
255
  font-size: 18px;
256
  }
 
257
  input:focus, select:focus {
258
- border-color: rgba(107, 63, 242, .62);
259
- box-shadow: 0 0 0 4px rgba(107, 63, 242, .09);
260
  }
261
  input[type="range"] {
262
  appearance: none;
@@ -273,7 +331,7 @@ input[type="range"]::-webkit-slider-thumb {
273
  border-radius: 50%;
274
  background: var(--accent);
275
  border: 0;
276
- box-shadow: 0 6px 18px rgba(107, 63, 242, .28);
277
  }
278
  input[type="range"]::-moz-range-thumb {
279
  width: 26px;
@@ -281,7 +339,7 @@ input[type="range"]::-moz-range-thumb {
281
  border-radius: 50%;
282
  background: var(--accent);
283
  border: 0;
284
- box-shadow: 0 6px 18px rgba(107, 63, 242, .28);
285
  }
286
  .range-caption {
287
  display: flex;
@@ -292,10 +350,10 @@ input[type="range"]::-moz-range-thumb {
292
  }
293
  .stepper {
294
  display: grid;
295
- grid-template-columns: 64px minmax(0, 1fr) 64px;
296
- margin-top: 12px;
297
  border: 1px solid var(--line);
298
- border-radius: 10px;
299
  overflow: hidden;
300
  background: #fff;
301
  }
@@ -306,60 +364,55 @@ input[type="range"]::-moz-range-thumb {
306
  border-right: 1px solid var(--line);
307
  border-radius: 0;
308
  text-align: center;
309
- font-size: 18px;
 
310
  }
311
  .step-button {
312
  border: 0;
313
  border-radius: 0;
314
  background: #fff;
315
  color: var(--text);
316
- font-size: 26px;
317
  line-height: 1;
318
  }
319
- .primary-actions-stack { display: grid; gap: 14px; margin-top: 40px; }
320
- button {
321
- border: 0;
322
- border-radius: 10px;
323
- padding: 14px 18px;
324
- color: var(--text);
325
- cursor: pointer;
326
- transition: transform .16s ease, box-shadow .16s ease, opacity .16s ease, border-color .16s ease, background .16s ease;
327
- }
328
- button:hover:not(:disabled) { transform: translateY(-1px); }
329
- button:disabled { opacity: .45; cursor: not-allowed; }
330
- .primary-button {
331
- min-width: 250px;
332
- background: linear-gradient(135deg, #7048f5, #5129d9);
333
- color: #fff;
334
- font-weight: 690;
335
- font-size: 18px;
336
- box-shadow: 0 14px 34px rgba(81, 41, 217, .22);
337
- }
338
- .primary-button span { margin-right: 8px; }
339
  .export-button, .secondary-button, .ghost-button {
340
  border: 1px solid var(--line-strong);
 
341
  background: #fff;
342
  color: var(--text-soft);
343
- font-weight: 610;
 
344
  }
345
- .export-button { min-height: 64px; font-size: 18px; }
346
- .ghost-button { padding: 10px 13px; font-size: 14px; }
347
- .full-width { width: 100%; margin-top: 16px; }
 
 
 
 
348
  .ghost-button.active, .secondary-button.active {
349
- border-color: rgba(100, 170, 69, .55);
350
- background: rgba(100, 170, 69, .10);
351
- color: #386b24;
352
  }
 
353
  .advanced-controls {
354
  margin-top: 28px;
355
  border-top: 1px solid var(--line);
356
- padding-top: 18px;
357
  }
358
  .advanced-controls summary,
359
  .utility-panel summary {
360
  cursor: pointer;
361
  color: var(--text-soft);
362
- font-weight: 680;
 
 
 
 
 
 
363
  }
364
  .control-grid {
365
  display: grid;
@@ -368,7 +421,7 @@ button:disabled { opacity: .45; cursor: not-allowed; }
368
  }
369
  .compact-controls label {
370
  font-size: 12px;
371
- font-weight: 720;
372
  letter-spacing: .01em;
373
  }
374
  .compact-controls input, .compact-controls select {
@@ -386,42 +439,42 @@ button:disabled { opacity: .45; cursor: not-allowed; }
386
  align-items: center;
387
  gap: 8px;
388
  font-size: 13px;
389
- font-weight: 650;
390
  }
391
  .toggles input { width: auto; margin: 0; }
 
392
 
393
  .section-heading {
394
  display: flex;
395
  align-items: end;
396
  justify-content: space-between;
397
  gap: 20px;
398
- margin: 10px 8px 20px;
399
  }
400
- h2 { margin: 0; font-size: 21px; letter-spacing: -.035em; font-weight: 620; }
401
- .section-heading p { margin: 0; color: var(--muted); max-width: 680px; }
402
  .sample-grid {
403
  display: grid;
404
- grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
405
  gap: 34px;
406
  }
407
  .sample-card {
408
  display: grid;
409
- grid-template-rows: 130px auto;
410
- min-height: 220px;
411
  border: 1px solid var(--line);
412
  border-top: 4px solid var(--accent);
413
- border-radius: 10px;
414
  background: #fff;
415
- box-shadow: 0 10px 26px rgba(20, 22, 30, .04);
416
  overflow: hidden;
417
  text-align: left;
418
  padding: 0;
419
  }
420
- .sample-card:hover { box-shadow: 0 16px 38px rgba(20, 22, 30, .08); }
421
- .sample-card.selected { outline: 3px solid rgba(107, 63, 242, .14); }
422
  .sample-wave {
423
  width: 100%;
424
- height: 130px;
425
  display: block;
426
  background: #fff;
427
  }
@@ -430,18 +483,18 @@ h2 { margin: 0; font-size: 21px; letter-spacing: -.035em; font-weight: 620; }
430
  grid-template-columns: 44px minmax(0, 1fr);
431
  gap: 12px;
432
  align-items: center;
433
- padding: 14px 14px 16px;
434
  }
435
  .play-dot {
436
- width: 44px;
437
- height: 44px;
438
  display: grid;
439
  place-items: center;
440
  border-radius: 999px;
441
  background: #fff;
442
  border: 1px solid var(--line);
443
  color: var(--text);
444
- font-size: 14px;
445
  }
446
  .sample-name {
447
  display: block;
@@ -449,43 +502,53 @@ h2 { margin: 0; font-size: 21px; letter-spacing: -.035em; font-weight: 620; }
449
  text-overflow: ellipsis;
450
  white-space: nowrap;
451
  font-size: 17px;
452
- font-weight: 570;
453
  letter-spacing: -.02em;
454
  }
455
  .sample-meta {
456
- display: block;
457
  margin-top: 4px;
458
  color: var(--muted);
459
  font-size: 12px;
460
  font-weight: 500;
461
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  .review-strip {
463
  display: grid;
464
  grid-template-columns: repeat(2, minmax(0, 1fr));
465
- gap: 22px;
466
  }
467
  .review-card {
468
  border: 1px solid var(--line);
469
  border-radius: var(--radius-lg);
470
- background: rgba(255, 255, 255, .82);
471
- box-shadow: var(--shadow);
472
- padding: 18px;
473
  }
474
  .review-card strong, .review-card span { display: block; }
475
  .review-card span { color: var(--muted); font-size: 13px; margin-top: 5px; line-height: 1.4; }
476
 
477
- .utility-panel {
478
- padding: 18px 20px;
479
- }
480
- .utility-panel > summary {
481
- display: grid;
482
- grid-template-columns: minmax(0, 1fr);
483
- gap: 4px;
484
- list-style: none;
485
- }
486
- .utility-panel > summary::-webkit-details-marker { display: none; }
487
- .utility-panel > summary span { font-size: 17px; color: var(--text); }
488
- .utility-panel > summary small { color: var(--muted); font-weight: 500; }
489
  .stage-list {
490
  display: grid;
491
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
@@ -503,7 +566,7 @@ h2 { margin: 0; font-size: 21px; letter-spacing: -.035em; font-weight: 620; }
503
  background: var(--surface-soft);
504
  }
505
  .stage .badge { width: 14px; height: 14px; border-radius: 999px; background: #d7d9e1; }
506
- .stage.running .badge { background: var(--accent); box-shadow: 0 0 0 5px rgba(107, 63, 242, .12); }
507
  .stage.done .badge { background: var(--good); }
508
  .stage.error .badge { background: var(--bad); }
509
  .stage strong { display: block; font-size: 13px; color: var(--text); }
@@ -518,148 +581,151 @@ h2 { margin: 0; font-size: 21px; letter-spacing: -.035em; font-weight: 620; }
518
  padding: 14px;
519
  margin: 14px 0 0;
520
  background: #fbfbfc;
521
- color: #606571;
522
  font-size: 12px;
523
- line-height: 1.45;
524
- white-space: pre-wrap;
525
  }
526
- .history-list { display: grid; gap: 8px; max-height: 360px; overflow: auto; margin-top: 14px; }
527
- .history-row {
528
- width: 100%;
 
 
 
529
  display: grid;
530
  grid-template-columns: minmax(0, 1fr) auto auto auto;
531
  gap: 12px;
532
  align-items: center;
533
- text-align: left;
534
  border: 1px solid var(--line);
535
- background: #fff;
536
  border-radius: 14px;
 
537
  padding: 12px;
 
538
  }
539
- .history-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
540
- .history-row small { display: block; color: var(--muted); margin-top: 3px; }
541
- .history-row span:not(:first-child) { color: var(--text-soft); font-size: 12px; font-variant-numeric: tabular-nums; }
542
- .empty { color: var(--muted); margin: 0; }
543
-
544
- h3 { margin: 0 0 10px; font-size: 16px; letter-spacing: -.015em; }
545
- .subtle { margin: -4px 0 12px; color: var(--muted); font-size: 13px; }
546
- .result-columns { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; margin-top: 18px; }
547
- .hit-table-wrap { max-height: 420px; }
548
- .table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 14px; }
549
- table { width: 100%; border-collapse: collapse; min-width: 860px; background: #fff; }
550
- th, td { text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--line); font-size: 13px; }
551
- th { position: sticky; top: 0; background: #fbfbfc; color: var(--muted); z-index: 1; }
552
- td { color: var(--text-soft); }
553
- tr:last-child td { border-bottom: 0; }
554
  .mini-button {
555
- padding: 7px 10px;
556
  border-radius: 999px;
557
  background: #fff;
558
- border: 1px solid var(--line);
559
  color: var(--text-soft);
 
560
  font-size: 12px;
561
- font-weight: 720;
562
- }
563
- tr.selected td { background: var(--accent-soft); }
564
- tr[data-hit-index] { cursor: pointer; }
565
- tr[data-hit-index]:hover td { background: #f6f4ff; }
566
- tr.low-confidence td { background: rgba(240,143,62,.06); }
567
- tr.low-confidence.selected td { background: var(--accent-soft); }
568
- tr.suppressed td { opacity: .62; text-decoration: line-through; }
569
-
570
  .supervision-header {
571
  display: flex;
572
- align-items: flex-start;
573
  justify-content: space-between;
574
- gap: 16px;
575
- margin: 18px 0 14px;
 
 
 
 
 
 
 
 
 
 
576
  }
577
- .supervision-header h3, .supervision-grid h4 { margin: 0; }
578
- .supervision-actions, .row-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
579
- .state-summary { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; color: var(--text-soft); }
580
  .state-summary span {
581
  border: 1px solid var(--line);
582
  border-radius: 999px;
583
- background: #fff;
584
- padding: 7px 10px;
 
585
  font-size: 12px;
586
- font-weight: 760;
587
  }
588
  .supervision-tools {
589
  display: grid;
590
- grid-template-columns: minmax(220px, 1fr) repeat(7, auto);
591
  gap: 10px;
592
  align-items: end;
593
- margin-bottom: 16px;
594
- }
595
- .supervision-tools label { font-size: 13px; font-weight: 720; }
596
- .supervision-tools input, .supervision-tools select { font-size: 13px; padding: 10px 11px; }
597
- .danger-button { border-color: rgba(217,87,103,.34); color: #973844; }
598
- .supervision-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
599
- .compact-list { display: grid; gap: 8px; max-height: 300px; overflow: auto; }
600
- .compact-row, .suggestion-row, .log-row {
601
- width: 100%;
602
  display: grid;
603
- grid-template-columns: minmax(0, 1fr) auto;
604
- gap: 10px;
605
- align-items: center;
 
 
606
  border: 1px solid var(--line);
607
- border-radius: 14px;
608
- padding: 10px;
609
- background: #fff;
610
- color: var(--text);
611
- text-align: left;
612
  }
613
- .compact-row strong, .suggestion-row strong, .log-row strong { display: block; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
614
- .compact-row small, .suggestion-row small, .log-row small { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; line-height: 1.35; }
615
- .compact-row.locked { border-color: rgba(100,170,69,.45); background: rgba(100,170,69,.08); }
616
- .compact-row.suppressed { opacity: .62; text-decoration: line-through; }
617
- .log-row.constraint { border-color: rgba(107,63,242,.22); }
618
  .explanation {
619
- min-height: 120px;
620
- max-height: 320px;
621
  overflow: auto;
622
  border: 1px solid var(--line);
623
  border-radius: 14px;
 
624
  background: #fbfbfc;
625
- color: #606571;
626
- padding: 12px;
627
- margin: 14px 0 0;
628
  font-size: 12px;
629
- line-height: 1.45;
630
  }
631
- .edited-downloads { margin: -4px 0 14px; padding: 0; }
632
- .edited-downloads:empty { display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
 
634
  @media (max-width: 1180px) {
 
635
  .app-layout {
636
  grid-template-columns: 1fr;
637
  grid-template-areas:
638
- "controls"
639
  "wave"
 
640
  "samples"
641
- "review"
642
- "progress"
643
- "history"
644
- "supervision"
645
- "data";
646
  }
647
- .control-card, .wave-card { min-height: 0; }
648
- .control-card { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
649
- .control-group, .primary-actions-stack, .advanced-controls { margin: 0; }
650
- .advanced-controls { grid-column: 1 / -1; }
651
  }
652
  @media (max-width: 760px) {
653
- .shell { width: min(100% - 24px, 1760px); padding-top: 20px; }
654
- .topbar, .section-heading, .supervision-header { display: block; }
655
- .topbar-actions { margin-top: 18px; justify-content: stretch; width: 100%; }
656
  .primary-button { width: 100%; min-width: 0; }
657
- .file-chip { min-width: 0; width: 100%; gap: 12px; }
658
- .wave-header, .transport-row, .downloads { padding-left: 18px; padding-right: 18px; }
659
- .waveform { height: 300px; min-height: 300px; }
660
- .transport-row, .review-strip, .control-card, .control-grid, .supervision-tools, .supervision-grid { grid-template-columns: 1fr; }
661
- .sample-grid { grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 18px; }
662
- .history-row { grid-template-columns: 1fr 1fr; }
663
- }
664
- #sourcePreview { grid-column: 1 / -1; }
665
- .downloads:empty { display: none; }
 
 
 
 
3
  --bg: #f7f7f8;
4
  --surface: #ffffff;
5
  --surface-soft: #fbfbfc;
6
+ --line: #e4e5e9;
7
  --line-strong: #d4d6dd;
8
+ --muted: #8d929d;
9
+ --text: #25262b;
10
+ --text-soft: #4d5058;
11
+ --accent: #6d45ec;
12
+ --accent-strong: #552bd8;
13
  --accent-soft: #eee9ff;
14
+ --good: #6cae48;
15
+ --bad: #d85867;
16
+ --warn: #ef9343;
17
+ --shadow: 0 16px 42px rgba(18, 21, 30, .055);
18
+ --radius-xl: 18px;
19
+ --radius-lg: 13px;
 
 
 
 
 
20
  font-synthesis-weight: none;
21
  }
22
 
 
24
  html, body {
25
  margin: 0;
26
  min-height: 100%;
27
+ background: var(--bg);
 
 
 
28
  color: var(--text);
29
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif;
30
  }
31
  button, input, select { font: inherit; }
32
+ button { cursor: pointer; }
33
+ button:disabled { opacity: .45; cursor: not-allowed; }
34
  code { color: var(--accent-strong); }
35
 
36
  .shell {
37
+ width: min(100% - 74px, 1740px);
38
  margin: 0 auto;
39
+ padding: 38px 0 48px;
40
  }
41
 
42
  .topbar {
43
  display: flex;
44
  align-items: center;
45
  justify-content: space-between;
46
+ gap: 28px;
47
  margin-bottom: 34px;
48
  }
49
  .file-chip {
50
  position: relative;
51
  display: inline-flex;
52
  align-items: center;
53
+ gap: 24px;
54
  min-width: min(620px, 100%);
55
  cursor: pointer;
56
  color: var(--text);
 
64
  .file-chip.dragging .brand-mark,
65
  .file-chip:hover .brand-mark { transform: translateY(-1px); }
66
  .brand-mark {
67
+ width: 48px;
68
+ height: 48px;
69
  display: inline-flex;
70
  align-items: center;
71
  justify-content: center;
 
76
  .brand-mark i {
77
  width: 4px;
78
  border-radius: 999px;
79
+ background: #202125;
80
  display: block;
81
  }
82
+ .brand-mark i:nth-child(1) { height: 16px; }
83
+ .brand-mark i:nth-child(2) { height: 28px; }
84
+ .brand-mark i:nth-child(3) { height: 38px; }
85
+ .brand-mark i:nth-child(4) { height: 24px; }
86
+ .brand-mark i:nth-child(5) { height: 12px; }
87
  .file-copy strong {
88
  display: block;
89
+ max-width: 760px;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ font-size: 23px;
94
  line-height: 1.1;
 
95
  letter-spacing: -.035em;
96
+ font-weight: 540;
97
  }
98
  .file-copy small {
99
  display: block;
100
+ margin-top: 4px;
101
  color: var(--muted);
102
+ font-size: 12px;
103
  }
104
  .topbar-actions {
105
  display: inline-flex;
106
  align-items: center;
107
+ gap: 16px;
 
 
108
  }
109
  .backend-pill {
110
  display: inline-flex;
111
  align-items: center;
112
+ gap: 7px;
113
+ color: var(--muted);
114
+ font-size: 11px;
115
+ opacity: .62;
 
 
 
116
  }
117
+ .backend-pill strong { display: block; font-size: 11px; line-height: 1; }
118
+ .backend-pill small { display: none; }
119
  .status-dot {
120
+ width: 7px;
121
+ height: 7px;
122
  border-radius: 999px;
123
  background: var(--warn);
 
124
  }
125
+ .status-dot.ok { background: var(--good); }
126
+ .status-dot.bad { background: var(--bad); }
127
+ .primary-button {
128
+ min-width: 252px;
129
+ border: 0;
130
+ border-radius: 8px;
131
+ padding: 19px 28px;
132
+ background: linear-gradient(135deg, #7048f5, #552bd8);
133
+ color: #fff;
134
+ font-weight: 610;
135
+ font-size: 20px;
136
+ box-shadow: 0 14px 34px rgba(85, 43, 216, .20);
137
+ transition: transform .16s ease, box-shadow .16s ease, opacity .16s ease;
138
+ }
139
+ .primary-button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 18px 40px rgba(85, 43, 216, .24); }
140
+ .primary-button span { margin-right: 9px; }
141
 
142
  .app-layout {
143
  display: grid;
144
+ grid-template-columns: minmax(0, 1fr) 356px;
145
  grid-template-areas:
146
  "wave controls"
147
  "samples samples"
148
+ "workbench workbench";
 
 
 
 
149
  gap: 28px;
150
  align-items: start;
151
  }
152
  .panel {
153
  border: 1px solid var(--line);
154
  border-radius: var(--radius-xl);
155
+ background: rgba(255, 255, 255, .88);
156
  box-shadow: var(--shadow);
157
  }
158
+ .wave-card {
159
+ grid-area: wave;
160
+ position: relative;
161
+ overflow: hidden;
162
+ min-height: 574px;
163
+ }
164
+ .control-card {
165
+ grid-area: controls;
166
+ padding: 31px 29px 28px;
167
+ min-height: 574px;
168
+ }
169
+ .samples-section { grid-area: samples; margin-top: 16px; }
170
+ .review-workbench { grid-area: workbench; }
171
 
172
+ .wave-status-row {
173
+ position: absolute;
174
+ z-index: 2;
175
+ top: 14px;
176
+ left: 18px;
177
+ right: 18px;
178
  display: flex;
179
  justify-content: space-between;
 
180
  align-items: flex-start;
181
+ gap: 16px;
182
+ pointer-events: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
  .job-pill {
185
  display: inline-flex;
186
  align-items: center;
187
  white-space: nowrap;
188
+ border: 1px solid rgba(228, 229, 233, .86);
189
  border-radius: 999px;
190
+ padding: 6px 10px;
191
  color: var(--muted);
192
+ background: rgba(255, 255, 255, .78);
193
+ font-size: 11px;
194
+ backdrop-filter: blur(10px);
195
+ }
196
+ .status-text {
197
+ margin: 0;
198
+ max-width: 560px;
199
+ color: rgba(77, 80, 88, .72);
200
+ text-align: right;
201
  font-size: 12px;
202
+ line-height: 1.3;
203
+ background: rgba(255, 255, 255, .72);
204
+ border: 1px solid rgba(228, 229, 233, .7);
205
+ border-radius: 999px;
206
+ padding: 6px 10px;
207
+ backdrop-filter: blur(10px);
208
  }
209
  .waveform {
210
  display: block;
211
  width: 100%;
212
+ height: 430px;
213
+ min-height: 430px;
 
214
  background: linear-gradient(180deg, #fff, #fbfbfc);
215
  cursor: crosshair;
216
  }
217
  .transport-row {
218
  display: grid;
219
+ grid-template-columns: 54px 112px minmax(0, 1fr);
220
+ gap: 24px;
221
  align-items: center;
222
+ padding: 0 29px 28px;
223
  }
224
+ .round-play {
225
+ width: 54px;
226
+ height: 54px;
227
+ display: grid;
228
+ place-items: center;
229
+ border: 1px solid var(--line);
230
+ border-radius: 999px;
231
+ background: #fff;
232
+ color: #1f2024;
233
+ font-size: 18px;
234
+ line-height: 1;
235
+ box-shadow: 0 2px 8px rgba(18, 21, 30, .035);
236
+ }
237
+ .transport-time {
238
+ color: #6d717b;
239
+ font-size: 18px;
240
+ font-variant-numeric: tabular-nums;
241
+ white-space: nowrap;
242
+ }
243
+ .transport-seek {
244
+ appearance: none;
245
+ width: 100%;
246
+ height: 2px;
247
+ margin: 0;
248
+ padding: 0;
249
+ border: 0;
250
+ border-radius: 999px;
251
+ background: #d9dbe2;
252
+ }
253
+ .transport-seek::-webkit-slider-thumb {
254
+ appearance: none;
255
+ width: 14px;
256
+ height: 14px;
257
+ border-radius: 50%;
258
+ background: var(--accent);
259
+ border: 0;
260
+ opacity: 0;
261
+ }
262
+ .transport-seek:hover::-webkit-slider-thumb { opacity: 1; }
263
+ .transport-seek::-moz-range-thumb {
264
+ width: 14px;
265
+ height: 14px;
266
+ border-radius: 50%;
267
+ background: var(--accent);
268
+ border: 0;
269
+ opacity: 0;
270
  }
271
+ .transport-seek:hover::-moz-range-thumb { opacity: 1; }
272
+ .hidden-audio-bank { display: none; }
273
  .downloads {
274
  display: flex;
275
  flex-wrap: wrap;
276
+ gap: 9px;
277
+ }
278
+ .compact-downloads {
279
+ padding: 0 29px 24px;
280
+ margin-top: -10px;
281
+ min-height: 30px;
282
  }
283
+ .downloads:empty { display: none; }
284
  .downloads a, .table-wrap a {
285
  color: var(--accent-strong);
286
  text-decoration: none;
287
+ font-weight: 680;
288
  background: var(--accent-soft);
289
+ border: 1px solid rgba(109, 69, 236, .16);
290
  border-radius: 999px;
291
+ padding: 7px 11px;
292
+ font-size: 12px;
293
  }
294
 
295
  label {
296
  display: block;
297
  color: var(--text-soft);
298
  font-size: 20px;
299
+ font-weight: 500;
300
+ letter-spacing: -.026em;
301
  }
302
+ .control-group { margin-bottom: 36px; }
303
  input, select {
304
  width: 100%;
305
+ margin-top: 13px;
306
  border: 1px solid var(--line);
307
+ border-radius: 9px;
308
  padding: 15px 18px;
309
  color: var(--text);
310
  background: #fff;
311
  outline: none;
312
  font-size: 18px;
313
  }
314
+ select { appearance: auto; }
315
  input:focus, select:focus {
316
+ border-color: rgba(109, 69, 236, .58);
317
+ box-shadow: 0 0 0 4px rgba(109, 69, 236, .075);
318
  }
319
  input[type="range"] {
320
  appearance: none;
 
331
  border-radius: 50%;
332
  background: var(--accent);
333
  border: 0;
334
+ box-shadow: 0 6px 18px rgba(109, 69, 236, .26);
335
  }
336
  input[type="range"]::-moz-range-thumb {
337
  width: 26px;
 
339
  border-radius: 50%;
340
  background: var(--accent);
341
  border: 0;
342
+ box-shadow: 0 6px 18px rgba(109, 69, 236, .26);
343
  }
344
  .range-caption {
345
  display: flex;
 
350
  }
351
  .stepper {
352
  display: grid;
353
+ grid-template-columns: 58px minmax(0, 1fr) 58px;
354
+ margin-top: 13px;
355
  border: 1px solid var(--line);
356
+ border-radius: 9px;
357
  overflow: hidden;
358
  background: #fff;
359
  }
 
364
  border-right: 1px solid var(--line);
365
  border-radius: 0;
366
  text-align: center;
367
+ font-size: 20px;
368
+ padding: 14px 8px;
369
  }
370
  .step-button {
371
  border: 0;
372
  border-radius: 0;
373
  background: #fff;
374
  color: var(--text);
375
+ font-size: 25px;
376
  line-height: 1;
377
  }
378
+ .primary-actions-stack { display: grid; gap: 14px; margin-top: 42px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  .export-button, .secondary-button, .ghost-button {
380
  border: 1px solid var(--line-strong);
381
+ border-radius: 9px;
382
  background: #fff;
383
  color: var(--text-soft);
384
+ font-weight: 580;
385
+ transition: transform .16s ease, box-shadow .16s ease, opacity .16s ease, border-color .16s ease, background .16s ease;
386
  }
387
+ .export-button {
388
+ min-height: 64px;
389
+ padding: 16px 18px;
390
+ font-size: 18px;
391
+ }
392
+ .ghost-button, .secondary-button { padding: 10px 13px; font-size: 14px; }
393
+ button:hover:not(:disabled) { transform: translateY(-1px); }
394
  .ghost-button.active, .secondary-button.active {
395
+ border-color: rgba(108, 174, 72, .55);
396
+ background: rgba(108, 174, 72, .10);
397
+ color: #3f702b;
398
  }
399
+ .danger-button { color: var(--bad); }
400
  .advanced-controls {
401
  margin-top: 28px;
402
  border-top: 1px solid var(--line);
403
+ padding-top: 17px;
404
  }
405
  .advanced-controls summary,
406
  .utility-panel summary {
407
  cursor: pointer;
408
  color: var(--text-soft);
409
+ font-weight: 650;
410
+ }
411
+ .preset-row {
412
+ display: grid;
413
+ grid-template-columns: 1fr;
414
+ gap: 10px;
415
+ margin: 16px 0;
416
  }
417
  .control-grid {
418
  display: grid;
 
421
  }
422
  .compact-controls label {
423
  font-size: 12px;
424
+ font-weight: 700;
425
  letter-spacing: .01em;
426
  }
427
  .compact-controls input, .compact-controls select {
 
439
  align-items: center;
440
  gap: 8px;
441
  font-size: 13px;
442
+ font-weight: 630;
443
  }
444
  .toggles input { width: auto; margin: 0; }
445
+ .full-width { width: 100%; margin-top: 16px; }
446
 
447
  .section-heading {
448
  display: flex;
449
  align-items: end;
450
  justify-content: space-between;
451
  gap: 20px;
452
+ margin: 0 8px 20px;
453
  }
454
+ h2 { margin: 0; font-size: 21px; letter-spacing: -.035em; font-weight: 540; }
 
455
  .sample-grid {
456
  display: grid;
457
+ grid-template-columns: repeat(8, minmax(135px, 1fr));
458
  gap: 34px;
459
  }
460
  .sample-card {
461
  display: grid;
462
+ grid-template-rows: 132px 72px;
463
+ min-height: 204px;
464
  border: 1px solid var(--line);
465
  border-top: 4px solid var(--accent);
466
+ border-radius: 9px;
467
  background: #fff;
468
+ box-shadow: 0 9px 23px rgba(18, 21, 30, .035);
469
  overflow: hidden;
470
  text-align: left;
471
  padding: 0;
472
  }
473
+ .sample-card:hover { box-shadow: 0 14px 34px rgba(18, 21, 30, .07); }
474
+ .sample-card.selected { outline: 3px solid rgba(109, 69, 236, .14); }
475
  .sample-wave {
476
  width: 100%;
477
+ height: 132px;
478
  display: block;
479
  background: #fff;
480
  }
 
483
  grid-template-columns: 44px minmax(0, 1fr);
484
  gap: 12px;
485
  align-items: center;
486
+ padding: 13px 13px 15px;
487
  }
488
  .play-dot {
489
+ width: 42px;
490
+ height: 42px;
491
  display: grid;
492
  place-items: center;
493
  border-radius: 999px;
494
  background: #fff;
495
  border: 1px solid var(--line);
496
  color: var(--text);
497
+ font-size: 13px;
498
  }
499
  .sample-name {
500
  display: block;
 
502
  text-overflow: ellipsis;
503
  white-space: nowrap;
504
  font-size: 17px;
505
+ font-weight: 500;
506
  letter-spacing: -.02em;
507
  }
508
  .sample-meta {
509
+ display: none;
510
  margin-top: 4px;
511
  color: var(--muted);
512
  font-size: 12px;
513
  font-weight: 500;
514
  }
515
+ .empty { color: var(--muted); }
516
+
517
+ .review-workbench {
518
+ padding: 18px 20px;
519
+ margin-top: 10px;
520
+ }
521
+ .utility-panel {
522
+ padding: 18px 20px;
523
+ }
524
+ .utility-panel > summary,
525
+ .review-workbench > summary {
526
+ display: grid;
527
+ grid-template-columns: minmax(0, 1fr);
528
+ gap: 4px;
529
+ list-style: none;
530
+ }
531
+ .utility-panel > summary::-webkit-details-marker,
532
+ .review-workbench > summary::-webkit-details-marker { display: none; }
533
+ .utility-panel > summary span,
534
+ .review-workbench > summary span { font-size: 17px; color: var(--text); }
535
+ .utility-panel > summary small,
536
+ .review-workbench > summary small { color: var(--muted); font-weight: 500; }
537
+ .review-workbench > * + * { margin-top: 18px; }
538
  .review-strip {
539
  display: grid;
540
  grid-template-columns: repeat(2, minmax(0, 1fr));
541
+ gap: 18px;
542
  }
543
  .review-card {
544
  border: 1px solid var(--line);
545
  border-radius: var(--radius-lg);
546
+ background: var(--surface-soft);
547
+ padding: 16px;
 
548
  }
549
  .review-card strong, .review-card span { display: block; }
550
  .review-card span { color: var(--muted); font-size: 13px; margin-top: 5px; line-height: 1.4; }
551
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  .stage-list {
553
  display: grid;
554
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
 
566
  background: var(--surface-soft);
567
  }
568
  .stage .badge { width: 14px; height: 14px; border-radius: 999px; background: #d7d9e1; }
569
+ .stage.running .badge { background: var(--accent); box-shadow: 0 0 0 5px rgba(109, 69, 236, .12); }
570
  .stage.done .badge { background: var(--good); }
571
  .stage.error .badge { background: var(--bad); }
572
  .stage strong { display: block; font-size: 13px; color: var(--text); }
 
581
  padding: 14px;
582
  margin: 14px 0 0;
583
  background: #fbfbfc;
584
+ color: #4f535d;
585
  font-size: 12px;
586
+ line-height: 1.5;
 
587
  }
588
+ .history-list, .compact-list {
589
+ display: grid;
590
+ gap: 10px;
591
+ margin-top: 14px;
592
+ }
593
+ .history-row, .compact-row, .suggestion-row, .log-row {
594
  display: grid;
595
  grid-template-columns: minmax(0, 1fr) auto auto auto;
596
  gap: 12px;
597
  align-items: center;
598
+ width: 100%;
599
  border: 1px solid var(--line);
 
600
  border-radius: 14px;
601
+ background: #fff;
602
  padding: 12px;
603
+ text-align: left;
604
  }
605
+ .compact-row, .suggestion-row, .log-row { grid-template-columns: minmax(0, 1fr) auto; }
606
+ .history-row strong, .compact-row strong, .suggestion-row strong, .log-row strong { display: block; color: var(--text); }
607
+ .history-row small, .compact-row small, .suggestion-row small, .log-row small { display: block; color: var(--muted); margin-top: 3px; line-height: 1.35; }
608
+ .compact-row.locked { border-color: rgba(109, 69, 236, .35); background: #fbf9ff; }
609
+ .compact-row.suppressed, tr.suppressed { opacity: .55; }
610
+ .row-actions { display: flex; flex-wrap: wrap; gap: 7px; justify-content: flex-end; }
 
 
 
 
 
 
 
 
 
611
  .mini-button {
612
+ border: 1px solid var(--line-strong);
613
  border-radius: 999px;
614
  background: #fff;
 
615
  color: var(--text-soft);
616
+ padding: 6px 10px;
617
  font-size: 12px;
618
+ font-weight: 650;
619
+ }
 
 
 
 
 
 
 
620
  .supervision-header {
621
  display: flex;
 
622
  justify-content: space-between;
623
+ gap: 20px;
624
+ align-items: flex-start;
625
+ margin-top: 16px;
626
+ }
627
+ .supervision-header h3, .result-columns h3 { margin: 0; }
628
+ .subtle { color: var(--muted); font-size: 13px; line-height: 1.45; }
629
+ .supervision-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
630
+ .state-summary {
631
+ display: flex;
632
+ flex-wrap: wrap;
633
+ gap: 8px;
634
+ margin: 14px 0;
635
  }
 
 
 
636
  .state-summary span {
637
  border: 1px solid var(--line);
638
  border-radius: 999px;
639
+ background: var(--surface-soft);
640
+ padding: 6px 10px;
641
+ color: var(--text-soft);
642
  font-size: 12px;
 
643
  }
644
  .supervision-tools {
645
  display: grid;
646
+ grid-template-columns: minmax(220px, 1fr) repeat(4, auto);
647
  gap: 10px;
648
  align-items: end;
649
+ margin-top: 14px;
650
+ }
651
+ .supervision-tools label { font-size: 13px; font-weight: 700; }
652
+ .supervision-grid {
 
 
 
 
 
653
  display: grid;
654
+ grid-template-columns: repeat(2, minmax(0, 1fr));
655
+ gap: 16px;
656
+ margin-top: 18px;
657
+ }
658
+ .supervision-grid article {
659
  border: 1px solid var(--line);
660
+ border-radius: 16px;
661
+ padding: 14px;
662
+ background: var(--surface-soft);
 
 
663
  }
664
+ .supervision-grid h4 { margin: 0 0 10px; }
 
 
 
 
665
  .explanation {
666
+ margin: 18px 0 0;
667
+ max-height: 360px;
668
  overflow: auto;
669
  border: 1px solid var(--line);
670
  border-radius: 14px;
671
+ padding: 14px;
672
  background: #fbfbfc;
 
 
 
673
  font-size: 12px;
 
674
  }
675
+ .edited-downloads { padding: 0; }
676
+
677
+ .result-columns {
678
+ display: grid;
679
+ grid-template-columns: minmax(0, 1fr);
680
+ gap: 20px;
681
+ margin-top: 16px;
682
+ }
683
+ .table-wrap {
684
+ overflow: auto;
685
+ border: 1px solid var(--line);
686
+ border-radius: 16px;
687
+ margin-top: 12px;
688
+ background: #fff;
689
+ }
690
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
691
+ th, td {
692
+ text-align: left;
693
+ padding: 10px 12px;
694
+ border-bottom: 1px solid var(--line);
695
+ white-space: nowrap;
696
+ }
697
+ th { color: var(--muted); font-weight: 700; background: var(--surface-soft); }
698
+ tr.selected { background: #f6f2ff; }
699
+ tr.low-confidence { background: #fff8ed; }
700
+ tr:last-child td { border-bottom: 0; }
701
 
702
  @media (max-width: 1180px) {
703
+ .shell { width: min(100% - 34px, 1740px); }
704
  .app-layout {
705
  grid-template-columns: 1fr;
706
  grid-template-areas:
 
707
  "wave"
708
+ "controls"
709
  "samples"
710
+ "workbench";
 
 
 
 
711
  }
712
+ .control-card { min-height: auto; }
713
+ .sample-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 18px; }
 
 
714
  }
715
  @media (max-width: 760px) {
716
+ .shell { width: min(100% - 22px, 1740px); padding-top: 18px; }
717
+ .topbar { align-items: stretch; flex-direction: column; gap: 16px; }
718
+ .topbar-actions { justify-content: space-between; }
719
  .primary-button { width: 100%; min-width: 0; }
720
+ .backend-pill { display: none; }
721
+ .file-copy strong { font-size: 19px; }
722
+ .waveform { height: 320px; min-height: 320px; }
723
+ .wave-status-row { position: static; padding: 14px 14px 0; }
724
+ .status-text { display: none; }
725
+ .transport-row { grid-template-columns: 48px 92px 1fr; gap: 12px; padding: 0 18px 20px; }
726
+ .round-play { width: 48px; height: 48px; }
727
+ .transport-time { font-size: 14px; }
728
+ .review-strip, .supervision-grid { grid-template-columns: 1fr; }
729
+ .supervision-tools { grid-template-columns: 1fr; }
730
+ .control-grid { grid-template-columns: 1fr; }
731
+ }