/* recap-mini — components (DESIGN.md §3). Owned by card U0.
   Behaviour for every class here lives in static/js/ui.js; the exported
   JS surface is frozen and documented in that file's header comment.

   House rules for every control:
     :hover · :focus-visible (2px --accent ring, offset 2px) · :disabled
     (opacity .5, no pointer) · min 44×44 px touch target
     transitions 150ms ease-out, colour/transform only. */

/* ================================================================ button */
.btn {
  --btn-bg: var(--surface-2);
  --btn-fg: var(--text);
  --btn-line: var(--line);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-4);
  border: 1px solid var(--btn-line);
  border-radius: var(--radius-m);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  text-decoration: none;
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease),
    color 150ms var(--ease), transform 150ms var(--ease);
  -webkit-tap-highlight-color: transparent;
}

.btn:hover { text-decoration: none; }
.btn:active:not(:disabled) { transform: translateY(1px); }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
  cursor: default;
}

 /* The TeamAlpha CTA (card U): a crimson gradient with white ink. The
    gradient runs DEEP -> BRIGHT (--accent-2 -> --accent) so white clears
    4.5:1 at every point of it; the reverse direction TeamAlpha uses puts
    white at 3.6:1 on its light end. --btn-bg stays set so a browser without
    the gradient (or a `background-color` override) still gets a solid
    accent fill, and .btn's colour transition keeps working. */
.btn--primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--accent-ink);
  --btn-line: transparent;
  background-image: var(--accent-grad);
  box-shadow: 0 1px 0 rgb(255 255 255 / 0.22) inset, var(--accent-glow);
}

/* hover flattens the gradient to the bright end and lifts the glow —
   still 4.62:1 for white ink. */
.btn--primary:hover {
  background-image: linear-gradient(135deg, var(--accent), var(--accent));
  box-shadow: 0 1px 0 rgb(255 255 255 / 0.22) inset, 0 8px 22px rgb(221 47 61 / 0.46);
}

.btn--primary:disabled,
.btn--primary[aria-disabled="true"] { box-shadow: none; }

.btn--secondary { --btn-bg: var(--surface-2); --btn-line: var(--line); }
.btn--secondary:hover { --btn-bg: #262b3a; --btn-line: #384054; }

.btn--ghost { --btn-bg: transparent; --btn-line: transparent; --btn-fg: var(--text-2); }
.btn--ghost:hover { --btn-bg: var(--surface-2); --btn-fg: var(--text); }

.btn--danger {
  --btn-bg: var(--danger-soft);
  --btn-fg: var(--danger);
  --btn-line: color-mix(in srgb, var(--danger) 40%, transparent);
}
.btn--danger:hover { --btn-bg: color-mix(in srgb, var(--danger) 24%, transparent); }

.btn--m { min-height: 44px; font-size: 14px; }
.btn--l { min-height: 48px; padding: 0 var(--space-5); font-size: 16px; }
.btn--block { width: 100%; }

/* busy state keeps the button width (DESIGN.md §3) */
.btn.is-busy { pointer-events: none; position: relative; }
.btn.is-busy .btn-label { visibility: hidden; }
.btn.is-busy .btn-spin { display: block; }

.btn-spin {
  display: none;
  position: absolute;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: rm-spin 700ms linear infinite;
}

@keyframes rm-spin { to { transform: rotate(360deg); } }

.btn-icon {
  width: 18px;
  height: 18px;
  flex: none;
  fill: currentColor;
}

/* ================================================================== card */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  padding: var(--space-4);
  box-shadow: var(--shadow-1);
}

@media (min-width: 900px) {
  .card { padding: var(--space-5); }
}

.card--flush { padding: 0; overflow: hidden; }

.card > header {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.card > header .row-end { flex: none; }

.card-sep {
  height: 1px;
  background: var(--line);
  margin: var(--space-4) calc(-1 * var(--space-4));
}

@media (min-width: 900px) {
  .card-sep { margin: var(--space-5) calc(-1 * var(--space-5)); }
}

/* ================================================================= field */
.field { display: block; }

.field > .label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
  color: var(--text-2);
}

/* `.label-caps` (DESIGN.md §3, defined further below) must win over the
   `.field > .label` rule above even when studio.js applies both classes to
   the same element (`class="label label-caps"`) — a 0,2,0 child selector
   otherwise beats the 0,1,0 class-only rule that follows later in the file,
   despite source order. Matched at equal (0,2,0) specificity here so the
   later cascade position decides it in `.label-caps`'s favour. */
.field > .label.label-caps {
  font-size: 11px;
  line-height: 16px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: 44px;
  padding: 10px var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  color: var(--text);
  font-size: 15px;
  transition: border-color 150ms var(--ease), background-color 150ms var(--ease);
}

.textarea { min-height: 120px; resize: vertical; line-height: 22px; }

.input::placeholder,
.textarea::placeholder { color: var(--text-3); }

.input:hover:not(:disabled),
.select:hover:not(:disabled),
.textarea:hover:not(:disabled) { border-color: #384054; }

.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible { border-color: var(--accent); }

.input:disabled,
.select:disabled,
.textarea:disabled { opacity: 0.5; cursor: not-allowed; }

.select {
  appearance: none;
  padding-right: var(--space-6);
  background-image: linear-gradient(45deg, transparent 50%, var(--text-2) 50%),
    linear-gradient(135deg, var(--text-2) 50%, transparent 50%);
  background-position: calc(100% - 18px) 20px, calc(100% - 12px) 20px;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

/* helper text a user must be able to READ uses --text-2 (8.60:1). --text-3
   (4.04:1) is reserved for placeholders, disabled and decorative marks, per
   its definition in DESIGN.md §2. */
.field .hint {
  margin-top: var(--space-2);
  font-size: 13px;
  line-height: 18px;
  color: var(--text-2);
}

.field .error {
  display: none;
  align-items: flex-start;
  gap: 6px;
  margin-top: var(--space-2);
  font-size: 13px;
  line-height: 18px;
  color: var(--danger);
}

/* never colour alone: the error line carries an icon + text (DESIGN.md §3) */
.field .error::before {
  content: "!";
  flex: none;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--danger);
  color: #2a0006;
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

.field.is-invalid .error { display: flex; }
.field.is-invalid .input,
.field.is-invalid .select,
.field.is-invalid .textarea { border-color: var(--danger); }

.counter { font-variant-numeric: tabular-nums; }
.counter.is-over { color: var(--danger); }

/* ============================================================= segmented */
/* minmax(0, 1fr), not 1fr: a plain 1fr track has an auto MINIMUM, so a long
   Burmese hint would push the control (and its card) wider than the screen.
   That was a real 390 px overflow. */
.segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 4px;
  padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
}

.segmented--wrap { grid-auto-flow: row; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }

@media (max-width: 519px) {
  .segmented--stack-sm { grid-auto-flow: row; grid-auto-columns: auto; }
  .segmented--stack-sm .seg {
    align-items: flex-start;
    text-align: left;
    padding: var(--space-3);
  }
}

.seg {
  display: flex;
  min-width: 0;
  overflow-wrap: anywhere;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--text-2);
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: background-color 150ms var(--ease), color 150ms var(--ease);
}

.seg:hover:not([disabled]) { background: rgb(255 255 255 / 0.05); color: var(--text); }

.seg[aria-checked="true"] {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--shadow-1);
}

/* full --accent-ink, not a translucent one: white at 78 % over --accent
   measures 3.31:1, only solid white clears 4.5:1. */
.seg[aria-checked="true"] .seg-hint { color: var(--accent-ink); }

.seg[disabled] { opacity: 0.5; cursor: not-allowed; }

.seg-hint { font-size: 12px; line-height: 16px; font-weight: 400; color: var(--text-2); }

.segmented-note[hidden],
.dropzone-error[hidden] { display: none; }

