9b655d5a07
* feat(windows): leaner portable package and opt-in in-app updater (#421) Issue #421 asked for Python embedded in a single EXE so updating would not mean copying ~20k loose files over an existing install. A onefile EXE is not viable for this stack (onefile modes re-extract the whole multi-GB payload on every launch, and torch/onnxruntime fight frozen-import hooks), so this addresses the root cause instead: ship less, and stop making users hand-copy a full zip for a release that only changed app code. Leaner package (make-portable.ps1): - Stripping is now unconditional. The -StripVenv opt-in gate was a silent regression risk: nothing stopped a future workflow edit from shipping the unstripped venv with no error. - Also strips stdlib base/Lib/test and per-package test/tests dirs. - Deliberately does NOT strip .dist-info/RECORD. pip needs it to replace a package, and install_cuda_torch pip-installs into this venv on every NVIDIA machine at first run; removing it yields "Failed to uninstall ... missing RECORD file". - Adds a post-strip import check so an over-aggressive strip fails the build rather than a release. Updater (main.rs, catalog.js): - New commands installed_runtime_id, download_app_update, apply_app_update. - Opt-in: the check on launch is unchanged, but download and apply are each an explicit click. It never auto-applies and never interrupts a running job. - Replaces StemDeck.exe and backend/ only. python/ is never touched, because an NVIDIA install rewrites it with CUDA torch at first run and torchDeviceSettled skips ensure_torch_device once the device is cuda, so swapping the directory would silently drop that machine to CPU with no recovery. - The runtime id (uv.lock + interpreter major.minor) is a compatibility gate, not a download trigger: if a release changed the Python dependency set the updater stands down and points at the full download. Only 19 of the last 200 commits touch uv.lock, so the fast path covers most releases. - apply_app_update stages and validates everything before any destructive rename, stops the backend synchronously first (the existing stop_backend returns before the process dies, which would have made every update fail on Windows), and retries renames past transient AV/indexer handles. - Known gap, documented in code: the two exe renames are not atomic. A hard crash in that window leaves StemDeck.exe.old needing a manual rename. Closing it needs a bootstrap launcher that is never itself replaced. CI publishes -app.zip, its .sha256 and -runtime-version.json alongside the unchanged full zips. Fresh installs are unaffected. i18n: the 5 new strings are translated into all 7 language tables, not just English. t() falls back to English silently, so an English-only key looks correct in testing and ships untranslated to six locales. Verified: Windows and Linux (WSL) both compile clean with no new clippy warnings, 39 Rust tests pass on both, JS suites pass, ruff clean. Two new unit tests pin the JSON contract between the PowerShell writer and the Rust reader. Not yet verified: no end-to-end run against a real release. * fix(updater): make the in-app update actually work, verified end to end (#421) Built both packages on a real Windows box and drove the whole flow. Four bugs that only surfaced by running it, none of which static checks could see. 1. Stale version after updating. app_version() read installed dist metadata, which lives in python/ -- the directory the updater deliberately never replaces. A self-updated install kept reporting the old version and would re-offer an update it had already applied, forever. It now prefers the app layer's static/version.json, which moves with backend/. Gitignored, so Docker and source checkouts still fall through to the hatch-vcs metadata. Proven: after a real update, python/ dist-info says 0.13.0 while /api/health reports 0.13.1. 2. The page CSP blocked the whole feature. The UI is served over http by the Python backend, so its connect-src applies: api.github.com is allowed, github.com and objects.githubusercontent.com are not, and that is where release assets live. Fetching the checksum and runtime id from JS was refused, so the pill would simply never appear. Those two reads moved into Rust (check_app_update), whose HTTP client is not bound by the page CSP, so the policy from #171 stays exactly as tight as it was. 3. plugin:event|listen refused by the Tauri ACL. App-defined commands are not ACL-gated but plugin commands are, and the capability does not cover the remote http origin the UI is served from. The progress bar is now indeterminate instead of granting a remote origin event permissions to put a percentage on a 5 MB download. 4. The post-strip import check re-bloated the package. Running Python regenerated 1,912 files / 39 MB of __pycache__ that the strip had just removed, cancelling nearly all of it: the net saving was 180 files. Swept once after the last interpreter run, and backend/ no longer ships a developer's local __pycache__ either. Also: the *.old sweep now runs on every launch rather than only on a version change. apply_app_update relaunches then exits, so on the first launch of the new build Windows still holds StemDeck.exe.old open, the delete fails silently, and gated on a change that already happened it would never retry. Observed for real: 15.7 MB stranded. Verified swept on the next launch. UI: "Update now" is an accent pill BESIDE Download, not a replacement, so the zip stays one click away and is the escape hatch if an update fails. Measured against the published v0.13.0 package: 18,143 -> 16,056 files (-2,087, -11.5%) and 883 -> 850 MB. The real win for #421 is the update path itself: 5 MB and 123 files instead of 284 MB and 16,056. Verified on this machine: a real 6-stem Demucs separation through the stripped package; the full notify -> Update now -> download -> restart -> relaunch cycle, after which user data (job, 7 stems, 130 MB of models), portable.txt, cpu-only and python/ were all untouched; and the safety gate correctly declining, with no download attempted, when the release's runtime id differs. Not covered: the NVIDIA package was not built, though the risk that motivated the gate is structurally gone now that python/ is never swapped. * fix: address code-quality review on the version-source change (#421) Both findings from the automated review were fair. Narrow the bare `except Exception: pass` in app_version() to (OSError, ValueError, AttributeError). That is bandit B110, which this repo's own security conventions call out. The three cover every real failure here -- absent or unreadable file, invalid JSON or bad encoding, and valid JSON that is not an object so has no .get -- while letting an actual bug in the function surface instead of silently degrading the reported version. Bandit now reports no issues for the file. Use one import style in test_health_api.py so app.main is no longer imported both as `import app.main as main` and `from app.main import app` in the same module. Also added a "[]" case: JSON that parses but is not an object, which is the AttributeError branch the narrowed except now names explicitly. * feat(updater): extend the in-app update to Linux (#421) Linux ships the same shape as Windows -- executable, backend/ and python/ side by side -- so the updater generalises rather than needing a second design. The platform-specific parts are now three small seams: the archive format, the executable name, and one new gate. Rust: - widen the updater's cfg gates from `windows` to `any(windows, linux)`, and replace extract_zip_archive with extract_update_archive, which uses zip on Windows and the existing extract_tar_archive on Linux - APP_EXE_NAME so the swap and the leftover sweep stop hardcoding StemDeck.exe - stop_backend_and_wait now sends SIGTERM and waits before escalating on unix, matching what stop_backend already does on window close - new app_root_is_writable gate: packaging/linux/install.sh offers a global install into /opt/stemdeck, which is root-owned while the app runs as the user. check_app_update declines up front rather than failing part way through a swap. Windows portable installs are user-writable by construction, but the probe is cheap and honest on both. tar rather than zip on Linux is deliberate: it preserves the executable bit. A zip would land StemDeck without +x and the relaunch after an update would fail with a permission error. Packaging (scripts/linux/make-portable.sh): - write python/runtime-version.json using the same uv.lock + interpreter major.minor formula as the Windows script, so the compatibility gate behaves identically on both - bring the strip to parity: stdlib test/, per-package test/tests, a post-strip import check, and a final __pycache__ sweep after the last interpreter run - PUBLISH_UPDATER_ASSETS=1 emits the slim app-layer tarball, its checksum and the runtime marker; wired into the CPU build in linux-release.yml since StemDeck and backend/ are identical between both variants Frontend: updaterAssetNames() maps the platform to its asset names, and the wiring is gated on that rather than on os === "windows". macOS is deliberately still excluded, and the comments now say why rather than just that it is: backend_dir() resolves the backend inside the downloaded runtime pack rather than the .app, so its app layer is a different thing and the existing runtime-pack updater already covers most of it. Verified: both platforms compile clean with no new clippy warnings, 42 tests on Windows and 43 on Linux (the extra one is the read-only-root gate, which is meaningless on Windows). The app-layer archive was round-tripped on Linux to confirm it contains exactly StemDeck + backend/, that python/ does not leak into it, that the executable bit survives, and that replacing a running binary works. Not yet run end to end against a real Linux release. * fix(updater): see pre-releases, and compile the Rust in CI (#421) Two gaps that would each have undermined the update flow on release day. The update check polled /releases/latest, which GitHub defines as the most recent NON-PRERELEASE, non-draft release. Ship a version with the pre-release box ticked and it becomes invisible: no notification, no update button, on any platform, with nothing in the logs to explain it. StemDeck has always published even its alphas as normal releases (v0.8.0-alpha.17 has prerelease=false), which is the only reason this has not bitten yet -- it was a trap waiting on someone ticking a box. Now polls the releases list and takes the newest non-draft, so it is correct either way. Drafts stay excluded: they are already invisible unauthenticated, and a maintainer should not be offered a release whose assets do not exist yet. windows-check.yml and macos-check.yml now also run on pull requests that touch desktop/src-tauri/**, not workflow_dispatch only. This PR added roughly 600 lines of mostly cfg-gated Rust across two commits and every CI check passed without compiling a single line of it; the comment at the top of windows-check.yml notes that exact gap already shipped a broken Windows build in v0.11.1's first release attempt. Scoped by path so the self-hosted runners see no extra load from the majority of PRs, which never go near src-tauri. This also gets the macOS branch compiled for the first time. Local verification covered Windows and Linux, so the cfg(not(any(windows, linux))) arm of the three updater commands has never been near a compiler. * test(e2e): match the releases-list shape the app now polls (#421) The update-check stub returned a single release object, which was right for /releases/latest. The app now polls the releases list so a pre-release is still seen, so the fixture has to return an array or checkForUpdate bails and the release card never appears. Caught by frontend-e2e on the previous commit, which is the suite doing exactly its job: the only assertion that covers this path is report-failure.spec.mjs:98, and it went red immediately. * feat(i18n): add French, and make the runtime id line-ending independent French is a complete table, not a partial one: 435 keys, the same set German and Portuguese carry (English's 443 minus the ten Polish-only .few/.many forms and the bare upload.skippedFiles, plus singular forms for the three playlist.skip.* families). French takes the one/other buckets, so plural() needs no change. Verified with the checks from .claude/rules/i18n.md: the drift check reports clean, and separately there are zero {placeholder} mismatches and zero HTML tag mismatches against English. The 27 strings identical to English are genuinely identical in French (Piano, Solo, Transport, Position, LUFS, Standard, Port, the brand names, CUDA (NVIDIA), MPS (Apple Silicon)). Separately: the runtime id was being computed from the raw bytes of uv.lock, so a Windows checkout with core.autocrlf=true hashed CRLF and Linux hashed LF, and the same lockfile produced two different ids -- caught by building the Linux package and seeing py3.12-d74d6ef80c5e9d1f where Windows had produced py3.12-dbda45e38e1044cf. Each platform stayed self-consistent so the gate still worked, but the id would shift spuriously if a runner's autocrlf ever changed, silently declining app-only updates that were in fact compatible. Both scripts now hash the content with newlines normalised; PowerShell, bash and a reference Python implementation all agree on d74d6ef80c5e9d1f. * chore: pin Unraid template to 0.14.0 Per .claude/rules/unraid-template-version.md this is an explicit decision each time, not a default. Confirmed for this release. The 0.14.0 GHCR image is published by docker-publish.yml when the release is created, so the tag exists shortly after this lands. --------- Co-authored-by: Thales <>
3229 lines
113 KiB
CSS
3229 lines
113 KiB
CSS
/* ═══════════════════════════════════════════
|
|
DAW UI — flat dark design
|
|
═══════════════════════════════════════════ */
|
|
|
|
/* ── Reset ── */
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; height: 100%; background: #0a0d10; overflow: hidden; }
|
|
body { font-family: var(--font-sans); color: var(--fg); }
|
|
button { font-family: inherit; }
|
|
input, textarea { font-family: inherit; }
|
|
|
|
/* ── Root ── */
|
|
.daw {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font-family: var(--font-sans);
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
letter-spacing: -0.005em;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
/* reset any old .app grid */
|
|
grid-template-columns: none !important;
|
|
grid-template-rows: none !important;
|
|
}
|
|
|
|
/* ── Utilities ── */
|
|
.daw .uplabel {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
.daw .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
.daw .hidden { display: none !important; }
|
|
|
|
/* ═══════════════════════════════════
|
|
TOPBAR
|
|
═══════════════════════════════════ */
|
|
.daw-topbar {
|
|
height: 77px;
|
|
flex-shrink: 0;
|
|
background: var(--bg-2);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 16px;
|
|
gap: 14px;
|
|
}
|
|
|
|
/* Brand */
|
|
.daw-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-brand img { height: 30px; width: auto; display: block; }
|
|
.daw-brand-name {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.015em;
|
|
line-height: 1;
|
|
}
|
|
.daw-brand-name .fg { color: var(--fg); }
|
|
.daw-brand-name .accent{ color: var(--accent); }
|
|
|
|
.daw-version {
|
|
display: none !important; /* version shown in notification panel instead */
|
|
}
|
|
|
|
/* Topbar separator */
|
|
.daw-sep {
|
|
width: 1px;
|
|
height: 28px;
|
|
background: var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Composer pill ── */
|
|
.daw-composer {
|
|
flex: 1;
|
|
min-width: 0;
|
|
height: 50px;
|
|
display: flex;
|
|
align-items: stretch;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius);
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02), 0 1px 8px rgba(0,0,0,0.3);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* URL zone */
|
|
.daw-url-zone {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 0 14px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
cursor: text;
|
|
}
|
|
/* Hide decorative SVG icons in the topbar */
|
|
.daw-url-zone > svg { display: none; }
|
|
.daw-process-btn > svg { display: none; }
|
|
.daw-url-zone svg { flex-shrink: 0; }
|
|
|
|
.daw-url-zone input[type="url"] {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: none;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--fg-2);
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
letter-spacing: -0.005em;
|
|
}
|
|
.daw-url-zone input[type="url"]::placeholder { color: var(--muted); }
|
|
|
|
/* Upload button inside URL zone */
|
|
.daw-upload-btn {
|
|
width: 30px;
|
|
height: 30px;
|
|
flex-shrink: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
|
|
}
|
|
.daw-upload-btn:hover { background: var(--panel-2); color: var(--fg-2); border-color: var(--border-strong); }
|
|
|
|
/* File pill (shown when file selected) */
|
|
.file-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 2px 8px;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 6px;
|
|
background: var(--panel-2);
|
|
color: var(--fg-2);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
}
|
|
.file-pill.hidden { display: none !important; }
|
|
.file-name { overflow: hidden; text-overflow: ellipsis; max-width: 140px; }
|
|
.file-size { color: var(--muted); flex-shrink: 0; }
|
|
.file-clear {
|
|
width: 16px; height: 16px;
|
|
background: none; border: none;
|
|
color: var(--muted); cursor: pointer;
|
|
font-size: 14px; line-height: 1;
|
|
display: flex; align-items: center; justify-content: center;
|
|
border-radius: 3px; flex-shrink: 0; padding: 0;
|
|
}
|
|
.file-clear:hover { color: var(--fg); background: var(--panel-3); }
|
|
|
|
/* url-wrap alias (JS uses .url-wrap for drag events) */
|
|
.daw-url-zone { position: relative; }
|
|
.daw-url-zone.drag-over {
|
|
background: rgba(244,183,64,0.06);
|
|
box-shadow: inset 0 0 0 1px rgba(244,183,64,0.3);
|
|
}
|
|
.daw-url-zone.has-file input[type="url"] { display: none; }
|
|
|
|
/* Composer vertical divider */
|
|
.daw-composer-sep {
|
|
width: 1px;
|
|
background: var(--border);
|
|
align-self: stretch;
|
|
margin: 6px 0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Stem section */
|
|
.daw-stem-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 0 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-extract-label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
.daw-stem-chips { display: flex; gap: 4px; }
|
|
|
|
/* Stem choice buttons */
|
|
.stem-choice {
|
|
height: 30px;
|
|
padding: 0 11px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
background: none;
|
|
color: var(--muted-2);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
letter-spacing: -0.005em;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
|
|
}
|
|
.stem-choice[aria-pressed="true"] {
|
|
border-color: color-mix(in srgb, var(--color) 60%, transparent);
|
|
background: color-mix(in srgb, var(--color) 14%, transparent);
|
|
color: var(--color);
|
|
}
|
|
.stem-dot {
|
|
width: 5px; height: 5px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
opacity: 0.85;
|
|
}
|
|
.stem-choice[aria-pressed="false"] .stem-dot { opacity: 0.4; }
|
|
|
|
/* "All" toggle */
|
|
.stem-choice-all {
|
|
border-color: var(--border-strong);
|
|
color: var(--fg-2);
|
|
font-size: 12px;
|
|
}
|
|
.stem-choice-all[aria-pressed="true"] {
|
|
border-color: var(--accent);
|
|
background: rgba(244,183,64,0.12);
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* Vocals: All / Lead + Backing (on-demand split, #275) */
|
|
.vocal-mode-toggle {
|
|
display: inline-flex;
|
|
height: 30px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
}
|
|
.vocal-mode-toggle.hidden { display: none; }
|
|
.vocal-mode-btn {
|
|
height: 100%;
|
|
padding: 0 10px;
|
|
border: none;
|
|
background: none;
|
|
color: var(--muted-2);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.vocal-mode-btn + .vocal-mode-btn { border-left: 1px solid var(--border); }
|
|
.vocal-mode-btn[aria-pressed="true"] {
|
|
background: color-mix(in srgb, var(--vocals) 16%, transparent);
|
|
color: var(--vocals);
|
|
}
|
|
|
|
/* Process button */
|
|
.daw-process-btn {
|
|
height: auto;
|
|
padding: 0 20px;
|
|
background: linear-gradient(180deg, var(--accent), var(--accent-2));
|
|
border: none;
|
|
border-left: 1px solid #b97f1c;
|
|
color: #1a1206;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
cursor: pointer;
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.22);
|
|
flex-shrink: 0;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.daw-process-btn:hover { background: linear-gradient(180deg, #f8c054, #e0a032); }
|
|
.daw-process-btn:active { transform: translateY(1px); }
|
|
.daw-process-btn:disabled {
|
|
opacity: 0.8;
|
|
cursor: progress;
|
|
filter: saturate(0.85);
|
|
}
|
|
|
|
/* Loading spinner on process btn */
|
|
.daw-process-btn.loading > svg { display: none; }
|
|
.daw-process-btn.loading { filter: saturate(0.85); }
|
|
.daw-process-btn.loading::before {
|
|
content: "";
|
|
width: 14px; height: 14px; flex: 0 0 14px;
|
|
border-radius: 50%;
|
|
background: conic-gradient(currentColor 0deg 270deg, transparent 270deg 360deg);
|
|
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 0);
|
|
mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 0);
|
|
animation: daw-spin 0.85s linear infinite;
|
|
}
|
|
@keyframes daw-spin { to { transform: rotate(360deg); } }
|
|
|
|
/* Icon button */
|
|
.daw-iconbtn {
|
|
width: 30px; height: 30px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
color: var(--fg-2);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-iconbtn:hover { background: var(--panel-2); border-color: var(--border); color: var(--fg); }
|
|
.daw-iconbtn:active { transform: scale(0.96); }
|
|
|
|
/* Notification panel */
|
|
.daw-notif-wrap { position: relative; }
|
|
.daw-notif-panel {
|
|
display: none;
|
|
position: absolute;
|
|
top: calc(100% + 8px);
|
|
right: 0;
|
|
width: 280px;
|
|
background: var(--panel-2);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius);
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
z-index: 200;
|
|
overflow: hidden;
|
|
}
|
|
.daw-notif-wrap.open .daw-notif-panel { display: block; }
|
|
.daw-notif-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.daw-notif-close {
|
|
width: 22px; height: 22px;
|
|
background: none; border: none;
|
|
color: var(--muted); cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
border-radius: 5px;
|
|
}
|
|
.daw-notif-close:hover { background: var(--panel-3); color: var(--fg); }
|
|
/* Sits between the label and the close button; the header is space-between, so
|
|
push it right and leave a gap before the X. */
|
|
.daw-notif-clear {
|
|
margin-left: auto; margin-right: 8px;
|
|
background: none; border: none; padding: 0;
|
|
color: var(--muted); cursor: pointer;
|
|
font-family: inherit; font-size: 11px;
|
|
}
|
|
.daw-notif-clear:hover { color: var(--fg); text-decoration: underline; }
|
|
.daw-notif-list { padding: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
.daw-notif-card {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
padding: 10px 12px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
.daw-notif-release { border-color: rgba(244,183,64,0.25); }
|
|
.daw-notif-card-icon {
|
|
width: 28px; height: 28px; flex-shrink: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
background: rgba(244,183,64,0.12);
|
|
border-radius: 6px;
|
|
color: var(--accent);
|
|
}
|
|
/* A failure, not an announcement. The amber tint lives in the base icon rule
|
|
rather than in .daw-notif-release, so both properties need overriding. */
|
|
.daw-notif-error { border-color: rgba(214,90,74,0.35); cursor: pointer; }
|
|
.daw-notif-error:hover { background: var(--panel-2); }
|
|
.daw-notif-error .daw-notif-card-icon {
|
|
background: rgba(214,90,74,0.14);
|
|
color: var(--danger);
|
|
}
|
|
.daw-notif-error:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
.daw-notif-card-body { flex: 1; min-width: 0; }
|
|
.daw-notif-card-title { font-size: 12px; font-weight: 600; color: var(--fg); }
|
|
.daw-notif-card-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
|
.daw-notif-badge {
|
|
position: absolute; top: 4px; right: 4px;
|
|
width: 7px; height: 7px;
|
|
background: #f4b740; border-radius: 50%;
|
|
pointer-events: none;
|
|
}
|
|
.daw-notif-dismiss {
|
|
flex-shrink: 0; align-self: flex-start;
|
|
width: 18px; height: 18px;
|
|
background: none; border: none; cursor: pointer;
|
|
color: var(--muted); display: flex; align-items: center; justify-content: center;
|
|
border-radius: 4px; padding: 0;
|
|
}
|
|
.daw-notif-dismiss:hover { background: var(--panel-3); color: var(--fg); }
|
|
.daw-notif-empty {
|
|
font-size: 12px; color: var(--muted);
|
|
text-align: center; padding: 16px 0; margin: 0;
|
|
}
|
|
|
|
/* Hidden collapsed strip (JS compat, not shown in new design) */
|
|
.appbar-icon-strip.hidden { display: none !important; }
|
|
|
|
/* ═══════════════════════════════════
|
|
BODY LAYOUT
|
|
═══════════════════════════════════ */
|
|
.daw-body {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
}
|
|
|
|
/* ═══════════════════════════════════
|
|
SIDEBAR / CATALOG
|
|
═══════════════════════════════════ */
|
|
.sidebar {
|
|
width: 390px;
|
|
flex-shrink: 0;
|
|
background: var(--bg-2);
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
overflow: hidden;
|
|
transition: width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
/* Rail */
|
|
.sidebar-rail {
|
|
width: 66px;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
gap: 4px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.rail-btn {
|
|
width: 40px; height: 40px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
border-radius: 8px;
|
|
color: var(--muted);
|
|
font-size: 9px;
|
|
font-family: var(--font-sans);
|
|
cursor: pointer;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
border: none;
|
|
background: none;
|
|
/* remove old pseudo-element strip */
|
|
}
|
|
.rail-btn::before { display: none !important; }
|
|
.rail-btn:hover { background: var(--panel); color: var(--fg-2); }
|
|
.rail-btn.active { color: var(--accent); background: var(--panel); }
|
|
.rail-btn svg { flex-shrink: 0; }
|
|
/* max-width + ellipsis: a translated label (e.g. Polish "Ustawienia") can run
|
|
longer than English at this 9px size inside the fixed 40px button. The
|
|
full text is still reachable via the button's title/aria-label; this just
|
|
guarantees the label itself can never visually overflow the rail column. */
|
|
.rail-btn > span { white-space: nowrap; max-width: 56px; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
/* Queue rail button. Only present while something is importing, so the rail
|
|
stays as it is today for anyone not using the queue. */
|
|
.rail-queue { position: relative; }
|
|
.rail-badge {
|
|
position: absolute; top: 2px; right: 2px;
|
|
min-width: 15px; height: 15px; padding: 0 4px;
|
|
border-radius: 8px;
|
|
background: var(--accent); color: #1a1a1a;
|
|
font-size: 9px; font-weight: 700; line-height: 15px;
|
|
text-align: center;
|
|
font-variant-numeric: tabular-nums;
|
|
pointer-events: none;
|
|
}
|
|
/* "We Recommend" is wider than the other one-word rail labels, so it stacks onto
|
|
two centered lines; let the button grow so the second line + icon aren't clipped. */
|
|
.rail-btn.rail-recommend { height: auto; min-height: 40px; padding: 3px 0; }
|
|
.rail-btn.rail-recommend span { white-space: normal; text-align: center; line-height: 1.1; }
|
|
|
|
/* Sidebar body */
|
|
.sidebar-body {
|
|
flex: 1;
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
opacity: 1;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
/* Search */
|
|
.daw-search {
|
|
height: 32px;
|
|
border: 1px solid var(--border);
|
|
background: var(--panel);
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 0 10px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
.daw-search svg { flex-shrink: 0; }
|
|
.daw-search input {
|
|
flex: 1; min-width: 0;
|
|
border: none; outline: none;
|
|
background: transparent;
|
|
color: var(--fg);
|
|
font: inherit;
|
|
}
|
|
.daw-search input::placeholder { color: var(--muted); }
|
|
.search-kbd {
|
|
font-size: 10px;
|
|
color: var(--muted-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 0 5px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Lib header row */
|
|
.daw-lib-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 4px;
|
|
min-height: 22px;
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-lib-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.daw-lib-count { color: var(--muted-2); font-weight: 500; margin-left: 4px; }
|
|
.new-folder-btn {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 3px 9px 3px 7px;
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
color: var(--muted);
|
|
font-size: 11px; font-weight: 500;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
|
|
}
|
|
.new-folder-btn:hover {
|
|
background: var(--panel);
|
|
color: var(--fg-2);
|
|
border-color: var(--border-2, rgba(255,255,255,0.15));
|
|
}
|
|
|
|
/* Lib list (JS populates with .lib-item, .folder, .cat-item) */
|
|
.daw-lib-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
/* Thin auto-hide scrollbar shared by the library list, the mixer column and the
|
|
waveform panel (both axes) — hidden until the area is hovered/focused, reserves
|
|
no visible gutter. `.daw .wave-scroll` overrides the gold bar from waves.css. */
|
|
.daw-lib-list,
|
|
.mixer-column,
|
|
.daw .wave-scroll {
|
|
/* Firefox: thin overlay-style bar, fades to transparent track */
|
|
scrollbar-width: thin;
|
|
scrollbar-color: transparent transparent;
|
|
}
|
|
.daw-lib-list:hover,
|
|
.daw-lib-list:focus-within,
|
|
.mixer-column:hover,
|
|
.mixer-column:focus-within,
|
|
.daw .wave-scroll:hover,
|
|
.daw .wave-scroll:focus-within {
|
|
scrollbar-color: rgba(148, 163, 184, 0.4) transparent;
|
|
}
|
|
/* WebKit (Chrome/Safari/WKWebView): thin thumb, hidden until hover/scroll */
|
|
.daw-lib-list::-webkit-scrollbar,
|
|
.mixer-column::-webkit-scrollbar,
|
|
.daw .wave-scroll::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
.daw-lib-list::-webkit-scrollbar-track,
|
|
.mixer-column::-webkit-scrollbar-track,
|
|
.daw .wave-scroll::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
.daw-lib-list::-webkit-scrollbar-thumb,
|
|
.mixer-column::-webkit-scrollbar-thumb,
|
|
.daw .wave-scroll::-webkit-scrollbar-thumb {
|
|
background: transparent;
|
|
border: 2px solid transparent;
|
|
background-clip: padding-box;
|
|
border-radius: 999px;
|
|
}
|
|
.daw-lib-list:hover::-webkit-scrollbar-thumb,
|
|
.daw-lib-list:focus-within::-webkit-scrollbar-thumb,
|
|
.mixer-column:hover::-webkit-scrollbar-thumb,
|
|
.mixer-column:focus-within::-webkit-scrollbar-thumb,
|
|
.daw .wave-scroll:hover::-webkit-scrollbar-thumb,
|
|
.daw .wave-scroll:focus-within::-webkit-scrollbar-thumb {
|
|
background: rgba(148, 163, 184, 0.4);
|
|
background-clip: padding-box;
|
|
}
|
|
.daw-lib-list::-webkit-scrollbar-thumb:hover,
|
|
.mixer-column::-webkit-scrollbar-thumb:hover,
|
|
.daw .wave-scroll::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(148, 163, 184, 0.6);
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
/* Library item */
|
|
.lib-item {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.lib-item:hover { background: var(--panel); }
|
|
.lib-item.active {
|
|
background: linear-gradient(90deg, rgba(244,183,64,0.10) 0%, var(--panel-2) 60%);
|
|
}
|
|
.lib-item.active::before {
|
|
content: '';
|
|
position: absolute; left: -2px; top: 6px; bottom: 6px;
|
|
width: 3px; border-radius: 2px;
|
|
background: var(--accent);
|
|
}
|
|
.lib-cover {
|
|
width: 36px; height: 36px; border-radius: 6px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 9px; font-weight: 700; letter-spacing: 0.1em;
|
|
color: rgba(255,255,255,0.85);
|
|
flex-shrink: 0;
|
|
}
|
|
.lib-meta { flex: 1; min-width: 0; }
|
|
.lib-meta .t { font-size: 13px; color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.lib-meta .s { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.lib-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
|
|
/* cat-item (legacy catalog items JS creates) */
|
|
.cat-item {
|
|
display: flex;
|
|
gap: 9px;
|
|
align-items: center;
|
|
padding: 7px 8px;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background var(--t-fast), border-color var(--t-fast);
|
|
}
|
|
.cat-item:hover { background: var(--panel-2); border-color: var(--border); }
|
|
.cat-item.active { background: var(--panel-2); border-color: rgba(244,183,64,0.3); }
|
|
.cat-thumb {
|
|
width: 36px; height: 36px;
|
|
border-radius: 6px; flex-shrink: 0;
|
|
overflow: hidden; border: 1px solid var(--border);
|
|
background: var(--panel);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--muted);
|
|
}
|
|
.cat-thumb svg, .cat-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
.cat-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
|
|
.cat-title { color: var(--fg); font-size: 12px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.cat-sub { color: var(--muted); font-size: 10.5px; display: flex; gap: 5px; white-space: nowrap; overflow: hidden; }
|
|
.cat-status { width: 6px; height: 6px; border-radius: 50%; background: #5fbc56; flex-shrink: 0; }
|
|
.cat-status.processing { background: var(--accent); animation: cat-pulse 1.4s infinite; }
|
|
.cat-status.unavailable { background: #666; }
|
|
.cat-item.unavailable { cursor: pointer; }
|
|
.cat-item.unavailable .cat-title { opacity: 0.55; }
|
|
.cat-unavailable-warning { color: var(--accent); font-weight: 600; opacity: 1; }
|
|
@keyframes cat-pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
|
|
|
|
/* Import queue: a waiting row and a running row must be told apart at a
|
|
glance, so they differ in three ways -- wording, the presence of a progress
|
|
bar, and a hollow versus filled status dot. */
|
|
.cat-queue-label { color: var(--accent); font-variant-numeric: tabular-nums; }
|
|
.cat-item.queue-waiting .cat-queue-label { color: var(--muted); }
|
|
/* Waiting rows are not loadable yet, so they read as pending rather than
|
|
inviting a click. */
|
|
.cat-item.queue-waiting { cursor: default; }
|
|
.cat-item.queue-waiting .cat-thumb { opacity: 0.55; }
|
|
.cat-item.queue-waiting .cat-title { color: var(--muted); }
|
|
.cat-item.queue-waiting .cat-status {
|
|
background: transparent;
|
|
box-shadow: inset 0 0 0 1.5px var(--muted);
|
|
animation: none;
|
|
}
|
|
.cat-progress {
|
|
height: 3px; border-radius: 2px; margin-top: 4px;
|
|
background: rgba(255, 255, 255, 0.09); overflow: hidden;
|
|
}
|
|
.cat-progress-fill {
|
|
height: 100%; width: 0; border-radius: 2px;
|
|
background: var(--accent);
|
|
transition: width 260ms linear;
|
|
}
|
|
|
|
/* Queue view rows. Unlike a library row these are not loadable -- there is
|
|
nothing to load yet -- so they get no hover affordance, just a cancel. */
|
|
.queue-row { cursor: default; }
|
|
.queue-row:hover { background: var(--panel); }
|
|
.queue-row .cat-meta { gap: 3px; }
|
|
.queue-cancel {
|
|
width: 22px; height: 22px; border: 0; border-radius: 5px;
|
|
background: transparent; color: var(--muted); cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
padding: 0; flex-shrink: 0; opacity: 0.65;
|
|
transition: opacity var(--t-fast), background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.queue-row:hover .queue-cancel { opacity: 1; }
|
|
.queue-cancel:hover { background: rgba(217, 83, 79, 0.16); color: #e06c68; }
|
|
.queue-cancel:disabled { opacity: 0.3; cursor: default; }
|
|
.queue-section .lib-section-head { margin-bottom: 6px; }
|
|
|
|
/* Restored-from-last-session banner. Opening the app never starts separating
|
|
on its own, so this is the only way that queue moves. */
|
|
.queue-paused-banner {
|
|
display: flex; align-items: center; gap: 8px;
|
|
margin: 0 0 8px; padding: 8px 10px;
|
|
border: 1px solid rgba(232, 168, 56, 0.28);
|
|
border-radius: 8px;
|
|
background: rgba(232, 168, 56, 0.08);
|
|
}
|
|
.queue-paused-text { flex: 1; font-size: 11px; color: var(--fg-2); line-height: 1.4; }
|
|
.queue-start-btn {
|
|
flex-shrink: 0; min-height: 26px; padding: 0 12px;
|
|
border-radius: 6px; cursor: pointer;
|
|
border: 1px solid rgba(232, 168, 56, 0.45);
|
|
background: rgba(232, 168, 56, 0.18);
|
|
color: var(--accent);
|
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
|
}
|
|
.queue-start-btn:hover { background: rgba(232, 168, 56, 0.28); }
|
|
.queue-start-btn:disabled { opacity: 0.5; cursor: default; }
|
|
|
|
/* Reordering. Position in a serial queue is the difference between "ready now"
|
|
and "ready in half an hour", so waiting rows drag. */
|
|
.queue-row.queue-waiting { cursor: grab; }
|
|
.queue-row.dragging { opacity: 0.4; cursor: grabbing; }
|
|
.queue-row.drop-below { box-shadow: 0 2px 0 0 var(--accent); }
|
|
.queue-grip {
|
|
display: flex; align-items: center; flex-shrink: 0;
|
|
margin-right: -2px; color: var(--muted); opacity: 0;
|
|
transition: opacity var(--t-fast);
|
|
}
|
|
.queue-row:hover .queue-grip { opacity: 0.7; }
|
|
.queue-top {
|
|
width: 22px; height: 22px; border: 0; border-radius: 5px;
|
|
background: transparent; color: var(--muted); cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center;
|
|
padding: 0; flex-shrink: 0; opacity: 0;
|
|
transition: opacity var(--t-fast), background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.queue-row:hover .queue-top { opacity: 0.65; }
|
|
.queue-top:hover { background: rgba(232, 168, 56, 0.16); color: var(--accent); opacity: 1; }
|
|
.queue-top:disabled { opacity: 0.3; cursor: default; }
|
|
.cat-del {
|
|
width: 20px; height: 20px; border: 0; border-radius: 5px;
|
|
background: transparent; color: var(--muted); cursor: pointer;
|
|
opacity: 0; display: flex; align-items: center; justify-content: center;
|
|
padding: 0; flex-shrink: 0; transition: opacity var(--t-fast), background var(--t-fast);
|
|
}
|
|
.cat-item:hover .cat-del { opacity: 1; }
|
|
.cat-del:hover { background: rgba(21,31,39,0.8); color: var(--danger); }
|
|
|
|
/* Folder items (legacy catalog) */
|
|
.folder { display: flex; flex-direction: column; }
|
|
.folder-head {
|
|
--folder-color: var(--accent);
|
|
display: flex; align-items: center; gap: 7px;
|
|
padding: 6px 8px; border-radius: 7px;
|
|
cursor: pointer; color: var(--muted);
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
user-select: none;
|
|
}
|
|
.folder-head:hover { background: var(--panel); color: var(--fg); }
|
|
.folder-head .f-chevron { width: 11px; height: 11px; transition: transform 180ms ease; flex-shrink: 0; color: var(--muted); }
|
|
.folder.collapsed .folder-head .f-chevron { transform: rotate(-90deg); }
|
|
.folder-head .f-icon { width: 13px; height: 13px; flex-shrink: 0; color: var(--folder-color); }
|
|
.folder-head .f-name {
|
|
font-size: 12px; font-weight: 500; color: inherit;
|
|
flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
background: transparent; border: none; padding: 0; font-family: inherit; outline: none; cursor: inherit;
|
|
}
|
|
.folder-head .f-count { font-size: 10.5px; color: var(--muted); }
|
|
.folder-color-dot {
|
|
width: 13px; height: 13px; border: 1px solid rgba(255,255,255,0.22);
|
|
border-radius: 999px; background: var(--folder-color); cursor: pointer; padding: 0;
|
|
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.22);
|
|
}
|
|
.folder-head .f-del,
|
|
.folder-head .f-subfolder {
|
|
width: 18px; height: 18px; border: none; background: transparent;
|
|
color: var(--muted); cursor: pointer; opacity: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
border-radius: 4px; padding: 0; transition: opacity var(--t-fast), color var(--t-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
.folder-head:hover .f-del,
|
|
.folder-head:hover .f-subfolder { opacity: 1; }
|
|
.folder-head .f-del:hover { background: rgba(21,31,39,0.8); color: var(--danger); }
|
|
.folder-head .f-subfolder:hover { background: rgba(21,31,39,0.8); color: var(--fg-2); }
|
|
/* Drag grip */
|
|
.f-grip {
|
|
display: none; align-items: center; justify-content: center;
|
|
cursor: grab; color: var(--muted); flex-shrink: 0;
|
|
padding: 2px; border-radius: 3px;
|
|
transition: color var(--t-fast);
|
|
}
|
|
.f-grip:active { cursor: grabbing; }
|
|
.folder-head:hover .f-grip { display: flex; }
|
|
/* Drop indicators for folder reorder / nest */
|
|
.folder.drop-before > .folder-head { box-shadow: 0 -2px 0 var(--accent); border-radius: 7px 7px 0 0; }
|
|
.folder.drop-after > .folder-head { box-shadow: 0 2px 0 var(--accent); border-radius: 0 0 7px 7px; }
|
|
.folder.drop-into > .folder-head { box-shadow: 0 0 0 1px var(--accent); background: var(--panel); }
|
|
/* Folder being dragged */
|
|
.folder.folder-dragging { opacity: 0.35; pointer-events: none; }
|
|
/* Subfolder indentation */
|
|
.folder.subfolder { margin-left: 12px; }
|
|
.folder-body { display: flex; flex-direction: column; gap: 3px; padding: 3px 0 3px 14px; margin-left: 7px; border-left: 1px solid rgba(157,170,182,0.1); }
|
|
.folder.collapsed .folder-body { display: none; }
|
|
.folder-empty { color: var(--muted); font-size: 11px; padding: 5px 8px; font-style: italic; }
|
|
|
|
/* Folder editor modal */
|
|
.folder-editor-backdrop {
|
|
position: fixed; inset: 0; z-index: 80;
|
|
display: grid; place-items: center;
|
|
background: rgba(3,8,13,0.42); backdrop-filter: blur(2px);
|
|
}
|
|
.folder-editor {
|
|
width: min(300px, calc(100vw - 32px));
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 10px;
|
|
background: linear-gradient(180deg, rgba(28,39,48,0.98), rgba(13,21,29,0.98));
|
|
box-shadow: var(--shadow-panel);
|
|
padding: 12px; color: var(--fg); font-family: var(--font-mono);
|
|
}
|
|
.folder-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; font-size: 13px; font-weight: 600; }
|
|
.folder-editor-close { width: 24px; height: 24px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; display: grid; place-items: center; padding: 0; }
|
|
.folder-editor-close:hover { background: rgba(21,31,39,0.8); color: var(--fg); }
|
|
.folder-editor-field { display: grid; gap: 7px; margin-bottom: 12px; color: var(--muted); font-size: 10.5px; }
|
|
.folder-editor-name { width: 100%; min-height: 34px; border: 1px solid var(--border); border-radius: 7px; background: rgba(10,17,24,0.78); color: var(--fg); font: inherit; font-size: 12px; padding: 0 10px; outline: none; }
|
|
.folder-editor-name:focus { border-color: rgba(244,183,64,0.5); box-shadow: 0 0 0 2px rgba(244,183,64,0.12); }
|
|
.folder-editor-colors { display: flex; align-items: center; gap: 9px; }
|
|
.folder-editor-colors .folder-color-dot { width: 22px; height: 22px; }
|
|
.folder-editor-msg { color: var(--danger); font-size: 11px; margin-top: 8px; }
|
|
.folder-editor-msg:empty { display: none; }
|
|
.folder-editor-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
.folder-editor-actions button { min-height: 30px; border-radius: 7px; font-family: var(--font-mono); font-size: 11px; cursor: pointer; padding: 0 11px; }
|
|
.folder-editor-cancel { border: 1px solid var(--border); background: rgba(21,31,39,0.52); color: var(--muted); }
|
|
.folder-editor-save { border: 1px solid rgba(244,183,64,0.3); background: rgba(244,183,64,0.16); color: var(--accent); }
|
|
|
|
/* Edit Library modal (Settings → opens this centered window) */
|
|
.library-editor-backdrop {
|
|
position: fixed; inset: 0; z-index: 80;
|
|
display: grid; place-items: center;
|
|
background: rgba(3,8,13,0.42); backdrop-filter: blur(2px);
|
|
}
|
|
.library-editor {
|
|
width: min(560px, calc(100vw - 32px));
|
|
/* Fixed height (not max-height) so the dialog stays one size across all tabs
|
|
-- each pane fills it via flex:1, so switching tabs never resizes it. */
|
|
height: min(70vh, 540px);
|
|
display: flex; flex-direction: column;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 10px;
|
|
background: linear-gradient(180deg, rgba(28,39,48,0.98), rgba(13,21,29,0.98));
|
|
box-shadow: var(--shadow-panel);
|
|
padding: 12px; color: var(--fg); font-family: var(--font-mono);
|
|
}
|
|
.library-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; font-size: 13px; font-weight: 600; }
|
|
.library-editor-close { width: 24px; height: 24px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; display: grid; place-items: center; padding: 0; }
|
|
.library-editor-close:hover { background: rgba(21,31,39,0.8); color: var(--fg); }
|
|
.library-editor-table-wrap { flex: 1; min-height: 0; overflow-y: auto; border: 1px solid var(--border); border-radius: 7px; background: rgba(10,17,24,0.5); }
|
|
.library-editor-table { width: 100%; border-collapse: collapse; font-size: 11.5px; }
|
|
.library-editor-table thead th {
|
|
position: sticky; top: 0;
|
|
text-align: left; font-weight: 600; font-size: 10px;
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
color: var(--muted); background: rgba(13,21,29,0.96);
|
|
padding: 8px 10px; border-bottom: 1px solid var(--border);
|
|
}
|
|
.library-editor-table tbody td { padding: 7px 10px; border-bottom: 1px solid rgba(148,163,184,0.07); color: var(--fg-2); vertical-align: middle; }
|
|
.library-editor-table tbody tr:last-child td { border-bottom: none; }
|
|
.library-editor-table .le-name { color: var(--fg); max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.library-editor-table .le-loc { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.library-editor-table tr.unavailable .le-name { color: var(--danger); }
|
|
.le-badge { margin-left: 7px; padding: 1px 6px; border-radius: 5px; font-size: 9px; text-transform: uppercase; letter-spacing: 0.04em; background: rgba(214,90,74,0.16); color: var(--danger); }
|
|
.library-editor-empty { color: var(--muted); text-align: center; padding: 22px 10px !important; }
|
|
/* Settings → tabs */
|
|
.settings-tabs { display: flex; gap: 2px; margin-bottom: 12px; border-bottom: 1px solid var(--border); }
|
|
.settings-tab { background: none; border: none; border-bottom: 2px solid transparent; color: var(--muted); font-family: var(--font-mono); font-size: 12px; font-weight: 600; padding: 5px 12px 9px; cursor: pointer; margin-bottom: -1px; }
|
|
.settings-tab:hover { color: var(--fg-2); }
|
|
.settings-tab.active { color: var(--fg); border-bottom-color: var(--accent); }
|
|
/* Every pane fills the fixed dialog body so all tabs are the same size; the
|
|
General pane (the tall one, with the tracks table) scrolls within it. */
|
|
.settings-pane { display: flex; flex-direction: column; min-height: 0; flex: 1; }
|
|
.settings-pane[data-pane="general"] {
|
|
flex: 1; overflow-y: auto;
|
|
/* Reserve a gutter for the scrollbar so it never overlaps the right-aligned
|
|
controls (Compute device, Out of sync tracks). padding-right insets the
|
|
content; the equal negative margin lets that gutter sit in the card's own
|
|
12px padding, so scrolling content stays aligned with the fixed
|
|
header/tabs/footer. Handles overlay scrollbars (macOS/WebKit, which draw
|
|
over the padding box) and reserves space for classic scrollbars
|
|
(Windows/Linux). */
|
|
scrollbar-gutter: stable;
|
|
padding-right: 12px; margin-right: -12px;
|
|
}
|
|
.settings-pane.hidden { display: none; }
|
|
.settings-pane[data-pane="general"] .library-editor-table-wrap { flex: none; max-height: 240px; margin-bottom: 2px; }
|
|
.settings-empty { color: var(--muted); font-size: 12px; text-align: center; padding: 28px 10px; }
|
|
.settings-foot { display: flex; justify-content: flex-end; margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--border); }
|
|
.settings-done { min-height: 32px; border-radius: 7px; border: 1px solid rgba(244,183,64,0.35); background: rgba(244,183,64,0.16); color: var(--accent); font-family: var(--font-mono); font-size: 12px; font-weight: 600; padding: 0 22px; cursor: pointer; }
|
|
.settings-done:hover { background: rgba(244,183,64,0.24); }
|
|
/* Right-aligned form controls share a fixed width so they line up down the column. */
|
|
.settings-num-input, .settings-select { flex-shrink: 0; width: 84px; background: rgba(10,17,24,0.6); border: 1px solid var(--border-strong); border-radius: 7px; color: var(--fg); font-family: var(--font-mono); font-size: 12px; padding: 6px 9px; }
|
|
.settings-num-input { text-align: right; }
|
|
.settings-select { cursor: pointer; }
|
|
/* Wider variant for options whose text doesn't fit the shared 84px column --
|
|
currently just the language picker (flag + native name, up to "Bahasa Indonesia"). */
|
|
.settings-select-wide { width: 190px; }
|
|
.settings-num-input:focus, .settings-select:focus { outline: none; border-color: rgba(244,183,64,0.5); }
|
|
|
|
/* Settings → network access section */
|
|
.settings-section { margin-bottom: 12px; }
|
|
.settings-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
|
|
.settings-row-text { min-width: 0; }
|
|
.settings-row-title { font-size: 12.5px; font-weight: 600; color: var(--fg); }
|
|
.settings-row-desc { font-size: 10.5px; color: var(--muted); margin-top: 3px; line-height: 1.45; }
|
|
.settings-lock-note { font-style: italic; opacity: 0.85; margin-top: 5px; }
|
|
.settings-switch { position: relative; flex-shrink: 0; width: 42px; height: 24px; cursor: pointer; }
|
|
.settings-switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
|
|
.settings-switch-track { position: absolute; inset: 0; border-radius: 999px; background: rgba(148,163,184,0.22); border: 1px solid var(--border-strong); transition: background 0.15s ease; }
|
|
.settings-switch-thumb { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--fg); transition: transform 0.15s ease; }
|
|
.settings-switch input:checked + .settings-switch-track { background: rgba(244,183,64,0.55); border-color: rgba(244,183,64,0.5); }
|
|
.settings-switch input:checked + .settings-switch-track .settings-switch-thumb { transform: translateX(18px); background: var(--accent); }
|
|
.settings-switch.disabled { cursor: default; opacity: 0.7; }
|
|
.settings-switch.disabled input { cursor: default; }
|
|
.settings-net { margin-top: 10px; font-size: 11px; color: var(--muted); }
|
|
.settings-net.hidden { display: none; }
|
|
.settings-net-empty { color: var(--muted); }
|
|
.settings-net-qr { display: flex; flex-direction: column; gap: 14px; margin-top: 6px; }
|
|
.qr-hint { font-size: 10px; color: var(--muted); margin: 0; line-height: 1.4; }
|
|
.qr-cards-row { display: flex; flex-wrap: wrap; gap: 28px; }
|
|
.qr-card { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
|
|
.qr-img-wrap { width: 130px; height: 130px; border-radius: 6px; border: 3px solid var(--accent); overflow: hidden; box-sizing: border-box; }
|
|
.qr-card img { width: 100%; height: 100%; display: block; transition: filter 0.25s ease, scale 0.25s ease; }
|
|
.qr-card.qr-blurred img { filter: blur(10px); scale: 1.12; }
|
|
.qr-label { font-size: 9.5px; color: var(--muted); text-align: center; max-width: 130px; word-break: break-all; }
|
|
.settings-server-note { font-size: 10.5px; color: var(--muted); margin: 0 0 12px; line-height: 1.5; }
|
|
.settings-subhead { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); font-weight: 600; margin: 4px 0 7px; }
|
|
|
|
/* Settings → registry (read-only viewer) */
|
|
.settings-registry-refresh { flex-shrink: 0; min-height: 28px; border-radius: 6px; border: 1px solid var(--border-strong); background: rgba(10,17,24,0.6); color: var(--fg-2); font-family: var(--font-mono); font-size: 11px; padding: 0 12px; cursor: pointer; }
|
|
.settings-registry-refresh:hover { color: var(--fg); border-color: rgba(148,163,184,0.4); }
|
|
.settings-registry-view {
|
|
flex: 1; min-height: 0; margin-top: 10px; resize: none;
|
|
background: rgba(10,17,24,0.5); border: 1px solid var(--border); border-radius: 7px;
|
|
color: var(--fg-2); font-family: var(--font-mono); font-size: 11px; line-height: 1.5;
|
|
padding: 10px 12px; white-space: pre; overflow: auto;
|
|
}
|
|
.settings-registry-view:focus { outline: none; }
|
|
|
|
/* Settings → General → danger zone (reset app data) */
|
|
.settings-danger-zone { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
|
|
.settings-danger-subhead { color: var(--danger); }
|
|
.settings-reset-btn {
|
|
flex-shrink: 0; min-height: 30px; border-radius: 7px;
|
|
border: 1px solid rgba(214,90,74,0.35); background: rgba(214,90,74,0.12);
|
|
color: var(--danger); font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
|
padding: 0 12px; cursor: pointer;
|
|
}
|
|
.settings-reset-btn:hover { background: rgba(214,90,74,0.2); }
|
|
|
|
/* Reset app data — confirm dialog (nested above the Settings dialog) */
|
|
.reset-confirm-backdrop {
|
|
position: fixed; inset: 0; z-index: 150;
|
|
display: grid; place-items: center;
|
|
background: rgba(3,8,13,0.55); backdrop-filter: blur(2px);
|
|
}
|
|
.reset-confirm-card {
|
|
width: min(340px, calc(100vw - 32px));
|
|
border: 1px solid rgba(214,90,74,0.4);
|
|
border-radius: 10px;
|
|
background: linear-gradient(180deg, rgba(28,39,48,0.98), rgba(13,21,29,0.98));
|
|
box-shadow: var(--shadow-panel);
|
|
padding: 16px; color: var(--fg); font-family: var(--font-mono);
|
|
}
|
|
.reset-confirm-title { font-size: 13px; font-weight: 700; color: var(--danger); margin-bottom: 8px; }
|
|
.reset-confirm-body { font-size: 11.5px; color: var(--fg-2); line-height: 1.5; margin: 0 0 10px; }
|
|
.reset-confirm-hint { font-size: 10.5px; color: var(--muted); margin: 0 0 7px; }
|
|
.reset-confirm-hint strong { color: var(--fg); letter-spacing: 0.04em; }
|
|
.reset-confirm-input {
|
|
width: 100%; min-height: 34px; border: 1px solid var(--border-strong); border-radius: 7px;
|
|
background: rgba(10,17,24,0.78); color: var(--fg); font: inherit; font-size: 12px;
|
|
padding: 0 10px; outline: none; box-sizing: border-box;
|
|
}
|
|
.reset-confirm-input:focus { border-color: rgba(214,90,74,0.5); box-shadow: 0 0 0 2px rgba(214,90,74,0.14); }
|
|
.reset-confirm-msg { color: var(--danger); font-size: 11px; margin-top: 8px; min-height: 1em; }
|
|
.reset-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
.reset-confirm-actions button { min-height: 30px; border-radius: 7px; font-family: var(--font-mono); font-size: 11px; cursor: pointer; padding: 0 12px; }
|
|
.reset-confirm-cancel { border: 1px solid var(--border); background: rgba(21,31,39,0.52); color: var(--muted); }
|
|
.reset-confirm-go { border: 1px solid rgba(214,90,74,0.4); background: rgba(214,90,74,0.18); color: var(--danger); font-weight: 600; }
|
|
.reset-confirm-go:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
/* Playlist import reuses the confirm shell, but it is a normal action rather
|
|
than a destructive one, so it drops the danger colouring. */
|
|
.playlist-confirm-title { color: var(--fg); }
|
|
.playlist-confirm .playlist-name { color: var(--accent); }
|
|
.playlist-confirm-go {
|
|
min-height: 30px; border-radius: 7px; padding: 0 12px; cursor: pointer;
|
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
|
border: 1px solid rgba(232, 168, 56, 0.45);
|
|
background: rgba(232, 168, 56, 0.16);
|
|
color: var(--accent);
|
|
}
|
|
.playlist-confirm-go:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
.library-editor-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 11px; }
|
|
.library-editor-status { font-size: 10.5px; color: var(--muted); }
|
|
.library-editor-status.out-of-sync { color: var(--danger); font-weight: 600; }
|
|
.library-editor-sync { min-height: 30px; border-radius: 7px; border: 1px solid rgba(244,183,64,0.3); background: rgba(244,183,64,0.16); color: var(--accent); font-family: var(--font-mono); font-size: 11px; padding: 0 13px; cursor: pointer; }
|
|
.library-editor-sync:disabled { opacity: 0.55; cursor: default; }
|
|
|
|
/* Color picker popover */
|
|
.color-picker-popover {
|
|
position: absolute; z-index: 90;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 8px; padding: 8px; display: flex; gap: 6px;
|
|
box-shadow: var(--shadow-panel);
|
|
}
|
|
|
|
/* Collapsed sidebar strip */
|
|
.cat-collapsed-strip { display: none; }
|
|
.daw.cat-collapsed .sidebar { width: 66px; }
|
|
.daw.cat-collapsed .sidebar-body { opacity: 0; pointer-events: none; visibility: hidden; }
|
|
.daw.cat-collapsed .cat-collapsed-strip { display: flex; flex-direction: column; gap: 8px; align-items: center; padding: 8px 0; overflow-y: auto; }
|
|
|
|
/* ═══════════════════════════════════
|
|
MAIN AREA (#lanes)
|
|
═══════════════════════════════════ */
|
|
.daw-main-col {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
}
|
|
.daw-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Job progress overlay ── */
|
|
.job {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 20;
|
|
background: rgb(7, 13, 19);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
padding: 32px;
|
|
}
|
|
/* Gold ambient glow -- matches the wave-loading-overlay aesthetic */
|
|
.job::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background: radial-gradient(circle at 50% 44%, rgba(216, 168, 74, 0.13), transparent 55%);
|
|
pointer-events: none;
|
|
}
|
|
.job > * { position: relative; z-index: 1; }
|
|
.job.hidden { display: none !important; }
|
|
.job .title { font-size: 15px; font-weight: 600; color: var(--fg); text-align: center; }
|
|
.job .stage { font-size: 12px; color: var(--muted); text-align: center; }
|
|
.job progress {
|
|
width: 280px; max-width: 100%;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
appearance: none; border: none;
|
|
background: var(--panel-3);
|
|
overflow: hidden;
|
|
}
|
|
.job progress::-webkit-progress-bar { background: var(--panel-3); border-radius: 2px; }
|
|
.job progress::-webkit-progress-value { background: var(--accent); border-radius: 2px; }
|
|
.job progress::-moz-progress-bar { background: var(--accent); border-radius: 2px; }
|
|
.job .job-detail { font-size: 11px; color: var(--muted-2); text-align: center; }
|
|
.cancel-btn {
|
|
height: 30px; padding: 0 16px;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
color: var(--fg-2); border-radius: 8px; font-size: 12px;
|
|
font-family: inherit; cursor: pointer;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.cancel-btn:hover { background: var(--panel-3); color: var(--fg); }
|
|
.cancel-btn.hidden { display: none !important; }
|
|
|
|
/* Error banner */
|
|
.error {
|
|
position: absolute;
|
|
top: 64px; left: 260px; right: 0;
|
|
z-index: 15;
|
|
padding: 12px 20px;
|
|
background: rgba(214,90,74,0.12);
|
|
border-bottom: 1px solid rgba(214,90,74,0.3);
|
|
color: #ff9090;
|
|
font-size: 13px;
|
|
}
|
|
.error.hidden { display: none !important; }
|
|
|
|
/* ── Track header ── */
|
|
.daw-track-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-2);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Row 1 */
|
|
.daw-info-row {
|
|
display: flex;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
/* Track card */
|
|
.daw-track-card {
|
|
display: flex;
|
|
gap: 14px;
|
|
align-items: flex-start;
|
|
padding: 4px 16px;
|
|
flex-shrink: 0;
|
|
width: 340px;
|
|
border-right: 1px solid var(--border);
|
|
}
|
|
|
|
/* Metadata cards */
|
|
.daw-meta-card {
|
|
flex: 1;
|
|
padding: 4px 16px;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
min-width: 0;
|
|
}
|
|
.daw-meta-card:last-child { border-right: none; }
|
|
.meta-card-label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.07em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
.meta-card-main {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.meta-card-value {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: var(--fg);
|
|
line-height: 1;
|
|
font-family: var(--font-mono);
|
|
}
|
|
.meta-card-value.accent { color: var(--accent); }
|
|
.meta-card-value.stability-high { color: #4caf7d; }
|
|
.meta-card-sub {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
}
|
|
.daw-cover {
|
|
width: 64px; height: 64px;
|
|
border-radius: 8px;
|
|
background: var(--panel-2);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0; color: var(--muted);
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.np-art-placeholder { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
|
|
.np-art-img {
|
|
position: absolute; inset: 0;
|
|
width: 100%; height: 100%; object-fit: cover; display: none;
|
|
}
|
|
.np-art-img[src]:not([src=""]) { display: block; }
|
|
|
|
.daw-track-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
.daw-track-title { font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
/* One compact line -- duration · stems · source · quality · extracted date --
|
|
instead of a duration/stems row plus three further label:value rows below
|
|
it. The four-line version was still the pre-redesign layout; this row was
|
|
the one part of the footer rebuild (#269 follow-up) I'd described matching
|
|
the reference but never actually rebuilt. */
|
|
.daw-track-meta-line {
|
|
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
|
font-size: 11px; color: var(--muted);
|
|
min-width: 0; overflow: hidden;
|
|
}
|
|
.daw-track-meta-line .num { color: var(--fg-2); }
|
|
.daw-track-sep { color: var(--border); font-size: 12px; flex-shrink: 0; }
|
|
#t-stems-chip {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
}
|
|
#t-stems-chip::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 7px; height: 7px;
|
|
border-radius: 50%;
|
|
background: #4caf7d;
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-fav-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--muted);
|
|
padding: 2px;
|
|
display: flex;
|
|
align-items: center;
|
|
transition: color 150ms;
|
|
}
|
|
.daw-fav-btn:hover { color: var(--fg); }
|
|
.daw-fav-btn.active { color: #e54e4e; }
|
|
.daw-fav-btn.active svg { fill: #e54e4e; stroke: #e54e4e; }
|
|
.daw-chip {
|
|
display: inline-flex; align-items: center;
|
|
padding: 2px 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
background: var(--panel-2);
|
|
font-size: 11px;
|
|
color: var(--fg-2);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Energy panel */
|
|
.daw-energy-panel {
|
|
padding: 12px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
.daw-energy-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.daw-playhead-label { color: var(--accent); display: flex; align-items: center; gap: 4px; }
|
|
.daw-energy-bars { flex: 1; display: flex; flex-direction: column; justify-content: space-between; gap: 4px; }
|
|
|
|
/* Energy rows (JS-driven) */
|
|
.energy-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.energy-row span {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
width: 44px;
|
|
flex-shrink: 0;
|
|
}
|
|
.energy-row.vocals span { color: var(--vocals); }
|
|
.energy-row.drums span { color: var(--drums); }
|
|
.energy-row.bass span { color: var(--bass); }
|
|
.energy-row.guitar span { color: var(--guitar); }
|
|
.energy-row.piano span { color: var(--piano); }
|
|
.energy-row.other span { color: var(--other); }
|
|
.energy-row.original span { color: var(--fg-2); }
|
|
|
|
.energy-row b {
|
|
flex: 1;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background: var(--bg);
|
|
overflow: hidden;
|
|
position: relative;
|
|
display: block;
|
|
}
|
|
.energy-row b::after {
|
|
content: '';
|
|
position: absolute; inset: 0;
|
|
width: var(--v, 0%);
|
|
border-radius: 3px;
|
|
transition: width 160ms;
|
|
}
|
|
.energy-row.vocals b::after { background: var(--vocals); }
|
|
.energy-row.drums b::after { background: var(--drums); }
|
|
.energy-row.bass b::after { background: var(--bass); }
|
|
.energy-row.guitar b::after { background: var(--guitar); }
|
|
.energy-row.piano b::after { background: var(--piano); }
|
|
.energy-row.other b::after { background: var(--other); opacity: 0.5; }
|
|
.energy-row.original b::after { background: var(--fg-2); }
|
|
|
|
.energy-row em {
|
|
font-style: normal;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--fg-2);
|
|
width: 36px;
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Stem presence cards — row 2 */
|
|
.stem-presence-panel {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, 1fr);
|
|
}
|
|
.stem-card {
|
|
padding: 14px 16px;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.stem-card:last-child { border-right: none; }
|
|
.stem-card-label {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.07em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
.stem-card-pct {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
font-family: var(--font-mono);
|
|
line-height: 1;
|
|
color: var(--fg);
|
|
transition: color 200ms;
|
|
}
|
|
.stem-card:not(.inactive)[data-stem="vocals"] .stem-card-pct { color: var(--vocals); }
|
|
.stem-card:not(.inactive)[data-stem="drums"] .stem-card-pct { color: var(--drums); }
|
|
.stem-card:not(.inactive)[data-stem="bass"] .stem-card-pct { color: var(--bass); }
|
|
.stem-card:not(.inactive)[data-stem="guitar"] .stem-card-pct { color: var(--guitar); }
|
|
.stem-card:not(.inactive)[data-stem="piano"] .stem-card-pct { color: var(--piano); }
|
|
.stem-card:not(.inactive)[data-stem="other"] .stem-card-pct { color: var(--other); }
|
|
.stem-card.inactive .stem-card-pct { color: var(--muted); }
|
|
|
|
/* Analysis panel */
|
|
.daw-analysis-panel {
|
|
padding: 12px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-analysis-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 80px;
|
|
}
|
|
.daw-analysis-val { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
|
|
.daw-analysis-num {
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
line-height: 1.1;
|
|
color: var(--fg);
|
|
}
|
|
.daw-analysis-num.accent { color: var(--accent); }
|
|
.daw-analysis-sub { font-size: 10px; color: var(--muted); margin-top: 2px; }
|
|
.daw-analysis-divider { width: 1px; background: var(--border); align-self: stretch; }
|
|
.daw-camelot-ring {
|
|
width: 28px; height: 28px; border-radius: 50%;
|
|
border: 1.5px solid var(--accent);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 10px; font-weight: 700; color: var(--accent);
|
|
background: rgba(244,183,64,0.08);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* ── Section ribbon ── */
|
|
.daw-section-ribbon {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.daw-mixer-label {
|
|
width: 300px;
|
|
flex-shrink: 0;
|
|
background: var(--bg-2);
|
|
border-right: 1px solid var(--border);
|
|
padding: 6px 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.daw-label-title { font-size: 12px; font-weight: 600; }
|
|
.daw-label-sub { font-size: 9px; }
|
|
.daw-sections-header { justify-content: space-between; }
|
|
.daw-sections-area {
|
|
flex: 1;
|
|
position: relative;
|
|
height: 36px;
|
|
background: var(--bg-2);
|
|
overflow: hidden;
|
|
user-select: none;
|
|
}
|
|
|
|
/* Section blocks (sections.js populates) */
|
|
.section-block {
|
|
position: absolute;
|
|
top: 4px; bottom: 4px;
|
|
box-sizing: border-box;
|
|
border: 1px solid var(--sc, #4a7fff);
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
overflow: hidden;
|
|
cursor: grab;
|
|
transition: filter 80ms;
|
|
}
|
|
.section-block:hover { filter: brightness(1.15); }
|
|
.section-block.sec-dragging { cursor: grabbing; filter: brightness(1.2); z-index: 10; }
|
|
.section-block.sec-resizing { filter: brightness(1.2); z-index: 10; }
|
|
|
|
.section-label {
|
|
flex: 1;
|
|
padding: 0 6px 0 10px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
color: var(--sc, #4a7fff);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.section-del {
|
|
display: none;
|
|
flex-shrink: 0;
|
|
width: 16px; height: 16px;
|
|
margin-right: 4px;
|
|
border: none; background: none;
|
|
color: var(--sc, #4a7fff);
|
|
font-size: 13px; line-height: 1;
|
|
cursor: pointer; border-radius: 3px;
|
|
align-items: center; justify-content: center;
|
|
opacity: 0.7;
|
|
}
|
|
.section-del:hover { opacity: 1; background: rgba(0,0,0,0.3); }
|
|
.section-block:hover .section-del { display: flex; }
|
|
|
|
.section-handle {
|
|
position: absolute;
|
|
top: 0; bottom: 0;
|
|
width: 6px;
|
|
cursor: col-resize;
|
|
z-index: 2;
|
|
flex-shrink: 0;
|
|
}
|
|
.section-handle-l { left: 0; }
|
|
.section-handle-r { right: 0; }
|
|
.section-handle:hover { background: rgba(255,255,255,0.12); }
|
|
|
|
.section-rename-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
font: inherit;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
color: var(--sc, #4a7fff);
|
|
padding: 0 6px 0 10px;
|
|
caret-color: var(--sc, #4a7fff);
|
|
}
|
|
|
|
/* Sections save indicator */
|
|
.sections-save-indicator {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 10px;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
transition: opacity 400ms;
|
|
}
|
|
.sections-save-indicator.hidden { display: none; }
|
|
.sections-save-indicator.saved { color: #4caf7d; }
|
|
.sections-save-indicator::before {
|
|
content: "";
|
|
display: block;
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
border: 1.5px solid currentColor;
|
|
border-top-color: transparent;
|
|
animation: sections-spin 600ms linear infinite;
|
|
}
|
|
.sections-save-indicator.saved::before {
|
|
animation: none;
|
|
border: none;
|
|
width: auto; height: auto;
|
|
content: "✓";
|
|
font-size: 11px;
|
|
}
|
|
@keyframes sections-spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Add section button — lives in the Sections label column */
|
|
.sections-add-btn-label {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
padding: 3px 8px 3px 6px;
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
color: var(--muted);
|
|
font-size: 10px; font-weight: 600;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
|
|
}
|
|
.sections-add-btn-label:hover {
|
|
background: var(--panel-2);
|
|
color: var(--fg);
|
|
border-color: rgba(255,255,255,0.18);
|
|
}
|
|
|
|
/* Wave label (left column in wave header — Mixer label) */
|
|
.daw-wave-label {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 14px;
|
|
}
|
|
|
|
/* ── Waveform header ── */
|
|
.daw-wave-header {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.daw-wave-label {
|
|
width: var(--daw-col-w);
|
|
flex-shrink: 0;
|
|
background: var(--bg-2);
|
|
border-right: 1px solid var(--border);
|
|
min-height: 32px;
|
|
}
|
|
.daw-ruler-area {
|
|
flex: 1;
|
|
background: var(--bg-2);
|
|
min-height: 32px;
|
|
/* Clip the horizontally-scrolled ruler spill. overflow-x: clip (not hidden)
|
|
keeps overflow-y visible so the vertical playhead line still extends down
|
|
over the waveform lanes. */
|
|
overflow-x: clip;
|
|
}
|
|
|
|
/* Ruler */
|
|
.lanes-ruler { position: relative; height: 18px; }
|
|
.lanes-ruler-time {
|
|
position: absolute; inset: 0;
|
|
font-family: var(--font-mono);
|
|
font-size: 9px;
|
|
color: var(--muted);
|
|
}
|
|
/* Match the zoomed waveform width so ticks/playhead stay aligned; JS translates
|
|
it by scrollLeft. Left-anchored (right: auto) so width drives the size. */
|
|
.daw .lanes-ruler-time {
|
|
right: auto;
|
|
width: calc(100% * var(--zoom, 1));
|
|
}
|
|
.playhead-marker {
|
|
position: absolute;
|
|
top: -1px;
|
|
transform: translateX(-50%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── Content area (mixer + waveform) ── */
|
|
.daw-content {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Stems panel (the studio's left column) ── */
|
|
.daw-stems-panel {
|
|
width: var(--daw-col-w);
|
|
flex-shrink: 0;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
background: var(--bg-2);
|
|
}
|
|
|
|
/* Stem list rows — kept in DOM for JS hook (.stem-mute/.stem-solo toggling, hidden visually) */
|
|
.stem-list {
|
|
position: absolute;
|
|
width: 0; height: 0; overflow: hidden;
|
|
visibility: hidden; pointer-events: none;
|
|
opacity: 0;
|
|
}
|
|
/* Also update the --lane-h var to match waveform lane height */
|
|
:root { --lane-h: 72px; } /* 70px wave + 2px separator */
|
|
.stem-icon { width: 14px; height: 14px; flex-shrink: 0; }
|
|
.stem-mute, .stem-solo { display: none; }
|
|
.stem-monitor { display: none; }
|
|
.drag-handle { display: none; }
|
|
.mini-meter { display: none; }
|
|
|
|
/* ── Mixer column (JS-built horizontal lanes) ── */
|
|
.mixer-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Each mixer row height is driven by --lane-h (set dynamically in JS to fill available space) */
|
|
.lane-header.mx-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 0 10px;
|
|
height: var(--lane-h, 72px);
|
|
min-height: 72px;
|
|
max-height: none;
|
|
border-bottom: 2px solid var(--border);
|
|
background: var(--bg-2);
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
transition: opacity var(--t-base);
|
|
box-sizing: border-box;
|
|
}
|
|
.lane-header.mx-row.muted { opacity: 0.45; }
|
|
.lane-header.mx-row.hidden { display: none !important; }
|
|
|
|
.lane-stripe { display: none; }
|
|
|
|
/* Name + VU stacked vertically */
|
|
.lane-name-vu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
width: 76px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Stem icon — hidden per user preference */
|
|
.mx-icon { display: none; }
|
|
|
|
/* Stem name */
|
|
.mx-name {
|
|
font-size: 13px; font-weight: 500;
|
|
min-width: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* Fader (horizontal, native range input) */
|
|
.lane-knob.mx-fader {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
min-width: 0;
|
|
}
|
|
.lane-knob-indicator { display: none; }
|
|
.mx-fader-input {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100%;
|
|
height: 22px;
|
|
background: transparent;
|
|
cursor: ew-resize;
|
|
outline: none;
|
|
margin: 0;
|
|
}
|
|
.mx-fader-input::-webkit-slider-runnable-track {
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: linear-gradient(
|
|
to right,
|
|
var(--fader-color, #888) 0%,
|
|
var(--fader-color, #888) calc(var(--lane-pos, 0.5) * 100%),
|
|
rgba(255,255,255,0.08) calc(var(--lane-pos, 0.5) * 100%)
|
|
);
|
|
}
|
|
.mx-fader-input::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 14px; height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--fader-color, #888);
|
|
margin-top: -5px;
|
|
border: 2px solid rgba(0,0,0,0.35);
|
|
box-shadow: 0 0 0 1px rgba(255,255,255,0.18);
|
|
cursor: ew-resize;
|
|
}
|
|
.mx-fader-input::-moz-range-track {
|
|
height: 4px; border-radius: 2px;
|
|
background: rgba(255,255,255,0.08);
|
|
}
|
|
.mx-fader-input::-moz-range-thumb {
|
|
width: 14px; height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--fader-color, #888);
|
|
border: 2px solid rgba(0,0,0,0.35);
|
|
cursor: ew-resize;
|
|
}
|
|
|
|
/* VU meter — sits below stem name in .lane-left-col, spans full column width */
|
|
.lane-vu.mx-meter {
|
|
position: relative;
|
|
height: 5px;
|
|
width: 100%;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
--vu-level: 0%;
|
|
--vu-peak: 0%;
|
|
}
|
|
.lane-vu-bar {
|
|
display: none;
|
|
}
|
|
.lane-vu-bar.mx-meter-fill {
|
|
display: block;
|
|
position: absolute;
|
|
inset: 0 auto 0 0;
|
|
width: var(--vu-level, 0%);
|
|
background: linear-gradient(90deg, #22c55e 0%, #86efac 80%, #f97316 95%, #ef4444 100%);
|
|
border-radius: 2px;
|
|
transition: width 40ms linear;
|
|
}
|
|
|
|
/* dB value */
|
|
.mx-val {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
color: var(--fg);
|
|
width: 36px;
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* M/S buttons (JS-built) */
|
|
.lane-icon-toggle.mx-btn,
|
|
.ms-btn.mx-btn,
|
|
.lane-dl.mx-btn {
|
|
width: 22px; height: 20px;
|
|
background: transparent;
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 5px;
|
|
color: var(--muted);
|
|
font-size: 10px; font-weight: 700;
|
|
font-family: var(--font-mono);
|
|
letter-spacing: 0.05em;
|
|
cursor: pointer;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
|
|
flex-shrink: 0;
|
|
text-decoration: none;
|
|
padding: 0;
|
|
opacity: 1;
|
|
}
|
|
.lane-icon-toggle.mx-btn:hover,
|
|
.ms-btn.mx-btn:hover { color: var(--fg); border-color: #3a4550; }
|
|
|
|
/* Mute button: "active" = NOT muted (normal). Engaged (muted) lights up blue,
|
|
mirroring the solo button's own lit-up treatment below. */
|
|
.lane-icon-toggle.mx-btn.mute.active { color: var(--fg-2); border-color: var(--border-strong); }
|
|
.lane-icon-toggle.mx-btn.mute:not(.active) {
|
|
background: rgba(59,130,246,0.18);
|
|
color: #60a5fa;
|
|
border-color: rgba(59,130,246,0.4);
|
|
}
|
|
|
|
/* Solo button: "active" = soloed */
|
|
.ms-btn.mx-btn.active { background: rgba(244,183,64,0.18); color: var(--accent); border-color: rgba(244,183,64,0.4); }
|
|
|
|
/* Download button */
|
|
.lane-dl.mx-btn {
|
|
color: var(--muted);
|
|
border-color: transparent;
|
|
}
|
|
.lane-dl.mx-btn:hover { color: var(--fg); border-color: var(--border); }
|
|
.lane-dl.mx-btn svg { width: 13px; height: 13px; }
|
|
|
|
/* Unavailable stems — gray entire mixer row and waveform row */
|
|
.lane-header.unavailable { opacity: 0.25; pointer-events: none; }
|
|
.stem-waveform-row.unavailable { opacity: 0.2; pointer-events: none; }
|
|
.mx-fader-input { touch-action: pan-y; }
|
|
|
|
/* ── Waveform panel (right) ── */
|
|
/* Layout only — waves.css handles all visual styling inside (.wave-scroll bg,
|
|
golden scrollbar, .loop-region, .waves-grid, .stem-waveform-layer etc.) */
|
|
.daw-wave-panel {
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* waves.css gives .wave-editor padding + border-radius for the old rounded panel
|
|
style. In the flat DAW layout those are unwanted — strip them. */
|
|
.daw .wave-editor,
|
|
.app:not(.is-import) .daw-wave-panel.wave-editor {
|
|
padding: 0 !important;
|
|
min-height: 0 !important;
|
|
border-radius: 0 !important;
|
|
}
|
|
|
|
/* Our zoom toolbar sits outside .wave-editor (in .daw-wave-header), so
|
|
the wave-scroll should fill the full panel height instead of calc(100% - 32px).
|
|
Also remove the rounded corners waves.css adds in active state. */
|
|
.daw .wave-scroll {
|
|
flex: 1 !important;
|
|
height: auto !important;
|
|
border-radius: 0 !important;
|
|
border: none !important;
|
|
overflow-y: auto;
|
|
/* waves.css sets scroll-behavior: smooth. In the studio the mixer column and
|
|
the wave lanes mirror each other's scrollTop (wireLaneScrollSync), and a
|
|
smooth programmatic scroll turns that mirror into a feedback loop: each
|
|
echo re-targets the in-flight animation, so a vertical wheel-scroll to the
|
|
middle oscillates instead of settling, i.e. the lanes shake (#182). Instant
|
|
scrolling makes the mirror a no-op once both panes match. */
|
|
scroll-behavior: auto;
|
|
}
|
|
|
|
/* Canvas fills the panel when lanes fit, but grows with the track stack when
|
|
they overflow (#159) so .wave-scroll can scroll the extra lanes vertically. */
|
|
.daw .wave-canvas { min-height: 100%; height: auto; }
|
|
|
|
/* In the original layout a 48px stem-icon strip lived INSIDE the wave panel,
|
|
offsetting waves-column/multitrack/waveform-layer by 48px. In our layout the
|
|
mixer is a separate 300px panel, so all those 48px offsets must be zero. */
|
|
.daw .waves-column,
|
|
.app:not(.is-import) .daw-wave-panel .waves-column {
|
|
padding-left: 0 !important;
|
|
--wave-gutter: 0px !important;
|
|
}
|
|
/* Show WaveSurfer native bar rendering; hide the SVG overlay layer */
|
|
.daw #multitrack-container {
|
|
opacity: 1 !important;
|
|
pointer-events: all !important;
|
|
position: relative !important; /* in flow → waves-column height driven by WaveSurfer */
|
|
inset: 0 !important; /* waves.css sets inset: 0 0 0 48px; zero left offset */
|
|
}
|
|
/* The wavesurfer-multitrack bundle wraps its tracks in its own
|
|
`<div style="overflow-x: scroll">`, which renders a permanently visible
|
|
horizontal scrollbar — an empty light bar when the content fits (the "white
|
|
bar" bug). We fit the bundle to its container and let the outer .wave-scroll
|
|
own horizontal scrolling, so suppress the bundle's wrapper scrollbar. */
|
|
.daw #multitrack-container > div {
|
|
overflow-x: hidden !important;
|
|
}
|
|
/* Zero waves.css 48px left offsets that assumed an icon strip inside the wave panel */
|
|
.daw .waves-grid { left: 0 !important; }
|
|
.daw .lanes-ruler { margin-left: 0 !important; }
|
|
.daw .stem-waveform-layer {
|
|
display: none !important;
|
|
/* waves.css offsets this layer by left:48px for the legacy icon-strip layout.
|
|
The DAW layout has no in-panel strip, so it must start at 0 like .waves-grid
|
|
and #multitrack-container above; otherwise the overview waveform is shifted
|
|
48px right of the ruler/playhead, so t=0 in the audio lands ~48px before the
|
|
drawn waveform begins (a constant gap that scales with track length, #189). */
|
|
left: 0 !important;
|
|
right: 0 !important;
|
|
}
|
|
/* When the Web Audio engine owns playback, the multitrack is mounted with null
|
|
URLs and never decodes audio, so the WaveSurfer canvas is empty. Show the SVG
|
|
overview layer (rendered from peaks.json) as the visual source instead. */
|
|
.daw.engine-waveforms .stem-waveform-layer {
|
|
display: flex !important;
|
|
}
|
|
/* waves-column must size naturally now that multitrack is in flow */
|
|
.daw .waves-column {
|
|
height: auto !important;
|
|
min-height: 0 !important;
|
|
}
|
|
|
|
.loop-region.hidden { display: none !important; }
|
|
|
|
/* Wave loading overlay */
|
|
.wave-loading-overlay {
|
|
position: absolute; inset: 0; z-index: 10;
|
|
display: flex; flex-direction: column;
|
|
align-items: center; justify-content: center;
|
|
gap: 16px;
|
|
background: rgb(11, 15, 18);
|
|
}
|
|
.wave-loading-overlay.hidden { display: none !important; }
|
|
.wave-loading-glow {
|
|
width: 80px; height: 80px; border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(244,183,64,0.2) 0%, transparent 70%);
|
|
animation: daw-pulse 2s ease-in-out infinite;
|
|
}
|
|
@keyframes daw-pulse { 0%,100%{opacity:0.5;transform:scale(0.9);} 50%{opacity:1;transform:scale(1.1);} }
|
|
.wave-loading-lines {
|
|
display: flex; gap: 3px; align-items: center;
|
|
}
|
|
.wave-loading-lines i {
|
|
display: block; width: 3px; height: 20px;
|
|
background: var(--accent); border-radius: 2px;
|
|
animation: daw-bar 1.2s ease-in-out infinite;
|
|
}
|
|
.wave-loading-lines i:nth-child(2) { animation-delay: 0.1s; }
|
|
.wave-loading-lines i:nth-child(3) { animation-delay: 0.2s; }
|
|
.wave-loading-lines i:nth-child(4) { animation-delay: 0.3s; }
|
|
.wave-loading-lines i:nth-child(5) { animation-delay: 0.4s; }
|
|
.wave-loading-lines i:nth-child(6) { animation-delay: 0.5s; }
|
|
@keyframes daw-bar { 0%,100%{transform:scaleY(0.4);opacity:0.5;} 50%{transform:scaleY(1.4);opacity:1;} }
|
|
.wave-loading-msg { font-size: 12px; color: var(--muted); }
|
|
|
|
/* Presence panel (hidden, JS compat) */
|
|
.presence-panel { display: none; }
|
|
|
|
/* ── Transport footer ── */
|
|
|
|
.daw-footer {
|
|
/* min-height, not height: the click-track cluster can wrap onto more than
|
|
one line in a narrow window, and a rigid height would clip it. */
|
|
min-height: 200px;
|
|
flex-shrink: 0;
|
|
border-top: 1px solid var(--border);
|
|
background: var(--bg-2);
|
|
/* Two columns on the studio's own grid: the track identity sits under the
|
|
stems/mixer panel and carries its width, everything time-related sits
|
|
under the lanes and starts exactly where the lane waveforms start, so a
|
|
position reads at the same x in both. The top border still spans the full
|
|
width -- that edge belongs to the footer/lanes boundary, not the grid. */
|
|
display: flex !important;
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.daw-footer-left {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 6px;
|
|
min-width: 140px;
|
|
flex: 1;
|
|
}
|
|
.daw-time-big {
|
|
font-size: 20px; font-weight: 600; letter-spacing: -0.01em;
|
|
}
|
|
.daw-time-total { font-size: 13px; color: var(--muted); }
|
|
.daw-footer-right {
|
|
display: flex; align-items: center; gap: 8px;
|
|
min-width: 140px; justify-content: flex-end;
|
|
flex: 1;
|
|
}
|
|
.daw-master-fader { accent-color: var(--accent); cursor: pointer; }
|
|
.daw-btn {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
color: var(--fg); border-radius: 8px; font-size: 12px;
|
|
font-family: inherit; cursor: pointer;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.daw-btn:hover { background: var(--panel-3); }
|
|
|
|
/* Transport controls */
|
|
.daw-transport-controls {
|
|
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
}
|
|
.daw-play-btn {
|
|
background: var(--accent);
|
|
color: #1a1206;
|
|
width: 56px; height: 56px;
|
|
border-radius: 50%; border: none;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
cursor: pointer;
|
|
box-shadow: 0 2px 10px rgba(244,183,64,0.35), inset 0 1px 0 rgba(255,255,255,0.18);
|
|
transition: transform var(--t-fast), box-shadow var(--t-fast), background var(--t-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
.daw-play-btn:hover { box-shadow: 0 2px 16px rgba(244,183,64,0.5), inset 0 1px 0 rgba(255,255,255,0.22); }
|
|
.daw-play-btn:active { transform: scale(0.96); }
|
|
/* Playing = green (go); idle/ready stays gold. */
|
|
.daw-play-btn.playing {
|
|
background: #4caf7d;
|
|
box-shadow: 0 2px 12px rgba(76,175,125,0.45), inset 0 1px 0 rgba(255,255,255,0.2);
|
|
}
|
|
.daw-play-btn.playing:hover { box-shadow: 0 2px 18px rgba(76,175,125,0.6), inset 0 1px 0 rgba(255,255,255,0.24); }
|
|
|
|
/* JS toggles these to show play vs pause icon */
|
|
.btn-transport.playing .pause-icon { display: block !important; }
|
|
.btn-transport.playing > svg:first-child { display: none; }
|
|
|
|
/* Stop/loop buttons inside the footer transport row */
|
|
.daw-footer .daw-iconbtn.btn-transport {
|
|
width: 40px; height: 40px;
|
|
border-radius: 9px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
}
|
|
|
|
/* Loop active state */
|
|
.daw-iconbtn.loop.active,
|
|
.btn-transport.loop.active { color: var(--accent); }
|
|
|
|
/* Export buttons in footer */
|
|
.daw-footer .footer-btn {
|
|
padding: 0 14px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
gap: 6px;
|
|
white-space: nowrap;
|
|
}
|
|
.daw-footer .footer-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: default;
|
|
}
|
|
|
|
/* ── Footer redesign ── */
|
|
|
|
/* ── Footer layout: three tiers (design 1b) ── */
|
|
/* Tier 1 -- what is loaded and what leaves the app (track identity, favourite,
|
|
Export Mix). Tier 2 -- every playback and click control, in labelled clusters
|
|
separated by hairline dividers. Tier 3 -- the timeline those clusters act on.
|
|
Each tier owns its own full-width budget instead of everything competing for
|
|
room in one line. */
|
|
|
|
.footer-main {
|
|
flex: 1; min-width: 0;
|
|
display: flex; flex-direction: column;
|
|
}
|
|
|
|
/* ── Left column: track identity ── */
|
|
/* Padded like the mixer rows above it (14px), so title, stem names and the
|
|
"Mixer" heading all share one left edge down the column. */
|
|
.footer-track {
|
|
width: var(--daw-col-w); flex: none;
|
|
box-sizing: border-box;
|
|
padding: 12px 14px 14px;
|
|
/* Centred, not top-aligned: the identity block is shorter than the control
|
|
column beside it, and centring splits the leftover height instead of
|
|
leaving it all as a void under the buttons. Stays balanced when the
|
|
controls wrap to another line and the footer grows. */
|
|
display: flex; flex-direction: column; justify-content: center; gap: 8px;
|
|
/* No overflow clipping: the export menu is absolutely positioned inside this
|
|
column and opens upward past its top edge. The title clamps itself and the
|
|
meta line has its own overflow rule, so nothing here needs clipping. */
|
|
min-width: 0;
|
|
}
|
|
.footer-track-head {
|
|
display: flex; align-items: flex-start; gap: 10px; min-width: 0;
|
|
}
|
|
.footer-row-actions {
|
|
display: flex; align-items: center; gap: 8px;
|
|
margin-top: 2px;
|
|
}
|
|
/* Boxed like the Export button beside it: both are actions on the track, and
|
|
a bare icon next to a filled button reads as decoration. */
|
|
.footer-row-actions .daw-fav-btn {
|
|
width: 32px; height: 32px; padding: 0;
|
|
justify-content: center;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 8px;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.footer-row-actions .daw-fav-btn:hover { background: var(--panel-3); }
|
|
.footer-row-actions .footer-chip { height: 32px; }
|
|
|
|
/* Hairline cluster separator: fades out at both ends so it reads as a seam
|
|
between groups rather than a hard rule across the footer. */
|
|
.footer-divider {
|
|
width: 1px; flex: none;
|
|
background: linear-gradient(
|
|
180deg, transparent, var(--border-strong) 22%, var(--border-strong) 78%, transparent
|
|
);
|
|
}
|
|
.footer-row-controls .footer-divider { height: 44px; }
|
|
/* Stranded at the end of a wrapped line, separating nothing (marked by
|
|
syncFooterDividers). Hidden rather than removed: taking its box out would
|
|
change the very wrap that was measured. */
|
|
.footer-divider.is-orphan { visibility: hidden; }
|
|
|
|
.footer-row-controls {
|
|
display: flex; align-items: flex-end; flex-wrap: wrap;
|
|
gap: 10px 16px;
|
|
padding: 12px 18px 14px 0;
|
|
}
|
|
.footer-group {
|
|
display: flex; flex-direction: column; gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
.footer-group-label {
|
|
display: flex; align-items: center; gap: 7px;
|
|
font-size: 10px; font-weight: 600; letter-spacing: 0.11em; text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
/* One control height across every cluster, so the row reads as a single band
|
|
of controls rather than a ragged stack. */
|
|
.footer-group-body {
|
|
display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
|
|
min-height: 34px;
|
|
}
|
|
/* Click track carries the most controls -- a wide basis so it either shares
|
|
the line with the other three clusters or takes a line of its own, rather
|
|
than squeezing into a leftover gap and wrapping into a ragged block. */
|
|
.footer-group-click { flex: 1 1 520px; }
|
|
|
|
.alpha-badge {
|
|
padding: 1px 5px; border-radius: 4px;
|
|
background: rgba(74,140,255,0.16); border: 1px solid rgba(74,140,255,0.4);
|
|
color: #4a8cff; font-size: 8.5px; font-weight: 700; letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/* Tier 3: the timeline. Full-bleed rather than an inset rounded panel -- its
|
|
left edge has to land exactly on the lane waveforms' left edge, and a side
|
|
border would offset the canvas by its own width. Top and bottom rules only,
|
|
the way the ruler area above the lanes is drawn. */
|
|
.footer-wave-region {
|
|
padding: 0 0 14px; flex-shrink: 0;
|
|
}
|
|
.footer-wave-panel {
|
|
border-top: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|
|
/* Closes the strip's open left edge and lands on the same x as the mixer
|
|
panel's right border, so that seam runs on down the page. Drawn as an
|
|
outset shadow rather than a border: a border would sit inside the box and
|
|
push the canvas a pixel off the lane waveforms it has to line up with. */
|
|
box-shadow: -1px 0 0 var(--border);
|
|
background: var(--panel);
|
|
overflow: hidden;
|
|
}
|
|
/* Ticks at the same times as the lanes ruler (buildFooterWaveTicks shares its
|
|
step), positioned by percentage so both rulers mark a time at the same x. */
|
|
.footer-wave-ticks {
|
|
height: 18px; position: relative;
|
|
}
|
|
/* No track, no timeline to label -- an empty ruler strip above the placeholder
|
|
waveform would be measuring nothing. */
|
|
.footer-wave-ticks:empty { display: none; }
|
|
.footer-wave-ticks .tick {
|
|
position: absolute; top: 0; bottom: 0;
|
|
border-left: 1px solid var(--border-strong);
|
|
padding-left: 4px;
|
|
display: flex; align-items: flex-start;
|
|
pointer-events: none;
|
|
}
|
|
.footer-wave-ticks .tick-label {
|
|
font-family: var(--font-mono); font-size: 9px; line-height: 1.6;
|
|
color: var(--muted-2);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.footer-wave-bar {
|
|
height: 31px; flex-shrink: 0;
|
|
position: relative; overflow: hidden;
|
|
}
|
|
#footer-waveform {
|
|
position: absolute; inset: 0;
|
|
width: 100%; height: 100%; display: block;
|
|
}
|
|
.footer-scrub {
|
|
position: absolute; inset: 0;
|
|
cursor: pointer; z-index: 2;
|
|
background: transparent;
|
|
}
|
|
.footer-scrub-fill { display: none; }
|
|
|
|
.footer-track-info {
|
|
flex: 1; min-width: 0;
|
|
display: flex; flex-direction: column; gap: 3px;
|
|
}
|
|
/* Two lines, then clipped: a column this narrow would otherwise cut most
|
|
titles after three or four words. */
|
|
.footer-track-title {
|
|
min-width: 0;
|
|
font-size: 13px; font-weight: 600; line-height: 1.35;
|
|
/* Overrides the nowrap/ellipsis on .daw-track-title, which is written for
|
|
single-line contexts. */
|
|
white-space: normal;
|
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
|
overflow: hidden; overflow-wrap: anywhere;
|
|
color: var(--fg);
|
|
}
|
|
.footer-cover {
|
|
width: 40px !important; height: 40px !important;
|
|
border-radius: 7px;
|
|
flex-shrink: 0;
|
|
}
|
|
/* Wraps freely here -- the column is narrow and the whole block is stacked,
|
|
so the meta reads as a short paragraph rather than a clipped line. */
|
|
.footer-track .daw-track-meta-line { row-gap: 3px; line-height: 1.45; }
|
|
.footer-art {
|
|
width: 44px; height: 44px; border-radius: 6px;
|
|
background: var(--panel-3); flex-shrink: 0; overflow: hidden;
|
|
display: none;
|
|
}
|
|
.footer-art.has-art { display: block; }
|
|
.footer-thumb-img {
|
|
width: 100%; height: 100%; object-fit: cover;
|
|
opacity: 0; transition: opacity 0.2s;
|
|
}
|
|
.footer-thumb-img.loaded { opacity: 1; }
|
|
.footer-info {
|
|
display: flex; flex-direction: column; gap: 3px; min-width: 0;
|
|
}
|
|
.footer-title {
|
|
font-size: 13px; font-weight: 600;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
color: var(--fg);
|
|
}
|
|
.footer-meta {
|
|
font-size: 11px; color: var(--muted);
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
|
|
/* Position group: elapsed / total time, then the loop controls that act on
|
|
that position (design 1b groups them together, not with the transport). */
|
|
.footer-group-position .footer-group-body { gap: 4px; }
|
|
.footer-elapsed {
|
|
font-size: 18px; font-weight: 500; letter-spacing: -0.02em; color: var(--fg);
|
|
}
|
|
.footer-total {
|
|
font-size: 12px; font-weight: 400; color: var(--muted-2);
|
|
}
|
|
.footer-time-sep { font-size: 12px; color: var(--muted-2); }
|
|
|
|
/* Exact loop start/end inputs (inline, right of the loop button) */
|
|
.footer-loop-times {
|
|
display: flex; align-items: center; gap: 5px;
|
|
margin-left: 4px; cursor: default; user-select: none;
|
|
}
|
|
.loop-times-sep { font-size: 12px; color: var(--muted); flex-shrink: 0; }
|
|
.loop-time-input {
|
|
width: 8ch; box-sizing: content-box;
|
|
padding: 2px 6px;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 5px; outline: none;
|
|
color: var(--fg); font-family: inherit; font-size: 12px; font-weight: 600;
|
|
font-variant-numeric: tabular-nums; text-align: center;
|
|
}
|
|
.loop-time-input:focus { border-color: var(--accent); }
|
|
.loop-time-input:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
|
|
/* Transport group: stop square + a play key wide enough to be the obvious
|
|
primary, both on the shared 34px control height. */
|
|
.footer-group-transport .daw-iconbtn.btn-transport {
|
|
width: 34px; height: 34px; border-radius: 8px;
|
|
background: var(--panel-2);
|
|
border-color: var(--border-strong);
|
|
color: var(--fg-2);
|
|
transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
|
|
}
|
|
.footer-group-transport .daw-iconbtn.btn-transport:hover {
|
|
background: var(--panel-3);
|
|
color: var(--fg);
|
|
}
|
|
.footer-group-transport .daw-play-btn {
|
|
width: 44px; height: 34px; border-radius: 8px;
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
|
|
}
|
|
.footer-group-transport .daw-play-btn:hover {
|
|
box-shadow: 0 2px 12px rgba(244,183,64,0.35), inset 0 1px 0 rgba(255,255,255,0.22);
|
|
}
|
|
.footer-group-transport .daw-play-btn.playing:hover {
|
|
box-shadow: 0 2px 12px rgba(76,175,125,0.45), inset 0 1px 0 rgba(255,255,255,0.24);
|
|
}
|
|
/* Active transport states: stop = red while stopped, loop = blue while looping. */
|
|
.footer-group-transport .btn-transport.stopped,
|
|
.footer-group-transport .btn-transport.stopped:hover {
|
|
background: rgba(214,90,74,0.16);
|
|
border-color: rgba(214,90,74,0.55);
|
|
color: var(--danger);
|
|
}
|
|
/* Loop sits in the Position group and reads as a modifier on the readout next
|
|
to it, so it is a short labelled pill rather than a full-height control. */
|
|
.footer-group-position .daw-iconbtn.btn-transport.loop {
|
|
width: auto; height: 26px; padding: 0 9px; gap: 6px; margin-left: 6px;
|
|
border-radius: 7px;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
color: var(--fg-2);
|
|
transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
|
|
}
|
|
.footer-group-position .btn-transport.loop:hover {
|
|
background: var(--panel-3); color: var(--fg);
|
|
}
|
|
.loop-btn-label { font-size: 11px; font-weight: 500; white-space: nowrap; }
|
|
.footer-group-position .btn-transport.loop.active,
|
|
.footer-group-position .btn-transport.loop.active:hover {
|
|
background: rgba(74,140,255,0.16);
|
|
border-color: rgba(74,140,255,0.55);
|
|
color: #4a8cff;
|
|
}
|
|
|
|
/* ── Segmented controls (Speed, click rate) ── */
|
|
/* One joined track rather than separate pills: the options are exclusive, and
|
|
butting them together says so (design 1b). */
|
|
.footer-seg {
|
|
display: flex; height: 34px; overflow: hidden;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 8px;
|
|
}
|
|
.footer-seg > button {
|
|
border: 0; background: transparent;
|
|
padding: 0 11px; cursor: pointer;
|
|
color: var(--muted);
|
|
font-family: var(--font-mono); font-size: 11px; font-weight: 500; line-height: 1;
|
|
white-space: nowrap;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.footer-seg > button:hover { background: var(--panel-3); color: var(--fg); }
|
|
.footer-seg > button + button { border-left: 1px solid var(--border); }
|
|
.speed-btn.active,
|
|
.speed-btn.active:hover {
|
|
background: rgba(232,200,64,0.16); color: var(--accent);
|
|
}
|
|
.metro-mult-btn.active,
|
|
.metro-mult-btn.active:hover {
|
|
background: rgba(74,140,255,0.16); color: #4a8cff;
|
|
}
|
|
|
|
/* Click track on/off toggle. Lit gold while the click is running, so its
|
|
state is visible without any other affordance. */
|
|
.click-toggle-btn {
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
height: 34px; padding: 0 10px;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 8px; color: var(--fg-2); cursor: pointer;
|
|
font-family: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
|
|
transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
|
|
}
|
|
.click-toggle-btn:hover:not(:disabled) { background: var(--panel-3); color: var(--fg); }
|
|
.click-toggle-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.click-toggle-btn.active,
|
|
.click-toggle-btn.active:hover {
|
|
background: rgba(232,200,64,0.18); border-color: rgba(232,200,64,0.55); color: var(--accent);
|
|
}
|
|
.click-metro-icon { flex-shrink: 0; }
|
|
|
|
/* Beat grid editor: canvas overlay + toolbar */
|
|
.beatgrid-canvas {
|
|
position: absolute; inset: 0; z-index: 12;
|
|
pointer-events: none; /* enabled only while editing */
|
|
}
|
|
.beatgrid-canvas.hidden { display: none; }
|
|
|
|
.beatgrid-toolbar {
|
|
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
padding: 7px 10px; margin: 0 0 6px;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 7px;
|
|
}
|
|
.beatgrid-toolbar.hidden { display: none; }
|
|
.bg-tool-label {
|
|
font-size: 10px; font-weight: 700; letter-spacing: 0.08em; color: var(--muted);
|
|
}
|
|
.bg-tools { display: flex; gap: 3px; }
|
|
.bg-btn {
|
|
padding: 3px 9px;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 5px; color: var(--fg-2); cursor: pointer;
|
|
font-family: inherit; font-size: 11px; font-weight: 600;
|
|
transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
|
|
}
|
|
.bg-btn:hover:not(:disabled) { background: var(--panel-3); color: var(--fg); }
|
|
.bg-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.bg-btn.active {
|
|
background: rgba(74,140,255,0.16); border-color: rgba(74,140,255,0.55); color: #4a8cff;
|
|
}
|
|
.bg-btn.bg-primary {
|
|
background: rgba(232,200,64,0.16); border-color: rgba(232,200,64,0.5); color: var(--accent);
|
|
}
|
|
.bg-btn.bg-danger:hover { border-color: rgba(214,90,74,0.55); color: var(--danger); }
|
|
.bg-check {
|
|
display: flex; align-items: center; gap: 4px;
|
|
font-size: 11px; font-weight: 600; color: var(--fg-2); cursor: pointer; user-select: none;
|
|
}
|
|
.bg-check input[type="checkbox"] { accent-color: var(--accent); cursor: pointer; }
|
|
.bg-meter input[type="number"] {
|
|
width: 4ch; padding: 2px 4px;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 4px; color: var(--fg);
|
|
font-family: inherit; font-size: 11px; font-weight: 600; text-align: center;
|
|
}
|
|
.bg-spacer { flex: 1; min-width: 8px; }
|
|
.bg-hint { font-size: 10.5px; color: var(--muted); flex-basis: 100%; }
|
|
|
|
/* Click track options: inline in the Click track cluster, always visible once
|
|
a track has a beat grid (#269 follow-up -- no popover, no click to reveal
|
|
them). Everything shares the 34px control height of the clusters beside it;
|
|
.daw-footer is min-height (not a fixed height) so a wrap never clips. */
|
|
.footer-metro-panel {
|
|
/* display:contents removes this element's own box and promotes its
|
|
children (the count-in switch, accent select, grid button, rate segment,
|
|
volume) to be direct flex items of .footer-group-body, as peers of the
|
|
on/off pill -- one flat wrap group the browser can line-break wherever
|
|
the available width actually allows, instead of the pill being forced
|
|
alone onto its own row and everything else confined to a second,
|
|
artificially narrow one (#269 follow-up). .hidden below still works: it
|
|
carries !important, which overrides this when the panel has no grid to
|
|
show options for. */
|
|
display: contents;
|
|
}
|
|
.footer-metro-panel.hidden { display: none; }
|
|
.metro-row { display: flex; align-items: center; gap: 9px; }
|
|
.metro-vol-row { width: 140px; }
|
|
.metro-vol-icon { color: var(--muted); flex-shrink: 0; }
|
|
.metro-label {
|
|
font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em;
|
|
color: var(--muted); flex-shrink: 0;
|
|
}
|
|
.metro-row input[type="range"] { flex: 1; min-width: 0; accent-color: var(--accent); }
|
|
.metro-val {
|
|
font-family: var(--font-mono); font-size: 11px; color: var(--muted);
|
|
width: 4ch; text-align: right;
|
|
font-variant-numeric: tabular-nums; flex-shrink: 0;
|
|
}
|
|
/* Native select, restyled to the same pill as the buttons around it: the
|
|
caret is drawn by the wrapper so no appearance-dependent arrow shows. */
|
|
.metro-select-wrap { position: relative; display: inline-flex; }
|
|
.metro-select-wrap::after {
|
|
content: ""; position: absolute; right: 10px; top: 50%;
|
|
margin-top: -2px; pointer-events: none;
|
|
border-left: 4px solid transparent; border-right: 4px solid transparent;
|
|
border-top: 5px solid var(--muted);
|
|
}
|
|
.metro-select {
|
|
appearance: none; -webkit-appearance: none;
|
|
height: 34px; padding: 0 26px 0 10px; max-width: 168px;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 8px; color: var(--fg); cursor: pointer;
|
|
font-family: inherit; font-size: 12px; font-weight: 500; outline: none;
|
|
}
|
|
.metro-select:hover { background: var(--panel-3); }
|
|
.metro-select:focus { border-color: var(--accent); }
|
|
|
|
/* Detection confidence / unavailability note: its own line under the
|
|
waveform, where it reads as a caption on the timeline it describes (design
|
|
1b). Hidden until it has text, clipped to one line with the full text as a
|
|
native tooltip. */
|
|
.metro-note {
|
|
margin-top: 8px; padding-right: 18px;
|
|
font-size: 11px; line-height: 1.3; color: var(--muted);
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.metro-note:empty { display: none; }
|
|
.metro-note.warn { color: var(--danger); }
|
|
|
|
/* Export chip button */
|
|
.footer-chip-wrap {
|
|
position: relative; flex-shrink: 0;
|
|
display: flex; align-items: center;
|
|
}
|
|
.footer-chip {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
color: var(--fg); border-radius: 8px; font-size: 12px; font-weight: 500;
|
|
font-family: inherit; cursor: pointer; padding: 0 12px; height: 36px;
|
|
white-space: nowrap;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.footer-chip:hover { background: var(--panel-3); }
|
|
.footer-chip-primary {
|
|
background: var(--accent); color: #1a1206;
|
|
border-color: transparent;
|
|
}
|
|
.footer-chip-primary:hover { background: color-mix(in srgb, var(--accent) 85%, white); }
|
|
.footer-chip-primary:disabled {
|
|
background: var(--panel-2); color: var(--fg-muted);
|
|
border-color: var(--border-strong); cursor: not-allowed; opacity: 0.5;
|
|
}
|
|
|
|
.footer-chip-panel {
|
|
/* Opens up and to the right: the button now lives in the left column, and a
|
|
right-aligned panel would hang past the footer's left edge over the
|
|
sidebar. */
|
|
position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 30;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 10px; padding: 4px;
|
|
min-width: 120px;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
}
|
|
.footer-chip-panel.hidden { display: none !important; }
|
|
|
|
.chip-panel-item {
|
|
display: flex; align-items: center; gap: 8px;
|
|
width: 100%; padding: 7px 12px;
|
|
background: none; border: none; border-radius: 7px;
|
|
color: var(--fg); font-size: 13px; font-family: inherit;
|
|
cursor: pointer; text-align: left; white-space: nowrap;
|
|
}
|
|
.chip-panel-item:hover { background: var(--panel-3); }
|
|
.chip-panel-item.active { color: var(--accent); font-weight: 600; }
|
|
|
|
/* ── Export split-button dropdown ── */
|
|
/* The whole button toggles the menu, so the caret is purely a decorative
|
|
open/close indicator (no separate hit area). */
|
|
.export-caret { opacity: 0.8; }
|
|
/* Primary button busy state: swap the download icon for a spinner. */
|
|
.export-spinner { display: none; }
|
|
.footer-chip.is-busy .export-dl-icon { display: none; }
|
|
.footer-chip.is-busy .export-spinner { display: inline-block; animation: sections-spin 700ms linear infinite; }
|
|
|
|
.chip-panel-export { min-width: 260px; padding: 6px; }
|
|
|
|
/* WAV / MP3 segmented toggle in the panel header */
|
|
.export-format-toggle {
|
|
display: flex; gap: 3px; padding: 3px;
|
|
margin-bottom: 4px;
|
|
background: var(--bg); border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
.export-fmt {
|
|
flex: 1; padding: 5px 0; border: none; border-radius: 6px;
|
|
background: transparent; color: var(--muted);
|
|
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
|
letter-spacing: 0.04em; cursor: pointer;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.export-fmt:hover { color: var(--fg-2); }
|
|
.export-fmt.active { background: var(--panel-3); color: var(--accent); }
|
|
|
|
/* Action rows — two-line: icon + (title/desc) + trailing check/count */
|
|
.export-item {
|
|
align-items: center; gap: 11px;
|
|
padding: 9px 10px; white-space: normal;
|
|
}
|
|
.export-item .chip-item-icon {
|
|
flex-shrink: 0; width: 22px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--muted);
|
|
}
|
|
.export-item .chip-item-text {
|
|
flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px;
|
|
}
|
|
.chip-item-title { font-size: 13px; font-weight: 500; color: var(--fg); }
|
|
.chip-item-desc { font-size: 11px; color: var(--muted); }
|
|
.chip-item-check { color: var(--accent); flex-shrink: 0; }
|
|
.export-item:hover:not([aria-disabled="true"]) { background: var(--panel-3); }
|
|
.export-item[aria-disabled="true"] { opacity: 0.4; pointer-events: none; }
|
|
|
|
/* MP4 is offered as an export format only when the job has a preserved video
|
|
track (mp4 upload or YouTube). player.js toggles .has-video on the wrap. */
|
|
/* Opt-in rather than automatic: baking a permanent click into a bounce you
|
|
meant to be clean is not obvious until you play it back. Applies to every
|
|
format, audio and video alike. */
|
|
.export-click-opt {
|
|
display: flex; align-items: center; gap: 7px;
|
|
margin: 2px 4px 4px; padding: 5px 6px;
|
|
border-radius: 6px; cursor: pointer; user-select: none;
|
|
font-size: 11.5px; font-weight: 600; color: var(--fg-2);
|
|
}
|
|
.export-click-opt:hover { background: var(--panel-3); color: var(--fg); }
|
|
.export-click-opt input[type="checkbox"] { accent-color: var(--accent); cursor: pointer; }
|
|
.export-click-opt.disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.export-click-opt.disabled:hover { background: none; color: var(--fg-2); }
|
|
|
|
.export-fmt-video { display: none; }
|
|
#footer-export-wrap.has-video .export-fmt-video { display: block; flex: 1; }
|
|
|
|
/* In MP4 mode only "Export Mix" applies — the audio-only Stems/Region rows
|
|
are hidden. main.js toggles .fmt-mp4 on the panel. */
|
|
#t-export-panel.fmt-mp4 #t-export-stems,
|
|
#t-export-panel.fmt-mp4 #t-export-region { display: none; }
|
|
|
|
/* ── About dialog ── */
|
|
.about-backdrop {
|
|
position: fixed; inset: 0; z-index: 100;
|
|
display: grid; place-items: center;
|
|
background: rgba(3,8,13,0.6); backdrop-filter: blur(6px);
|
|
}
|
|
.about-backdrop.hidden { display: none !important; }
|
|
.about-card {
|
|
width: min(300px, calc(100vw - 32px));
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 18px; box-shadow: 0 24px 64px rgba(0,0,0,0.6);
|
|
padding: 28px 24px 24px; text-align: center;
|
|
position: relative; display: flex; flex-direction: column; align-items: center; gap: 0;
|
|
}
|
|
.about-close {
|
|
position: absolute; top: 12px; right: 12px;
|
|
width: 26px; height: 26px;
|
|
background: transparent; border: 1px solid var(--border);
|
|
border-radius: 6px; color: var(--muted);
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.about-close:hover { background: var(--panel-3); color: var(--fg); }
|
|
.about-logo {
|
|
width: 56px; height: 56px; border-radius: 14px;
|
|
background: linear-gradient(135deg, rgba(139,92,246,0.2), rgba(244,183,64,0.15));
|
|
border: 1px solid rgba(139,92,246,0.25);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--accent); margin-bottom: 14px;
|
|
}
|
|
.about-card h2 { margin: 0 0 4px; font-size: 18px; font-family: var(--font-mono); font-weight: 700; }
|
|
.about-tagline { margin: 0 0 12px; font-size: 12px; color: var(--muted); }
|
|
.about-version-badge {
|
|
display: inline-block; font-size: 11px; font-family: var(--font-mono);
|
|
color: var(--muted); background: var(--panel); border: 1px solid var(--border);
|
|
border-radius: 20px; padding: 2px 10px; margin-bottom: 20px;
|
|
}
|
|
.about-primary-links {
|
|
display: flex; gap: 8px; width: 100%; margin-bottom: 16px;
|
|
}
|
|
.about-link {
|
|
flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
padding: 8px 10px; border-radius: 8px; font-size: 12px;
|
|
text-decoration: none; transition: background 0.15s;
|
|
}
|
|
.about-link-primary {
|
|
background: var(--panel); border: 1px solid var(--border);
|
|
color: var(--fg-2); font-weight: 600;
|
|
}
|
|
.about-link-primary:hover {
|
|
background: var(--accent); border-color: var(--accent); color: #000;
|
|
}
|
|
.about-link-secondary {
|
|
background: var(--panel); border: 1px solid var(--border);
|
|
color: var(--fg-2);
|
|
}
|
|
.about-link-secondary:hover { background: var(--accent); border-color: var(--accent); color: #000; }
|
|
/* The in-app update pill: sits beside the plain Download link and is the
|
|
recommended action, so it carries the accent fill by default rather than
|
|
only on hover. */
|
|
.about-link-accent {
|
|
background: var(--accent); border: 1px solid var(--accent);
|
|
color: #000; font-weight: 600; cursor: pointer;
|
|
}
|
|
.about-link-accent:hover { filter: brightness(1.08); }
|
|
.about-link-accent:disabled { opacity: 0.55; cursor: default; filter: none; }
|
|
.about-divider {
|
|
width: 100%; height: 1px; background: var(--border); margin-bottom: 16px;
|
|
}
|
|
.about-socials {
|
|
display: flex; gap: 10px; align-items: center; justify-content: center;
|
|
}
|
|
.about-social-btn {
|
|
width: 36px; height: 36px; border-radius: 8px;
|
|
background: var(--panel); border: 1px solid var(--border);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--muted); text-decoration: none; transition: all 0.15s;
|
|
}
|
|
.about-social-btn:hover { background: var(--accent); border-color: var(--accent); color: #000; }
|
|
|
|
/* ── Release notes dialog ── */
|
|
.release-card {
|
|
width: min(520px, calc(100vw - 32px));
|
|
max-height: calc(100vh - 64px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
.release-notes {
|
|
width: 100%;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
text-align: left;
|
|
margin: 0 0 16px;
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
color: var(--fg-2);
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
.release-notes h4 {
|
|
margin: 12px 0 4px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: var(--fg);
|
|
}
|
|
.release-notes h4:first-child { margin-top: 0; }
|
|
.release-notes p { margin: 0 0 8px; }
|
|
.release-notes ul { margin: 0 0 8px; padding-left: 18px; }
|
|
.release-notes li { margin: 2px 0; }
|
|
.release-notes a { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; }
|
|
.release-notes a:hover { text-decoration: underline; }
|
|
.release-notes code {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 0 4px;
|
|
}
|
|
.release-notes pre {
|
|
margin: 0 0 8px;
|
|
padding: 8px 10px;
|
|
overflow-x: auto;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
}
|
|
.release-notes pre code { border: 0; background: none; padding: 0; }
|
|
.release-notes blockquote {
|
|
margin: 0 0 8px;
|
|
padding: 8px 12px;
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: 0 6px 6px 0;
|
|
background: rgba(139, 92, 246, 0.08);
|
|
}
|
|
.release-notes blockquote > :last-child { margin-bottom: 0; }
|
|
.release-callout {
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
text-transform: uppercase;
|
|
font-size: 10px;
|
|
letter-spacing: 0.06em;
|
|
margin: 0 0 6px !important;
|
|
}
|
|
/* Server/Docker mode: image-pull guidance in place of a desktop download. */
|
|
.release-docker {
|
|
width: 100%;
|
|
text-align: left;
|
|
margin: 0 0 16px;
|
|
}
|
|
.release-docker-label {
|
|
margin: 0 0 6px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--accent);
|
|
}
|
|
.release-docker-text { margin: 0 0 8px; font-size: 12px; color: var(--fg-2); }
|
|
.release-docker-cmd {
|
|
margin: 0 0 8px;
|
|
padding: 8px 10px;
|
|
overflow-x: auto;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--fg);
|
|
}
|
|
.release-docker-note { margin: 0; font-size: 11px; color: var(--muted); }
|
|
|
|
/* Windows desktop: in-app download + apply, in place of a browser download. */
|
|
.release-inapp { width: 100%; text-align: left; margin: 0 0 16px; }
|
|
.release-inapp-progress-bar {
|
|
height: 3px; border-radius: 2px; margin-bottom: 6px;
|
|
background: rgba(255, 255, 255, 0.09); overflow: hidden;
|
|
}
|
|
.release-inapp-progress-fill {
|
|
height: 100%; width: 0; border-radius: 2px;
|
|
background: var(--accent);
|
|
transition: width 260ms linear;
|
|
}
|
|
/* Indeterminate: the updater cannot listen to the Rust progress event from this
|
|
origin (see catalog.js), so the bar shows activity rather than a percentage. */
|
|
.release-inapp-progress.indeterminate .release-inapp-progress-fill {
|
|
width: 40%;
|
|
animation: release-inapp-slide 1.1s ease-in-out infinite;
|
|
}
|
|
@keyframes release-inapp-slide {
|
|
0% { margin-left: -40%; }
|
|
100% { margin-left: 100%; }
|
|
}
|
|
.release-inapp-progress-text { margin: 0; font-size: 11px; color: var(--muted); }
|
|
.release-inapp-error { margin: 0; font-size: 12px; color: #e54e4e; }
|
|
|
|
/* The release dialog lives outside `.daw`, so the scoped `.daw .hidden` rule
|
|
does not reach its inner elements and base.css's global `.hidden` is not
|
|
loaded on this page. Hide the mode-specific parts explicitly (same approach
|
|
as `.about-backdrop.hidden`) so desktop hides the docker block and server
|
|
hides the download button. */
|
|
.release-docker.hidden,
|
|
.release-inapp.hidden,
|
|
.release-inapp-progress.hidden,
|
|
.release-inapp-error.hidden,
|
|
.release-card .about-link.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* The "New release available" card opens the dialog on click. */
|
|
.daw-notif-release { cursor: pointer; }
|
|
|
|
/* ── Failure dialog ── */
|
|
/* Same shell as the release dialog, in the danger colour, with the technical
|
|
block the report will carry shown verbatim -- nothing is sent that the user
|
|
has not been able to read first. */
|
|
.failure-card {
|
|
width: min(560px, calc(100vw - 32px));
|
|
max-height: calc(100vh - 64px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
.failure-logo { color: var(--danger); }
|
|
.failure-when { color: var(--muted); }
|
|
.failure-message {
|
|
margin: 12px 0 0;
|
|
font-size: 13px; line-height: 1.5; color: var(--fg-2);
|
|
white-space: pre-line; text-align: center;
|
|
}
|
|
.failure-hint {
|
|
margin: 10px 0 0;
|
|
font-size: 11px; line-height: 1.5; color: var(--muted);
|
|
text-align: center;
|
|
}
|
|
.failure-tech {
|
|
width: 100%; flex: 1 1 auto; min-height: 0;
|
|
margin: 12px 0 0; padding: 10px 12px;
|
|
overflow: auto; text-align: left;
|
|
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
}
|
|
.failure-tech code {
|
|
font-family: var(--font-mono); font-size: 11px; line-height: 1.55;
|
|
color: var(--fg-2); white-space: pre-wrap; overflow-wrap: anywhere;
|
|
}
|
|
/* Opt-in, not automatic: this pulls extra log content into what's about to be
|
|
copied into a public report, so it gets its own deliberate click rather
|
|
than firing every time the dialog opens (matches .library-editor-sync's
|
|
amber "extra action" treatment). */
|
|
.failure-logs-btn {
|
|
align-self: flex-start;
|
|
min-height: 28px; margin-top: 8px; padding: 0 12px;
|
|
border-radius: 7px; border: 1px solid rgba(244, 183, 64, 0.3);
|
|
background: rgba(244, 183, 64, 0.16); color: var(--accent);
|
|
font-family: var(--font-mono); font-size: 11px; cursor: pointer;
|
|
}
|
|
.failure-logs-btn:disabled { opacity: 0.55; cursor: default; }
|
|
/* Reporting is the reason this dialog exists, so it gets the app's primary
|
|
button treatment (as Export Mix does) rather than the muted link style the
|
|
About/release dialogs use for their several equal-weight links. */
|
|
.failure-card .about-link-primary {
|
|
background: var(--accent); border-color: transparent; color: #1a1206;
|
|
}
|
|
.failure-card .about-link-primary:hover {
|
|
background: color-mix(in srgb, var(--accent) 85%, white);
|
|
color: #1a1206;
|
|
}
|
|
|
|
/* ── Library sections (Recent · Stem Collections · Tags) ── */
|
|
.lib-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding-top: 14px;
|
|
}
|
|
.lib-section + .lib-section {
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.lib-section-head {
|
|
padding: 0 4px 6px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/* Tag chips */
|
|
/* ── Tag autocomplete dropdown ── */
|
|
.tag-suggest {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
left: 0; right: 0;
|
|
z-index: 50;
|
|
background: var(--panel-2);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: 8px;
|
|
padding: 4px;
|
|
list-style: none;
|
|
margin: 0;
|
|
box-shadow: var(--shadow-panel);
|
|
}
|
|
.tag-suggest:empty { display: none; }
|
|
.tag-suggest-item {
|
|
padding: 7px 10px;
|
|
border-radius: 5px;
|
|
font-size: 13px;
|
|
color: var(--fg-2);
|
|
cursor: pointer;
|
|
}
|
|
.tag-suggest-item:hover,
|
|
.tag-suggest-item.focused {
|
|
background: var(--panel-3);
|
|
color: var(--fg);
|
|
}
|
|
|
|
.lib-tags-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
padding: 2px 0 8px;
|
|
}
|
|
.lib-tag-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 5px 11px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
color: var(--fg-2);
|
|
cursor: pointer;
|
|
transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
|
|
white-space: nowrap;
|
|
}
|
|
.lib-tag-chip:hover {
|
|
background: var(--panel-2);
|
|
color: var(--fg);
|
|
border-color: rgba(255,255,255,0.15);
|
|
}
|
|
.lib-tag-chip.active {
|
|
background: rgba(244,183,64,0.12);
|
|
color: var(--accent);
|
|
border-color: rgba(244,183,64,0.3);
|
|
}
|
|
.lib-tag-count { color: var(--muted); font-size: 11px; }
|
|
|
|
/* Supporters (partner tiles, shown in the TV-icon dialog) */
|
|
/* Slightly wider card so 3 tiles breathe; grid spans the card. */
|
|
.friends-card { width: min(360px, calc(100vw - 32px)); }
|
|
.friends-card .lib-friends-grid { width: 100%; margin-top: 4px; }
|
|
.lib-friends-grid {
|
|
/* Masonry columns: each column stacks independently so a tall tile does not
|
|
push others down. Tiles are round-robined into these columns in JS. */
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
padding: 2px 0 0;
|
|
}
|
|
.lib-friends-col {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.lib-friend {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
padding: 9px 5px 12px;
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--fg-2);
|
|
text-decoration: none;
|
|
text-align: center;
|
|
/* deliberately-uneven "frames on a wall" tilt; per-tile angle set in JS */
|
|
transform: rotate(var(--tilt, 0deg));
|
|
transition: background var(--t-fast), color var(--t-fast),
|
|
border-color var(--t-fast), transform var(--t-fast);
|
|
}
|
|
.lib-friend:hover {
|
|
background: var(--panel-2);
|
|
color: var(--fg);
|
|
border-color: rgba(255,255,255,0.15);
|
|
/* straighten the frame on hover */
|
|
transform: rotate(0deg);
|
|
}
|
|
/* Logos are transparent wordmarks; span the tile width, keep aspect, cap height. */
|
|
.lib-friend-logo {
|
|
width: 100%;
|
|
height: auto;
|
|
max-height: 30px;
|
|
object-fit: contain;
|
|
}
|
|
/* Instagram profile photos render as round avatars */
|
|
.lib-friend-avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 1px solid var(--border);
|
|
}
|
|
/* Monogram fallback when a tile has no image (or it fails to load): matches the
|
|
round avatar size, in the accent colour, so the grid stays on-brand. */
|
|
.lib-friend-monogram {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: var(--font-sans);
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
background: var(--panel-3);
|
|
border: 1px solid var(--border);
|
|
}
|
|
/* Small Instagram glyph under the text on tiles that link to Instagram */
|
|
.lib-friend-ig {
|
|
/* block + no-shrink avoids the WebKit baseline clip on small inline SVGs */
|
|
display: block;
|
|
flex: 0 0 auto;
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-top: 2px;
|
|
fill: currentColor;
|
|
opacity: 0.65;
|
|
}
|
|
.lib-friend:hover .lib-friend-ig {
|
|
opacity: 1;
|
|
}
|
|
.lib-friend-name {
|
|
width: 100%;
|
|
font-size: 10.5px;
|
|
line-height: 1.2;
|
|
/* full name across up to 2 lines, then ellipsis */
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.lib-friend-role {
|
|
width: 100%;
|
|
margin-top: 2px;
|
|
font-size: 9px;
|
|
line-height: 1.2;
|
|
color: var(--fg-3, var(--fg-2));
|
|
/* show the full role; the tile grows to fit it */
|
|
}
|
|
|
|
/* ── Clear bin bar (trash view only) ── */
|
|
.clear-bin-bar { display: none; padding: 6px 10px 4px; }
|
|
.sidebar.trash-view .clear-bin-bar { display: block; }
|
|
.sidebar.trash-view .lib-section-head .new-folder-btn { display: none; }
|
|
.sidebar.favorites-view .lib-section-head .new-folder-btn { display: none; }
|
|
.clear-bin-btn {
|
|
display: flex; align-items: center; gap: 5px;
|
|
width: 100%; padding: 6px 10px; border-radius: 6px;
|
|
background: rgba(229, 78, 78, 0.12); border: 1px solid rgba(229, 78, 78, 0.28);
|
|
color: #e54e4e; font-size: 12px; font-weight: 500; cursor: pointer;
|
|
transition: background 150ms, border-color 150ms;
|
|
}
|
|
.clear-bin-btn:hover { background: rgba(229, 78, 78, 0.22); border-color: rgba(229, 78, 78, 0.5); }
|
|
.clear-bin-btn:active { background: rgba(229, 78, 78, 0.32); }
|
|
|
|
/* ── App state classes (set by JS) ── */
|
|
/* No track loaded: hide main waveform content */
|
|
.daw.is-import .daw-track-header,
|
|
.daw.is-import .daw-section-ribbon,
|
|
.daw.is-import .daw-wave-header,
|
|
.daw.is-import .daw-content { opacity: 0.3; pointer-events: none; }
|
|
.daw.is-import .daw-content .mixer-column { opacity: 1; pointer-events: auto; }
|
|
|
|
.daw.no-track .daw-track-header,
|
|
.daw.no-track .daw-section-ribbon,
|
|
.daw.no-track .daw-wave-header,
|
|
.daw.no-track .daw-content { opacity: 0.3; pointer-events: none; }
|
|
/* Overlay and job progress live inside .daw-content but must stay fully visible while processing */
|
|
.daw.no-track .daw-content .wave-loading-overlay,
|
|
.daw.no-track .daw-content .job { opacity: 1; pointer-events: auto; }
|
|
|
|
.app.no-track #t-export-btn,
|
|
.app.is-import #t-export-btn {
|
|
opacity: 0.35;
|
|
pointer-events: none;
|
|
cursor: default;
|
|
}
|
|
|
|
/* When track is playing */
|
|
.daw-play-btn .pause-icon { display: none; }
|
|
|
|
/* ── Responsive: hide zoom in small windows ── */
|
|
@media (max-width: 900px) {
|
|
.daw-info-row { flex-wrap: wrap; }
|
|
.daw-track-card { width: 100%; border-right: none; border-bottom: 1px solid var(--border); }
|
|
.stem-presence-panel { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
|
|
/* Settings -> Logs. Read-only: paths, sizes and what writes each file. Log
|
|
*contents* are deliberately not served -- a traceback can capture anything,
|
|
and the files are on the machine the user is already sitting at. */
|
|
.settings-logs-dir {
|
|
margin: 2px 2px 10px;
|
|
padding: 8px 10px;
|
|
background: var(--panel); border: 1px solid var(--border-strong);
|
|
border-radius: 6px;
|
|
}
|
|
.settings-logs-path {
|
|
font-size: 11.5px; color: var(--fg-2); word-break: break-all; user-select: all;
|
|
}
|
|
.settings-logs-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.settings-log-row {
|
|
padding: 8px 10px;
|
|
background: var(--panel); border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
}
|
|
.settings-log-main {
|
|
display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
|
|
}
|
|
.settings-log-name { font-size: 12px; font-weight: 600; color: var(--fg); }
|
|
.settings-log-meta {
|
|
font-size: 10.5px; color: var(--muted); white-space: nowrap;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.settings-log-desc { margin-top: 3px; font-size: 11px; color: var(--muted); line-height: 1.4; }
|
|
.settings-logs-empty { font-size: 11.5px; color: var(--muted); padding: 6px 2px; }
|
|
|
|
.settings-export-logs {
|
|
padding: 6px 12px;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 7px; color: var(--fg-2); cursor: pointer;
|
|
font-family: inherit; font-size: 12px; font-weight: 600; white-space: nowrap;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.settings-export-logs:hover:not(:disabled) { background: var(--panel-3); color: var(--fg); }
|
|
.settings-export-logs:disabled { opacity: 0.5; cursor: default; }
|
|
|
|
/* Stems location (#354). The path is long, so this row stacks instead of
|
|
sitting on one line with the label. */
|
|
.settings-row-stack { display: block; }
|
|
.settings-btn {
|
|
padding: 6px 12px;
|
|
background: var(--panel-2); border: 1px solid var(--border-strong);
|
|
border-radius: 7px; color: var(--fg-2); cursor: pointer;
|
|
font-family: inherit; font-size: 12px; font-weight: 600; white-space: nowrap;
|
|
transition: background var(--t-fast), color var(--t-fast);
|
|
}
|
|
.settings-btn:hover:not(:disabled) { background: var(--panel-3); color: var(--fg); }
|
|
.settings-btn:disabled { opacity: 0.5; cursor: default; }
|
|
.stems-location {
|
|
display: flex; align-items: center; gap: 8px; margin-top: 8px;
|
|
}
|
|
.stems-location-path {
|
|
flex: 1; min-width: 0;
|
|
padding: 6px 8px; border-radius: 6px;
|
|
background: var(--panel); border: 1px solid var(--border);
|
|
color: var(--fg-2); font-family: var(--font-mono); font-size: 11px;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.stems-location-size {
|
|
flex-shrink: 0; font-size: 11px; color: var(--muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.stems-location-msg {
|
|
margin-top: 6px; font-size: 11px; line-height: 1.4; color: var(--muted);
|
|
}
|
|
.stems-location-msg.error { color: var(--danger); }
|
|
.stems-location-msg.ok { color: var(--accent); }
|
|
|
|
/* Logs sub-navigation: Location / Application / Setup. */
|
|
.settings-subtabs {
|
|
display: flex; gap: 2px; margin: 0 0 10px;
|
|
border-bottom: 1px solid var(--border-strong);
|
|
}
|
|
.settings-subtab {
|
|
padding: 5px 10px; background: none; border: none;
|
|
border-bottom: 2px solid transparent; margin-bottom: -1px;
|
|
color: var(--muted); cursor: pointer;
|
|
font-family: inherit; font-size: 11.5px; font-weight: 600;
|
|
transition: color var(--t-fast), border-color var(--t-fast);
|
|
}
|
|
.settings-subtab:hover { color: var(--fg-2); }
|
|
.settings-subtab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
/* The pane is a flex column and the registry/log textareas rely on flex:1 to
|
|
fill it, so the subpane has to be one too -- otherwise the textarea collapses
|
|
to its default character width and the lines get cut off. */
|
|
.settings-subpane { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
.settings-subpane.hidden { display: none; }
|
|
/* The tail views are monospace and taller than the registry dump: log lines
|
|
are long and wrap badly. */
|
|
.settings-logtail-view {
|
|
width: 100%; box-sizing: border-box;
|
|
min-height: 260px; white-space: pre; overflow-wrap: normal; overflow-x: auto;
|
|
}
|