/* karaokit web — minimal stylesheet for project + align views. */

:root {
  --bg:        #1a1b26;
  --fg:        #c0caf5;
  --muted:     #565f89;
  --accent:    #7aa2f7;
  --accent-2:  #bb9af7;
  --pink:      #f7768e;
  --ok:        #9ece6a;
  --missing:   #565f89;
  --border:    #2a2b3d;
  --row-hover: #222436;
  --selection: rgba(247, 118, 142, 0.20);

  /* Waveform lane fills.  All three sit ≥ 7:1 against #16161e (WCAG AAA)
     and span three distinct hue axes (blue / cyan / amber) so they remain
     distinguishable under deuteranopia and protanopia. */
  --wave-mix:    #7aa2f7;   /* blue   — full mix      */
  --wave-music:  #73daca;   /* teal   — instrumental  */
  --wave-voice:  #e0af68;   /* amber  — voice         */

  /* Envelope curve colours — same hue family as the waves but darker
     and more saturated so the user-drawn gain curve reads clearly
     against its lane's wave fill (per change request: "thicker and
     darker than the soundwave itself"). */
  --env-mix:     #3a5fc4;
  --env-music:   #2c8c7d;
  --env-voice:   #a26418;

  /* Vertical gap between the lane-header band and the song timeline.
     Applied as padding-top on every lane's content wrapper so t=0
     sits clearly BELOW the header (not flush against it).  Exposed as
     a CSS variable so trim/waveform/lyrics stay locked to the same
     offset and the editor.js coord math has one place to read from. */
  --timeline-gap: 6px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 1.5rem 2rem;
  font: 14px/1.5 ui-monospace, "SFMono-Regular", Consolas, monospace;
  background: var(--bg);
  color: var(--fg);
}

header h1 {
  margin: 0 0 0.25rem;
  font-size: 1.6rem;
  letter-spacing: 0.02em;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

.muted   { color: var(--muted); }
.empty   { color: var(--muted); font-style: italic; padding: 1rem 0; }
.ok      { color: var(--ok); }
.missing { color: var(--missing); }
.back    { display: inline-block; margin-bottom: 0.5rem; }

footer {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}

/* Home-page fetch form — paste a URL, submit, watch the job log. */
.fetch {
  margin: 1rem 0 1.5rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #16161e;
}
.fetch h2 {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  color: var(--muted);
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.fetch-form {
  display: grid;
  grid-template-columns: 2fr 1fr auto;
  gap: 0.6rem;
  align-items: end;
}
@media (max-width: 720px) {
  .fetch-form { grid-template-columns: 1fr; }
}
.fetch-field { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.85rem; }
.fetch-field span { color: var(--muted); }
.fetch-field input {
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.35rem 0.5rem;
}
.fetch-field input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.fetch-form button[type=submit] {
  font-family: inherit;
  font-size: 0.9rem;
  background: var(--accent);
  color: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 0.4rem 1rem;
  cursor: pointer;
}
.fetch-form button[type=submit]:hover { filter: brightness(1.1); }
.fetch-hint { margin: 0.4rem 0 0; font-size: 0.8rem; }
#fetch-status { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; }

/* Upload-your-own-file form — same card chrome as .fetch, wider grid for the
   extra (file / title / artist / slug) inputs. */
.upload-form { grid-template-columns: 1.4fr 1fr 1fr 0.8fr auto; }
@media (max-width: 720px) {
  .upload-form { grid-template-columns: 1fr; }
}
.fetch-field input[type=file] { padding: 0.3rem; font-size: 0.8rem; }
#upload-status { margin-top: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; }
.job-actions { margin: 0.5rem 0 0; }

/* Project list */
.songs {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1rem;
}
.songs th, .songs td {
  text-align: left;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--border);
}
.songs th { color: var(--muted); font-weight: normal; }
.songs tr:hover td { background: var(--row-hover); }
.songs code { color: var(--accent); }

/* Inline-edit cells (Author / Title) — invisible until hover or focus,
   so the table reads as static text in its rest state but reveals an
   editable affordance under interaction. */
.songs input.cell-edit {
  width: 100%;
  font: inherit;
  color: var(--fg);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 3px;
  padding: 0.1rem 0.3rem;
  margin: -0.1rem -0.3rem;   /* keep text x-aligned with non-edit cells */
}
.songs input.cell-edit::placeholder { color: var(--muted); }
.songs tr:hover input.cell-edit { border-color: var(--border); }
.songs input.cell-edit:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg);
}