.segmented-note {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-s);
  background: var(--warn-soft);
  color: var(--warn);
  font-size: 13px;
  line-height: 18px;
}

/* ================================================================ toggle */
.toggle {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  padding: var(--space-2) 0;
  border: 0;
  background: transparent;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.toggle-label { flex: 1; min-width: 0; font-size: 15px; }

.toggle-track {
  flex: none;
  position: relative;
  width: 44px;
  height: 26px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease);
}

.toggle-track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--text-3);
  transition: transform 150ms var(--ease), background-color 150ms var(--ease);
}

.toggle[aria-checked="true"] .toggle-track { background: var(--accent); border-color: transparent; }
.toggle[aria-checked="true"] .toggle-track::after { transform: translateX(18px); background: var(--accent-ink); }
.toggle:hover:not([disabled]) .toggle-track { border-color: #384054; }
.toggle[disabled] { opacity: 0.5; cursor: not-allowed; }

/* ================================================================== chip */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}

.chip::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}

.chip--plain::before { display: none; }
.chip--queued { color: var(--warn); background: var(--warn-soft); border-color: rgb(245 166 35 / 0.3); }
.chip--processing { color: var(--info); background: var(--info-soft); border-color: rgb(90 169 255 / 0.3); }
.chip--processing::before { animation: rm-pulse 1.4s ease-in-out infinite; }
.chip--completed { color: var(--ok); background: var(--ok-soft); border-color: rgb(61 214 140 / 0.3); }
.chip--failed { color: var(--danger); background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
.chip--cancelled { color: var(--text-2); }
.chip--accent { color: var(--accent-text); background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 38%, transparent); }

@keyframes rm-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.45; transform: scale(0.8); }
}

/* single-button language toggle (card C revision 1): `.chip--plain` already
   handles color/border/hover; this adds just the font floor (DESIGN.md §2's
   13px minimum) and cheap padding, plus a ::after 44px hit-area
   (`.chip--plain::before` is already
   display:none, so ::after is free here too). */
.chip--lang {
  position: relative;
  font-size: 13px;
  padding: 0 var(--space-2);
  height: 34px;
  min-width: 44px;
  justify-content: center;
  cursor: pointer;
}

.chip--lang:hover {
  background: #262b3a;
}

.chip--lang::after {
  content: "";
  position: absolute;
  inset: -5px 0;
}

/* ============================================================== dropzone */
.dropzone {
  position: relative;
  display: block;
  width: 100%;
  padding: var(--space-5) var(--space-4);
  border: 1.5px dashed var(--line);
  border-radius: var(--radius-m);
  background: rgb(255 255 255 / 0.015);
  color: var(--text-2);
  text-align: center;
  cursor: pointer;
  transition: border-color 150ms var(--ease), background-color 150ms var(--ease);
}

.dropzone:hover { border-color: #4a536a; background: rgb(255 255 255 / 0.03); }
.dropzone.is-dragover { border-color: var(--accent); background: var(--accent-soft); }
.dropzone.is-invalid { border-color: var(--danger); }
.dropzone input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }

.dropzone-icon {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  margin: 0 auto var(--space-3);
  border-radius: var(--radius-s);
  background: var(--accent-soft);
  color: var(--accent-text);
}

.dropzone-title { color: var(--text); font-weight: 600; }
.dropzone-caps { margin-top: var(--space-2); font-size: 13px; line-height: 18px; color: var(--text-2); }

.dropzone-file {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  text-align: left;
}

.dropzone-thumb {
  width: 92px;
  height: 60px;
  flex: none;
  border-radius: var(--radius-s);
  background: #000;
  object-fit: cover;
}

.dropzone-meta { flex: 1; min-width: 0; }
.dropzone-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dropzone-sub { font-size: 13px; line-height: 18px; color: var(--text-2); font-variant-numeric: tabular-nums; }

.dropzone-error {
  display: flex;
  gap: 6px;
  margin-top: var(--space-2);
  color: var(--danger);
  font-size: 13px;
  line-height: 18px;
  text-align: left;
}

/* Small round controls stay visually small but keep a 44 px hit area
   (DESIGN.md §3) via a transparent ::after — no layout cost. */
.banner-x::after,
.toast-x::after,
.avatar-btn::after {
  content: "";
  position: absolute;
  inset: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

@keyframes rm-blink { 50% { opacity: 0.4; } }

/* =============================================================== stepper */
.stepper { display: block; }

.steps { display: grid; gap: var(--space-3); list-style: none; }

/* future (not yet reached) steps use --text-3 deliberately: they are the
   "not applicable yet" state, the same class of thing as :disabled, which
   DESIGN.md §3 already puts at opacity .5. Reached/current/failed steps are
   all --text-2 or better. */
.step {
  display: grid;
  min-width: 0;
  overflow-wrap: anywhere;
  grid-template-columns: 26px minmax(0, 1fr);
  align-items: start;
  gap: var(--space-3);
  color: var(--text-3);
  font-size: 14px;
}

.step-dot svg,
.chip svg,
.toast-icon svg,
.banner-x svg,
.toast-x svg {
  display: block;
  margin: auto;
}

.step-dot {
  position: relative;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1.5px solid var(--line);
  background: var(--surface-2);
  color: var(--text-3);
  font-size: 12px;
  font-weight: 700;
  line-height: 23px;
  text-align: center;
}

.step--done { color: var(--text-2); }
.step--done .step-dot { border-color: var(--ok); background: var(--ok-soft); color: var(--ok); }
.step--current { color: var(--text); font-weight: 600; }
.step--current .step-dot {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: 0 0 0 0 var(--accent-soft);
  animation: rm-ring 1.8s var(--ease) infinite;
}
.step--danger { color: var(--danger); font-weight: 600; }
.step--danger .step-dot { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }

@keyframes rm-ring {
  0% { box-shadow: 0 0 0 0 rgb(221 47 61 / 0.5); }
  70% { box-shadow: 0 0 0 10px rgb(221 47 61 / 0); }
  100% { box-shadow: 0 0 0 0 rgb(221 47 61 / 0); }
}

/* .step is a 2-col grid (26px dot, 1fr label); .step-error is the li's
   3rd child so grid auto-placement would otherwise drop it into the
   26px dot column (one char per line, overflow-wrap:anywhere) — pin it
   under the label. No-op at >=900px where .step collapses to 1 column. */
.step-error { grid-column: 2; margin-top: 4px; font-weight: 400; }

.progress {
  height: 6px;
  margin-top: var(--space-4);
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent-2), var(--accent));
  transition: width 300ms var(--ease);
}

.progress-fill.is-danger { background: var(--danger); }
.progress-fill.is-ok { background: linear-gradient(90deg, var(--ok), #8af0c4); }

.progress-line {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-2);
  font-size: 13px;
  line-height: 18px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

@media (min-width: 900px) {
  .steps {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 0;
  }
  .step {
    grid-template-columns: 1fr;
    justify-items: center;
    align-content: start; /* auto rows must not absorb the tallest step's height */
    text-align: center;
    gap: var(--space-2);
    position: relative;
  }
  .step::before {
    content: "";
    position: absolute;
    top: 13px;
    right: 50%;
    left: -50%;
    height: 1.5px;
    background: var(--line);
  }
  .step:first-child::before { display: none; }
  .step--done::before,
  .step--current::before { background: var(--ok); }
  .step--danger::before { background: var(--danger); }
  .step-label { max-width: 12ch; }
  /* .step is ONE column up here, so the base `.step-error { grid-column: 2 }`
     (correct below 900 px, where the dot owns column 1) asked for a track
     that does not exist: the grid grew an implicit second column and the
     failed step was pushed ~34 px off the rail, out of line with its
     siblings. Put the error back under its own label. */
  .step-error { grid-column: 1; }
}

/* ================================================================= tabs */
.tabs {
  display: flex;
  gap: var(--space-1);
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface);
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar { display: none; }

.tab {
  flex: 1 0 auto;
  min-height: 44px;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--text-2);
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 150ms var(--ease), color 150ms var(--ease);
}

.tab:hover { background: var(--surface-2); color: var(--text); }
.tab[aria-selected="true"] { background: var(--accent); color: var(--accent-ink); }

.tabpanel[hidden] { display: none; }

/* ================================================================ table */
.rm-table-wrap { width: 100%; overflow-x: auto; }
.rm-table { width: 100%; border-collapse: collapse; }

.rm-table th {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--line);
  color: var(--text-2);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}

