Files
Thales 8c0a96689f 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.
2026-08-23 10:07:33 +01:00

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;
}