/* Trailing action cell — the existing `align ›` link plus the new
   delete button.  Lay them on one row so the column stays compact. */
.songs td.row-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  white-space: nowrap;
}

/* ── Align view layout ──────────────────────────────────────────────────
   Body is a vertical stack: header (title + ops + tasks log) → EXPORT
   panel → editor grid (waveforms + lyrics, ALWAYS at the bottom).
   The editor is the only element that scrolls internally so the
   waveform's y-axis aligns with each lyric row. */
.align-view {
  margin: 0;
  padding: 0.75rem 1rem;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Single consolidated navbar.  Replaces the previous two-row design
 * (page header + a separate TASKS button strip below the editor) — the
 * user reported that two horizontal navbars read as confusing.  This
 * one bar contains: title line, then a flex-wrap row of editor ops
 * (undo/redo/play/snap) + long-running job buttons (separate /
 * transcribe / fetch lyrics / provide / align) + a right-aligned
 * cursor readout. */
.align-view header {
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.align-view header .header-title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.6rem;
}
.align-view header h1 {
  display: inline-block;
  margin: 0;
}
.align-view header .header-meta {
  font-size: 0.85em;
}
.align-view header .header-sep {
  color: var(--border);
  margin: 0 0.1rem;
}
.align-view header .header-cursor {
  margin-left: auto;
}
.align-view header .tasks-list {
  /* SSE cards land here; only contributes height when a job is
   * running.  Reserve 0 baseline so the page doesn't shift each time
   * a card appears. */
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.align-view header .tasks-list:empty { display: none; }

/* Snap-mode toggle — pressed (.is-on) means quantize; released means
 * raw freedom.  JS flips classes + aria-pressed on click. */
button.op.snap-toggle {
  font-size: 0.85rem;
  padding: 0.25rem 0.7rem;
}
button.op.snap-toggle:not(.is-on) {
  background: var(--bg);
  color: var(--muted);
  border-color: var(--muted);
}
button.op.snap-toggle:not(.is-on):hover {
  background: var(--accent);
  color: var(--bg);
}

.editor-grid {
  display: grid;
  /* TRIM | MIX | LYRICS — the trim column is a thin handle; mix and
     lyrics flex with the available width. */
  grid-template-columns: 24px 110px 1fr;
  gap: 0.75rem;
  position: relative;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: #16161e;
  /* Server-computed reserve based on ffprobe duration (≈ duration_s ·
   * pxPerSecMin + headers).  Set as an inline style on <body> so the
   * editor-grid has its eventual height on first paint — no more page
   * "shake" when wavesurfer decode finishes and JS resizes the canvas.
   * Falls back to a generous 600px floor when duration_s is unknown. */
  min-height: var(--reserved-grid-h, 600px);
}

/* When Demucs stems are present, two extra waveform lanes appear
   between MIX and LYRICS — the trim column stays at the leftmost
   position. */
.editor-grid.with-stems {
  grid-template-columns: 24px 110px 90px 90px 1fr;
}

/* Trim handle column — full timeline height, dim out-of-window areas,
   draggable head + tail grips on a coloured center band. */
.window-handle {
  position: relative;
  background: #14141c;
  margin: 0;
  border-right: 1px solid var(--border);
}
.window-handle h2 {
  /* Match .waveform h2 padding + visual treatment so every lane's
     sticky header has the same vertical extent and shares the same
     bottom border.  The h2 contains a non-breaking space (in HTML)
     so its line-height matches the labelled lane h2s — that's what
     keeps the trim column's bottom border (and therefore the trim
     band's t=0) at the same y as MIX/MUSIC/VOICE/LYRICS. */
  position: sticky;
  top: 0;
  margin: 0;
  padding: 0.4rem 0.6rem calc(0.4rem + var(--timeline-gap));
  font-size: 0.8rem;
  color: var(--muted);
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: #14141c;
  border-bottom: 1px solid var(--border);
  z-index: 4;
  pointer-events: none;
  text-align: center;
}

/* Trim reset button — mirrors `.waveform h2 .env-reset` (compact ↺
 * icon, absolute top-right inside the sticky h2).  Hidden by default;
 * editor.js shows it only when the trim window has been moved off its
 * default of [head=0, tail=open]. */
.window-handle h2 .window-reset {
  position: absolute;
  top: 0.25rem;
  right: 0.2rem;
  pointer-events: auto;
  font-family: inherit;
  font-size: 0.85rem;
  line-height: 1;
  background: rgba(22, 22, 30, 0.7);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 1px 3px;
  cursor: pointer;
  text-transform: none;
  letter-spacing: 0;
}
.window-handle h2 .window-reset:hover { color: var(--fg); border-color: var(--accent); }
.window-handle h2 .window-reset[hidden] { display: none; }
.window-handle-content {
  position: relative;
  height: 100%;
  /* See `.waveform-content` for why the visual gap is not done here:
     same overlay-vs-flow alignment constraint applies to the trim
     band, dim overlays, and grip handles. */
}
/* Dim overlays — direct children of .editor-grid so they span every
   lane (TRIM | MIX | MUSIC | VOICE | LYRICS).  Hidden by default; JS
   shows + sizes them based on the current trim window. */
.window-dim {
  /* `position: absolute` takes the dim out of grid flow entirely;
     left/right span the full container width regardless of column
     template.  Top/height are set by JS from timeToY(head_s) /
     timeToY(tail_s). */
  position: absolute;
  left: 0;
  right: 0;
  background: rgba(10, 10, 16, 0.55);
  pointer-events: none;
  z-index: 5;
  display: none;
}
.window-dim.show { display: block; }
.window-dim-head { top: 0; }
.window-dim-tail { bottom: 0; }

/* Active window band — thin coloured strip in the trim column showing
   [head_s, tail_s].  Sits below the dim overlays in the trim column
   itself but above them across siblings (which only see the dim). */
.window-band {
  position: absolute;
  left: 4px;
  right: 4px;
  background: linear-gradient(to bottom,
              rgba(122, 162, 247, 0.35),
              rgba(122, 162, 247, 0.15));
  border-top: 1px solid var(--accent);
  border-bottom: 1px solid var(--accent);
  pointer-events: none;
  z-index: 6;
  display: none;
}
.window-band.show { display: block; }

.window-handle-grip {
  position: absolute;
  left: -4px;
  right: -4px;
  height: 10px;
  cursor: ns-resize;
  pointer-events: auto;
  background: var(--accent);
  border-radius: 2px;
  z-index: 7;
}
/* Grips sit ENTIRELY inside the band's edges so they never poke into
 * the lane-header band above (or below) the timeline content area —
 * the user reads any handle pixel above the header bottom as "the
 * song starts inside the header", which is wrong.  Top: 0 / bottom: 0
 * means the head grip occupies the band's first 10 px and the tail
 * grip occupies its last 10 px. */
.window-handle-grip.handle-head { top:    0; }
.window-handle-grip.handle-tail { bottom: 0; }
.window-handle-grip:hover,
.window-handle-grip.dragging { background: var(--accent-2); }

/* Lyric rows that fall outside the active window — greyed to signal
   they will be dropped from the rendered output.  Non-interactive
   styling only: the row remains draggable / editable in the editor. */
.lyric-row.outside-window {
  opacity: 0.35;
}
.lyric-row.outside-window .text { text-decoration: line-through; }

/* Envelope overlay — Audacity-style piecewise-linear gain curve drawn
   on top of each waveform lane.  The SVG itself is non-interactive so
   waveform clicks pass through to seek; only the visible control
   points (.env-point) capture pointer events. */
.envelope-overlay {
  position: absolute;
  left: 0;
  top:  0;
  width:  100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
  overflow: visible;
}
.envelope-overlay .env-curve {
  fill: none;
  stroke-width: 3;          /* heavy enough to read over the wave fill */
  opacity: 1.0;
}
/* Default-line (constant at lane default) — drawn as a thin dashed
   guide so the user knows where to double-click to add the first
   control point.  Slimmer than the active curve so a "no points yet"
   lane reads as quiescent. */
.envelope-overlay .env-default {
  fill: none;
  stroke-width: 1.5;
  stroke-dasharray: 3 3;
  opacity: 0.55;
}
.envelope-overlay .env-point {
  pointer-events: auto;
  cursor: move;            /* 2D drag — vertical = time, horizontal = value */
  stroke: #16161e;
  stroke-width: 1;
  r: 5;
}
.envelope-overlay .env-point:hover,
.envelope-overlay .env-point.dragging { stroke-width: 2; }

/* Synthetic boundary corners at t=0 / t=duration — slightly larger and
   hollow so users can tell them apart from explicit user-placed
   anchors.  Materialise into envelopeData on first drag; auto-cleanup
   on click-without-drag so an empty envelope reads clean. */
.envelope-overlay .env-corner {
  r: 7;
  fill-opacity: 0.4;
  stroke-width: 2;
}
.envelope-overlay .env-corner:hover,
.envelope-overlay .env-corner.dragging {
  fill-opacity: 1.0;
  stroke-width: 2.5;
}

/* Per-lane reset button placed inside the sticky h2 header.
 *
 * Positioned ABSOLUTELY in the h2's top-right so it can't push the
 * h2's height when the lane column is too narrow to fit "<LABEL>
 * reset" inline (without this, a 90-px column wraps the button onto
 * a second line, raising that lane's h2 by ~19 px while siblings
 * stay at their natural 32 px — visually the active envelopes and
 * waveforms then misalign across lanes).  The h2 is sticky, so the
 * absolute child sticks with it during scroll.
 *
 * The h2 itself is `pointer-events: none` for click-through; the
 * button opts back in. */
.waveform h2 .env-reset {
  position: absolute;
  top: 0.25rem;
  right: 0.3rem;
  pointer-events: auto;
  font-family: inherit;
  font-size: 0.85rem;
  line-height: 1;
  background: rgba(22, 22, 30, 0.7);   /* readable over the gradient bg */
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 2px;
  padding: 1px 4px;
  cursor: pointer;
  text-transform: none;
  letter-spacing: 0;
}
.waveform h2 .env-reset:hover { color: var(--fg); border-color: var(--accent); }
.waveform h2 .env-reset[hidden] { display: none; }

/* Drag-guide already exists; bump it above the envelope overlay so it
   stays visible over envelope curves. */

.waveform {
  position: relative;
  background: #16161e;
  margin: 0;
}

/* Inner container that holds the canvas + absolute overlays (selection-band,
   playhead, drag-guide) below the sticky `<h2>` lane label.  Without this
   wrapper, those overlays were positioned relative to the section itself —
   which includes the h2 in normal flow — so their `top=0` landed *above*
   the canvas and the time origin disagreed with the wave content by the
   header height (~30 px).  The wrapper is a sibling of h2, so its top
   coincides with the canvas's natural top in flow.

   The visual gap between the header band and where the song starts
   (00:00.00 line) is achieved by enlarging the h2 itself
   (`.waveform h2` carries the extra bottom padding) — NOT by padding
   here.  Padding on this wrapper would push the in-flow canvas down
   but leave the absolutely-positioned overlays (selection-band,
   envelope SVG, playhead) at the padding-box top, breaking the
   per-lane "canvas pixel y == overlay pixel y == timeToY(t)" invariant
   that the click and drag math relies on. */
.waveform-content {
  position: relative;
}

/* Lane label — sticky header above each waveform, mirroring `.lyrics h2`.
 *
 * `border-bottom` paints an explicit divider between the header band
 * and the song timeline, so users see unambiguously where t=0 begins.
 * Background is opaque all the way down (no fade-to-transparent) so
 * the header never bleeds visually into the timeline content below. */
.waveform h2 {
  position: sticky;
  top: 0;
  margin: 0;
  /* Asymmetric padding: extra bottom space (--timeline-gap) creates
     a clear visual gap between the lane label and where the song
     timeline starts.  The header's border-bottom marks t=0 of the
     song, and .waveform-content's natural top in flow lands just
     below the border — so canvas, selection-band, envelope SVG, and
     playhead all align at the same y inside .waveform-content. */
  padding: 0.4rem 0.6rem calc(0.4rem + var(--timeline-gap));
  font-size: 0.8rem;
  color: var(--muted);
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: #16161e;
  border-bottom: 1px solid var(--border);
  z-index: 4;
  pointer-events: none;
}

#ws-host {
  /* Wavesurfer's own container; we keep it 1px tall and offscreen — we
     only use wavesurfer for decode + play state; the visible waveform is
     drawn into #ws-canvas by editor.js. */
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  visibility: hidden;
  pointer-events: none;
}

/* All waveform-lane canvases (mix / music / voice) share the same sizing
   rule — without `width: 100%` they fall back to the canvas default 300px
   and overflow their narrow grid columns into the lyrics area. */
.waveform canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Selection band — one instance per waveform lane.  Each band shows the
   focused lyric row's [start, end] interval, with edge handles on top
   and bottom for independent retiming. */
.selection-band {
  position: absolute;
  left: 0;
  right: 0;
  background: var(--selection);
  border-top:    1px solid var(--pink);
  border-bottom: 1px solid var(--pink);
  pointer-events: none;
  display: none;
  z-index: 1;
}

.selection-band .handle-top,
.selection-band .handle-bottom {
  position: absolute;
  left: 0;
  right: 0;
  height: 6px;
  cursor: ns-resize;
  pointer-events: auto;
  background: transparent;
  z-index: 2;
}

.selection-band .handle-top    { top:    -3px; }
.selection-band .handle-bottom { bottom: -3px; }

.selection-band .handle-top:hover,
.selection-band .handle-bottom:hover {
  background: var(--pink);
  opacity: 0.4;
}

/* Playhead — one instance per waveform lane.  All instances are kept
   in sync at the current playback time.
 *
 * No `margin-top` — the line sits entirely INSIDE the timeline area
 * (positioned by `top: timeToY(t)` from JS).  Earlier the line had
 * `margin-top: -1px` so it visually centred on its anchor, but at t=0
 * that put half of the playhead's pixel above .waveform-content's top
 * (= straddling the header boundary) which the user reads as "the
 * song starts inside the header". */
.playhead {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--pink);
  pointer-events: none;
  z-index: 2;
}