.rm-table td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--line);
  font-size: 14px;
  vertical-align: middle;
}

.rm-table tbody tr { transition: background-color 150ms var(--ease); }
.rm-table tbody tr:hover { background: var(--surface-2); }
.rm-table tbody tr:last-child td { border-bottom: 0; }
.rm-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.rm-table .mono { font-family: var(--font-mono); font-size: 13px; color: var(--text-2); }

/* collapses to stacked cards below 900 px (DESIGN.md §3) */
.rm-cards { display: grid; gap: var(--space-3); }

.rm-card-row {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
}

.rm-card-cell { display: flex; gap: var(--space-3); align-items: baseline; font-size: 14px; }
.rm-card-cell > dt { flex: none; width: 34%; color: var(--text-2); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.rm-card-cell > dd { flex: 1; min-width: 0; margin: 0; overflow-wrap: anywhere; }

/* ADDITIVE (U0 revise): a chip is `white-space: nowrap` by design, but a
   bilingual status chip measured 208 px inside a 185 px <dd> once the table
   collapses to cards, which pushed the whole page into horizontal scroll at
   320 px. Inside a collapsed row only, a chip may wrap onto a second line. */
.rm-card-cell .chip {
  max-width: 100%;
  height: auto;
  min-height: 26px;
  padding-top: 4px;
  padding-bottom: 4px;
  white-space: normal;
}

/* ================================================================ empty */
.empty {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

.empty-icon {
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-l);
  background: var(--accent-soft);
  color: var(--accent-text);
  font-size: 26px;
  line-height: 1;
}

.empty-text { max-width: 46ch; color: var(--text-2); }
/* headline treatment for the empty-state sentence, whichever language RM.t
   emits (single span now — matches either lang="my" or lang="en") */
.empty-text .t > span { display: block; color: var(--text); font-size: 16px; font-weight: 600; }

/* ============================================================= skeleton */
.sk {
  border-radius: var(--radius-s);
  background: linear-gradient(90deg, var(--surface-2) 25%, #262b39 37%, var(--surface-2) 63%);
  background-size: 400% 100%;
  animation: rm-shimmer 1.4s ease-in-out infinite;
}

@keyframes rm-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

.sk-line { height: 12px; margin-bottom: var(--space-2); }
.sk-line:last-child { margin-bottom: 0; }
.sk-title { height: 20px; width: 45%; margin-bottom: var(--space-3); }
.sk-block { height: 120px; }
.sk-chip { height: 26px; width: 92px; border-radius: 999px; }
.sk-row { height: 56px; margin-bottom: var(--space-2); }
.sk-w-90 { width: 90%; }
.sk-w-70 { width: 70%; }
.sk-w-50 { width: 50%; }

/* ================================================================ toast */
.toasts {
  position: fixed;
  z-index: 60;
  left: var(--space-4);
  right: var(--space-4);
  /* card U removed the bottom tab bar, so a toast sits --space-4 from the
     bottom at every width. studio.css still lifts them over its mobile
     cost dock with its own `body.has-studio-dock .toasts` rule. */
  bottom: var(--space-4);
  display: grid;
  gap: var(--space-2);
  pointer-events: none;
}

@media (min-width: 900px) {
  .toasts { left: auto; right: var(--space-5); top: calc(var(--nav-h) + var(--space-4)); bottom: auto; width: 380px; }
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--tn, var(--info));
  border-radius: var(--radius-m);
  background: var(--surface);
  box-shadow: var(--shadow-2);
  pointer-events: auto;
  animation: rm-toast-in 200ms var(--ease);
}

.toast--ok { --tn: var(--ok); }
.toast--error { --tn: var(--danger); }
.toast--warn { --tn: var(--warn); }
.toast--info { --tn: var(--info); }

.toast-icon { flex: none; width: 20px; height: 20px; border-radius: 50%; background: var(--tn); color: #0b0d12; font-size: 12px; font-weight: 700; line-height: 20px; text-align: center; }
.toast-body { flex: 1; min-width: 0; font-size: 14px; line-height: 20px; }
.toast-x {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 0;
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  border-radius: var(--radius-s);
}
.toast-x:hover { background: var(--surface-2); color: var(--text); }

@keyframes rm-toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ================================================================ modal */
.modal-scrim {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--space-4);
  background: rgb(6 8 12 / 0.66);
  animation: rm-fade 150ms var(--ease);
}

@keyframes rm-fade { from { opacity: 0; } to { opacity: 1; } }

.modal {
  width: min(520px, 100%);
  max-height: 86vh;
  overflow: auto;
  padding: var(--space-5);
  border: 1px solid var(--line);
  border-radius: var(--radius-l);
  background: var(--surface);
  box-shadow: var(--shadow-2);
  animation: rm-modal-in 180ms var(--ease);
}

@keyframes rm-modal-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to { opacity: 1; transform: none; }
}

.modal-title { margin-bottom: var(--space-3); }
.modal-body { color: var(--text-2); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-5); flex-wrap: wrap; }
.modal-actions .btn { flex: 1 1 auto; }

@media (min-width: 560px) { .modal-actions .btn { flex: 0 0 auto; min-width: 120px; } }

/* ================================================================== nav */
.rm-nav {
  position: sticky;
  top: 0;
  z-index: 40;
  height: var(--nav-h);
  border-bottom: 1px solid var(--line);
  background: rgb(23 26 33 / 0.86);
  backdrop-filter: blur(10px);
}

.rm-nav-in {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 100%;
  max-width: var(--page-max);
  margin: 0 auto;
  padding: 0 var(--space-4);
}

.brand { position: relative; display: flex; align-items: center; gap: var(--space-2); color: var(--text); font-weight: 700; letter-spacing: -0.01em; }
.brand:hover { text-decoration: none; }

/* brand renders 28px tall (mark height); the logo+wordmark itself is
   already wider than 44px, so only the vertical hit area needs growing. */
.brand::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -8px;
  bottom: -8px;
}

.brand-mark {
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: 9px;
  background: linear-gradient(140deg, var(--accent), #ff8a3c);
  box-shadow: 0 2px 8px rgb(221 47 61 / 0.36);
}

.nav-right { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }

.avatar-btn {
  position: relative;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: border-color 150ms var(--ease), background-color 150ms var(--ease);
}

.avatar-btn:hover { border-color: var(--accent); }

.menu {
  position: absolute;
  top: calc(var(--nav-h) - 6px);
  right: var(--space-4);
  z-index: 50;
  min-width: 200px;
  padding: var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface);
  box-shadow: var(--shadow-2);
}

.menu[hidden] { display: none; }
.menu-head { padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--line); margin-bottom: var(--space-2); }
.menu-email { font-size: 13px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; }

.menu-item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}

.menu-item:hover { background: var(--surface-2); text-decoration: none; }
.menu-item--danger { color: var(--danger); }

/* The bottom tab bar was deleted by card U (2026-09-05): the drawer is the
   navigation at every width, exactly as on TeamAlpha. `--tabbar-h` survives
   as 0px because studio.css still measures its mobile dock from it. */

/* ====================================================== hamburger + drawer
   Card U (2026-09-05). ONE navigation at every width: the top tabs and the
   mobile bottom tab bar are both gone. Built by nav.js. */
