/* Riparr — *arr-family interface conventions.
 *
 * Every colour resolves through the theme.park custom-property contract
 * (github.com/themepark-dev/theme.park, MIT). A user already running a theme.park
 * theme can point Riparr at the same stylesheet and it inherits their whole look.
 * Fallbacks keep this readable with no theme file loaded at all.
 */

/* The wordmark face. Russo One (SIL OFL 1.1 — see fonts/RussoOne-OFL.txt) is used for
   the "Riparr" name only, never for body copy: the *arr look is Roboto everywhere, and
   the brand face earns its place by being the one thing that is *not* Roboto.
   swap keeps the name visible in Roboto while the 13 KB woff2 arrives. */
@font-face {
  font-family: "Russo One";
  src: url("/static/fonts/RussoOne-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --bg:        var(--main-bg-color, #1f1f1f);
  --panel:     var(--modal-bg-color, #333);
  --panel-hd:  var(--modal-header-color, #232323);
  --dropdown:  var(--drop-down-menu-bg, #1b1b1b);
  --btn:       var(--button-color, #2cabe3);
  --btn-hover: var(--button-color-hover, #298fbc);
  --btn-text:  var(--button-text, #eee);
  --accent:    rgb(var(--accent-color, 44, 171, 227));
  --accent-rgb: var(--accent-color, 44, 171, 227);
  --link:      var(--link-color, #2cabe3);
  --link-hover:var(--link-color-hover, #3cc5ff);
  --fg:        var(--text, #96a2b4);
  --fg-strong: var(--text-hover, #fff);
  --fg-muted:  var(--text-muted, #999);
  --queue:     var(--arr-queue-color, #2cabe3);

  /* Servarr surfaces; fall back to the generic panel colours for other themes. */
  --sidebar:     var(--sidebar-bg, var(--modal-header-color, #2a2a2a));
  --sidebar-on:  var(--sidebar-active-bg, rgba(255,255,255,.06));
  --sidebar-fg2: var(--sidebar-fg, var(--text, #e1e2e3));
  --toolbar:     var(--toolbar-bg, var(--modal-bg-color, #262626));
  --btn-bg:      var(--default-btn-bg, rgba(255,255,255,.06));
  --btn-bd:      var(--default-btn-border, rgba(255,255,255,.14));
  --btn-bg-hover:var(--default-btn-hover, rgba(255,255,255,.12));

  --ok:    var(--ok-color,   #27c24c);
  --warn:  var(--warn-color, #ffa500);
  --bad:   var(--bad-color,  #f05050);
  --line:  rgba(255,255,255,.08);
  --line-2:rgba(255,255,255,.14);

  /* Sonarr Variables/dimensions.js */
  --topbar-h: 60px;   /* headerHeight */
  --side-w:  210px;   /* sidebarWidth */
  --radius:   4px;

  /* Icon sizes, measured off a running Prowlarr 2.4.0 rather than guessed. Every one
     of these is a font-size: an .ic is 1em square, so the context sets the size. */
  --ic-nav:     14px;  /* PageSidebarItem                       */
  --ic-tool:    21px;  /* PageToolbarButton                     */
  --ic-row:     12px;  /* IconButton inside a table row         */
  --ic-header:  14px;  /* PageHeader actions                    */
  --ic-empty:   48px;  /* our own: empty-state illustration     */
}

/* Font Awesome's own inline-SVG metrics (svg-inline--fa). Height 1em is the whole
   point: it makes an icon exactly as tall as the text beside it, at any size. */
.ic {
  display: inline-block;
  height: 1em;
  width: auto;
  min-width: 1em;
  vertical-align: -.125em;
  overflow: visible;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* The `hidden` attribute loses to any rule that sets `display`, and several here do --
   .btn-row is a flex row, so a hidden one was visible. This is what the HTML attribute
   is supposed to mean. */
[hidden] { display: none !important; }

body {
  background: var(--bg);
  background-attachment: fixed;
  color: var(--fg);
  font: 14px/1.528571429 Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--link); text-decoration: none; }
a:hover { color: var(--link-hover); }
.terms {
  margin: 0 0 4px 18px; padding: 0; font-size: 13px; color: var(--fg);
  display: grid; gap: 6px;
}
.terms li { padding-left: 3px; }

.hidden { display: none !important; }
.grow { flex: 1; }
.muted { color: var(--fg-muted); }
h1 { font-size: 21px; font-weight: 300; color: var(--fg-strong); }
h2 { font-size: 16px; font-weight: 400; color: var(--fg-strong); }
h3 { font-size: 13px; font-weight: 500; color: var(--fg-strong);
     text-transform: uppercase; letter-spacing: .06em; }

/* ── shell ─────────────────────────────────────────────── */
.shell { min-height: 100vh; }

.topbar {
  position: fixed; top: 0; left: 0; right: 0; height: var(--topbar-h);
  background: var(--sidebar);
  display: flex; align-items: center; gap: 10px; padding: 0 16px;
  border-bottom: 1px solid var(--line); z-index: 40;
}
.logo {
  color: var(--fg-strong);
  display: flex; align-items: center; gap: 9px;
}
/* Russo One is a display face: it sets much heavier than Roboto at the same size, so
   the wordmark is stepped down to 17px and given a little tracking to stop the double
   'r' closing up. Capitalisation is left to the markup -- "Riparr", not "RIPARR". */
.logo-word {
  font-family: "Russo One", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 17px; font-weight: 400; letter-spacing: .045em;
  line-height: 1;
}
.logo-mark { width: 30px; height: 30px; display: block; flex: none; }
.hamburger {
  display: none; background: none; border: 0; color: var(--fg);
  font-size: 17px; cursor: pointer; line-height: 1;
}
.icon-btn {
  background: none; border: 0; color: var(--fg); font-size: var(--ic-header);
  cursor: pointer; padding: 8px 10px; border-radius: var(--radius);
  line-height: 1;
}
.icon-btn:hover { color: var(--fg-strong); background: rgba(255,255,255,.06); }

.search {
  display: flex; align-items: center; gap: 7px; margin-left: 22px;
  border-bottom: 1px solid var(--line-2); padding: 3px 2px; min-width: 260px;
}
.search .mag {
  color: var(--fg-muted); font-size: var(--ic-header); line-height: 1;
  display: inline-flex;
}
/* Outranks the global input styling below, which would otherwise draw a box. */
.topbar .search input {
  background: none; border: 0; border-radius: 0; padding: 2px 0;
  max-width: none; width: 100%; color: var(--fg-strong); font-size: 13.5px;
}
.topbar .search input:focus { outline: none; box-shadow: none; border: 0; }
.search:focus-within { border-bottom-color: var(--accent); }

.pills { display: flex; gap: 6px; margin-right: 6px; }
.pill {
  font-size: 11px; padding: 3px 9px; border-radius: 11px;
  background: rgba(255,255,255,.08); color: var(--fg);
  display: flex; align-items: center; gap: 5px;
}
.pill.warn { background: rgba(255,159,26,.18); color: var(--warn); }
.pill.bad  { background: rgba(240,80,80,.18);  color: var(--bad); }
.pill.ok   { background: rgba(39,194,76,.16);  color: var(--ok); }

.menu-wrap { position: relative; }
.menu {
  position: absolute; right: 0; top: 38px; min-width: 178px;
  background: var(--dropdown); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 5px; z-index: 60;
  box-shadow: 0 8px 26px rgba(0,0,0,.45);
}
.menu a { display: block; padding: 8px 11px; border-radius: 3px; color: var(--fg); font-size: 13px; }
.menu a:hover { background: rgba(255,255,255,.08); color: var(--fg-strong); }
.menu-head {
  padding: 7px 11px 8px; margin-bottom: 4px; font-size: 13px;
  color: var(--fg-strong); border-bottom: 1px solid var(--line-2);
}

/* The only ask in the whole interface, so it gets a nudge of warmth on hover and
   nothing else -- no badge, no colour at rest, no competing with the health pills. */
.coffee { display: inline-flex; align-items: center; }
.coffee:hover { color: #ffdd00; background: rgba(255,221,0,.12); }

.sidebar {
  position: fixed; top: var(--topbar-h); bottom: 0; left: 0; width: var(--side-w);
  background: var(--sidebar); color: var(--sidebar-fg2); border-right: 1px solid var(--line);
  padding: 12px 0; overflow-y: auto; z-index: 30;
  display: flex; flex-direction: column;
}
/* Geometry from Prowlarr's PageSidebarItem: 45px rows, 12px/24px padding, an 18px
   icon column with a 7px gutter, children indented to 35px. */
.sidebar a.nav-top {
  display: flex; align-items: center;
  padding: 12px 24px; color: var(--fg); font-size: 14px; min-height: 45px;
  border-left: 3px solid transparent;
}
.sidebar a.nav-top:hover { color: var(--fg-strong); background: rgba(255,255,255,.05); }
.sidebar a.nav-top.on {
  color: var(--fg-strong); border-left-color: var(--accent);
  background: var(--sidebar-on);
}
.sidebar a.nav-sub {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 24px 10px 52px; font-size: 14px; color: var(--fg);
  border-left: 3px solid transparent;
}
.sidebar a.nav-sub:hover { color: var(--fg-strong); background: rgba(255,255,255,.04); }
.sidebar a.nav-sub.on { color: var(--accent); border-left-color: var(--accent); }
.sidebar .ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; margin-right: 7px; flex: none;
  font-size: var(--ic-nav); opacity: .9;
}
.nav-badge {
  background: var(--warn); color: #1a1a1a; font-size: 10.5px; font-weight: 600;
  border-radius: 3px; padding: 1px 6px; margin-left: auto;
}
.side-foot { margin-top: auto; padding: 14px 18px 4px; border-top: 1px solid var(--line); }
.cap { font-size: 11.5px; color: var(--fg-muted); line-height: 1.7; }
.cap b { color: var(--fg-strong); font-weight: 500; }

/* Inline with a sentence, not a row of its own: this one sits inside an explanation and
   would read as a section action anywhere else. */
.btn.tiny {
  padding: 2px 9px; font-size: 11px; border-radius: 6px;
  margin-left: 6px; vertical-align: baseline;
}

/* The version, where a version belongs: quiet, always there, and the first thing anybody
   is asked for when they report something. */
.side-ver {
  margin-top: 10px; font-size: 10.5px; color: var(--fg-muted); opacity: .7;
  font-variant-numeric: tabular-nums;
}


.content {
  margin: var(--topbar-h) 0 0 var(--side-w);
  padding: 22px 26px 60px; min-height: calc(100vh - var(--topbar-h));
}

/* ── page furniture ────────────────────────────────────── */
.page-head {
  display: flex; align-items: center; gap: 14px;
  padding-bottom: 14px; margin-bottom: 18px; border-bottom: 1px solid var(--line);
}
.page-head .sub { font-size: 12.5px; color: var(--fg-muted); margin-top: 3px; }
/* The page toolbar, moved into the header row. Same 60px PageToolbarButton geometry
   as `.toolbar` below, just right-aligned instead of banded across its own row. */
.head-acts { margin-left: auto; display: flex; align-items: stretch; gap: 4px; }

.tabs { display: flex; gap: 2px; flex-wrap: wrap; margin-bottom: 20px; }
.tabs a {
  padding: 7px 14px; font-size: 13px; color: var(--fg);
  border-radius: var(--radius) var(--radius) 0 0;
  border-bottom: 2px solid transparent;
}
.tabs a:hover { color: var(--fg-strong); background: rgba(255,255,255,.05); }
.tabs a.on { color: var(--fg-strong); border-bottom-color: var(--accent); }

/* Prowlarr's FieldSet/legend, measured: 20px between sections, a 21px/400 heading with
   a full-width rule under it and 21px of air below that. Sections are full width and
   stacked — the *arrs never put two of these side by side, which is why an interface
   built out of side-by-side cards reads as "not quite an *arr" however close the
   colours are. */
.section { margin-bottom: 20px; }
.section > h2 {
  font-size: 21px; font-weight: 400; color: var(--fg);
  padding-bottom: 6px; margin-bottom: 21px; border-bottom: 1px solid var(--line-2);
  display: flex; align-items: center; gap: 11px; width: 100%;
}
.section > h2 .grow { flex: 1; }

/* Toolbar of icon-over-label actions, the row *arr apps put above content. */
.toolbar {
  display: flex; align-items: stretch; gap: 4px; flex-wrap: wrap;
  padding-bottom: 14px; margin-bottom: 20px; border-bottom: 1px solid var(--line);
}
/* PageToolbarButton: a 60px square, 21px icon, 11px label under it. */
.tool {
  background: none; border: 0; cursor: pointer; color: var(--toolbar-fg, var(--fg));
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 4px 3px 0; border-radius: var(--radius);
  font-size: 11px; line-height: 1.2; text-align: center;
  min-width: 60px; min-height: 60px;   /* toolbarButtonWidth */
}
.tool:hover:not(:disabled) { background: rgba(255,255,255,.07); color: var(--fg-strong); }
.tool:disabled { opacity: .4; cursor: default; }
.tool .ti {
  font-size: var(--ic-tool); line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; height: 24px;
}
.tool.sep { border-left: 1px solid var(--line-2); margin-left: 20px; padding-left: 20px; }

.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); margin-bottom: 16px; overflow: hidden;
}
.card > header {
  padding: 12px 16px; background: var(--panel-hd);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: 10px;
}
.card > header h3 { flex: 1; }
.card > .body { padding: 16px; }
.card .body > p:first-child { margin-top: 0; }

.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px,1fr)); gap: 16px; }
.card .body { min-width: 0; }

/* ── forms ─────────────────────────────────────────────
 * Geometry from Sonarr: FormGroup.css, FormLabel.css, FormInputGroup.css, Input.css
 * and Variables/dimensions.js. Rows are horizontal with a fixed, right-aligned bold
 * label column, collapsing to stacked below the large breakpoint -- not label-above-
 * input, which is what this used to be and what made it read as "not quite an *arr".
 */
:root {
  --input-bg-c:     var(--input-bg, rgba(0,0,0,.28));
  --input-bd-c:     var(--input-border, var(--line-2));
  --input-focus-c:  var(--input-focus-border, var(--accent));
  --input-focus-sh: var(--input-focus-shadow, rgba(var(--accent-rgb), .5));
  --input-sh:       var(--input-shadow, rgba(0,0,0,.075));

  --form-label-w:   150px;  /* formLabelSmallWidth */
  --form-label-gap: 20px;   /* formLabelRightMarginWidth */
  --form-group-w:   650px;  /* formGroupSmallWidth */
  --bp-large:       1200px; /* breakpointLarge */
}

.f {
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 20px;
  max-width: var(--form-group-w);
}
.f > span:first-child {
  display: flex;
  flex: 0 0 var(--form-label-w);
  justify-content: flex-end;
  margin-right: var(--form-label-gap);
  padding-top: 8px;
  min-height: 35px;
  text-align: end;
  font-weight: bold;
  font-size: 14px;
  color: var(--fg);
}

/* Help text wraps onto its own line and lines up under the field, not the label. */
.f .help {
  flex: 0 0 100%;
  margin-top: 5px;
  margin-left: calc(var(--form-label-w) + var(--form-label-gap));
  color: var(--fg-muted);
  line-height: 20px;
  font-size: 13px;
}

@media (max-width: 1200px) {
  .f > span:first-child {
    flex: 0 0 100%;
    justify-content: flex-start;
    text-align: start;
    min-height: 0;
    padding-top: 0;
    margin: 0 0 5px;
  }
  .f .help { margin-left: 0; }
}

input[type=text], input[type=password], input[type=number], input:not([type]),
select, textarea {
  padding: 6px 16px;
  width: 100%;
  height: 35px;
  font: inherit;
  font-size: 14px;
  border: 1px solid var(--input-bd-c);
  border-radius: 4px;
  background: var(--input-bg-c);
  box-shadow: inset 0 1px 1px var(--input-sh);
  color: var(--fg-strong);
}
textarea { height: auto; min-height: 74px; resize: vertical;
           font-family: ui-monospace, Menlo, monospace; }
input:focus, select:focus, textarea:focus {
  outline: 0;
  border-color: var(--input-focus-c);
  box-shadow: inset 0 1px 1px var(--input-sh), 0 0 8px var(--input-focus-sh);
}
select option { background: var(--dropdown); }

/* Must come after the block above: same specificity, so source order decides. Without
   this the inputs keep width:100%, which gives them a full-width flex basis and wraps
   them onto their own line instead of sitting beside the label. */
.f > input, .f > select, .f > textarea {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}

/* CheckInput.css: a 20px square, not an iOS toggle. The one deliberate exception is
   Auto Rip, which is a hero control rather than a settings row. */
.switch {
  display: flex;
  align-items: flex-start;
  margin-bottom: 20px;
  cursor: pointer;
  max-width: var(--form-group-w);
}
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch .track {
  flex: 0 0 auto;
  margin-top: 7px;
  margin-right: 5px;
  width: 20px;
  height: 20px;
  border: 1px solid #ccc;
  border-radius: 2px;
  background: var(--white, #fff);
  color: #fff;
  text-align: center;
  line-height: 18px;
  font-size: 14px;
  transition: background .12s, border-color .12s;
}
.switch input:checked + .track {
  background: var(--btn);
  border-color: var(--btn);
}
.switch input:checked + .track::after { content: "✓"; display: block; }
.switch input:focus + .track {
  outline: 0;
  border-color: var(--input-focus-c);
  box-shadow: inset 0 1px 1px var(--input-sh), 0 0 8px var(--input-focus-sh);
}
.switch .lbl { min-height: 21px; padding-top: 6px; font-weight: normal; font-size: 14px; }
.switch .lbl small {
  display: block; color: var(--fg-muted); font-size: 13px;
  line-height: 20px; margin-top: 5px;
}

/* ── buttons ───────────────────────────────────────────
 * Sonarr Components/Link/Button.css plus the button tokens from dark.js.
 * Medium is 6px 16px there, which is what these use.
 */
.btn {
  display: inline-block;
  overflow: hidden;
  padding: 6px 16px;
  border: 1px solid var(--default-btn-border, var(--line-2));
  border-radius: 4px;
  background: var(--default-btn-bg, rgba(255,255,255,.06));
  color: var(--fg-strong);
  font: inherit;
  font-size: 14px;
  vertical-align: middle;
  text-align: center;
  white-space: nowrap;
  line-height: normal;
  cursor: pointer;
}
.btn:hover:not(:disabled) {
  background: var(--default-btn-hover, rgba(255,255,255,.12));
  text-decoration: none;
}
/* .65 left a disabled primary button looking blue enough to click. The one that
   matters is "Download and install" before the licence is accepted, where the
   whole point is that it does not look available yet. */
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn.primary { border-color: var(--btn); background: var(--btn); color: #fff; }
.btn.primary:hover:not(:disabled) { background: var(--btn-hover); border-color: var(--btn-hover); }

.btn.danger { border-color: var(--bad); background: var(--bad); color: #fff; }
.btn.danger:hover:not(:disabled) { filter: brightness(.92); }

/* A solid red button per row turns a list of two shares into a wall of alarm. This is
   the same action, drawn as an ordinary button until the pointer is on it -- the
   colour arrives at the moment it means something. */
.btn.quiet-danger { color: var(--fg-muted); }
.btn.quiet-danger:hover:not(:disabled) {
  border-color: var(--bad); background: var(--bad); color: #fff;
}

.btn-row { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; margin-top: 4px; }

/* Side-by-side fields have no room for a 150px label column, so they stack. */
.grid2 .f { display: block; max-width: none; }
.grid2 .f > span:first-child {
  display: block; flex: none; text-align: start;
  min-height: 0; padding-top: 0; margin: 0 0 5px;
}
.grid2 .f > input, .grid2 .f > select, .grid2 .f > textarea { width: 100%; }
.grid2 .f .help { margin-left: 0; }

/* ── tables & lists ────────────────────────────────────── */
/* Prowlarr's Table: 14px throughout, 8px cells, bold sentence-case headers. Not the
   uppercase letter-spaced micro-header this used to have -- that is a dashboard
   convention, and the *arrs deliberately do not use it. */
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th {
  text-align: left; font-weight: 700; font-size: 14px; color: var(--fg);
  padding: 8px; border-bottom: 1px solid var(--line-2); white-space: nowrap;
}
td { padding: 8px; border-bottom: 1px solid var(--line); color: var(--fg); }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: rgba(255,255,255,.035); }
th.num, td.num { text-align: right; }
/* The trailing actions column: icon buttons, right-aligned, 12px like Prowlarr's. */
th.act, td.act { text-align: right; white-space: nowrap; width: 1%; }
td.act .icon-btn { font-size: var(--ic-row); padding: 5px 6px; }
/* A leading status-icon column, e.g. the level dot on Events. */
th.stat, td.stat { width: 1%; padding-left: 12px; padding-right: 4px; }
td.stat .ic { font-size: var(--ic-nav); }

.empty-state { text-align: center; padding: 46px 20px; color: var(--fg-muted); }

/* ── the tray ──
   The disc-loaded state is the one moment this appliance has something to show off,
   so it is allowed to be the largest thing on the page: the mark turns, the label is
   set at heading size, and the drive that holds it is a quiet line underneath rather
   than a section of its own. */
.tray-loaded .big { opacity: .75; color: var(--accent); }
.tray-loaded h2 { font-size: 20px; letter-spacing: .01em; }
.tray-none .big { color: var(--bad); opacity: .5; }
.tray-none .why {
  max-width: 60ch; margin: 12px auto 0; font-size: 12.5px; line-height: 1.6;
  text-align: left; color: var(--fg-muted);
}
.tray-drive {
  margin-top: 14px; font-size: 12px; color: var(--fg-muted);
}
.tray-drive .dev { opacity: .65; margin-left: 6px; }

/* ── what the drive reads ──
   Three chips, always all three, so the unlit one is as legible as the lit ones. A
   drive that cannot do 4K has to *say* it cannot do 4K -- an absent chip reads as an
   interface that ran out of room, and this is the one capability that costs money to
   get wrong. Unlit is a visible outline, not a dimmed label. */
.drive-tags { display: inline-flex; gap: 5px; margin-left: 9px; vertical-align: middle; }
.drive-tags .tag {
  font-size: 9.5px; letter-spacing: .04em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 3px;
  border: 1px solid var(--line); color: var(--fg-muted); opacity: .7;
}
.drive-tags .tag.on {
  border-color: transparent; opacity: 1;
  background: rgba(var(--accent-rgb), .18); color: var(--accent);
}
.tray-loaded.bad .big { color: var(--warn); opacity: .6; }
.tray-loaded.bad .why {
  max-width: 56ch; margin: 10px auto 0; font-size: 12.5px; line-height: 1.6;
  color: var(--warn);
}

/* 8 seconds, not 2: at reading speed a fast spin reads as a busy indicator, and this
   is not one -- it says "there is a disc in there", which is a resting state. */
.spinning .ic { animation: disc-spin 8s linear infinite; }
@keyframes disc-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinning .ic { animation: none; } }

/* The same fact as `.tray-*`, demoted to a footer once the table above it is the
   thing worth watching. */
.tray-strip {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 14px; border-top: 1px solid var(--line);
  background: rgba(0,0,0,.14);
  font-size: 12px; color: var(--fg-muted);
}
.tray-strip .dev { opacity: .65; margin-left: 5px; }
.tray-strip .loaded { color: var(--fg-strong); }
.tray-strip.bad { color: var(--bad); }
.empty-state .big { font-size: var(--ic-empty); opacity: .3; margin-bottom: 16px;
                    display: block; line-height: 1; }
.empty-state h2 { margin-bottom: 6px; }

.rowitem {
  display: flex; align-items: center; gap: 12px; padding: 11px 14px;
  border-bottom: 1px solid var(--line); cursor: pointer;
}
.rowitem:last-child { border-bottom: 0; }
.rowitem:hover { background: rgba(255,255,255,.04); }
.rowitem.on { background: rgba(var(--accent-rgb), .14); }
.rowitem .t { color: var(--fg-strong); font-size: 13.5px; }
.rowitem .s { font-size: 11.5px; color: var(--fg-muted); }

.badge {
  font-size: 10.5px; padding: 2px 7px; border-radius: 3px;
  background: rgba(255,255,255,.1); color: var(--fg);
}
.badge.ok   { background: rgba(39,194,76,.2);  color: var(--ok); }
.badge.warn { background: rgba(255,159,26,.2); color: var(--warn); }
.badge.bad  { background: rgba(240,80,80,.2);  color: var(--bad); }
.badge.burst  { background: rgba(var(--accent-rgb),.2); color: var(--accent); }

/* ── a job in flight ──
   Not a table row: `needs_input` has to hold a question and a form, and every state
   has to hold a phase line. The phase line is the load-bearing part -- "Reading title
   0" moving under a bar that is barely moving is what tells somebody the box is
   working rather than hung, and that is the judgement the whole amber-LED problem
   comes down to. */
.job { padding: 20px 22px; border-bottom: 1px solid var(--line); }

/* Inside the disc cell the job is a column, not a full-bleed row. Left to spread, the
   title sat at one edge and its own status badge at the other with half a metre of
   nothing between them, and the progress bar ran underneath the poster. Holding the
   content to a readable measure fixes the alignment and gives the artwork the right
   half to live in. */
.disc-cell .job { max-width: min(72%, 720px); border-bottom: 0; }
@media (max-width: 900px) { .disc-cell .job { max-width: 100%; } }
.job:last-of-type { border-bottom: 0; }
.job-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.job-title { color: var(--fg-strong); font-size: 14.5px; }
.job-phase { font-size: 12px; color: var(--fg-muted); margin-top: 2px; }
.job .bar { height: 6px; }
/* ── the job, as one legible block ──────────────────────
 * This is the thing every eye is on during a manual rip, so it is laid out as a
 * hierarchy rather than a stack of small grey text: what it is, what it is doing, how
 * far, and which of the three stages is live.
 */
.job-meter { margin-top: 12px; }
.job-figs {
  display: flex; align-items: baseline; gap: 10px;
  margin-top: 6px; font-size: 12px; color: var(--fg-muted);
}
.job-pct { color: var(--fg); font-size: 13px; font-variant-numeric: tabular-nums; }
.job-eta { color: var(--fg); font-variant-numeric: tabular-nums; }
/* The learned guess is quieter than the measured elapsed time next to it, because one
   of them is a fact and the other is this box's average. */
.job-guess { color: var(--fg-muted); font-weight: 400; }

/* Three steps, always all three. The active one has to be obvious across a room --
   the previous version distinguished it with a colour change on 11.5px muted text,
   which is technically marked and practically invisible. */
.job-steps {
  display: flex; align-items: center; gap: 8px;
  margin-top: 14px; flex-wrap: wrap;
}
.step {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px 5px 9px; border-radius: 999px;
  font-size: 12.5px; line-height: 1;
  border: 1px solid transparent;
  color: var(--fg-muted); background: rgba(255,255,255,.03);
}
.step .ic { width: 13px; height: 13px; flex: none; }
.step-v { font-variant-numeric: tabular-nums; opacity: .85; }

/* Waiting: outlined dot, quiet. */
.step.todo .pip {
  width: 9px; height: 9px; border-radius: 50%;
  border: 1.5px solid currentColor; opacity: .45;
}
/* Running: filled dot, accent, and the only step that carries weight. */
.step.now {
  color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  font-weight: 500;
}
.step.now .pip {
  width: 9px; height: 9px; border-radius: 50%; background: currentColor;
  animation: step-pulse 1.6s ease-in-out infinite;
}
@keyframes step-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
/* Finished: check, settled green, no emphasis competing with the live step. */
.step.done { color: var(--ok); background: color-mix(in srgb, var(--ok) 9%, transparent); }

@media (prefers-reduced-motion: reduce) { .step.now .pip { animation: none; } }

/* ── two steps that are really one ──
 * Writing straight to the library means the bytes coming off the disc land on the
 * share as they are read: rip and upload are the same operation. Two lit pills side by
 * side would read as a bug, so they are joined into one shape and labelled "at once".
 * The pulses share a timing origin, so they breathe together rather than beating
 * against each other.
 */
.job-steps.paired .step.pair-a {
  border-top-right-radius: 0; border-bottom-right-radius: 0;
  border-right-color: transparent; padding-right: 9px;
}
.job-steps.paired .step.pair-b {
  border-top-left-radius: 0; border-bottom-left-radius: 0;
  margin-left: -8px; padding-left: 9px;
}
/* The seam: a hairline where the two pills meet, only while both are live. */
.job-steps.paired .step.pair-b.now { border-left-color: color-mix(in srgb, var(--accent) 28%, transparent); }
.job-steps.paired .step.pair-a.now { border-right-color: transparent; }
.job-steps.paired .step.pair-a .pip,
.job-steps.paired .step.pair-b .pip { animation-delay: 0s; }
.pair-note {
  font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--accent); opacity: .75; margin-left: -2px;
}

/* ── how every rip behaves ──
 * Two matched cards: the control on the top line, the consequence underneath. The same
 * shape twice is what makes a panel read as designed rather than accumulated -- these
 * settings had been bolted onto the end of the Auto Rip blurb one at a time until it
 * was a paragraph with form controls buried in it.
 */
.rip-opts {
  display: grid; gap: 12px;
  /* The Auto Rip panel above carries its own 20px, and the disc panel below is a
     .card with 16px. Sitting between two things that each space themselves, this had
     none of its own and butted straight into the rip panel. */
  margin: -4px 0 20px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.ropt {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: rgba(255,255,255,.02); padding: 13px 15px;
}
/* Label, then the button, then the control -- all on one line, so the card is two
   rows tall instead of three. The button sits *left* of the select because it is what
   you press to find out which option to pick: it reads as leading into the choice
   rather than as an afterthought under it. */
.ropt-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.ropt-k {
  display: inline-flex; align-items: center; gap: 7px; margin-right: auto;
  font-size: 12.5px; font-weight: 600; color: var(--fg-strong);
}
.ropt-k .ic { width: 14px; height: 14px; color: var(--fg-muted); }
.ropt-head select { width: auto; min-width: 190px; }
.ropt-head .btn.sm { flex: 0 0 auto; }
.ropt-why {
  margin: 9px 0 0; font-size: 12px; line-height: 18px; color: var(--fg-muted);
}
.ropt-why b { color: var(--fg); font-weight: 600; }
.ropt-warn {
  display: flex; gap: 7px; align-items: flex-start;
  margin: 9px 0 0; padding: 7px 9px; border-radius: var(--radius);
  background: rgba(255,159,26,.09); color: var(--fg);
  font-size: 12px; line-height: 18px;
}
.ropt-warn .ic { color: var(--warn); flex: 0 0 auto; margin-top: 2px; width: 13px; height: 13px; }
/* Only takes space once it has something to say. */
.ropt-out { display: block; margin-top: 8px; line-height: 18px; }
.ropt-out:empty { display: none; }
@media (max-width: 620px) {
  .ropt-k { margin-right: 0; flex: 1 1 100%; }
  .ropt-head select { width: 100%; }
}

/* ── the rips gallery ───────────────────────────────────
 * A shelf of films, not a spreadsheet. The poster is the index; the words are the
 * detail you look for second. Tiles are a fixed 2:3 so a wall of them lines up
 * whatever mix of artwork and disc-icon fallbacks it contains.
 */
.rips {
  display: grid; gap: 20px 18px;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.rip { margin: 0; min-width: 0; }
.rip-art {
  position: relative; aspect-ratio: 2 / 3; border-radius: var(--radius);
  background: var(--panel) center/cover no-repeat;
  border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  transition: transform .18s ease, border-color .18s ease;
}
.rip-art.has { border-color: transparent; }
.rip:hover .rip-art { transform: translateY(-2px); border-color: var(--accent); }
/* The disc icon is the resting state, so a film we could not identify is a plain tile
   rather than a broken image. It goes when a poster lands on top of it. */
.rip-fallback { color: var(--fg-muted); opacity: .35; }
.rip-fallback .ic { width: 42px; height: 42px; }
.rip-art.has .rip-fallback { display: none; }
.rip-flag {
  position: absolute; top: 7px; right: 7px; color: var(--warn);
  background: rgba(0,0,0,.55); border-radius: 50%; padding: 4px; line-height: 0;
}
.rip figcaption { margin-top: 9px; }
.rip-title {
  color: var(--fg-strong); font-size: 13px; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.rip-meta { margin-top: 3px; font-size: 11.5px; color: var(--fg-muted); }
.rip-meta .bad { color: var(--bad, #e05c5c); }
/* ── MakeMKV's own websites ──
 * Two hosts that fail independently, and the difference between them decides whether
 * the user is stuck or one copy-and-paste from fixed. So each row says what its being
 * down actually costs, rather than showing a red dot and leaving them to guess.
 */
.sites { display: grid; gap: 10px; margin: 4px 0 14px; }
/* The panel before the probe has answered. Same vertical space the two rows will take,
   so the button below it does not jump when the answer lands. */
.sites-wait {
  display: flex; align-items: center; gap: 10px;
  min-height: 76px; margin: 4px 0 14px; padding: 0 2px;
  font-size: 13px; line-height: 19px; color: var(--fg-muted);
}
.site {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel);
}
.site.up   { border-left: 3px solid var(--ok, #27c24c); }
.site.down { border-left: 3px solid var(--warn, #ff9f1a); }
.site-dot { line-height: 0; padding-top: 2px; }
.site.up .site-dot   { color: var(--ok, #27c24c); }
.site.down .site-dot { color: var(--warn, #ff9f1a); }
.site-name { font-size: 14px; color: var(--fg-strong); }
.site-name a { color: var(--fg-strong); }
.site-name a:hover { color: var(--accent); }
.site-state { font-size: 12px; color: var(--fg-muted); margin-left: 8px; font-weight: 400; }
.site-why { font-size: 12.5px; color: var(--fg-muted); margin-top: 3px; line-height: 19px; }
.site-do {
  display: flex; gap: 7px; margin-top: 7px; font-size: 12.5px; line-height: 19px;
  color: var(--fg); background: rgba(255,159,26,.09);
  border-radius: var(--radius); padding: 8px 10px;
}
.site-do .ic { color: var(--warn); flex: 0 0 auto; margin-top: 2px; }
.site .btn { flex: 0 0 auto; align-self: center; }
.site-note { margin-top: 12px; display: flex; gap: 7px; align-items: flex-start;
             color: var(--fg-muted); font-size: 12.5px; line-height: 19px; }
.site-note .ic { flex: 0 0 auto; margin-top: 2px; }
@media (max-width: 700px) {
  .site { flex-wrap: wrap; }
  .site .btn { width: 100%; margin-top: 8px; }
}

/* ── still crossing the network ──
 * The disc is already back in the user's hand and the next one may be spinning. These
 * are finished as far as anyone is concerned; they just are not *there* yet. So: a
 * quiet strip under the drive, not a panel competing with the disc in the machine.
 */
.sending {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: rgba(255,255,255,.02); padding: 11px 14px; margin-bottom: 16px;
}
.sending-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--fg-strong); margin-bottom: 9px;
}
.sending-head .ic { width: 14px; height: 14px; color: var(--accent); }
.sending-head .muted { font-size: 11.5px; font-weight: 400; }
.send-row {
  display: flex; align-items: center; gap: 10px;
  padding: 5px 0; font-size: 12px;
}
.send-name {
  color: var(--fg); flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 26ch;
}
.send-bar {
  flex: 1 1 90px; height: 5px; border-radius: 3px;
  background: var(--panel); overflow: hidden; min-width: 60px;
}
.send-bar i {
  display: block; height: 100%; background: var(--accent);
  transition: width .6s linear;
}
.send-pct { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--fg-muted);
            min-width: 4.5ch; text-align: right; }
.send-size { flex: 0 0 auto; font-size: 11.5px; }
.send-row .icon-btn { flex: 0 0 auto; font-size: var(--ic-row); padding: 3px 5px; }
@media (max-width: 560px) {
  .send-size { display: none; }
  .send-name { max-width: 14ch; }
}

/* ── which disc is this ──
 * The same chip in the same three colours on the Queue, on History and on a poster.
 * Two copies of one film -- the DVD and the Blu-ray -- are the case this exists for,
 * and they are only distinguishable if the answer looks identical everywhere.
 */
.fam {
  display: inline-block; vertical-align: middle;
  font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 3px; margin-left: 8px; line-height: 1.5;
  border: 1px solid transparent; white-space: nowrap;
}
.fam-dvd    { color: #d8c07a; border-color: rgba(216,192,122,.45); background: rgba(216,192,122,.12); }
.fam-bluray { color: #7fb2f0; border-color: rgba(127,178,240,.45); background: rgba(127,178,240,.12); }
.fam-uhd    { color: #c79bf0; border-color: rgba(199,155,240,.45); background: rgba(199,155,240,.14); }
/* On a poster it has to survive whatever is behind it. */
.fam.on-art {
  position: absolute; top: 7px; left: 7px; margin: 0;
  background: rgba(0,0,0,.68); backdrop-filter: blur(2px);
}

/* ── "you already ripped that" ──
 * The disc goes in, ten seconds of noise happen, the disc comes out. Without this the
 * box has done something deliberate and looks like it has malfunctioned.
 */
.dupe-note {
  display: flex; align-items: flex-start; gap: 16px; padding: 18px 20px;
  margin-bottom: 20px;
  border-left: 3px solid var(--accent);
}
.dupe-note h2 { font-size: 16px; color: var(--fg-strong); margin: 0 0 6px; }
.dupe-note p { margin: 0 0 5px; font-size: 13px; line-height: 20px; }
.dupe-note p:last-child { margin-bottom: 0; }
.dupe-ic { color: var(--accent); opacity: .8; line-height: 0; padding-top: 2px; }
.dupe-ic .ic { width: 26px; height: 26px; }

/* Three pulses, then it stops. A tile that flashes forever becomes furniture. */
.rip.dupe .rip-art {
  animation: dupe-pulse 1.1s ease-in-out 4;
  border-color: var(--accent);
}
@keyframes dupe-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(93, 156, 236, 0); }
  50%      { box-shadow: 0 0 0 5px rgba(93, 156, 236, .55); }
}
@media (prefers-reduced-motion: reduce) {
  /* Still findable, just not moving. */
  .rip.dupe .rip-art { animation: none; box-shadow: 0 0 0 3px rgba(93, 156, 236, .55); }
}

.rip-acts { display: flex; gap: 6px; margin-top: 8px; }
.rip-acts .btn { flex: 1; }
@media (prefers-reduced-motion: reduce) { .rip:hover .rip-art { transform: none; } }

/* ── History: the data table ───────────────────────────────
 * Deliberately plain. Every column answers one question a person actually asks about
 * a rip that has already happened, and the stage bar answers the one they cannot ask
 * because they do not know it is askable: where did the half hour go.
 */
.hist-table td { vertical-align: top; }
/* "try 7 of 7", "32 MiB", "24 min ago" are short and must never break in half. Once
   they wrap, the row grows a line and the table reads as damaged rather than dense. */
.hist-table td.num, .hist-table th.num { white-space: nowrap; }
.hist-table tr.hist td { border-bottom: 0; padding-bottom: 4px; }
.hist-dest td { padding-top: 0; font-size: 12px; }
.hist-dest .badge { margin-left: 8px; }
.hist-title { color: var(--fg-strong); }
.hist-sub { font-size: 12px; color: var(--fg-muted); margin-top: 2px; }
.hist-err { font-size: 12px; color: var(--bad, #e05c5c); margin-top: 3px; max-width: 46ch; }
tr.hist.done > td.stat { color: var(--ok, #27c24c); }
tr.hist.failed > td.stat { color: var(--bad, #e05c5c); }
tr.hist.cancelled > td.stat { color: var(--fg-muted); }
/* Buttons line up with the title, not with the middle of a row whose height is set
   by an error message underneath it. */
.hist-table .act { vertical-align: top; padding-top: 6px; }
.hist-table .act .btn { margin-left: 6px; }
.btn.tiny { font-size: 11.5px; padding: 4px 8px; }
.btn.tiny .ic { font-size: 11px; margin-right: 4px; }

/* One stage, one colour, used identically in the bar, the numbers under it and the
   key in the header -- so the wide band a user asks about is findable. */
/* Capped, not stretched: the bar is a proportion, and a proportion drawn across
   half the page reads as a progress bar for something still happening. */
.stagebar { display: flex; height: 7px; border-radius: 4px; overflow: hidden;
            width: 220px; max-width: 100%; background: var(--panel); }
.stagebar i { display: block; min-width: 2px; }
.stagenums { display: flex; gap: 9px; margin-top: 4px; font-size: 11px;
             color: var(--fg-muted); flex-wrap: wrap; }
.stagenums i, .sk i { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
                      margin-right: 4px; vertical-align: -1px; }
.sg-identify { background: #7d8cc4; }
.sg-decrypt  { background: #c48f7d; }
.sg-save     { background: #5d9cec; }
.sg-upload   { background: #27c24c; }
.sg-verify   { background: #d2b04c; }

/* The key, in the page header. Always on screen, because it is what makes every bar
   on the page readable. */
.stage-key { display: flex; gap: 14px; flex-wrap: wrap; justify-content: flex-end;
             max-width: 440px; }
.sk { font-size: 11.5px; color: var(--fg-muted); white-space: nowrap;
      display: inline-flex; align-items: baseline; gap: 3px; cursor: default; }
.sk b { color: var(--fg); font-weight: 600; }
.sk .sk-t { font-variant-numeric: tabular-nums; }
/* A stage with no median yet is still in the key -- the colour has to be readable
   before there is any history -- but it does not pretend to a number. */
.sk.unknown { opacity: .55; }
.stage-note { margin-top: 14px; font-size: 12.5px; max-width: 78ch; }

/* One row per kind of disc, because that is the unit the numbers are true for. */
.stage-key-full { margin-top: 16px; padding: 16px 18px; }
.stage-key-full h3 { font-size: 14px; font-weight: 700; color: var(--fg-strong);
                     margin: 0 0 12px; text-transform: none; letter-spacing: 0; }
.kind-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
            padding: 7px 0; border-top: 1px solid var(--line); }
.kind-row:first-of-type { border-top: 0; }
.kind-row .fam { margin-left: 0; }
.kind-times { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px;
              color: var(--fg); font-variant-numeric: tabular-nums; }
.kind-times i { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
                margin-right: 5px; vertical-align: -1px; }

/* ── History on a narrow screen ──
 * Eight columns cannot survive a phone, and a table that scrolls sideways is a table
 * nobody reads the right-hand half of. Below 900px each attempt becomes a card: the
 * title and its error at the top, the four figures as a labelled wrapping row, the
 * stage bar full width because it is the reason to be here, then the retries.
 */
/* 1100, not 900: eight columns plus two buttons stop fitting well before a phone.
   Between those widths the table technically rendered, with every short figure broken
   across two lines -- cards are simply the better layout by then. */
@media (max-width: 1100px) {
  .hist-table, .hist-table tbody, .hist-table tr, .hist-table td { display: block; }
  .hist-table thead { display: none; }
  /* Flex, not grid: the four figures have to share one *wrapping* line, and a grid
     gives each of them a row of its own. (Floats are no help either -- a grid item's
     float is ignored outright, which is how the first attempt at this went wrong.) */
  .hist-table tr.hist {
    padding: 12px 2px 4px; border-top: 1px solid var(--line);
    display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 10px;
  }
  .hist-table tbody tr.hist:first-child { border-top: 0; }
  .hist-table tr.hist > td { padding: 0; border: 0; }
  .hist-table tr.hist > td.stat { flex: 0 0 auto; width: auto; }
  /* The title takes the whole of line one and pushes the four figures onto line two.
     Sharing a line with them squeezed it to about sixty pixels, which turned a film
     title into two lines and an error message into a one-word-per-line ribbon. */
  .hist-table tr.hist > td.hist-name { flex: 1 0 calc(100% - 32px); min-width: 0; }
  .hist-table tr.hist > td.num {
    flex: 0 0 auto; width: auto;
    display: inline-flex; align-items: baseline; gap: 5px;
    font-size: 12px; margin: 9px 8px 0 0; text-align: left;
  }
  .hist-table tr.hist > td[data-label].num::before {
    content: attr(data-label); color: var(--fg-muted); font-size: 11px;
  }
  /* The stage bar and the buttons each get a full line of their own, *after* all four
     figures -- `When` comes last in the markup, so without this it would be stranded
     below the bar instead of sitting with the other three. */
  .hist-table tr.hist > td.hist-stages { flex: 1 0 100%; padding-top: 10px; order: 1; }
  .hist-table tr.hist > td.hist-stages::before {
    content: attr(data-label); display: block; color: var(--fg-muted);
    font-size: 11px; margin-bottom: 4px;
  }
  /* Full width on a phone, capped on a tablet -- a proportion drawn across 780px of
     empty space starts to read as a progress bar for something still running. */
  .stagebar { width: 100%; max-width: 460px; }
  .hist-table tr.hist > td.act { flex: 1 0 100%; text-align: left; padding-top: 10px;
                                 order: 2; }
  .hist-table .act .btn { margin: 0 6px 6px 0; }
  .hist-table tr.hist-dest { padding: 0 2px 12px 0; }
  .hist-table tr.hist-dest > td:first-child { display: none; }
  .hist-table tr.hist-dest > td { padding: 0; border: 0; word-break: break-all; }
  .hist-err { max-width: none; }
  /* The key stops being a right-aligned toolbar and becomes a normal wrapping row. */
  .page-head { flex-wrap: wrap; }
  .stage-key { max-width: none; justify-content: flex-start; margin-left: 0; gap: 10px 14px; }
  .head-acts { margin-left: 0; width: 100%; }
}
@media (max-width: 520px) {
  .sk .sk-t { display: none; }        /* colour and name survive; the median can go */
}

/* The counting timer on an in-flight job. Sits under the stepper, because it is the
   answer to "is this thing stuck", and that question is asked while looking there. */
.clock {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-top: 10px; font-size: 12px; color: var(--fg-muted);
}
.clock-stage { color: var(--fg-strong); font-weight: 600; }
.clock-stage i { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
                 margin-right: 6px; }
.clock-el { font-variant-numeric: tabular-nums; }
.clock-est b { color: var(--fg); font-weight: 600; }
.clock.over .clock-est { color: var(--warn); }
.clock-bar { height: 3px; border-radius: 2px; background: var(--panel);
             margin-top: 5px; overflow: hidden; }
.clock-bar i { display: block; height: 100%; opacity: .8;
               transition: width .6s linear; }

/* The verify choice used to live here; both it and the route now sit in .rip-opts
   below the switch. This rule is kept for the row that still uses it. */
.ar-verify {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 10px; font-size: 12px; color: var(--fg-muted);
}
.ar-verify select { height: 26px; padding: 0 8px; font-size: 12px; max-width: none; width: auto; }
.ar-verify-note { flex: 1 1 240px; min-width: 0; }

.job.needs { background: rgba(255,159,26,.06); border-left: 2px solid var(--warn); }

/* Non-fatal, and it must not look fatal: the rip is running and the user is being
   told something about it, which is a different sentence from "this failed". */
.job-warn {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 10px; padding: 8px 10px; border-radius: 3px;
  background: rgba(255,159,26,.09); color: var(--warn);
  font-size: 12px; line-height: 1.5;
}
.job-warn .ic { flex: none; margin-top: 1px; }
.job.needs .f { margin-top: 4px; }
/* The *arr form geometry -- a fixed 150px right-aligned label column -- is right for a
   settings page and wrong for a focused question, which is the same complaint the
   backlog already records about the share step. Here the label stacks. */
.f.wide { max-width: none; display: block; }
.f.wide > span:first-child {
  display: block; flex: none; justify-content: flex-start; text-align: start;
  margin-right: 0; padding-top: 0; min-height: 0; margin-bottom: 5px;
  font-size: 13px;
}
.f.wide > input { max-width: 420px; width: 100%; }
.f.wide .help { margin-left: 0; display: block; margin-top: 5px; }

.ni-titles { margin: 12px 0 14px; }
.ni-label { font-size: 12px; color: var(--fg-muted); margin-bottom: 6px; }
.ni-title {
  display: grid; grid-template-columns: auto 68px 1fr auto; gap: 10px;
  align-items: center; padding: 6px 8px; border-radius: var(--radius);
  cursor: pointer; font-size: 13px;
}
.ni-title:hover { background: rgba(255,255,255,.04); }
.ni-title .ni-dur { color: var(--fg-strong); font-variant-numeric: tabular-nums; }
.ni-title .ni-size { font-size: 11.5px; }
/* ── the episode plan ──
   A table, because the question is "is this the right order" and that is not a question
   a radio group can ask. Each row is one file that will be written, and the number in
   the first column is what it will be called -- it renumbers live as rows are moved or
   unticked, so what is on screen is always what would be written.

   The grid is fixed-width except for the episode name, which takes the slack: names run
   from "Pilot" to "The One Where Everybody Finds Out", and a column that resizes with
   them makes every row jump when one arrives. */
.ep-warnings {
  margin: 10px 0 14px; padding: 10px 12px 10px 26px; border-radius: var(--radius);
  background: rgba(255,159,26,.09); border: 1px solid rgba(255,159,26,.22);
  font-size: 12.5px; color: var(--fg); line-height: 1.5;
}
.ep-warnings li + li { margin-top: 5px; }
.ep-controls { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 12px; }
.ep-controls .f { max-width: 260px; }
.ep-controls select { margin-bottom: 6px; }
.ep-trust { display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
            font-size: 12px; flex-wrap: wrap; }
.ep-table { display: grid; gap: 2px; margin-bottom: 14px; }
.ep-row {
  display: grid; gap: 10px; align-items: center; padding: 5px 8px;
  grid-template-columns: auto 96px 62px minmax(0, 1fr) auto auto;
  border-radius: var(--radius); font-size: 13px;
}
.ep-row:hover { background: rgba(255,255,255,.04); }
.ep-row.dropped { opacity: .45; }
.ep-row.dropped .ep-title { text-decoration: line-through; }
.ep-num { color: var(--fg-strong); font-variant-numeric: tabular-nums;
          font-weight: 500; }
.ep-dur { color: var(--fg-muted); font-variant-numeric: tabular-nums; }
.ep-title { width: 100%; min-width: 0; }
.ep-src { font-size: 11.5px; white-space: nowrap; }
.ep-move { display: flex; gap: 3px; }
.ep-move .btn.tiny { padding: 2px 7px; line-height: 1.2; }
.ep-credit { font-size: 11.5px; }
.ep-credit a { color: inherit; }
.season-tag {
  font-size: 11.5px; font-weight: 500; color: var(--fg-muted);
  font-variant-numeric: tabular-nums; margin-left: 6px; white-space: nowrap;
}

/* Below this the six columns stop fitting and the row wraps into a block: the number
   and runtime on one line, the name under them. Nothing is hidden -- a phone is where
   somebody checks a box set they are feeding disc by disc. */
@media (max-width: 720px) {
  .ep-row { grid-template-columns: auto 96px 62px auto; row-gap: 4px; }
  .ep-title { grid-column: 1 / -1; }
  .ep-src { display: none; }
}

.notify-events { display: grid; gap: 2px; margin-bottom: 4px; }

/* ── where each kind of disc goes ──
   Two blocks, each pairing a share with a folder and showing the path they add up to.
   The path line is the load-bearing part: "share" and "folder" only mean anything
   together, and seeing the whole thing is how somebody notices they have pasted a
   whole path into the share box -- the mistake that produces an SMB logon failure and
   sends them off to check a password that was never wrong. */
.dests { display: grid; gap: 16px; }
.dest {
  border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 14px 16px 4px;
}
.dest-h {
  font-size: 14px; font-weight: 700; color: var(--fg-strong);
  margin-bottom: 12px;
}
.dest-f { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0 16px; }
.dest-f .f { display: block; max-width: none; }
.dest-f .f > span:first-child {
  display: block; flex: none; text-align: start;
  min-height: 0; padding-top: 0; margin: 0 0 5px;
}
.dest-f .f > input, .dest-f .f > select { width: 100%; }
.dest-path {
  font-family: ui-monospace, Menlo, monospace; font-size: 12.5px;
  color: var(--fg-strong); background: rgba(0,0,0,.22);
  border-radius: var(--radius); padding: 8px 11px;
  word-break: break-all; margin-bottom: 10px;
}
.dest-note {
  font-size: 12.5px; line-height: 18px; color: var(--fg-muted);
  padding-bottom: 12px;
}
.dest-note code { font-size: 12px; }

.shares { border: 1px solid var(--line-2); border-radius: var(--radius);
          overflow: hidden; margin-bottom: 14px; }
.shares .rowitem { cursor: default; }
.share-add {
  margin-top: 4px; padding: 16px 16px 6px;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  background: rgba(255,255,255,.03);
}
.share-add h3 { font-size: 15px; color: var(--fg-strong); margin-bottom: 6px; }
.share-add .muted { margin-bottom: 16px; }
.share-add .result .why { white-space: pre-wrap; }
/* The shares a server listed, as buttons that fill in the Share field. */
.share-picks { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.sf-listed { margin: 4px 0 14px; }

/* ── notification channels ──
   Four rows, each wearing the mark of the service it configures, each opening onto
   its own setup instructions. Two things carry the state and neither is a word at the
   far end of the row: the mark is full brand colour when the channel is set up and
   grey when it is not, and a set-up one carries a tick on its corner. The eye lands
   on the mark first, which is the point of using the real mark at all.

   Brand hexes are the projects' own (Simple Icons): Discord #5865F2, ntfy #317F6F.
   ntfy's green is lifted on dark surfaces -- the published value is chosen for white
   paper and reads as near-black against #1f1f1f. */
.channels {
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  overflow: hidden;
}
.chan + .chan { border-top: 1px solid var(--line-2); }
.chan-head {
  display: flex; align-items: center; gap: 14px; width: 100%;
  padding: 13px 16px; text-align: left; cursor: pointer;
  background: none; border: 0; color: inherit; font: inherit;
}
.chan-head:hover { background: rgba(255,255,255,.04); }
.chan-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.chan.open > .chan-head { background: rgba(255,255,255,.05); }

.chan-ic {
  position: relative; flex: 0 0 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px; font-size: 21px;
  background: rgba(255,255,255,.06); color: var(--fg-muted);
}
.chan.on .chan-ic { background: rgba(255,255,255,.09); }
.chan.on .chan-ntfy    { color: #4ec9ae; }   /* #317F6F, lifted for a dark surface */
.chan.on .chan-discord { color: #5865f2; }
.chan.on .chan-email   { color: var(--accent); }
.chan.on .chan-webhook { color: var(--accent); }

/* The tick sits on the mark, not beside it, so "is this one done" is answered in the
   same glance that identifies which one it is. The ring is the panel colour so the
   badge reads as on top of the tile rather than clipped into it. */
.chan-tick {
  position: absolute; right: -5px; bottom: -5px;
  font-size: 14px; line-height: 0; color: var(--ok);
  background: var(--bg); border-radius: 50%; padding: 2px;
}
.chan-txt { flex: 1; min-width: 0; display: block; }
.chan-name { display: block; font-size: 14.5px; font-weight: 600; color: var(--fg-strong); }
.chan-blurb {
  display: block; margin-top: 2px; font-size: 12.5px; line-height: 17px;
  color: var(--fg-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chan-state {
  flex: 0 0 auto; font-size: 11px; letter-spacing: .3px;
  padding: 3px 8px; border-radius: 3px;
  background: rgba(255,255,255,.08); color: var(--fg-muted);
}
.chan.on .chan-state { background: rgba(39,194,76,.18); color: var(--ok); }
.chan-caret {
  flex: 0 0 14px; font-size: 12px; color: var(--fg-muted);
  transition: transform .15s ease;
}
.chan.open .chan-caret { transform: rotate(180deg); }

/* The list spans the section like every other list on a settings page; the *fields*
   inside a panel keep the 650px form-group width, so a text input is the same width
   here as it is on any other settings page rather than stretching to the window. */
.chan-body { padding: 4px 16px 2px; border-top: 1px solid var(--line); }
.chan-body .steps { margin-top: 14px; }
/* Line the buttons up with the fields rather than the panel edge. A test button sitting
   under a right-aligned label column, at the far left, reads as belonging to the panel
   rather than to the form above it. Same indent #w-detail uses, and dropped at the same
   breakpoint where the labels stop being a column. */
.chan-body .btn-row {
  margin-bottom: 16px;
  margin-left: calc(var(--form-label-w) + var(--form-label-gap));
}
@media (max-width: 1200px) { .chan-body .btn-row { margin-left: 0; } }
/* ...unless the panel used a stacked two-column grid, where the fields start at the
   panel edge and there is no label column to line up with. Email is the one that does. */
.chan-body .grid2 ~ .btn-row { margin-left: 0; }

@media (max-width: 620px) {
  .chan-head { gap: 11px; padding: 11px 12px; }
  .chan-state { display: none; }
  .chan-body { padding: 4px 12px 2px; }
}

/* A short JSON sample inside a step: a fenced block, not an inline run of code. */
code.block {
  display: block; margin-top: 6px; padding: 8px 11px;
  background: rgba(0,0,0,.25); border-radius: var(--radius);
  font-family: ui-monospace, Menlo, monospace; font-size: 12px; line-height: 18px;
  color: var(--fg); white-space: pre-wrap; word-break: break-word;
}

/* ── the Discord walkthrough ──
   A webhook URL is four clicks deep in somebody else's app, and the docs page for it
   is written for people building bots. Numbered steps in the product beat a link out
   of it, and the fourth step -- the user ID -- is the one nobody would have found. */
.steps { margin: 4px 0 16px; padding-left: 22px; max-width: 640px; }
.steps li { margin-bottom: 9px; line-height: 20px; font-size: 13px;
            color: var(--fg-muted); }
.steps li b { color: var(--fg); font-weight: 600; }
.steps li::marker { color: var(--accent); font-weight: 700; }
.dc-when { margin: 14px 0 4px; padding-left: calc(var(--form-label-w) + var(--form-label-gap)); }
.dc-when-l { font-weight: 700; font-size: 14px; margin-bottom: 6px; }
.dc-when .help { color: var(--fg-muted); font-size: 13px; line-height: 20px;
                 margin-top: 6px; }
@media (max-width: 1200px) { .dc-when { padding-left: 0; } }
.test-out { font-size: 12.5px; color: var(--fg-muted); }
.test-out.ok { color: var(--ok); }
.test-out.bad { color: var(--bad); }
/* "There is no LED wired up" is neither success nor failure — it is the answer. */
.test-out.warn { color: var(--warn); }
.warn-text { color: var(--warn); }
.btn.sm { font-size: 11.5px; padding: 4px 10px; }

.tray-go { justify-content: center; margin-top: 16px; }
.tray-go .btn { font-size: 14px; padding: 9px 20px; }
.tray-go .ic { margin-right: 7px; }

/* A path is the one thing on the History page somebody retypes into Plex, so it wraps
   rather than being clipped, and sets in a face where l/1 and 0/O differ. */
td.path {
  font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 11.5px; color: var(--fg-muted); word-break: break-all; max-width: 340px;
}

.bar { height: 5px; border-radius: 3px; background: rgba(255,255,255,.12); overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--queue); border-radius: 3px;
           transition: width .4s ease; }

/* Work with nothing to measure yet -- reading the disc, or ripping before MakeMKV has
   emitted its first progress line. A bar frozen at zero is indistinguishable from a
   box that has hung, which is the impression that makes people pull the cable. This
   says "moving, but I cannot tell you how far" instead. */
.bar.working { position: relative; }
.bar.working > i {
  width: 34% !important; transition: none;
  animation: bar-sweep 1.4s ease-in-out infinite;
}
@keyframes bar-sweep {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(330%); }
}
@media (prefers-reduced-motion: reduce) {
  .bar.working > i { animation: none; width: 100% !important; opacity: .35; }
}
.bar.dual > i.sent { background: var(--ok); }

/* DescriptionList: a 160px right-aligned bold label column with the value at 180px,
   exactly as Prowlarr's System > Status "About" block does it. Grid rather than
   float/margin, because grid degrades to a stack on narrow screens for free. */
.kv {
  display: grid; grid-template-columns: 160px 1fr; column-gap: 20px; row-gap: 0;
  font-size: 14px; margin-bottom: 10px;
}
.kv .k {
  color: var(--fg); font-weight: 700; text-align: right;
  overflow-wrap: anywhere; line-height: 21px;
}
.kv .v {
  color: var(--fg); overflow-wrap: anywhere; min-width: 0; line-height: 21px;
}

/* ── sign in ───────────────────────────────────────────── */
.gate {
  min-height: 100vh; display: grid; place-items: center;
  background: var(--bg); background-attachment: fixed; padding: 20px;
}
.gate-card {
  width: 100%; max-width: 340px; background: var(--panel);
  border: 1px solid var(--line); border-radius: 6px; padding: 30px 26px;
  text-align: center; box-shadow: 0 14px 44px rgba(0,0,0,.4);
}
.gate-mark { width: 64px; height: 64px; display: block; margin: 0 auto; }
.gate-card h1 {
  font-family: "Russo One", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 25px; font-weight: 400; letter-spacing: .045em;
  margin: 12px 0 3px;
}
/* The gate is a narrow card, not a settings page. The *arr form geometry — a fixed
   150px right-aligned label column — has nowhere to go in 288px of content, so it
   wraps the input onto its own line while the label keeps its 150px box and stays
   flush to the right of it. The result reads as neither left-aligned nor centred.
   Stack it instead: label above input, both flush left with the field. */
.gate-card .f {
  display: block; max-width: none; text-align: left; margin-top: 16px;
  margin-bottom: 0;
}
.gate-card .f > span:first-child {
  display: block; flex: none;
  text-align: left; justify-content: flex-start;
  margin-right: 0; margin-bottom: 6px;
  padding-top: 0; min-height: 0;
}
.gate-card input { max-width: none; width: 100%; }
.gate-err { color: var(--bad); font-size: 12.5px; min-height: 18px; margin: 8px 0; }

/* ── first-run wizard ──────────────────────────────────── */
.wizard {
  min-height: 100vh; background: var(--bg); background-attachment: fixed;
  display: grid; grid-template-rows: auto 1fr;
}
.wz-head {
  padding: 26px 30px 0; max-width: 780px; margin: 0 auto; width: 100%;
}
.wz-rail { display: flex; gap: 6px; margin-top: 18px; }
.wz-rail div {
  flex: 1; height: 3px; border-radius: 2px; background: rgba(255,255,255,.13);
}
.wz-rail div.done { background: var(--accent); }
.wz-body { max-width: 780px; margin: 0 auto; width: 100%; padding: 24px 30px 60px; }
.wz-step { font-size: 11.5px; text-transform: uppercase; letter-spacing: .09em;
           color: var(--fg-muted); }
.wz-body h1 { margin-bottom: 6px; }
.wz-actions {
  display: flex; gap: 10px; align-items: center;
  margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--line);
}

/* Prowlarr's Alert: a full-width 15px panel with a 1px tinted border, used for the
   standing explanatory note at the top of a page (where the log files live, what the
   health icons mean). Distinct from .result, which reports the outcome of an action. */
.alert {
  border: 1px solid var(--alert-bd, #31708f);
  background: var(--alert-bg, #253d50);
  color: var(--fg);
  border-radius: var(--radius); padding: 15px; margin: 5px 0 20px;
  font-size: 14px; line-height: 1.5;
}
.alert.warn { --alert-bd: #ec971f; --alert-bg: #3d3223; }
.alert.bad  { --alert-bd: #d43f3a; --alert-bg: #442726; }
.alert p + p { margin-top: 4px; }

.result {
  border-radius: var(--radius); padding: 12px 14px; font-size: 13px;
  margin-top: 14px; border: 1px solid transparent;
}
.result.ok  { background: rgba(39,194,76,.12);  border-color: rgba(39,194,76,.3);  color: var(--ok); }
.result.bad { background: rgba(240,80,80,.12);  border-color: rgba(240,80,80,.3);  color: var(--bad); }
.result.busy{ background: rgba(255,255,255,.05); border-color: var(--line-2); color: var(--fg); }
.result b { display: block; color: inherit; margin-bottom: 3px; }
.result .why { color: var(--fg-muted); font-size: 12px; margin-top: 5px;
               font-family: ui-monospace, Menlo, monospace; word-break: break-word; }

.spin {
  width: 13px; height: 13px; border-radius: 50%; display: inline-block;
  border: 2px solid rgba(255,255,255,.22); border-top-color: var(--accent);
  animation: spin .7s linear infinite; vertical-align: -2px; margin-right: 7px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── toasts ────────────────────────────────────────────── */
.toast-host {
  position: fixed; right: 18px; bottom: 18px; z-index: 90;
  display: flex; flex-direction: column; gap: 8px;
}
.toast {
  background: var(--dropdown); border: 1px solid var(--line-2);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: 11px 15px; font-size: 13px;
  color: var(--fg-strong); box-shadow: 0 8px 26px rgba(0,0,0,.45);
  animation: slidein .22s ease;
}
.toast.bad { border-left-color: var(--bad); }
.toast.ok  { border-left-color: var(--ok); }
@keyframes slidein { from { opacity: 0; transform: translateX(16px); } }

/* ── responsive ────────────────────────────────────────── */
@media (max-width: 820px) {
  .hamburger { display: block; }
  .sidebar { transform: translateX(-100%); transition: transform .2s ease; }
  .sidebar.open { transform: none; }
  .content { margin-left: 0; padding: 18px 16px 50px; }
  .kv { grid-template-columns: 1fr; gap: 2px 0; }
  .kv .k { margin-top: 9px; text-align: left; }
  /* The search box has a 260px minimum, which on a narrow screen pushed the account
     and coffee buttons off the right edge -- reachable only by scrolling a fixed
     header sideways, which is to say not reachable. Let it collapse instead. */
  .search { margin-left: 8px; min-width: 0; flex: 1 1 auto; }
  .head-acts { gap: 0; }
}
@media (max-width: 520px) { .search { display: none; } }

/* Shown while the service is still coming up. A login form is the wrong answer to
   "cannot reach the server" — it implies an account exists and invites the user to
   guess at credentials that were never created. */
.gate-wait { display: grid; gap: 14px; place-items: center; padding: 8px 0 2px; }
.gate-wait .spin {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent);
  animation: gate-spin .9s linear infinite;
}
@keyframes gate-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .gate-wait .spin { animation: none; } }
.gate-wait p { color: var(--fg-muted); font-size: 13px; margin: 0; }

/* Manual server entry, always present rather than only as a consolation prize when
   discovery finds nothing. A server that does not advertise itself is not a server
   that does not exist — and discovery cannot see another subnet at all. */
.manual-row {
  display: flex; gap: 8px; align-items: center;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line);
}
.manual-row input { flex: 1; max-width: none; }

/* ── wizard: the share detail panel ─────────────────────
 * This is the only .card in the app that holds a plain form rather than a table or
 * an empty state, so it is the only place `.card > .body`'s padding has to carry the
 * whole layout -- and the markup used to omit `.body` entirely, leaving the form
 * flush against the card border.
 *
 * Spacing here comes from one rule rather than from each control's own bottom
 * margin, which is what the stray inline `margin:-4px 0 14px` was cancelling out.
 * An empty result slot must not leave a gap at the foot of the card.
 */
.sf-detail .card > .body > * { margin-top: 0; margin-bottom: 0; }
.sf-detail .card > .body > * + * { margin-top: 18px; }
.sf-detail .card > .body > .sf-testres:empty { margin-top: 0; }
/* Buttons belong to the input column, not the label column -- the same offset
   `.f .help` already uses, and dropped at the same breakpoint where labels stack. */
.sf-detail .btn-row { margin-left: calc(var(--form-label-w) + var(--form-label-gap)); }
@media (max-width: 1200px) { .sf-detail .btn-row { margin-left: 0; } }

/* ── disc artwork ───────────────────────────────────────
 * The poster for the disc in the tray, composed into the right-hand end of the disc
 * panel itself rather than behind the whole viewport. Anchored to the panel it has
 * something to be composed against, it cannot depend on the page happening to be
 * empty, and it survives narrow windows -- so unlike a page-wide backdrop it works on
 * a phone, which is where this box is most likely to be checked on.
 *
 * The panel already has overflow:hidden, so the art is clipped to the rounded corners
 * for free; it only needs a positioning context.
 */
.card.has-art { position: relative; }

/* The disc cell keeps one height whatever it is showing. It swaps between an idle
   tray, a "reading the disc" row and a full progress view, and letting it resize on
   every transition made the page jump under the pointer -- and gave the artwork a
   different crop each time. One height, contents centred. */
.disc-cell { min-height: 296px; display: flex; flex-direction: column; justify-content: center;
              position: relative; }
@media (max-width: 700px) { .disc-cell { min-height: 260px; } }

/* The drive, what it can read, and the disc in it are facts about the machine, not
   about the job -- so they sit on the floor of the cell rather than floating in the
   middle of it. `margin-top: auto` in the cell's flex column pushes them down and lets
   the job or the tray keep the optical centre. */
.disc-cell > .tray-strip { margin-top: auto; }
.disc-cell .tray-drive {
  margin: 0; position: absolute; left: 22px; right: 22px; bottom: 16px;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  justify-content: center;
  font-size: 12px; color: var(--fg-muted);
}
/* The empty/loaded tray centres its own content, so the drive line is lifted out of
   that flow and pinned instead -- otherwise it rides up with the text above it. */
.disc-cell .empty-state { position: static; }
.disc-cell.has-art .tray-drive, .disc-cell .tray-drive { z-index: 1; }

.tray-art {
  position: absolute; inset: 0 0 0 auto;
  width: min(58%, 420px);
  z-index: 0; pointer-events: none; user-select: none;
  background-size: cover;
  /* Posters put the art in the upper two-thirds and the billing block along the
     bottom. Anchoring high keeps the illustration in frame and the credits out of it. */
  background-position: 50% 18%;
  opacity: .15; filter: saturate(.75);
  /* Fades to nothing toward the content so it dissolves into the panel instead of
     ending on a visible edge, and eases off top and bottom so it does not collide
     with the panel's borders. */
  -webkit-mask-image:
    linear-gradient(to left, #000 0%, rgba(0,0,0,.5) 42%, transparent 96%),
    linear-gradient(to bottom, transparent 0%, #000 14%, #000 80%, transparent 100%);
  mask-image:
    linear-gradient(to left, #000 0%, rgba(0,0,0,.5) 42%, transparent 96%),
    linear-gradient(to bottom, transparent 0%, #000 14%, #000 80%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* Everything in the panel sits above it. .card's children are not positioned by
   default, so without this the art would paint over the disc name. */
.card.has-art > *:not(.tray-art) { position: relative; z-index: 1; }

/* Light themes have far less headroom before a poster starts to muddy body text. */
:root[data-theme="light"] .tray-art,
.theme-light .tray-art { opacity: .10; filter: saturate(.6); }

/* On a phone the panel is the whole width, so the art takes a larger share of it and
   fades harder -- text runs closer to the right edge there than it ever does on a
   desktop, and legibility wins over atmosphere. */
@media (max-width: 700px) {
  .tray-art {
    /* The panel is the full width of a phone and its content is centred, so the disc
       name runs straight across the art. Narrower and fainter than on a desktop, and
       the fade starts sooner, so the poster stays behind the right-hand margin where
       there is no text rather than behind the title. */
    width: 56%; opacity: .10;
    /* Keep both layers -- dropping the vertical one let the poster's own tagline show
       along the top edge of the panel, which reads as a stray image rather than tone. */
    -webkit-mask-image:
      linear-gradient(to left, #000 0%, rgba(0,0,0,.45) 40%, transparent 92%),
      linear-gradient(to bottom, transparent 0%, #000 16%, #000 78%, transparent 100%);
    mask-image:
      linear-gradient(to left, #000 0%, rgba(0,0,0,.45) 40%, transparent 92%),
      linear-gradient(to bottom, transparent 0%, #000 16%, #000 78%, transparent 100%);
  }
}

/* The offered beta key. Shown in full rather than truncated: the point is that it can
   be read and checked, not just clicked. */
.keyoffer {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel, rgba(255,255,255,.02));
}
.keyoffer .grow { min-width: 0; }
.keyoffer .keyval {
  font-size: 11.5px; line-height: 1.5; word-break: break-all;
  color: var(--fg-strong, inherit); margin-bottom: 3px; user-select: all;
}
.keyoffer .muted { font-size: 12px; }
.mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }

/* Release notes come out of a GitHub release body: preformatted, but it has to wrap or
   a long line pushes the whole page sideways. */
.notes {
  white-space: pre-wrap; word-break: break-word;
  font-family: ui-monospace, Menlo, monospace; font-size: 12.5px;
  color: var(--fg); background: rgba(0,0,0,.22);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; max-height: 460px; overflow: auto;
}
code {
  font-family: ui-monospace, Menlo, monospace; font-size: .92em;
  background: rgba(255,255,255,.07); border-radius: 3px; padding: 1px 5px;
}
/* Status-tinted icons inside tables. */
.ic.ok { color: var(--ok); } .ic.warn { color: var(--warn); }
.ic.bad { color: var(--bad); } .ic.muted { color: var(--fg-muted); }

/* ── Auto Rip ──────────────────────────────────────────────
   The one deliberate exception to CheckInput.css (see the .switch note above): this is
   a hero control on the Queue page, not a settings row, so it gets a real toggle. The
   markup for it has existed since the queue page was written; the styling had not, so
   the track rendered as a bare browser checkbox. */
.autorip {
  display: flex; align-items: flex-start; gap: 16px;
  padding: 18px 20px; margin-bottom: 20px;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  background: rgba(255,255,255,.03);
}
.autorip.on {
  border-color: rgba(var(--accent-rgb), .45);
  background: rgba(var(--accent-rgb), .08);
}
.autorip.blocked { opacity: .92; }

.ar-switch { position: relative; flex: none; cursor: pointer; padding-top: 2px; }
.ar-switch.off { cursor: not-allowed; }
.ar-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.ar-switch .track {
  display: block; width: 46px; height: 26px; border-radius: 13px;
  background: rgba(255,255,255,.16); border: 1px solid var(--line-2);
  transition: background .15s, border-color .15s;
}
.ar-switch .track::after {
  content: ""; display: block;
  width: 20px; height: 20px; margin: 2px; border-radius: 50%;
  background: #fff; transition: transform .15s;
}
.ar-switch input:checked + .track {
  background: var(--btn); border-color: var(--btn);
}
.ar-switch input:checked + .track::after { transform: translateX(20px); }
.ar-switch input:disabled + .track { opacity: .4; }
.ar-switch input:focus-visible + .track {
  box-shadow: 0 0 0 3px var(--input-focus-sh);
}

.ar-text { min-width: 0; }
.ar-title { font-size: 16px; color: var(--fg-strong); margin-bottom: 2px; }
.ar-sub { font-size: 13px; color: var(--fg-muted); }
.ar-text { flex: 1; }

/* ── the Auto Rip checklist ──
   A three-column grid so the marks, the names and the values line up down the page.
   A checklist whose columns do not line up has to be read row by row; one that does
   can be scanned for the row that is not green, which is the entire point of it. */
.ar-checks { margin-top: 10px; }
.ar-checks > summary {
  cursor: pointer; font-size: 12.5px; color: var(--fg-muted);
  list-style: none; display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 0; user-select: none;
}
.ar-checks > summary::-webkit-details-marker { display: none; }
.ar-checks > summary::after {
  content: ""; width: 5px; height: 5px; margin-left: 2px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .15s;
}
.ar-checks[open] > summary::after { transform: rotate(-135deg) translate(-1px, -1px); }
.ar-checks > summary:hover { color: var(--fg-strong); }
.cl-sum { display: inline-flex; align-items: center; gap: 7px; }
.cl-sum.ok   { color: var(--ok); }
.cl-sum.warn { color: var(--warn); }
.cl-sum.fail { color: var(--bad); }

.ar-checks ul {
  margin: 9px 0 2px; padding: 0; list-style: none;
  display: grid; grid-template-columns: auto minmax(0, max-content) minmax(0, 1fr);
  gap: 1px 12px; font-size: 12.5px;
}
.ar-checks li { display: contents; }
.cl-mark { line-height: 1.7; }
li.ok   .cl-mark { color: var(--ok); }
li.warn .cl-mark { color: var(--warn); }
li.fail .cl-mark { color: var(--bad); }
.cl-what { color: var(--fg); line-height: 1.7; }
.cl-detail { color: var(--fg-muted); line-height: 1.7; min-width: 0; }
li.warn .cl-detail, li.fail .cl-detail { color: var(--fg-strong); }
/* The reason sits under the row it belongs to, spanning the two right-hand columns so
   it hangs off the name rather than restarting at the left edge. */
.cl-why {
  grid-column: 2 / -1; color: var(--fg-muted);
  font-size: 11.5px; line-height: 1.5; padding-bottom: 6px;
}

/* Three columns on a phone leaves the value column about ten characters wide, so
   "Pioneer BDR-XD08U · /dev/sr0" wraps to four lines. Stack instead -- each row
   becomes its own little grid, the mark keeps a column, and the value and the reason
   hang under the name. `display: contents` has to go with it: a row that stacks
   cannot also be a participant in one grid shared with every other row.

   This has to live *below* the rules it overrides rather than in the shell's
   media query up the file, which is earlier in source order and therefore loses. */
@media (max-width: 820px) {
  .ar-checks ul { display: block; }
  .ar-checks li {
    display: grid; grid-template-columns: auto minmax(0, 1fr);
    gap: 0 10px; padding-bottom: 9px;
  }
  .cl-what, .cl-detail { line-height: 1.5; }
  .cl-detail, .cl-why { grid-column: 2; }
  .cl-why { padding-bottom: 0; padding-top: 3px; }
}

/* One-off notices that need an answer before the page carries on -- the beta key
   renewal is the only one. A native <dialog>, so focus, Escape and the backdrop come
   from the browser rather than from code here. */
.notice-dialog {
  background: var(--panel); color: var(--fg); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 22px 24px 18px; margin: auto;
  width: min(480px, calc(100vw - 32px)); box-shadow: 0 18px 50px rgba(0,0,0,.5);
}
.notice-dialog::backdrop { background: rgba(0,0,0,.55); }
.notice-dialog h2 {
  display: flex; align-items: center; gap: 10px; margin: 0 0 12px;
  font-size: 19px; color: var(--fg-strong);
}
.notice-dialog p { margin: 0 0 12px; line-height: 1.55; }
.notice-dialog .btn-row { margin-top: 18px; justify-content: flex-end; }

/* The naming preview: one long path, so it wraps anywhere rather than overflowing. */
.naming-preview code { word-break: break-all; white-space: normal; }

/* TMDb's suggestions in the "which film is this?" question. */
.ni-tmdb { margin: 6px 0 14px; }
.tmdb-picks { display: flex; flex-wrap: wrap; gap: 10px; margin: 8px 0 10px; }
.tmdb-pick {
  width: 104px; padding: 6px; border-radius: 8px; cursor: pointer; text-align: left;
  background: rgba(255,255,255,.03); border: 1px solid var(--line-2); color: inherit;
  display: flex; flex-direction: column; gap: 4px; font: inherit;
}
.tmdb-pick:hover { border-color: var(--fg-muted); }
.tmdb-pick.on { border-color: var(--accent, #5a8dee); box-shadow: 0 0 0 1px var(--accent, #5a8dee); }
.tmdb-poster {
  display: block; width: 100%; aspect-ratio: 2 / 3; border-radius: 4px;
  background: rgba(255,255,255,.06) center / cover no-repeat;
}
.tmdb-name { font-size: 12px; line-height: 1.25; color: var(--fg-strong); }
.tmdb-year { font-size: 11px; }

/* "Will be saved as" under a job's title. One long path, so it wraps anywhere. */
.job-planned { display: flex; gap: 8px; align-items: baseline; margin: 6px 0 10px;
               font-size: 12.5px; color: var(--fg-muted); }
.job-planned code { word-break: break-all; white-space: normal; }

/* The live log on System -> Log Files. */
.live-log {
  height: 420px; overflow: auto; margin: 0 0 10px; padding: 10px 12px;
  background: rgba(0,0,0,.28); border: 1px solid var(--line-2); border-radius: var(--radius);
  font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre-wrap; word-break: break-word;
}
.live-log:empty::before { content: "Waiting for log lines…"; color: var(--fg-muted); }
.lv-line { padding: 1px 0; }
.lv-t { color: var(--fg-muted); margin-right: 6px; }
.lv-c { color: var(--accent, #5a8dee); }
.lv-debug { color: var(--fg-muted); }
.lv-warning { color: var(--warn, #e3a33b); }
.lv-error, .lv-critical { color: var(--bad, #f85149); }
.section h2 .switch.sm { font-size: 12px; font-weight: 400; margin: 0 6px 0 0; }

a.tool { text-decoration: none; }
/* Disc details. */
.disc-dlg { width: min(860px, 94vw); max-height: 86vh; overflow: auto; }
.dlg-head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.dlg-head h3 { font-size: 17px; color: var(--fg-strong); margin: 0; }
.disc-titles { display: flex; flex-direction: column; gap: 10px; margin: 12px 0; }
.disc-title { border: 1px solid var(--line-2); border-radius: var(--radius); padding: 10px 12px; }
.disc-title.chosen { border-color: var(--accent, #5a8dee); }
.dt-head { display: flex; flex-wrap: wrap; gap: 12px; align-items: baseline; font-size: 13px; }
.dt-media { margin: 6px 0 4px; display: flex; flex-wrap: wrap; gap: 6px; }
.dt-streams { margin: 4px 0 0; padding-left: 18px; font-size: 12px; color: var(--fg-muted); }