#drag-guide {
  position: absolute;
  left: -200px;
  right: -1000px;
  height: 1px;
  background: var(--accent-2);
  pointer-events: none;
  display: none;
  z-index: 3;
  opacity: 0.6;
}

.lyrics {
  position: relative;
  padding: 0;
}
.lyrics h2 {
  position: sticky;
  top: 0;
  margin: 0;
  padding: 0.4rem 0.6rem calc(0.4rem + var(--timeline-gap));
  font-size: 0.8rem;
  color: var(--muted);
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: #16161e;
  border-bottom: 1px solid var(--border);
  z-index: 4;
  pointer-events: none;
}

#lyric-list {
  position: relative;
  counter-reset: row;
  padding: 0 0.5rem;
  /* No top padding — lyric rows are absolute-positioned by `top:
     timeToY(t)`, so any padding here would push them down out of
     sync with the canvas/SVG content in sibling lanes.  The visual
     gap to the header is provided by the h2's enlarged bottom padding. */
}

.lyric-row {
  position: absolute;
  left: 0.5rem;
  right: 0.5rem;
  display: grid;
  grid-template-columns: 2.5rem 5rem 0.5rem 1fr auto;
  gap: 0.5rem;
  align-items: center;
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  cursor: ns-resize;
  outline: none;
  counter-increment: row;
  transition: background 0.1s;
  user-select: none;
  touch-action: none;
}
.lyric-row:hover    { background: var(--row-hover); }
.lyric-row:focus    { background: var(--row-hover); outline: 2px solid var(--accent); outline-offset: -2px; }
.lyric-row.dragging { background: var(--row-hover); cursor: grabbing; opacity: 0.95; }
/* Multi-row selection — distinct from :focus, since several rows can carry
 * .selected at once while only one can be the keyboard-focused row.  Using
 * an inset box-shadow rather than `outline` so it stacks cleanly with the
 * focus outline on the primary row of the selection. */