.hamburger {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-left: calc(-1 * var(--space-2));
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease),
    color 150ms var(--ease);
}

.hamburger:hover { border-color: #4a536a; color: var(--accent-text); }
.hamburger:disabled { opacity: 0.5; pointer-events: none; }
.hamburger svg { width: 20px; height: 20px; }

.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: rgb(6 8 12 / 0.62);
  opacity: 0;
  visibility: hidden;
  transition: opacity 200ms var(--ease), visibility 0s linear 200ms;
}

.drawer-scrim--open { opacity: 1; visibility: visible; transition: opacity 200ms var(--ease), visibility 0s; }

/* `visibility` (not `display`) is what takes the closed drawer out of the
   tab order while still letting the transform animate. */
.drawer {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 55;
  display: flex;
  flex-direction: column;
  width: min(var(--drawer-w), 84vw);
  padding: var(--space-3);
  border-right: 1px solid var(--line);
  background: var(--surface);
  box-shadow: var(--shadow-2);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translateX(-100%);
  visibility: hidden;
  transition: transform 220ms var(--ease), visibility 0s linear 220ms;
}

.drawer--open { transform: none; visibility: visible; transition: transform 220ms var(--ease), visibility 0s; }

@media (prefers-reduced-motion: reduce) {
  .drawer,
  .drawer--open,
  .drawer-scrim,
  .drawer-scrim--open { transition: none; }
}

.drawer-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-2) var(--space-3);
  color: var(--text);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.drawer-brand .row-end { margin-left: auto; }

.drawer-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 48px;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-m);
  background: transparent;
  color: var(--text-2);
  font-size: 14px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  transition: background-color 150ms var(--ease), color 150ms var(--ease);
}

.drawer-item + .drawer-item { margin-top: 2px; }
.drawer-item:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.drawer-item:disabled,
.drawer-item[aria-disabled="true"] { opacity: 0.5; pointer-events: none; }

/* the active row: a red rail + a crimson tint, never colour alone —
   aria-current="page" carries the same fact to a screen reader. */
.drawer-item--active {
  background: var(--accent-soft);
  color: var(--text);
}

.drawer-item--active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
}

.drawer-item--active .drawer-item-icon {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent-text);
}

.drawer-item-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-2);
}

.drawer-item-icon svg { width: 18px; height: 18px; }

.drawer-sep { height: 1px; margin: var(--space-3) var(--space-2); background: var(--line); }

.drawer-foot { margin-top: auto; padding-top: var(--space-3); }
.drawer-foot .drawer-item--danger { color: var(--danger); }
.drawer-foot .drawer-item--danger:hover { background: var(--danger-soft); color: var(--danger); }
.drawer-foot .chip--lang { margin: var(--space-2) var(--space-3) 0; }

.preview-chip {
  height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  background: var(--info-soft);
  border: 1px solid rgb(90 169 255 / 0.4);
  color: var(--info);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 22px;
}

/* ============================================================ errortext */
.errtext { display: block; }
.errtext .code {
  display: inline-block;
  margin-top: var(--space-2);
  padding: 2px 6px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: 12px;
  word-break: break-all;
}

/* small inline `<a class="t-small">` links (e.g. the studio "See all"
   recent-jobs link) render at the 18px line-height of .t-small — grow the
   hit area on all sides without touching the visible text box. Scoped to
   anchors only: plain .t-small text (labels, hints) is not interactive and
   must not get position:relative side effects. */
a.t-small { position: relative; }
a.t-small::before {
  content: "";
  position: absolute;
  top: -13px;
  bottom: -13px;
  left: -12px;
  right: -12px;
}

/* ============================================================== helpers */
.divider { height: 1px; background: var(--line); }
.mono { font-family: var(--font-mono); font-size: 13px; }
.nowrap { white-space: nowrap; }
.tabular { font-variant-numeric: tabular-nums; }

/* ================================================== card U — studio parts
   New classes added 2026-09-05 for the TeamAlpha restyle. Cards D / E1 / E2
   consume these names verbatim; nothing above this block changed shape.
   House rules still apply to every one of them: :hover, :focus-visible (the
   global 2 px --accent ring in tokens.css), :disabled = opacity .5 +
   pointer-events none, 44 px minimum touch target, 150 ms colour/transform
   transitions (tokens.css turns them off under prefers-reduced-motion). */

/* ------------------------------------------------------------ form label
   the small uppercase caps TeamAlpha puts over every control
   ("VIDEO URL", "CROP RATIO"). It is TEXT a user reads, so it takes the
   4.5:1 text floor (DESIGN.md §2): --text-2 measures 7.92:1 on --surface and
   7.12:1 on --surface-2. --text-3 (3.73 / 3.35:1) would fail it. */
.label-caps {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--text-2);
  font-size: 11px;
  line-height: 16px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ------------------------------------------------------------ step pills
   4 columns of `minmax(0,1fr)` (not `1fr`, which has an auto MINIMUM and
   would push a narrow properties card wider than its container) so the
   four words "Source · Voice · Effects · Advanced" have to share the row
   evenly rather than each claiming their content width — that is what a
   plain grid-auto-flow: column track already does, but the pill's own
   padding/gap/font-size were tuned for a full-width layout and truncated
   ("Sour…", "Effe…", "Adv…") once this component sits inside a ~380 px
   desktop sidebar card instead of the page column. */
.step-pills {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 2px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
}

.step-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 0;
  min-height: 44px;
  padding: 0 2px;
  border: 0;
  border-radius: var(--radius-s);
  background: transparent;
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  transition: background-color 150ms var(--ease), color 150ms var(--ease);
}

.step-pill svg { flex: none; width: 16px; height: 16px; }
/* overflow-wrap/text-overflow only as a last-resort backstop — the padding,
   gap and 12px font above are what make the four full words fit; nothing
   should actually reach ellipsis at >=420px. */
.step-pill-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow-wrap: anywhere;
  font-size: 12px;
}
.step-pill:hover:not(:disabled) { background: rgb(255 255 255 / 0.05); color: var(--text); }
.step-pill:disabled,
.step-pill[aria-disabled="true"] { opacity: 0.5; pointer-events: none; }

.step-pill--active {
  background: var(--accent);
  background-image: var(--accent-grad);
  color: var(--accent-ink);
  box-shadow: var(--shadow-1);
}

.step-pill--active:hover:not(:disabled) { background: var(--accent); color: var(--accent-ink); }

/* a finished step is green-tinted AND keeps its check icon — never colour
   alone (DESIGN.md §5). */
.step-pill--done { background: var(--ok-soft); color: var(--ok); }
.step-pill--done:hover:not(:disabled) { background: rgb(61 214 140 / 0.22); color: var(--ok); }

/* under 420 px four labels do not fit; the icons + the active label do */
@media (max-width: 419px) {
  .step-pill .step-pill-label { display: none; }
  .step-pill--active .step-pill-label { display: block; }
}

/* -------------------------------------------------- collapsible fx block
   TeamAlpha's "effect" section: icon + uppercase title + a switch on the
   right + a chevron; the body expands underneath. `.fx--on` = the effect is
   enabled, `.fx--open` = the body is expanded. They are independent. */
.fx {
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  overflow: hidden;
  transition: border-color 150ms var(--ease);
}

.fx + .fx { margin-top: var(--space-3); }
.fx--on { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/* `.fx-head` is a plain `<div>` (studio.js builds a real `.toggle` and a
   real `<button class="fx-expand">` as its two interactive children — a
   button-in-a-button would be invalid HTML), so it carries no button reset
   properties (`border: 0`, `:disabled`) and no native disabled state; the
   row itself stays clickable as an enhancement over its two real children
   (see studio.js's fxSection comment). `:focus-within` mirrors `:hover` so
   the row tints when either child button is keyboard-focused, not only on
   pointer hover. */
.fx-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 52px;
  padding: var(--space-2) var(--space-3);
  color: var(--text);
  cursor: pointer;
  transition: background-color 150ms var(--ease);
}

.fx-head:hover,
.fx-head:focus-within { background: rgb(255 255 255 / 0.04); }

/* the enable switch: a real `.toggle` button sized by its own class — this
   just keeps it from stretching in the `.fx-head` flex row. */
.fx-switch { flex: none; }

/* the disclosure button: `.btn .btn--ghost .btn--m` already gives it
   `min-height: 44px`, a transparent `--btn-bg` and hover/focus states —
   this only squares its hit area to 44x44 and drops the ghost button's
   horizontal padding, which would otherwise widen the row's right edge. */
.fx-expand {
  flex: none;
  width: 44px;
  padding: 0;
}

.fx-title {
  flex: 1;
  min-width: 0;
  color: var(--text-2);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.fx--on .fx-title { color: var(--text); }

.fx-chevron {
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--text-3);
  transition: transform 150ms var(--ease), color 150ms var(--ease);
}

.fx-head:hover .fx-chevron { color: var(--text-2); }
.fx--open .fx-chevron { transform: rotate(180deg); }

.fx-body { display: none; padding: 0 var(--space-3) var(--space-3); }
.fx--open .fx-body { display: block; }

/* ----------------------------------------------------------- voice modal
   `vm-` prefixed so the old flat picker (.voice*) keeps working on the live
   site until card D lands and card F deletes it. */
.vm { display: block; }

.vm-filters { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.vm-filter {
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease),
    color 150ms var(--ease);
}

/* 36 px tall by design; the hit area is grown to 44 px with ::after. */
.vm-filter { position: relative; }
.vm-filter::after { content: ""; position: absolute; inset: -4px 0; }
.vm-filter:hover { border-color: #4a536a; color: var(--text); }
.vm-filter:disabled { opacity: 0.5; pointer-events: none; }

.vm-filter--active {
  border-color: transparent;
  background: var(--accent);
  background-image: var(--accent-grad);
  color: var(--accent-ink);
}

/* the modal's search box carries no .input class (ui.js builds a bare
   <input type="search">), so it restates the .input surface here: same
   44 px height, padding, border, radius, focus ring. appearance:none kills
   the WebKit search decorations that otherwise clip the padding. */
.vm-search {
  appearance: none;
  width: 100%;
  min-height: 44px;
  margin-top: var(--space-3);
  padding: 10px var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  transition: border-color 150ms var(--ease), background-color 150ms var(--ease);
}

.vm-search::-webkit-search-decoration,
.vm-search::-webkit-search-cancel-button { appearance: none; }
.vm-search::placeholder { color: var(--text-3); }
.vm-search:hover:not(:disabled) { border-color: #384054; }
.vm-search:focus-visible { border-color: var(--accent); }
.vm-search:disabled { opacity: 0.5; cursor: not-allowed; }
.vm-count { margin-top: var(--space-2); color: var(--text-2); font-size: 13px; line-height: 18px; }

.vm-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
  margin-top: var(--space-3);
}

@media (min-width: 700px) { .vm-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.vm-card {
  display: grid;
  gap: 6px;
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  text-align: left;
  transition: border-color 150ms var(--ease), background-color 150ms var(--ease);
}

.vm-card:hover { border-color: #4a536a; }

/* gold border on the chosen card, as on TeamAlpha — plus the card's own
   aria-checked, so the state is never carried by colour alone. */
.vm-card--active { border-color: var(--warn); background: var(--warn-soft); }

.vm-card-name { color: var(--text); font-size: 15px; font-weight: 700; }
.vm-card-meta { display: flex; flex-wrap: wrap; gap: var(--space-2); color: var(--text-2); font-size: 13px; line-height: 18px; }
.vm-card-tags { display: flex; flex-wrap: wrap; gap: 6px; }

.vm-tag {
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-2);
  font-size: 11px;
  line-height: 16px;
  font-weight: 600;
}

.vm-card-foot { display: flex; align-items: center; gap: var(--space-2); margin-top: 2px; }
.vm-card-foot .vm-use { margin-left: auto; }

.vm-play {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--accent-text);
  cursor: pointer;
  transition: background-color 150ms var(--ease), color 150ms var(--ease), border-color 150ms var(--ease);
}

.vm-play::after { content: ""; position: absolute; inset: 50%; width: 44px; height: 44px; transform: translate(-50%, -50%); }
.vm-play:hover { border-color: transparent; background: var(--accent); color: var(--accent-ink); }
.vm-play:disabled { opacity: 0.5; pointer-events: none; }
.vm-play.is-loading { color: var(--text-3); animation: rm-blink 900ms ease-in-out infinite; }

.vm-use {
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 150ms var(--ease), border-color 150ms var(--ease), color 150ms var(--ease);
}

.vm-use { position: relative; }
.vm-use::after { content: ""; position: absolute; inset: -4px -2px; }
.vm-use:hover { border-color: transparent; background: var(--accent); background-image: var(--accent-grad); color: var(--accent-ink); }
.vm-use:disabled { opacity: 0.5; pointer-events: none; }

/* the closed state: one full-width .input-like button showing the voice that
   is currently chosen. ui.js (card D) emits exactly three children —
   .vm-trigger-name, an optional .vm-trigger-meta, and .vm-trigger-change —
   as FLAT siblings, so placement is explicit rather than auto: name and meta
   stack in column 1, "Change" sits right and spans both rows. With no voice
   chosen there is no meta row and the span collapses to the single row. */
.vm-trigger {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px var(--space-3);
  width: 100%;
  min-height: 44px;
  padding: 10px var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: border-color 150ms var(--ease), background-color 150ms var(--ease);
}

.vm-trigger-name {
  grid-column: 1;
  grid-row: 1;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--text);
  font-size: 15px;
  line-height: 20px;
  font-weight: 700;
}

.vm-trigger-meta {
  grid-column: 1;
  grid-row: 2;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--text-2);
  font-size: 12px;
  line-height: 16px;
}

/* a link-style affordance, not a second button: accent as TEXT must use
   --accent-text (6.32:1 on --surface, 5.69:1 on --surface-2), never
   --accent, and it is underlined on hover so it is not colour-only. */
.vm-trigger-change {
  grid-column: 2;
  grid-row: 1 / -1;
  justify-self: end;
  align-self: center;
  flex: none;
  color: var(--accent-text);
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
}