.lyric-row.selected            { background: var(--row-hover); box-shadow: inset 0 0 0 1px var(--accent); }
.lyric-row.selected:focus      { box-shadow: inset 0 0 0 1px var(--accent); }

/* Rubber-band rectangle — dragged in the empty area of #lyric-list to
 * lasso multiple rows.  Positioned absolutely inside the .lyrics section
 * (which we promote to `position: relative` from JS).  pointer-events:
 * none so the live pointermove still hits the underlying rows. */
.rubber-band {
  position: absolute;
  z-index: 10;
  background: rgba(120, 170, 255, 0.18);
  border: 1px solid rgba(120, 170, 255, 0.7);
  border-radius: 2px;
  pointer-events: none;
}

.lyric-row .num   { color: var(--muted); text-align: right; }
.lyric-row .num::before { content: counter(row); }
.lyric-row .ts    { color: var(--accent); }
.lyric-row .dash  { color: var(--muted); text-align: center; }
.lyric-row .text  { color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lyric-row .text input,
.lyric-row .text textarea.text-edit {
  width: 100%;
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 3px;
  padding: 0.05rem 0.3rem;
}
/* Inline-edit textarea — sized to one line by default (auto-grows on
   input via JS).  No resize handle since the user already gets free
   resize via the auto-grow loop, and the visible drag affordance would
   obscure the row's actual click target. */
.lyric-row .text textarea.text-edit {
  resize: none;
  overflow-y: auto;
  line-height: 1.2;
  display: block;
  vertical-align: middle;
}
.lyric-row.empty-rejected {
  animation: shake 220ms ease-in-out;
  outline: 2px solid var(--pink) !important;
}
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-3px); }
  75%      { transform: translateX(3px); }
}

.row-ops {
  display: flex;
  gap: 0.2rem;
  opacity: 0;
  transition: opacity 0.1s;
  align-items: center;
}
.lyric-row:hover .row-ops,
.lyric-row:focus-within .row-ops,
.lyric-row.selected .row-ops { opacity: 1; }

button.op {
  font-family: inherit;
  font-size: 0.75rem;
  background: var(--row-hover);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.1rem 0.45rem;
  cursor: pointer;
}
button.op:hover { background: var(--accent); color: var(--bg); }
button.op:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
button.op.danger:hover  { background: var(--pink);   color: var(--bg); }
button.op:disabled,
button.op:disabled:hover {
  background: var(--row-hover);
  color: var(--muted);
  cursor: default;
  opacity: 0.7;
}
/* Click cooldown — brief flash when a second click is suppressed.
   The data-last-click-ts attribute is set by static/click_cooldown.js. */
button.op.cooldown-hint { animation: op-cooldown-flash 0.6s ease-out; }
@keyframes op-cooldown-flash {
  0%   { box-shadow: 0 0 0 0 var(--accent); }
  40%  { box-shadow: 0 0 0 4px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.header-ops {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  margin-top: 0.2rem;
  flex-wrap: wrap;
}
.header-ops button.op { font-size: 0.85rem; padding: 0.25rem 0.7rem; }
.header-ops #cursor   { font-variant-numeric: tabular-nums; margin-left: 0.4rem; }

/* Lyrics ops cluster: lives on its own row below the editor/audio
 * controls.  The leading `.header-group-label` reads as a section
 * heading for the six single-word verbs (transcribe / fetch / show /
 * reset / paste / align), so users can tell at a glance that the
 * whole row acts on the lyric stack. */
.header-ops-lyrics { margin-top: 0.35rem; }
.header-group-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  font-weight: 600;
  margin-right: 0.25rem;
}

.tasks h2,
.export h2 {
  margin: 0 0 0.4rem;
  font-size: 0.85rem;
  color: var(--muted);
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

#tasks-list { display: flex; flex-direction: column; gap: 0.5rem; }

/* `provide lyrics` is a <details> control: the <summary> renders as an
   op-button in the row, and when opened the form expands onto its own
   full-width line below.  Closed state contributes no more visual space
   than any other button. */
.provide-lyrics-summary {
  /* Mirror button.op exactly — same font, padding, colours, hover state. */
  font-family: inherit;
  font-size: 0.75rem;
  background: var(--row-hover);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.1rem 0.45rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.provide-lyrics-summary::-webkit-details-marker { display: none; }
.provide-lyrics-summary:hover { background: var(--accent); color: var(--bg); }
.provide-lyrics[open] > .provide-lyrics-summary {
  background: var(--accent);
  color: var(--bg);
}

/* Open the details onto a fresh flex row so the form below doesn't squeeze
   the surrounding op buttons.  Closed state keeps the summary inline. */
.provide-lyrics[open] { flex-basis: 100%; }

/* Hide the form by default; the [open] details reveals it.  Without this
   guard our `display: flex` would override the browser's UA stylesheet
   rule that hides non-summary children when the details is closed. */
.provide-lyrics-form { display: none; }
.provide-lyrics[open] .provide-lyrics-form {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.4rem;
  max-width: 720px;
}
.provide-lyrics-form p { margin: 0; font-size: 0.85rem; }
.provide-lyrics-form code {
  background: var(--row-hover);
  padding: 0 0.25rem;
  border-radius: 2px;
}
.provide-lyrics-form textarea {
  font: 13px/1.5 ui-monospace, "SFMono-Regular", Consolas, monospace;
  background: #16161e;
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.5rem;
  min-height: 8rem;
  resize: vertical;
}
.provide-lyrics-form textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.provide-lyrics-actions { display: flex; gap: 0.4rem; }

/* Lyrics-modal — opened by the navbar's `get lyrics` button.  Native
 * <dialog> handles backdrop + Esc; we only style the chrome. */
.lyrics-modal {
  background: #16161e;
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1rem;
  max-width: min(720px, 90vw);
  width: 100%;
  max-height: 85vh;
  display: flex;            /* not laid out until dialog.showModal() */
  flex-direction: column;
  gap: 0.6rem;
}
.lyrics-modal:not([open]) { display: none; }
.lyrics-modal::backdrop {
  background: rgba(10, 10, 16, 0.65);
}
.lyrics-modal-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}
.lyrics-modal-header h2 {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.lyrics-modal-source {
  font-size: 0.75rem;
  font-style: italic;
  flex: 1;
  text-align: left;
  margin-left: 0.5rem;
}
.lyrics-modal-source:empty { display: none; }
.lyrics-modal textarea {
  flex: 1;
  min-height: 12rem;
  font: 13px/1.5 ui-monospace, "SFMono-Regular", Consolas, monospace;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.5rem;
  resize: none;
}
.lyrics-modal textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.lyrics-modal p { margin: 0; font-size: 0.8rem; }

/* Non-blocking hint surface (empty-edit rejection etc.) */
.hint {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  background: var(--row-hover);
  border: 1px solid var(--pink);
  color: var(--fg);
  padding: 0.3rem 0.6rem;
  border-radius: 3px;
  font-size: 0.85rem;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s;
  z-index: 9;
  max-width: 50vw;
}
.hint.show { opacity: 1; }

/* Job + export panels (unchanged behaviour) */
.export-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 0.5rem 0;
}
@media (max-width: 1100px) {
  .export-grid { grid-template-columns: 1fr; }
}
.export-card h3 {
  font-size: 0.85rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: normal;
  margin: 0 0 0.3rem;
}
.export video {
  display: block;
  margin: 0.5rem 0;
  background: #000;
  border: 1px solid var(--border);
  border-radius: 4px;
  max-width: 100%;
}
.export-ops {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
}
.export-ops button.op { font-size: 0.85rem; padding: 0.25rem 0.7rem; }
.export-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
}
.export-toggle input { accent-color: var(--accent); }
.export-toggle:hover { color: var(--fg); }

.job {
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.5rem 0.75rem;
  background: #16161e;
}
.job-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.job-time { font-size: 0.8em; }
.job-stop {
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.15rem 0.7rem;
  border-radius: 999px;
  background: #4a1f24;
  color: var(--pink);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.job-stop:hover  { background: #5a262c; border-color: var(--pink); }
.job-stop:disabled,
.job-stop[hidden] { cursor: default; }
.job-stop:disabled { opacity: 0.6; }
.job-clear {
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.15rem 0.7rem;
  border-radius: 999px;
  background: #22222e;
  color: #6b7099;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.job-clear:hover { background: #2a2a3a; border-color: #6b7099; color: #a9b1d6; }
.job-clear[hidden] { display: none; }
.job-log {
  margin: 0;
  font-size: 0.75rem;
  background: #0c0c14;
  color: #c0caf5;
  padding: 0.4rem 0.6rem;
  border-radius: 3px;
  max-height: 280px;
  overflow-y: auto;
  white-space: pre-wrap;
}
.job-progress {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0.3rem 0 0.4rem;
}
/* Track + fill — the outer bar is the empty rail; the inner div is the
   filled portion that JS grows from 0% to 100% as ffmpeg reports
   `out_time_ms` from its `-progress pipe:1` stream.  Earlier rendition
   used `flex: 1` directly on the gradient bar, which always filled all
   available flex space regardless of inline `width`.  Hence: always full. */
.job-progress-bar {
  flex: 1;
  height: 8px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
  min-width: 0;
}
.job-progress-bar-fill {
  height: 100%;
  width: 0;
  background: linear-gradient(to right, var(--accent), var(--ok));
  transition: width 0.2s;
}
.job-progress-text { font-size: 0.75rem; min-width: 13rem; text-align: right; }

.status {
  display: inline-block;
  padding: 0.15rem 0.6rem;
  border-radius: 999px;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
}
.status.pending,
.status.running   { background: #3b3f5c; color: var(--accent); }
.status.done      { background: #2c3a25; color: var(--ok); }
.status.error     { background: #4a1f24; color: var(--pink); }
.status.cancelled { background: #3b3f5c; color: var(--muted); }

/* htmx indicator: only visible while a request is in flight */
.htmx-indicator { opacity: 0; transition: opacity 0.15s; }
.htmx-request .htmx-indicator { opacity: 1; }
.htmx-request.htmx-indicator { opacity: 1; }

/* Per-field project-list filters (client-side, see static/song_filter.js) */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1rem;
}
.filters-label { color: var(--muted); font-size: 0.85rem; }
.filter-field { position: relative; display: inline-flex; align-items: center; }
.filter-field .song-filter {
  font: inherit;
  font-size: 0.85rem;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.3rem 1.5rem 0.3rem 0.5rem;   /* room for the per-field × */
  width: 9rem;
}
.filter-field .song-filter:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.filter-clear-one {
  position: absolute;
  right: 0.35rem;
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0;
}
.filter-clear-one:hover { color: var(--pink); }
.filter-clear-all {
  font-family: inherit;
  font-size: 0.8rem;
  background: var(--bg);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0.3rem 0.7rem;
  cursor: pointer;
}
.filter-clear-all:hover { color: var(--fg); border-color: var(--accent); }

/* Rows hidden by an active filter (defensive: ensure UA hidden wins over any
   table display rule). */
.songs tbody tr[hidden] { display: none; }

/* ── landing + login (auth surface) ──────────────────────────────────────────
   Public pages shown before sign-in.  They reuse the palette variables above
   so the marketing landing and the login card match the editor's theme.  All
   foreground/accent pairs below clear WCAG 2.2 AA contrast against --bg. */

body.landing,
body.login {
  max-width: 880px;
  margin: 0 auto;
  padding: 2.5rem 1.5rem;
}

.landing-hero {
  text-align: center;
  padding: 2rem 0 1rem;
  border-bottom: 1px solid var(--border);
}
.landing-hero h1 {
  font-size: 2.6rem;
  margin: 0 0 0.5rem;
  color: var(--accent);
}
.landing-hero .tagline {
  font-size: 1.2rem;
  color: var(--fg);
  max-width: 40rem;
  margin: 0 auto 1.25rem;
}
.cta-row { margin: 1rem 0; }
.op.cta {
  display: inline-block;
  font-size: 1.05rem;
  padding: 0.6rem 1.1rem;
}
.landing-locked { margin-top: 0.75rem; }

.landing-main { padding: 1.5rem 0; }
.selling-points h2,
.landing-getstarted h2 { color: var(--accent-2); }

ul.features {
  list-style: none;
  padding: 0;
  margin: 1rem 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
}
ul.features li {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;
  background: var(--row-hover);
}
ul.features h3 {
  margin: 0 0 0.4rem;
  color: var(--accent);
  font-size: 1.05rem;
}
ul.features p { margin: 0; color: var(--fg); }

/* Login card */
.login-card {
  max-width: 24rem;
  margin: 3rem auto 1rem;
  padding: 2rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--row-hover);
}
.login-card h1 { margin: 0 0 0.25rem; }
.login-home { color: var(--accent); text-decoration: none; }
.login-form { margin-top: 1.25rem; display: flex; flex-direction: column; gap: 0.9rem; }
.login-field { display: flex; flex-direction: column; gap: 0.3rem; }
.login-field span { color: var(--muted); font-size: 0.9rem; }
.login-field input {
  padding: 0.55rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 1rem;
}
.login-error {
  border: 1px solid var(--pink);
  background: var(--selection);
  color: var(--fg);
  padding: 0.6rem 0.75rem;
  border-radius: 6px;
  margin: 1rem 0 0;
}

/* Header sign-out control on the app project page */
.header-bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}
.logout-form { margin: 0; }
.op.logout { font-size: 0.85rem; }