.vm-trigger:hover { border-color: #4a536a; background: var(--surface); }
.vm-trigger:hover .vm-trigger-change { text-decoration: underline; }
.vm-trigger:focus-visible { border-color: var(--accent); }
.vm-trigger:focus-visible .vm-trigger-change { text-decoration: underline; }
.vm-trigger:disabled { opacity: 0.5; pointer-events: none; }

/* ---------------------------------------------------------- preview panel */
.pv-head { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.pv-title { flex: 1; min-width: 0; font-size: 18px; line-height: 24px; font-weight: 600; }

.pv-badge {
  flex: none;
  padding: 3px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 12px;
  line-height: 18px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pv-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }

.pv-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  position: relative;
  min-height: 32px;
  padding: 0 var(--space-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  /* chip text, not a glyph: --text-2 is 7.12:1 on --surface-2 (the 4.5:1 text
     floor); the OFF state is carried by the neutral colour + aria-pressed. */
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 150ms var(--ease), color 150ms var(--ease), background-color 150ms var(--ease);
}

.pv-chip::after { content: ""; position: absolute; inset: -6px 0; }
.pv-chip svg { flex: none; width: 14px; height: 14px; }
.pv-chip:hover { border-color: #4a536a; color: var(--text); }
.pv-chip:disabled { opacity: 0.5; pointer-events: none; }

/* "this effect is on" is carried by the tint AND by aria-pressed, and the
   label text is always present — colour is never the only signal. */
.pv-chip--on {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--accent-soft);
  color: var(--accent-text);
}

/* --------------------------------------------------------- result screen */
.result { display: grid; gap: var(--space-4); grid-template-columns: minmax(0, 1fr); }

@media (min-width: 900px) {
  .result { grid-template-columns: 96px minmax(0, 1fr) 300px; align-items: start; }
}

.result-side { display: grid; gap: var(--space-2); grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }

@media (min-width: 900px) { .result-side { grid-auto-flow: row; grid-auto-columns: auto; } }

.result-side-btn {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 6px;
  min-width: 0;
  min-height: 76px;
  padding: var(--space-3) var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: border-color 150ms var(--ease), color 150ms var(--ease), background-color 150ms var(--ease);
}

.result-side-btn svg { width: 20px; height: 20px; }
.result-side-btn:hover { border-color: color-mix(in srgb, var(--accent) 50%, transparent); color: var(--accent-text); text-decoration: none; }
.result-side-btn:disabled,
.result-side-btn[aria-disabled="true"] { opacity: 0.5; pointer-events: none; }

.result-main { min-width: 0; display: grid; gap: var(--space-3); }
.result-main video { width: 100%; border-radius: var(--radius-m); background: #000; }
.result-speed {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  color: var(--text-2);
  font-size: 13px;
}
.result-speed-select,
.vp-speed {
  min-width: 72px;
  min-height: 44px;
  padding: 0 var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.result-speed-select:focus-visible,
.vp-speed:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.result-props { min-width: 0; display: grid; gap: var(--space-3); }

.caption-card {
  padding: var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-m);
  background: var(--surface-2);
  color: var(--text);
  font-size: 15px;
  line-height: 22px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.hashtag {
  display: inline-block;
  margin: 0 6px 6px 0;
  padding: 3px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--accent-text);
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
}

.copy-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.copy-row .btn { flex: 1 1 auto; }

@media (min-width: 560px) { .copy-row .btn { flex: 0 0 auto; } }

/* ---------------------------------------------------------- wizard footer */
.wizard-foot {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}

.wizard-foot .wizard-next { margin-left: auto; }

/* ========================================================= videoPreview
   RM.videoPreview (ui.js). Added by card B — ADDITIVE only, nothing above
   this line is restyled. The geometry here is load-bearing: read the
   COORDINATE LAW comment above RM.videoPreview before changing it. */
.vp { display: block; }

/* The stage's aspect-ratio is set INLINE by the component: the target crop
   ratio when one is chosen, otherwise the source's own ratio. That is what
   makes the visible frame identical to the server's centred crop, and what
   removes the letterbox that would otherwise have to be mapped across. */
/* HEIGHT CAP: the page may set `--vp-max-h` (studio.css does, desktop only)
   to stop a 9:16 stage from pushing the player controls below the fold. The
   trio `width: auto` + `max-width: 100%` + `max-height` is what keeps
   aspect-ratio governing the box: with a definite `width: 100%` the max-height
   would SQUASH the stage (measured 700x500 on a 9:16 ratio) and the frame
   would no longer be the server's centred crop. With width auto the ratio
   transfers the clamped height back into the width (measured 281x500), so the
   box only gets smaller — and `.vp-frame`/`.vp-layer` are inset:0 of it, so
   the overlay percentage mapping is unchanged. margin-inline centres what is
   now a narrower box in the column. */
/* THE STAGE GROUND (card S2 2026-09-05, revised by card X2 2026-09-06).
   `.vp-ground` is only a sizing wrapper now: it stretches the full card
   width so `.vp-stage` (width:auto + aspect-ratio + max-height, see below)
   has a definite width to compute `margin-inline: auto` centring against.
   It paints NOTHING -- the card's own `--surface` shows through on either
   side of a portrait stage, matching TeamAlpha (no black bar, no blurred or
   mirrored side-fill). `display: block` on purpose, same reason as always:
   a flex/grid parent would size `.vp-stage` as a shrink-to-fit item instead
   of letting `aspect-ratio` govern it. The stage itself keeps its own
   `border-radius`/`overflow: hidden`/`#000` background below -- that black is
   the letterboxed *frame* content, not ground around it. */
.vp-ground {
  display: block;
}

.vp-stage {
  position: relative;
  width: auto;
  max-width: 100%;
  max-height: var(--vp-max-h, none);
  margin-inline: auto;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-m);
  overflow: hidden;
  background: #000;
  touch-action: none; /* a drag on a box must not scroll the page */
}

.vp-frame {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/* zoom + mirror live HERE, on the media's own wrapper. `.vp-layer` is a
   sibling, so the overlay boxes are never inside this transform. */
.vp-media {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
}

.vp-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
}

/* THE POSTER IS `contain`, THE VIDEO IS `cover` — card S5, and the pair is
   deliberate. The video's frame IS the stage (the stage carries its aspect
   ratio, so `cover` crops nothing and IS the server's centred crop). The
   poster is the fetch's THUMBNAIL, whose aspect has nothing to do with the
   source: YouTube ships a 1280x720 JPEG for a 1080x1920 Short with a blurred
   pillarbox baked into the pixels. Under `cover` that landscape picture was
   stretched edge to edge and its baked-in blurred bars read as the stage —
   the CEO's 2026-09-06 report. `contain` letterboxes it on the black ground
   instead, so a placeholder can never misdescribe the frame; ui.js drops it
   entirely the moment the video has a decoded frame. */
.vp-poster {
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

/* The "there is a source but no decoded frame yet" placeholder. The stripe
   layer is not decoration: it is the only high-frequency detail on the stage
   while there is no media, and without it a blur box (which falls back to
   backdrop-filter when it cannot draw a canvas) is invisible — in ?preview=1
   and in the seconds before a fetched frame arrives alike. */
.vp-ph {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  background:
    repeating-linear-gradient(
      135deg,
      rgb(255 255 255 / 0.05) 0 8px,
      rgb(255 255 255 / 0) 8px 18px
    ),
    linear-gradient(135deg, #232838, #12141b 65%);
  color: var(--text-3);
}

.vp-layer {
  position: absolute;
  inset: 0;
}

.vp-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--space-4);
  text-align: center;
  color: var(--text-2);
  font-size: 14px;
  line-height: 20px;
  background: linear-gradient(135deg, #1b1f2b, #12141b 70%);
}

.vp-empty[hidden] { display: none; }

.vp-status {
  position: absolute;
  left: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  max-width: calc(100% - var(--space-5));
  padding: 6px var(--space-3);
  border-radius: 999px;
  background: rgb(15 17 21 / 0.82);
  color: var(--text);
  font-size: 13px;
  line-height: 18px;
}

.vp-status[hidden] { display: none; }

.vp-spin {
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid rgb(255 255 255 / 0.22);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: rm-spin 700ms linear infinite;
}

.vp-badge {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  background: rgb(15 17 21 / 0.82);
}

.vp-badge[hidden] { display: none; }

/* ----------------------------------------------------------- overlay box */
.vp-box {
  position: absolute;
  border: 2px solid var(--accent);
  border-radius: 4px;
  background: rgb(221 47 61 / 0.16);
  cursor: default;
}

.vp-layer.is-editable .vp-box { cursor: move; }

/* The blur box draws the frame under it into its own canvas and blurs THAT
   (ui.js paintBlur), at the sigma the server's boxblur radius implies — so
   the strength slider is honest. The 20 % fill and the old unprefixed
   backdrop-filter would sit ON TOP of that, so they move to .vp-box--nocv,
   which ui.js sets only when there is no frame to sample at all. */
.vp-box--blur {
  border-color: var(--info);
  background: transparent;
}

.vp-box--blur.vp-box--nocv {
  background: rgb(90 169 255 / 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* inset -2px, not 0: an absolutely positioned child is placed against the
   PADDING box, and .vp-box carries a 2 px border — the blurred region has to
   line up with the rectangle the user sees, which is the border box. */
.vp-blur-clip {
  position: absolute;
  inset: -2px;
  overflow: hidden;
  border-radius: 4px;
  pointer-events: none;
}

.vp-blur-clip[hidden] { display: none; }

/* offset by the bleed margin; ui.js writes left/top/width/height in px */
.vp-blur-cv {
  position: absolute;
  display: block;
}

/* ------------------------------------------------------- caption box
   Where the burned caption block will land. Deliberately EMPTY — no sample
   text (CEO decision 2026-09-04); the width and height are the truth, the
   glyphs would only be a guess at libass's metrics. Green + dashed so it
   never reads as one of the blur/logo rectangles. */
.vp-box--caption {
  border-style: dashed;
  border-color: var(--ok);
  background: rgb(61 214 140 / 0.08);
}

.vp-layer.is-editable .vp-box--caption { cursor: ns-resize; }

/* The caption box's sample line (card S5): a STYLE swatch at the real font
   size, centred in the box exactly as subtitles.cues_to_ass centres the cue
   block in the band. `--vp-cap-fs` is written by ui.js paintCaptionFont();
   the fallback only ever shows under jsdom, which has no layout.

   Card 2 (2026-09-06) — the swatch now shows the whole ASS Style row, so
   "bold pop" (yellow + thick black ring + bold) is visible BEFORE a render.
   Four more custom properties, all written by ui.js syncCaptionLook() on the
   BOX and all mirrors of a SERVER value, never of a chrome token:
     --vp-cap-fill    ASS PrimaryColour   (subtitles.TEXT_COLORS via
                                           ui.js VP_TEXT_COLORS)
     --vp-cap-weight  ASS Bold 0 | -1     -> 400 | 700
     --vp-cap-ol      ASS Outline width   (subtitles.OUTLINE_RATIOS x the
                                           font size, i.e. an em here)
     --vp-cap-olc     ASS OutlineColour   (black | white)
   The fallbacks below are the pre-card look (white, unbolded, no ring), so a
   caller that never sets a style still gets exactly what it used to. */
.vp-cap-text {
  /* --vp-cap-fill / --vp-cap-weight / --vp-cap-ol / --vp-cap-olc are
     DELIBERATELY NOT DECLARED HERE, only read with a var() fallback below.
     ui.js writes them on the caption BOX and they reach this line by
     inheritance — and a declaration on THIS element would beat that
     inherited value outright (cascade origin, not specificity). Measured in
     headless chromium while writing this: with `--vp-cap-ol: 0em` sitting in
     this rule, a box carrying 0.08em still computed every ring offset to
     0px, i.e. the outline silently never drew. Put a default in the var()
     fallback, never in a declaration on the consumer. */
  /* --vp-cap-halo is the exception ON PURPOSE: its override
     (.vp-box--cap-shadow .vp-cap-text) targets this same element, so a
     declaration here is a real default rather than a shadow of an inherited
     one. Keeping the halo and the ring in SEPARATE properties is what lets
     table B's "shadow + thick outline" row draw both. */
  --vp-cap-halo: 0 0 0 rgb(0 0 0 / 0);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 2px;
  overflow: hidden;
  pointer-events: none;
  color: var(--vp-cap-fill, #fff);
  font-size: var(--vp-cap-fs, 13px);
  font-weight: var(--vp-cap-weight, 400);
  line-height: 1.15;
  text-align: center;
  white-space: nowrap;
  /* THE OUTLINE RING. An ASS outline lies OUTSIDE the glyph, which
     -webkit-text-stroke does not (it straddles the path and eats the fill at
     half the width), so it is drawn as eight zero-blur shadows on a circle
     of radius --vp-cap-ol: 4 axis-aligned + 4 diagonals at 0.7071 r. `em`
     resolves against THIS element's font size, which paintCaptionFont has
     already set to the burned font size scaled to the stage — so the ring
     scales with the caption exactly as the ASS Outline does (card 1's
     OUTLINE_RATIOS x font size). At the 0em fallback every copy sits behind
     the opaque glyph and nothing shows, which is why "no outline" needs no
     second rule. */
  text-shadow:
    var(--vp-cap-ol, 0em) 0 0 var(--vp-cap-olc, #000),
    calc(-1 * var(--vp-cap-ol, 0em)) 0 0 var(--vp-cap-olc, #000),
    0 var(--vp-cap-ol, 0em) 0 var(--vp-cap-olc, #000),
    0 calc(-1 * var(--vp-cap-ol, 0em)) 0 var(--vp-cap-olc, #000),
    calc(0.7071 * var(--vp-cap-ol, 0em)) calc(0.7071 * var(--vp-cap-ol, 0em)) 0
      var(--vp-cap-olc, #000),
    calc(-0.7071 * var(--vp-cap-ol, 0em)) calc(0.7071 * var(--vp-cap-ol, 0em)) 0
      var(--vp-cap-olc, #000),
    calc(0.7071 * var(--vp-cap-ol, 0em)) calc(-0.7071 * var(--vp-cap-ol, 0em)) 0
      var(--vp-cap-olc, #000),
    calc(-0.7071 * var(--vp-cap-ol, 0em)) calc(-0.7071 * var(--vp-cap-ol, 0em)) 0
      var(--vp-cap-olc, #000),
    var(--vp-cap-halo);
}

/* Card W2 (2026-09-06): the sample line is one Burmese-first string that
   also carries a Latin word, and RM.t wraps it in <span class="t"><span
   lang="my">. tokens.css's `[lang="my"]` rule would then give THAT inner
   span --font-my and line-height 1.7, so the swatch would be drawn in a
   different face and a taller line box than the one it is imitating. Both
   are pinned back here (a descendant rule — the attribute selector targets
   the inner element, so a declaration on .vp-cap-text itself cannot reach
   it):
     line-height  1.15 = subtitles.LINE_HEIGHT_FACTOR, the spacing libass
                  uses and the number vpLineBlockHeight sizes the band with;
                  a 1.7 line box would overflow the band it is drawn inside.
     font-family  --font-ui = Inter + Noto Sans Myanmar, i.e. Latin from the
                  Latin face and Burmese from the Myanmar face — the same
                  split libass makes when it falls back from Noto Sans to
                  Noto Sans Myanmar (fontselect, verified in the burn log).
   nowrap is repeated because the clip that keeps a long sample inside a
   narrow stage is `overflow: hidden` on the parent plus nowrap here. */
.vp-cap-text .t,
.vp-cap-text [lang] {
  display: inline;
  font-family: var(--font-ui);
  line-height: inherit;
  white-space: nowrap;
}

/* Card M2 (2026-09-06): the sample line's BURMESE runs, which ui.js
   wrapMyanmarRuns() splits out of the string character by character (RM.t's
   lang attribute is the UI language, not the script, so it cannot be used
   for this). libass draws the two scripts at two different ems because it
   normalises every face to (ascender + descender) == Fontsize:
     Noto Sans Bold          1069 + 293 = 1.362 em  ->  0.734 x Fontsize
     Noto Sans Myanmar Bold  1324 + 860 = 2.184 em  ->  0.458 x Fontsize
   so --vp-cap-my (written by ui.js syncCaptionLook from
   vpMyanmarSampleScale) is 1 for a Burmese job — card M1 burns those cues at
   round(size * 1.6035) and the two ems meet — and 1/1.6035 = 0.6236 for a
   Latin one, where Burmese still burns 38 % smaller. A MULTIPLIER on the
   inherited font size (1em resolves against the parent), never a size of its
   own, so --vp-cap-fs stays the single source of the caption's scale. The
   fallback 1 keeps a caller that never sets a script at today's look. */
.vp-cap-text .vp-cap-my {
  font-size: calc(1em * var(--vp-cap-my, 1));
}

/* PHYSICAL MIRROR OF AN FFMPEG/libass CONSTANT, NOT A CHROME COLOUR (DESIGN
   law, same rule as CAPTION_BAND_TINT). The server draws background
   "shadow" as a blurred black ASS outline:
     subtitles.SHADOW_OUTLINE_RATIO   = 0.12 x font size  -> 0.12em
     subtitles.SHADOW_BLUR_RATIO      = 0.06 x font size  -> 0.06em
     subtitles.SHADOW_OUTLINE_OPACITY = 0.70              -> rgb(0 0 0 / .7)
   CSS has no outline for text, so the halo is approximated by stacked
   shadows at those radii. Change one side and the preview stops being the
   twin — grep SHADOW_OUTLINE_RATIO to find both halves.
   Card 2: this sets the HALO variable rather than `text-shadow` itself. On
   "shadow" the server replaces a none/thin glyph outline with this halo and
   keeps a thick one alongside it (card-set table B), and ui.js's
   vpEffectiveOutline is the half that decides which — here both simply
   coexist. */
.vp-box--cap-shadow .vp-cap-text {
  --vp-cap-halo:
    0 0 0.06em rgb(0 0 0 / 0.7),
    0 0 0.12em rgb(0 0 0 / 0.7),
    0 0 0.24em rgb(0 0 0 / 0.7);
}

.vp-cc {
  position: absolute;
  top: -10px;
  left: 6px;
  padding: 0 5px;
  border-radius: 4px;
  background: var(--ok);
  color: #06301c;
  font-size: 10px;
  line-height: 16px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

.vp-box--logo {
  background: rgb(221 47 61 / 0.18);
  border-style: dashed;
}

.vp-box:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Handles read as 12 px but grab at 44 px: the visible square is the element,
   the hit area is a centred pseudo-element (same trick as .brand::before). */
.vp-h {
  position: absolute;
  width: 12px;
  height: 12px;
  border: 2px solid var(--bg);
  border-radius: 3px;
  background: var(--accent);
  display: none;
}

.vp-layer.is-editable .vp-h { display: block; }

.vp-h::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.vp-h--nw { top: -7px; left: -7px; cursor: nwse-resize; }
.vp-h--ne { top: -7px; right: -7px; cursor: nesw-resize; }
.vp-h--sw { bottom: -7px; left: -7px; cursor: nesw-resize; }
.vp-h--se { bottom: -7px; right: -7px; cursor: nwse-resize; }
/* the caption's only two handles: width, symmetric about a fixed centre */
.vp-h--w { top: calc(50% - 6px); left: -7px; cursor: ew-resize; }
.vp-h--e { top: calc(50% - 6px); right: -7px; cursor: ew-resize; }
/* card S2: the caption's third handle — the BAND height (subtitle_h). On the
   bottom edge, because that edge is the caption anchor and dragging it is
   what makes the band taller. */
.vp-h--s { bottom: -7px; left: calc(50% - 6px); cursor: ns-resize; }
.vp-box--caption .vp-h { background: var(--ok); }

/* ------------------------------------------------- subtitle blur band
   `subtitle_background: "blur"`. Its OWN rectangle, not the caption box's:
   with subtitle_w null the render draws the band across the whole frame
   while the ASS text keeps its 20 px side margins (ui.js vpCaptionBandBox,
   the twin of ffmpeg_tools.caption_band_rect). It is appended BEFORE the
   caption box because ffmpeg draws it before the `ass` filter, so the screen
   stacks the way the render does, and it takes no pointer events — the
   dashed box on top of it is the control.
   ui.js paintBlur draws the frame under it into the same canvas machinery
   the blur boxes use, at CAPTION_BAND_RADIUS, fills CAPTION_BAND_TINT black
   over it and masks a CAPTION_BAND_FEATHER_PX ramp onto the clip. THOSE
   THREE NUMBERS ARE ffmpeg CONSTANTS copied by name from
   app/providers/ffmpeg_tools.py — the tint is PICTURE, not chrome, so it is
   deliberately not a token and must never be "themed" to the accent
   (DESIGN.md's token law is about UI surfaces).
   Card M2 (2026-09-06): background "frost" draws this SAME band — same
   rectangle, same sigma, same feather — with the light tint forced on, so it
   needs no rule of its own: ui.js vpBandTint() maps it to
   .vp-band--tint-light below. */
.vp-band {
  position: absolute;
  border-radius: 2px;
  pointer-events: none;
}

/* no 2 px border here, unlike .vp-box — so the clip is inset 0 and the
   feather ramp lands exactly on the band's edges */
.vp-band .vp-blur-clip {
  inset: 0;
  border-radius: 2px;
}

/* the no-frame fallback, exactly like .vp-box--blur.vp-box--nocv: with
   nothing to sample (fixtures, a fetch still downloading, an undecodable
   codec) the band still has to READ as a blurred strip.
   Card 2 (2026-09-06): the tint is a per-job choice now, so the fallback
   has to follow it — ui.js syncBandTint() puts .vp-band--tint-<none|light|
   dark> on the node and the two tinted rules below carry the SAME alphas the
   canvas path fills (CAPTION_BAND_TINT / CAPTION_BAND_LIGHT_TINT, ffmpeg
   constants copied by name). The base fill here is NOT a tint: it is a faint
   neutral scrim so an un-tinted band is still locatable when there is no
   frame to blur at all — with a real frame, "none" fills nothing. */
.vp-band.vp-box--nocv {
  background: rgb(255 255 255 / 0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

.vp-band--tint-light.vp-box--nocv { background: rgb(255 255 255 / 0.18); }
.vp-band--tint-dark.vp-box--nocv { background: rgb(0 0 0 / 0.35); }

.vp-box-x {
  position: absolute;
  top: -12px;
  right: -12px;
  display: none;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}

.vp-layer.is-editable .vp-box-x { display: grid; }

.vp-box-x::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.vp-box-x:hover { border-color: var(--danger); color: var(--danger); }

/* -------------------------------------------------------------- controls */
.vp-controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.vp-controls[hidden] { display: none; }

.vp-ctl {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--line);
  border-radius: var(--radius-s);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  transition: border-color 150ms var(--ease), color 150ms var(--ease);
}

.vp-ctl:hover:not(:disabled) { border-color: #384054; color: var(--accent-text); }

.vp-ctl:disabled { opacity: 0.5; pointer-events: none; }

.vp-seek {
  flex: 1;
  min-width: 40px;
}

.vp-time {
  flex: none;
  color: var(--text-2);
  font-size: 13px;
  line-height: 18px;
}

.vp-speed {
  flex: none;
}

/* under ~520 px the controls plus speed and time readout do not fit on one line */
@media (max-width: 519px) {
  .vp-controls { flex-wrap: wrap; }
  .vp-seek { order: 10; flex: 1 1 100%; }
  .vp-time { margin-left: auto; }
}

/* The subtitle swatch uses the exact static faces installed for libass. */
@font-face { font-family: "Recap Caption Latin"; src: url("/fonts/NotoSans-Regular.ttf"); font-weight: 400; }
@font-face { font-family: "Recap Caption Latin"; src: url("/fonts/NotoSans-Bold.ttf"); font-weight: 700; }
@font-face { font-family: "Recap Caption Myanmar"; src: url("/fonts/NotoSansMyanmar-Regular.ttf"); font-weight: 400; }
@font-face { font-family: "Recap Caption Myanmar"; src: url("/fonts/NotoSansMyanmar-Bold.ttf"); font-weight: 700; }
.vp-cap-text, .vp-cap-text .t, .vp-cap-text [lang] {
  font-family: "Recap Caption Latin", "Recap Caption Myanmar", sans-serif;
  white-space: normal;
  overflow-wrap: anywhere;
}
