/* Protectarr - Servarr-style dark UI (modelled on Prowlarr/Sonarr) */
:root {
  --page-bg: #202020;
  --sidebar-bg: #262626;
  --topbar-bg: #262626;
  --card-bg: #2a2a2a;
  --card-head: #2f2f2f;
  --border: #3a3a3a;
  --border-light: #444;
  --text: #ced0d2;
  --text-dim: #999b9d;
  --text-head: #e8e8e8;
  --accent: #35c46b;         /* Protectarr green ("protect") */
  --accent-hover: #2fae5f;
  --accent-dim: #35c46b33;
  --danger: #e5484d;
  --warn: #d8a200;
  --sidebar-w: 240px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font);
  background: var(--page-bg);
  color: var(--text);
  font-size: 15.5px;
  line-height: 1.55;
}
a { color: var(--accent); text-decoration: none; }

/* ---- layout: full-width header on top, sidebar below (Servarr shell) ----
   A two-column grid rather than a fixed sidebar plus a matching margin on the
   content. The old shell stated the sidebar's width twice, so the content
   could only reclaim collapsed space if both agreed. Here `--sidebar-w` is the
   grid track: change the variable and the content column resizes because
   there is nothing else for the remaining width to do.

   The sidebar is `sticky`, not `fixed`, which keeps the nav in place while the
   page scrolls (its old behaviour) without taking it out of flow, so it still
   occupies a grid track. */
:root { --header-h: 66px; --rail-w: 60px; }
body {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  grid-template-rows: var(--header-h) 1fr;
  min-height: 100vh;
}
/* minmax(0, 1fr) above, not 1fr: a grid track's default minimum is its
   content, so one wide table would push the column past the viewport and
   scroll the whole page instead of scrolling inside its own wrapper. */
.app-header {
  grid-column: 1 / -1; grid-row: 1;
  position: sticky; top: 0; height: var(--header-h);
  background: var(--topbar-bg); border-bottom: 1px solid var(--border);
  display: flex; align-items: center; z-index: 30;
}
.header-logo {
  display: flex; align-items: center; gap: 10px;
  width: var(--sidebar-w); min-width: var(--sidebar-w); box-sizing: border-box;
  padding: 0 18px; height: 100%; overflow: hidden;
  font-size: 20px; font-weight: 600; color: var(--text-head);
  border-right: 1px solid var(--border);
}
.header-logo .logo { font-size: 24px; }
.brand-icon { height: 42px; width: auto; display: block; flex: 0 0 auto; }
.brand-name { font-size: 21px; font-weight: 600; color: var(--text-head); letter-spacing: .2px; white-space: nowrap; }
.header-spacer { flex: 1; }
.header-icons { display: flex; align-items: center; gap: 16px; padding: 0 22px; color: var(--text-dim); font-size: 13px; }

/* ---- sidebar collapse toggle ---- */
.nav-toggle {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; margin-left: 14px; flex: 0 0 auto;
  background: transparent; border: 1px solid transparent; border-radius: 4px;
  color: var(--text-dim); cursor: pointer; padding: 0;
}
.nav-toggle:hover { background: #2e2e2e; color: var(--text-head); }
.nav-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.nav-toggle svg { width: 18px; height: 18px; display: block; }
/* The chevron points the way the sidebar will go, so it flips with the state. */
:root.nav-collapsed .nav-toggle svg { transform: scaleX(-1); }

.sidebar {
  grid-column: 1; grid-row: 2;
  position: sticky; top: var(--header-h); align-self: start;
  height: calc(100vh - var(--header-h));
  width: auto; min-width: 0; overflow: hidden;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column; z-index: 20;
}
.brand {  /* retained for the login card */
  display: flex; align-items: center; gap: 10px;
  font-size: 20px; font-weight: 600; color: var(--text-head);
}
.brand .logo { font-size: 22px; }
.brand small { display:block; font-size: 10px; color: var(--text-dim); font-weight: 400; letter-spacing:.5px; }
.nav { flex: 1; padding: 10px 0; overflow-y: auto; overflow-x: hidden; }
.nav a {
  display: flex; align-items: center; gap: 11px;
  padding: 11px 20px; font-size: 15px; color: var(--text); border-left: 3px solid transparent;
  white-space: nowrap;
}
.nav a:hover { background: #2e2e2e; }
.nav a.active { border-left-color: var(--accent); background: #2e2e2e; color: var(--text-head); }
.nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.nav .ico { width: 18px; height: 18px; flex: 0 0 18px; opacity: .85; display: block; }
.nav a.active .ico { opacity: 1; }
.sidebar-foot { padding: 12px 18px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-dim); white-space: nowrap; }

/* ---- collapsed rail ----
   The label is moved out of sight rather than `display:none`, so it stays in
   the accessibility tree: an icon-only link still has to announce itself. */
:root.nav-collapsed .nav .label,
:root.nav-collapsed .brand-name,
:root.nav-collapsed .sidebar-foot .foot-text {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
:root.nav-collapsed .nav a { justify-content: center; padding-left: 0; padding-right: 0; }
:root.nav-collapsed .header-logo { padding: 0; justify-content: center; }
/* Settings sections are text; there is no room and no flyout in this release,
   so the Settings icon leads to the index, which lists them. */
:root.nav-collapsed .subnav { display: none; }
:root.nav-collapsed .sidebar-foot { padding: 12px 0; text-align: center; }

/* settings sub-menu under the sidebar's Settings item */
.subnav { display: flex; flex-direction: column; background: #202020; }
.subnav a { padding: 7px 18px 7px 42px; font-size: 13px; color: var(--text-dim); border-left: 3px solid transparent; }
.subnav a:hover { background: #2a2a2a; color: var(--text); }
.subnav a.active { color: var(--text-head); border-left-color: var(--accent); background: #2a2a2a; }

/* settings index (list of sub-sections) */
.settings-link { display: block; padding: 14px 2px; border-bottom: 1px solid var(--border); }
.settings-link:last-child { border-bottom: none; }
.settings-link:hover .settings-link-title { text-decoration: underline; }
.settings-link-title { display: block; color: var(--accent); font-size: 15px; font-weight: 600; }
.settings-link .hint { display: block; margin-top: 2px; }

.content { grid-column: 2; grid-row: 2; min-width: 0; padding: 28px 32px; max-width: 1320px; }
/* min-width:0 for the same reason as the track's minmax: without it a grid
   item refuses to shrink below its content, and a wide table becomes a
   page-level scrollbar instead of a wrapper-level one. */

/* Data-heavy pages opt into more width. Settings and other forms keep the
   1320px cap, because a label-and-input column stretched across a 1920px
   monitor is harder to read, not easier. Applied by page type in `page()`,
   so it is one decision rather than a per-template pixel hack. */
.content.wide { max-width: 1800px; }

/* ---- sidebar collapse state ----
   `--sidebar-w` is the grid track, so these three rules are the entire
   mechanism: the content column takes whatever the sidebar gives up.

   Auto-collapse is a media query so it still works with JavaScript disabled.
   An explicit choice outranks it because `.nav-expanded` is the more specific
   selector, which is what makes a manual override stick below the breakpoint
   without JavaScript having to fight the stylesheet.

   1200px, not 1400px. The rail is worth a flat ~180px at every width, so the
   threshold is not a question about pixels gained but about when text labels
   stop being worth their cost. 1400 made icon-only navigation the default on
   an ordinary 1366px laptop, which is too early to spend legibility on width
   the page was not short of. */
@media (max-width: 1200px) { :root { --sidebar-w: var(--rail-w); } }
:root.nav-collapsed { --sidebar-w: var(--rail-w); }
:root.nav-expanded { --sidebar-w: 240px; }

/* Below this the rail is not a preference. A 240px sidebar on a 768px tablet
   leaves 464px of content, which is less than the 536px the same page gets at
   600px wide once the mobile layout drops the sidebar entirely: shrinking the
   window would hand back width, which is the specific inversion this release
   exists to remove. Honouring a stored "expanded" here would preserve it for
   exactly the users who once clicked the toggle on a desktop.

   Same specificity as the rule above and deliberately after it, so it wins. */
@media (max-width: 900px) { :root.nav-expanded { --sidebar-w: var(--rail-w); } }

/* ---- content toolbar (icon-over-label action buttons, Servarr-style) ---- */
.toolbar { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 18px; flex-wrap: wrap; }
.toolbar form { display: flex; margin: 0; }
.tool-btn {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 11px 20px; background: transparent; border: none; border-radius: 0;
  color: var(--text); cursor: pointer; font-size: 13px; font-family: var(--font);
}
.tool-btn:hover { background: #2e2e2e; }
.tool-btn .ti { font-size: 23px; line-height: 1; }
a.tool-btn { text-decoration: none; }
.tool-btn.active { box-shadow: inset 0 -2px 0 var(--accent); font-weight: 600; }

/* ---- status pills ---- */
.statusbar { display: flex; flex-wrap: wrap; gap: 22px; align-items: center; margin-bottom: 18px; }
.stat { font-size: 13px; color: var(--text-dim); }
.stat b { color: var(--text); }
.pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing:.3px; }
.pill.on { background: var(--accent); color: #08240f; }
.pill.off { background: #555; color: #eee; }
.pill.dry { background: var(--warn); color: #241d00; }
.pill.live { background: var(--accent); color: #08240f; }
/* Protectarr deliberately stopped and did nothing further. It has to outrank
   the greys around it, so it takes the existing danger colour rather than
   introducing a new one. */
.pill.bad { background: var(--danger); color: #2a0406; }

/* ---- cards ---- */
.card {
  background: var(--card-bg); border: 1px solid var(--border);
  border-radius: 6px; margin-bottom: 18px; overflow: hidden;
}
.card > h2 {
  margin: 0; padding: 14px 18px; font-size: 16px; font-weight: 600; color: var(--text-head);
  background: var(--card-head); border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 8px;
}
.card .body { padding: 18px 20px; }
.hint { color: var(--text-dim); font-size: 12px; }

/* A division inside a card, not a card of its own.
   0.7.0 folded the multi-card Settings sections into one card each, because a
   card with its own heading reads as something that saves on its own and these
   do not: one card is one <form> is one POST that overwrites the whole
   section. `h3.sub` is what an Archive Detection or a Budget gets instead of
   the card header it used to have. The rule above it is the seam. */
h3.sub {
  margin: 26px 0 12px; padding-top: 18px; font-size: 13px; font-weight: 600;
  letter-spacing: .3px; text-transform: uppercase; color: var(--text-head);
  border-top: 1px solid var(--border);
}
.card .body > h3.sub:first-child { margin-top: 0; padding-top: 0; border-top: none; }
/* Anchored cards must clear the fixed mobile header when jumped to. */
.card[id] { scroll-margin-top: calc(var(--header-h) + 12px); }

/* ---- two levels of disclosure, both native <details> ----
   `.howto` hides explanation: what a feature is, and why it behaves the way it
   does. `.adv` hides controls: engine tuning most operators can leave alone
   forever.

   Neither may hide a safety warning or an ordinary policy control. A page that
   is shorter but harder to answer a question from is not an improvement, and
   Advanced Tuning is not a place to put things that were merely wordy. Note
   that a closed <details> still submits everything inside it, so `.adv` costs
   nothing at the boundary - unlike `fieldset disabled`, which drops its
   controls from the POST and would erase them against these overwrite
   semantics. */
details.howto, details.adv {
  margin: 14px 0 0; border: 1px solid var(--border); border-radius: 5px;
  background: #242424;
}
details.howto > summary, details.adv > summary {
  cursor: pointer; padding: 11px 13px; font-size: 12px; font-weight: 600;
  letter-spacing: .3px; color: var(--text-dim);
  display: flex; align-items: center; gap: 9px;
  list-style: none; -webkit-user-select: none; user-select: none;
}
/* The default triangle cannot be styled or positioned, so it is replaced. */
details > summary::-webkit-details-marker { display: none; }
details.howto > summary::before, details.adv > summary::before {
  content: "\25B8"; font-size: 12px; line-height: 1;
  transition: transform .12s ease;
}
details[open] > summary::before { transform: rotate(90deg); }
details.howto > summary:hover, details.adv > summary:hover { color: var(--text); }
details.howto > summary:focus-visible, details.adv > summary:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 4px;
}
details.adv > summary { color: var(--text); }
/* ---- per-card unsaved changes ----
   One card is one form is one save boundary, so the marking is per card and
   several can be marked at once. Nothing here spans cards, because nothing in
   the backend does.

   `.card` drops `overflow: hidden` for `clip`, which clips painting the same
   way without making the card a scroll container. A scroll container would
   pin `position: sticky` to the card instead of the viewport, and the whole
   point of the strip is that it stays reachable while you edit a long card. */
.card { overflow: clip; }
.card.dirty {
  border-color: var(--warn);
  box-shadow: 0 0 0 1px #d8a20033;
}
.card.dirty > h2 {
  position: sticky; top: var(--header-h); z-index: 4;
  background: #332e1f; border-bottom-color: #d8a20055;
}
/* `margin-left: auto` belongs to the chip, which is what pushes this group to
   the right. Giving the bar an auto margin too would split the free space
   between them and strand the chip in the middle of the header. Every card
   that can be dirty renders a chip, which test_settings_summary pins. */
.dirtybar { display: flex; align-items: center; gap: 10px; margin-left: 14px; }
.dirtybar[hidden] { display: none; }
.dirtymsg {
  font-size: 12px; font-weight: 600; color: var(--warn);
  letter-spacing: .2px; white-space: nowrap;
}
/* A dirty card has its own Save in the header, so the one in the body would be
   a second control for the same action. Hidden only once the script has said
   it exists: with JavaScript off, `.has-dirty-ui` is never set and the in-body
   button is the only Save, exactly as before. */
.has-dirty-ui .card-actions { display: none; }
.card-ops { margin-top: 14px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }

/* A sub-section a master switch has made irrelevant. Readable, not hidden:
   the values are still there, still submitted, and still worth seeing when
   deciding whether to switch the feature back on. */
.inactive { opacity: .45; }
.inactive, .inactive * { cursor: default; }

/* ---- saved-state chips ----
   What the config on disk says right now, rendered by the server and never
   touched by the dirty-state code. A chip that followed an unsaved edit would
   be answering a different question from the one it looks like it answers. */
.chip {
  margin-left: auto; flex: 0 0 auto;
  font-size: 11.5px; font-weight: 600; letter-spacing: .2px;
  padding: 3px 9px; border-radius: 10px; border: 1px solid transparent;
  white-space: nowrap;
}
h3.sub .chip { font-size: 11px; text-transform: none; letter-spacing: .2px; }
.chip.on   { color: var(--accent); background: var(--accent-dim); border-color: #35c46b55; }
.chip.off  { color: var(--text-dim); background: #33333366; border-color: var(--border); }
/* "switched on and still doing nothing" is the state worth catching the eye. */
.chip.warn { color: var(--warn); background: #d8a2001f; border-color: #d8a20055; }
h3.sub { display: flex; align-items: center; gap: 9px; }

/* ---- the Current Policy box ----
   Firewall-style: one row per decision, label left, value right, in the order
   the engine actually asks the questions. A row the current mode makes
   inapplicable is muted and says so, rather than vanishing - a row that
   disappears reads as a setting that was lost. */
.policy {
  margin: 0 0 18px; border: 1px solid var(--border-light); border-radius: 6px;
  background: #242424; overflow: hidden;
}
.policy-head {
  padding: 9px 14px; font-size: 11.5px; font-weight: 600; letter-spacing: .5px;
  text-transform: uppercase; color: var(--text-dim);
  background: #2c2c2c; border-bottom: 1px solid var(--border);
}
.policy dl { margin: 0; padding: 4px 0; }
.policy-row {
  display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 6px 16px;
  padding: 7px 14px; align-items: baseline;
}
.policy dt {
  font-size: 12px; font-weight: 600; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: .3px;
}
.policy dd { margin: 0; font-size: 13.5px; color: var(--text); }
.policy-row.muted dd { color: var(--text-dim); font-style: italic; }

.disclosure { padding: 4px 14px 14px; border-top: 1px solid var(--border); }
.disclosure > *:first-child { margin-top: 12px; }
.disclosure p.hint:last-child { margin-bottom: 0; }

/* ---- Settings, narrow ----
   Every number here was measured against this content rather than picked. */

/* The path-mapping row. `.row`'s generic 160px minimum was measured to be too
   small for the thing this field holds: `/General Storage/torrents`, the
   example in our own documentation, needs 188px of input, and a realistic
   unRAID path like `/mnt/user/data/torrents/complete` needs 233. At 768px the
   generic rule gave these 179px each, so the documented example did not fit in
   the field it documents. 236 is that 233 plus slack.

   The Remove button is taken out of the sharing entirely: `flex: 1` was
   stretching a 79px button to 321px and then wrapping its label. */
.maprow > div { min-width: 236px; }
.maprow > button { flex: 0 0 auto; min-width: 0; align-self: end; }

/* The API key. `.secret-field` is one flex line: Copy and Show take 136px of
   it, which left the field 133px at 375 for a 64-character key. Wrapping is
   driven by the input's own minimum rather than a breakpoint, so the buttons
   drop to their own line exactly when they stop fitting beside it. */
.secret-field { flex-wrap: wrap; }
.secret-field input { min-width: 200px; }

/* Touch targets. Width alone is the wrong signal for a tablet held in two
   hands, so a coarse pointer opts in at any width. 0.6.1 sized the History
   Details button to 44px for the same reason and by the same number. */
/* Scoped to Settings cards by the anchor id the old per-section URLs redirect
   to, which nothing outside Settings carries. Applications, Dashboard, History
   and System are out of scope for 0.7.0 and their controls are left alone. */
@media (max-width: 720px), (pointer: coarse) {
  .card[id^="section-"] label.check { min-height: 44px; align-items: center; }
  .card[id^="section-"] .body .btn,
  .card[id^="section-"] .body button[type=submit],
  .dirtybar .btn { min-height: 44px; }
  details.howto > summary, details.adv > summary { min-height: 44px; }
  /* A scroll region inside a scrolling page is a trap on a touch screen: the
     inner one swallows the gesture and the page appears stuck. The list is
     short in practice, so down here it simply becomes part of the page. */
  .card[id^="section-"] .checklist { max-height: none; overflow-y: visible; }
}

/* The log-file table, stacked into records below the width where it stops
   fitting. Measured: four columns (File / Size / Modified / Download) have a
   min-content width of 433px against a card that is the viewport less 106, so
   539 is the last width where the whole row is on screen. It scrolled sideways
   inside its wrapper below that, which is the same defect History had before
   0.6.1 and the same fix.

   Its own breakpoint rather than `.rowcards`' 869: this table is narrower than
   History's and stacking it at 869 would cost density on a tablet for nothing. */
@media (max-width: 538px) {
  .logcards, .logcards tbody { display: block; }
  .logcards thead { display: none; }
  .logcards tr {
    display: block; border: 1px solid var(--border); border-radius: 6px;
    background: #262626; padding: 12px 14px; margin-bottom: 10px;
  }
  .logcards tr:last-child { margin-bottom: 0; }
  .logcards td { display: block; border: none; padding: 0; text-align: left; }
  .logcards td + td { margin-top: 8px; }
  .logcards td[data-label]::before {
    content: attr(data-label); display: block;
    font-size: 11px; font-weight: 600; letter-spacing: .4px;
    text-transform: uppercase; color: var(--text-dim); margin-bottom: 3px;
  }
  .logcards code { overflow-wrap: break-word; }
}

/* The one narrow breakpoint Settings has, taken from the tightest thing on
   the page: a dirty card header holding a title, a saved-state chip and three
   controls. Measured on each of the seven, the widest need is Remediation
   Policy at 587px of card, which is 653px of viewport. Below that the strip
   takes its own line.

   The Current Policy rows stack here too rather than at a number of their own.
   Their label column is a fixed 180px, so they are under the same pressure
   from the same direction, and one breakpoint for the page is easier to keep
   honest than two that drift apart. */
@media (max-width: 652px) {
  .card > h2 { flex-wrap: wrap; }
  .chip { margin-left: auto; }
  .dirtybar { margin-left: 0; width: 100%; justify-content: flex-end; }
  .card.dirty > h2 { padding-bottom: 12px; }
  .policy-row { grid-template-columns: minmax(0, 1fr); gap: 2px; padding: 9px 14px; }
}
/* Right-aligned numeric columns, without an inline style per cell. */
.applist th.num, .applist td.num { text-align: right; }

/* ---- dashboard ----
   Removed with the 0.6.0 redesign: `.cards`/`.statcard`, and the `.chart` /
   `.legend` SVG chart styles with them. The Dashboard was their only consumer
   and the surfaces they drew are gone - the all-time reaped counter, the
   library-monitored chart and the all-time by-indexer chart. The bar styles
   below survived because they were never wired to anything and are now what
   the two trend cards use.

   Four vitals, and the breakpoint is measured rather than inherited from the
   Details dialog's 900px: this grid has different contents and no reason to
   share a number with it. See the media queries at the end of this block. */
.vitals { display: grid; gap: 14px; margin-bottom: 18px; }
.vitals .card { margin-bottom: 0; }
.vital .body { padding: 16px 18px; }
/* Vitals sit side by side, so their bodies have to start on the same line
   whatever each one contains. A grid row rather than a min-height on the body:
   the row is as tall as its tallest card and every card fills it, which a
   fixed height would only approximate. */
.vitals .card { display: flex; flex-direction: column; }
.vital .body { flex: 1; }
.vital .num { font-size: 40px; font-weight: 700; color: var(--text-head); line-height: 1.15; }
/* A date is not a number and does not get a number's weight. Without this the
   Last Remediation card shouts "3 hours ago" at the same volume the Attention
   card shouts a fault count. */
.vital .num.small { font-size: 22px; font-weight: 600; }
.vital .num.quiet { color: var(--text-dim); font-weight: 500; }
.vital .num.bad { color: var(--danger); }
.vital .sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.vital .sub.muted { margin-top: 10px; opacity: .75; }
/* The one card allowed to colour its own border, and only when it is not zero.
   Healthy has to be the quiet state or there is no contrast left to spend on
   an actual problem. */
.vital.alarm { border-color: var(--danger); }
.attn-list { margin: 10px 0 8px; padding: 0 0 0 16px; font-size: 12.5px; }
.attn-list li { margin-bottom: 3px; }

/* Service rows. One line each, pill hard right, and the failure reason on its
   own line under it rather than beside it: a connection error is a sentence,
   and letting it share the line sets a floor the narrow vitals column cannot
   meet. */
.svc { list-style: none; margin: 0; padding: 0; }
.svc + .svc { margin-top: 6px; }
.svc li { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; font-size: 13px; }
.svc li:last-child { margin-bottom: 0; }
.svc-name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.svc li .pill { margin-left: auto; }
.svc-detail { flex-basis: 100%; }
.svc-why { margin: 8px 0 0; }

/* Trends: two half-width cards on desktop, stacked below. */
.trends { display: grid; gap: 14px; }
.trends .card { margin-bottom: 0; }

/* ---- horizontal bar breakdown ---- */
.bars { display: flex; flex-direction: column; gap: 9px; }
.bar-row { display: flex; align-items: center; gap: 10px; }
.bar-label { width: 160px; min-width: 160px; font-size: 13px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { flex: 1; min-width: 0; background: #1c1c1c; border-radius: 4px; height: 18px; overflow: hidden; }
.bar-fill { height: 100%; background: var(--accent); border-radius: 4px; min-width: 2px; }
/* Volume is not an achievement. Remediation counts and indexer associations
   are facts about what happened, so they take the neutral grey; the accent
   green stays reserved for a state that is confirmed good. */
.bar-fill.neutral { background: #5a6068; }
.bar-val { width: 40px; text-align: right; font-size: 13px; color: var(--text-head); font-weight: 600; }

/* ---- dashboard breakpoints ----
   After `.bar-label`, deliberately. These were above it in the first draft and
   the narrow-label override silently lost on source order, which does not show
   up as a broken rule - it shows up as a 49px bar at 375px that looks like a
   layout result rather than a specificity one.

   Three different numbers, because these are three different grids and sharing
   one breakpoint between them would be a tidiness argument, not a measurement.
   None of them is 900px: that is the Details dialog's knee and it has no
   bearing on this page.

   1024 - four vitals. The block is 226px tall at 1920 and flat at 246px all
   the way from 1600 down to 1024, then starts climbing as content wraps: 265
   at 980, 309 at 820, 346 at 768. So 1024 is the last width where the fourth
   column is still free.

   980 - two trend cards. This one is decided by the bar, not by height. Two
   columns always save about 165px of page height, but the track narrows with
   it: 181px at 1024, 159px at 980, 119px at 900, 53px at 768. Below ~150px the
   bar stops showing proportion, and the bar is the card's entire content, so
   height bought by making the chart unreadable is not a saving. The giveaway
   was that one column at 600px gave a 274px track while two columns at 768px
   gave 53px - narrowing the window improved the page, which is always a sign
   the breakpoint is in the wrong place.

   480 - two vitals. Measured on heading wrap, which is what cramping looks
   like in these cards: every heading holds one line down to 480px (199px per
   card), and "Attention Required" takes a second line at 440px (179px). Two
   columns are worth keeping this low because the alternative is expensive -
   the block is 421px tall in two columns against 719px in one.

   620 - the bar label. 160px is most of a narrow card's width; at 375px it
   left the track 49px. */
@media (min-width: 480px) { .vitals { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .vitals { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 980px) { .trends { grid-template-columns: 1fr 1fr; } }
@media (max-width: 620px) { .bar-label { width: 104px; min-width: 104px; } }

/* ---- Triage Queue and Recent Activity as stacked cards ----
   The action cell's alignment moved off the element and into here so the
   narrow layout below can reason about it in CSS rather than lose to a style
   attribute. It is right-aligned at every width - as the last line of a
   stacked card it reads as that card's action - and renders identically to the
   inline rule it replaced. */
.rowcards td.act { text-align: right; }

/* 870 - the width below which these two tables stop fitting. Measured on the
   stress fixture: Recent Activity will not go under 704px (the Outcome column
   carries a sentence, not a word), and the card body gives it the viewport
   less 166px of shell and padding, so 870 is the last width that fits it.
   Triage's own floor is 507px and it could stay a table down to about 670,
   but two identical-looking tables on one page changing shape at different
   widths reads as a rendering fault, so they move together on the wider of the
   two numbers.

   Scrolling was the old answer and it failed in a specific way worth naming:
   the columns that went off the right were Issue, Outcome and Details, which
   are the whole reason to look at either table. Worse, the off-screen cell
   still set the row height, so at 768 the rows were 251px tall with two lines
   of visible text in them.

   None of the other dashboard breakpoints applies here. 980 and 1024 are about
   grids of cards, 620 is a label width, and 720 is the shell's. This one is
   the tables'. */
@media (max-width: 869px) {
  .rowcards, .rowcards tbody, .rowcards tr, .rowcards td { display: block; }
  /* The headings survive as the per-field labels below; the row itself has
     nothing left to align to. */
  .rowcards thead { display: none; }
  .rowcards tr {
    border: 1px solid var(--border); border-radius: 6px;
    background: #262626; padding: 12px 14px; margin-bottom: 10px;
  }
  .rowcards tr:last-child { margin-bottom: 0; }
  .rowcards td { border: none; padding: 0; text-align: left; }
  .rowcards td + td { margin-top: 9px; }
  .rowcards td[data-label]::before {
    content: attr(data-label); display: block;
    font-size: 11px; font-weight: 600; letter-spacing: .4px;
    text-transform: uppercase; color: var(--text-dim); margin-bottom: 3px;
  }
  /* A release title is one long unbroken token. `break-word` rather than
     `break-all`: it only splits a word that cannot fit on a line of its own,
     so ordinary text still wraps at spaces instead of shredding. */
  .rowcards code { overflow-wrap: break-word; }
  .rowcards .btn.small { padding: 7px 14px; font-size: 12.5px; }
}

/* ---- History rows as records ----
   Same idea as `.rowcards` above, at a different width and in a different
   order, so it is a separate block rather than a fourth thing bolted onto
   that one.

   1000, and the reason is arithmetic rather than taste. The History table has
   seven columns and will not compress below 834px once the release names are
   ordinary length (measured on the stress fixture: When 67, Release 298,
   Why 85, Action 104, Replacement 141, Peers 60, action 79). The History card
   hands the table the viewport less 166px while the rail is up, so 1000 is the
   last width where 834 still fits. At 980 the deficit is 20px and two cells
   are already past the right edge; by 768 it is 232px and six cells.

   A release name is a single unbreakable token, so a 106-character one raises
   the table's floor to 1366px on its own and it will still scroll sideways on
   a 1440 desktop. That is pre-existing, it is what the `overflow-x:auto`
   wrapper is for, and the desktop table is explicitly out of scope here.
   Note that `overflow-wrap: break-word` does not help a table: it permits a
   break during layout but does not reduce the cell's min-content contribution,
   so the column still demands the whole word. Measured - it changed the floor
   by 0px. Inside a card there is no column to widen, so it works there.

   999 is not any of the other breakpoints: 1200 and 900 are the sidebar's,
   869 is the Dashboard tables', 1024 and 980 are card grids, 720 is the
   shell's. */
.histcards td.act, .histcards td.num, .histcards th.num { text-align: right; }

@media (max-width: 999px) {
  /* No `td` in here, unlike `.rowcards` above: the row below is a grid, and a
     grid item is blockified whatever its own `display` said, so a rule for the
     cells would be dead. Confirmed by mutation - removing it moved nothing.
     These two are in the same position on this engine, since `display: grid`
     on the row already takes it out of table layout and leaves the table and
     tbody boxes wrapping ordinary blocks. They stay because table fixup is the
     part of layout browsers least agree on, and the cost of saying it out loud
     is two declarations. */
  .histcards, .histcards tbody { display: block; }
  .histcards thead { display: none; }
  /* A grid, not a plain stack, because the brief's reading order is not the
     DOM order: When and the outcome pills share the top line and everything
     else is full width underneath. Grid placement gets that without moving a
     single cell in the markup, which is what keeps the desktop table honest.
     Row 1 is claimed explicitly; the remaining cells auto-place in source
     order, which is already Release, Why, Replacement, Peers, Details. */
  .histcards tr {
    display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px 12px;
    border: 1px solid var(--border); border-radius: 6px;
    background: #262626; padding: 12px 14px; margin-bottom: 10px;
  }
  .histcards tr:last-child { margin-bottom: 0; }
  .histcards td {
    border: none; padding: 0; text-align: left;
    grid-column: 1 / -1; min-width: 0;
  }
  /* `minmax(0, 1fr)` above and `min-width: 0` here are the usual guard against
     a grid item's automatic minimum being its own content - a release name
     that does not break. Measured, neither is currently doing anything: the
     `overflow-wrap` below already lets that name wrap, so the automatic
     minimum never gets large, and swapping the track to `auto auto` or
     dropping the `min-width` moves nothing. They are kept as the backstop for
     the rule that IS load bearing; take the wrapping away and the card is as
     wide as the title again. Mutating the wrapping alone proves that. */
  .histcards td.h-when { grid-column: 1; grid-row: 1; }
  .histcards td.h-action { grid-column: 2; grid-row: 1; }
  /* Both of these are right-aligned in the table and must not stay that way in
     a card. Peers is a number in a column of numbers up there; down here there
     is no column, and a lone right-aligned figure reads as belonging to
     whatever sits beside it rather than to its own label. The outcome cell is
     worse: right-aligning it strands the pills against the far edge, away from
     the timestamp they belong to, and leaves two lines of explanation ragged
     down the left. The Details cell keeps its alignment - a card action does
     belong at the end. */
  .histcards td.num, .histcards td.h-action { text-align: left; }
  .histcards td[data-label]::before {
    content: attr(data-label); display: block;
    font-size: 11px; font-weight: 600; letter-spacing: .4px;
    text-transform: uppercase; color: var(--text-dim); margin-bottom: 3px;
  }
  /* `break-word`, not `break-all`: only a token that cannot fit a line of its
     own is split, so the prose in Why and Replacement still wraps at spaces. */
  .histcards code, .histcards td { overflow-wrap: break-word; }
  /* Taller, not wider. The desktop button is 26px high, which is under the
     44px a fingertip wants. `min-height` rather than more padding because the
     button is already a centring flex box, so the height is stated once and
     does not have to be re-derived from the font every time the type changes.
     Not full width: measured, going full width buys nothing here and spends a
     whole row of the card on a secondary action. */
  .histcards .btn.small {
    min-height: 44px; padding: 8px 18px; font-size: 13px;
  }
}

/* The outcome pills stop sharing the top line here. Measured: a row of pills
   needs 177px, and the widest pair is the worst case rather than a sample
   because the labels are a closed set - "reaped" next to "Failed Unverified",
   the longest entry in _MILESTONES. The When cell takes 145px for a timestamp,
   so the card runs out of room at 470px of viewport. 500 is that floor with
   headroom, and nothing between the two is a real device width.

   Below this the pills drop to their own full-width line directly under the
   timestamp. Still the top of the card, still before the release name, so the
   reading order the page promises survives; it just stops being two-up. */
@media (max-width: 499px) {
  .histcards tr { grid-template-columns: minmax(0, 1fr); }
  .histcards td.h-when { grid-column: 1; grid-row: 1; }
  .histcards td.h-action { grid-column: 1; grid-row: 2; }
  .histcards td[data-label="Release"] { grid-row: 3; }
  .histcards td[data-label="Why"] { grid-row: 4; }
  .histcards td[data-label="Replacement"] { grid-row: 5; }
  .histcards td[data-label="Peers"] { grid-row: 6; }
  .histcards td.act { grid-row: 7; }
}

/* ---- Active Downloads ----
   The age of the projection, stated before the table rather than under it.
   This page is the last completed scan, not a live feed, and someone who reads
   it as live will take a 40-second-old row as proof that nothing is happening.
   `stale` is the stronger case: a pass has failed since, so the rows are the
   last thing that was true rather than the most recent thing that is. */
.snapage { font-size: 13px; color: var(--text-dim); margin: 0 0 14px; }
.snapage.stale {
  color: var(--warn); background: #d8a2001a; border: 1px solid #d8a20055;
  border-radius: 4px; padding: 9px 12px;
}
.warnbox {
  font-size: 13px; color: var(--warn); background: #d8a2001a;
  border: 1px solid #d8a20055; border-radius: 4px; padding: 9px 12px;
  margin: 0 0 14px;
}
/* A filter nothing is in. Left in place rather than removed, so the bar's
   shape does not change with the data, but not dressed up as a destination. */
.tool-btn.empty { opacity: .45; }
.tool-btn .fcount { color: var(--text-dim); font-weight: 400; margin-left: 4px; }
.tool-btn.active .fcount { color: inherit; }

.dlcards td.act, .dlcards td.num, .dlcards th.num { text-align: right; }

/* One declaration decides this table's floor, and it is the whole reason this
   page is not History.

   `anywhere`, not `break-word`. A release name is a single unbreakable token.
   `break-word` permits a break during layout but does not reduce the cell's
   min-content contribution, so the column still demands the whole word and the
   table cannot shrink past it - measured on History, where a 106-character
   name raises the floor to 1366px and `break-word` moves it by 0px.
   `anywhere` does reduce min-content, which is what lets the column give way.

   Measured on the rendered page: these eight columns come to 688px of
   min-content, a 854px viewport once the card has taken its 166px. The check
   that matters is that name length stops mattering - a 124-character name
   gives the same 688px, to the pixel.

   There was a `max-width: 260px` here too, on the assumption that the cap was
   what gave `anywhere` something to reduce to. It was not. Measured across
   caps from 260px to none, the floor is 688px in every case including none,
   and the table's own width at a given viewport is identical in every case:
   table layout was already distributing the space. All the cap did was make
   the release column narrower and therefore taller. With a 124-character name
   at 1440 it cost nine lines and a 249px row where uncapped is four lines and
   131px. So it is gone, and nothing about the floor changed. */
.dlcards td.dl-name code { overflow-wrap: anywhere; }

/* Same 999px as History, but not for the same reason, and the difference is
   worth saying. History breaks at 999 because that is genuinely where its
   table stops fitting. This table fits down to 854, so 999 is a choice rather
   than a constraint: two tables on adjacent pages changing shape at different
   widths reads as a rendering fault, and 145px of headroom is a cheaper price
   than that. If the column set grows, check the floor again before assuming
   this number still has slack in it. */
@media (max-width: 999px) {
  .dlcards, .dlcards tbody, .dlcards td { display: block; }
  .dlcards thead { display: none; }
  /* A grid, not a plain stack, and the reason is measured. Stacked one field
     per line the card came to 460px at 768, and 172px of that was four cells -
     Progress, Category, Owner, Ownership - each spending a whole 43px line on
     one short word. Two columns puts those four on two lines instead of four
     and the prose fields keep the full width they need.

     Auto-placement, with only the full-width cells named. Which cell is third
     depends on what is hidden below, so anything keyed on position would put
     the wrong field in the left column the moment a row has no remediation. */
  .dlcards tr {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 9px 14px;
    border: 1px solid var(--border); border-radius: 6px;
    background: #262626; padding: 12px 14px; margin-bottom: 10px;
  }
  .dlcards tr:last-child { margin-bottom: 0; }
  .dlcards td { border: none; padding: 0; text-align: left; }
  .dlcards td.num { text-align: left; }
  .dlcards td.dl-name,
  .dlcards td.dl-state,
  .dlcards td[data-label="Remediation"],
  .dlcards td.act { grid-column: 1 / -1; }
  /* A field with nothing in it. On the desktop table the cell has to exist so
     the column lines up; in a card it is a heading over a dash, and four of
     those is most of the card. */
  .dlcards td.dl-empty { display: none; }
  .dlcards td[data-label]::before {
    content: attr(data-label); display: block;
    font-size: 11px; font-weight: 600; letter-spacing: .4px;
    text-transform: uppercase; color: var(--text-dim); margin-bottom: 3px;
  }
  .dlcards code { overflow-wrap: anywhere; }
  /* Taller, not wider, and the same rule History's card uses. The desktop
     button is under the 44px a fingertip wants; `min-height` rather than more
     padding because the button is already a centring flex box, so the height
     is stated once instead of being re-derived from the font. */
  .dlcards .btn.small {
    min-height: 44px; padding: 8px 18px; font-size: 13px;
  }
}

/* There is deliberately no second breakpoint dropping these cards to one
   column. There was one at 380px, on the assumption that two columns would
   stop being readable somewhere around the narrower phones; measured through
   an iframe (chromium will not open a window below 500px) it holds to 280px
   with no overflow and nothing clipped, and the pairs that share a line are
   short values like `39%` and `tv-sonarr` rather than prose. At 375 - the
   commonest phone width - the rule was costing 103px of card height to solve
   a problem that does not start until below any real device. */

/* ---- forms ---- */
label { display: block; font-size: 12px; color: var(--text-dim); margin: 12px 0 4px; }
label:first-child { margin-top: 0; }
input[type=text], input[type=url], input[type=password], input[type=number], textarea, select {
  width: 100%; padding: 9px 11px; background: #1c1c1c; color: var(--text);
  border: 1px solid var(--border-light); border-radius: 4px; font-family: var(--font); font-size: 14.5px;
}
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
textarea { min-height: 70px; font-family: ui-monospace, Menlo, Consolas, monospace; resize: vertical; }
.row { display: flex; gap: 14px; flex-wrap: wrap; }
.row > * { flex: 1; min-width: 160px; }
.check { display: flex; align-items: center; gap: 8px; margin-top: 12px; color: var(--text); }
.check input { width: auto; }
.checklist { margin-top: 8px; display: flex; flex-direction: column; gap: 2px; max-height: 260px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; }
.checklist .check { margin-top: 4px; }

/* ---- buttons ---- */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 9px 17px; border-radius: 4px; border: 1px solid var(--border-light);
  background: #333; color: var(--text); cursor: pointer; font-size: 14px; font-family: var(--font);
}
.btn:hover { background: #3a3a3a; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #08240f; font-weight: 600; }
.btn.primary:hover { background: var(--accent-hover); }
.btn.small { padding: 5px 10px; font-size: 12px; }

/* ---- secret fields (masked API keys with reveal) ---- */
.secret-field { display: flex; gap: 6px; align-items: stretch; }
.secret-field input { flex: 1; }
.secret-field .reveal,
.secret-field .copybtn { flex: 0 0 auto; padding: 0 12px; }
/* fixed width so swapping to "Copied"/"Failed" doesn't shift the row */
.secret-field .copybtn { min-width: 68px; text-align: center; }

/* ---- arr instance blocks ---- */
.instance { border: 1px solid var(--border); border-radius: 5px; padding: 12px; margin-bottom: 12px; background: #262626; }
.msg { font-size: 12px; margin-left: 8px; }

/* ---- application list (Prowlarr-style rows) ---- */
.applist td { vertical-align: middle; }
.st { font-size: 13px; }
.st-ok { color: var(--accent); }
.st-err { color: var(--danger); }
.st-unknown { color: #666; }
/* The row currently being tested, during Test All. */
.st-testing { color: var(--warn); animation: pulse 1s ease-in-out infinite; }

/* ---- Test All progress ----
   Inline in the button, no overlay: the page stays readable and each row
   resolves in place as its own answer arrives. */
.tool-btn[aria-busy="true"] { opacity: .85; cursor: progress; }
.tool-btn:disabled { cursor: default; }
.spinner {
  display: inline-block; width: 14px; height: 14px; flex: 0 0 auto;
  border: 2px solid var(--border-light); border-top-color: var(--accent);
  border-radius: 50%; animation: spin .7s linear infinite;
}
/* In the toolbar the spinner stands where the icon stood, so it takes the
   icon's box (measured: 23px) rather than its own. A 14px ring there moved the
   label up nine pixels every time a test started. */
.tool-btn .spinner { width: 23px; height: 23px; border-width: 2.5px; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 50% { opacity: .35; } }
/* The count in the label carries the progress on its own, so with motion
   turned down the ring simply stops rather than being replaced by nothing. */
@media (prefers-reduced-motion: reduce) {
  .spinner, .st-testing { animation: none; }
}

/* Announced but not drawn. Used for the Test All progress, which a changing
   button label does not reliably report to a screen reader. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.iconbtn {
  display: inline-block; padding: 4px 8px; margin-left: 2px; border-radius: 4px;
  background: transparent; border: none; color: var(--text-dim); cursor: pointer;
  font-size: 15px; line-height: 1; text-decoration: none;
}
.iconbtn:hover { background: #333; color: var(--text-head); }
.btn.danger { background: #4a2224; border-color: var(--danger); color: #f3b6b8; }
.btn.danger:hover { background: #5a2a2c; }

/* ---- modal (Prowlarr-style edit dialog) ---- */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 50;
  display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; overflow-y: auto;
}
.modal[hidden], .modal-overlay[hidden] { display: none; }
.modal {
  background: var(--card-bg); border: 1px solid var(--border-light); border-radius: 8px;
  width: 100%; max-width: 560px; box-shadow: 0 12px 40px rgba(0,0,0,.5);
}
/* 560px suits a key/value dialog, which is what every modal was until the peer
   profile put a four-column table inside one. At that width the Release column
   is narrow enough to wrap release names a character at a time, so the wider
   variant is for modals whose body is tabular rather than a list of fields. */
.modal.wide { max-width: 940px; }

/* Encounter history table, in the peer profile dialog.
   The header cells only. "Remediation Outcome" is the one long enough to wrap,
   and measurement showed it wrapping at every width from 1920 down whenever the
   profile happened to contain a long release name, so the header row height
   differed between two dialogs showing the same columns. This is scoped to
   thead deliberately: body cells keep wrapping, which is what lets Release and
   Finding absorb the flex, and the table sits in an overflow-x:auto wrapper so
   an unbreakable header can only make the dialog scroll, never the page. That
   distinction is the one the History table got wrong once already. */
.enc-table thead th { white-space: nowrap; }

/* ---- History Details dossier ----
   Single column by default, which is what mobile keeps and what every record
   used to get at every width: the dialog was a fixed 560px from 1920 down to
   600, so a full record was 1840px tall and needed 1020px of overlay scrolling
   on a 900px screen while ~1300px of desktop width sat unused beside it.

   Auto-flow, not named areas. Sections with nothing to show are dropped before
   render, so "the third section" is Replacement search on one record and
   Timeline on the next; anything keyed on position would lay out a different
   record wrongly. Letting sections fill the columns in DOM order also keeps
   reading order and visual order identical, which explicit placement would
   not. `align-items: start` stops a short section stretching to its row's
   height and leaving a table ruled to the bottom of a mostly empty cell.

   Separation is the grid's row gap rather than a margin on each heading, so
   that two sections sharing a row start on the same line. With the margin the
   left section began 14px above the right one, because only the very first
   heading had its top margin removed and the right column's first heading is
   not it. */
.detail-grid { display: grid; gap: 16px 26px; }
.dsec h3 { margin: 0 0 6px; font-size: 13px; color: var(--text-head); }
.dsec th {
  width: 190px; text-align: left; vertical-align: top; font-weight: 600;
}
.dsec td { word-break: break-word; }
/* 900px, and it is a measured knee rather than the usual round number. Sweeping
   the two-column layout down in 20px steps, the tallest stress record sits flat
   at 1276px from 1920 all the way to 880, then climbs: 1299 at 860, 1367 at 820,
   1412 at 768. The second record leaves its floor one step earlier, at 880. So
   900 is the narrowest width at which neither record has started paying for the
   second column in extra wrapping, and it is above the 720px mobile stack with
   room to spare. Two columns still beat one at 768 (1412 vs 1840), but by then
   value cells are down to 166px, and at 600 the layout loses outright: 2051 vs
   1840. Below the breakpoint this is a plain single-column stack, unchanged. */
@media (min-width: 900px) {
  .detail-grid { grid-template-columns: 1fr 1fr; align-items: start; }
  .dsec.span { grid-column: 1 / -1; }
  /* Timeline only, and only where the section is full width. Its labels are
     25-character timestamps, which wrap in a 190px column at any dialog width -
     they did before this change too. The fix is confined to the media query
     because the rule is only safe while the section is guaranteed 832px or
     more; letting it reach the single-column stack would put an unbreakable
     200px cell in a 343px dialog, which is the History regression again. */
  .dsec.span th { white-space: nowrap; }
}
/* The relative age sits under the timestamp rather than beside it. Beside it
   would add ~12 characters to a cell that is already `white-space: nowrap` at
   900px and up, which is the one place in this dialog where a long string
   cannot wrap its way out of trouble. Block, so each line is independently
   nowrap-safe, and outside the media query so the stacked layout gets the same
   two lines rather than a different shape. */
.dsec.span th .tl-rel { display: block; font-weight: 400; }
.modal-head {
  display: flex; align-items: center; padding: 14px 18px; font-size: 15px; font-weight: 600;
  color: var(--text-head); background: var(--card-head); border-bottom: 1px solid var(--border);
  border-radius: 8px 8px 0 0;
}
.modal-x { margin-left: auto; background: none; border: none; color: var(--text-dim); font-size: 22px; cursor: pointer; line-height: 1; }
.modal-x:hover { color: var(--text-head); }
.modal-body { padding: 18px; }
.modal-foot { display: flex; align-items: center; gap: 8px; padding: 12px 18px; border-top: 1px solid var(--border); background: #242424; border-radius: 0 0 8px 8px; }

/* ---- tables / log ---- */
table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--border); }
th { color: var(--text-dim); font-weight: 600; }
/* Live controls above the activity log. Not `.toolbar`, which is the tab strip
   with a bottom rule under it; this is one line of controls over a box. */
.log-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.log-live { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-dim); }
.log-live input { margin: 0; }
.log-bar .btn.small { margin-left: auto; }
/* `.btn` sets `display: inline-flex`, and an explicit display beats the user
   agent's `[hidden] { display: none }`, so the attribute alone leaves the
   button on screen. `.modal[hidden]` above exists for the same reason. */
.log-bar .btn[hidden] { display: none; }
pre.log {
  background: #181818; border: 1px solid var(--border); border-radius: 4px;
  padding: 12px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
  max-height: 260px; overflow: auto; color: #bcd; white-space: pre-wrap;
}
.flash { background: var(--accent-dim); border: 1px solid var(--accent); color: var(--text-head);
  padding: 10px 14px; border-radius: 4px; margin-bottom: 16px; }

/* ---- login ---- */
.login-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.login-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px;
  padding: 30px; width: 100%; max-width: 340px; }
.login-card .brand { border: none; justify-content: center; padding: 0 0 8px; font-size: 24px; }
.brand-logo-lg { width: 240px; max-width: 100%; height: auto; display: block; margin: 0 auto; }
.login-card p.sub { text-align: center; color: var(--text-dim); margin: 0 0 22px; font-size: 13px; }
.login-card .btn.primary { width: 100%; justify-content: center; margin-top: 18px; }
.login-error { background: #4a2224; border: 1px solid var(--danger); color: #f3b6b8;
  padding: 9px 12px; border-radius: 4px; font-size: 13px; margin-bottom: 14px; }

/* ---- responsive ----
   Below this width the shell leaves the grid entirely and returns to normal
   flow: the sidebar becomes a wrapping top nav, which already costs zero
   horizontal space (measured: 0px, against 93px of vertical). The rail solves
   a problem that does not exist down here, so it is switched off rather than
   layered on top. Everything in this block is the pre-0.4.0 mobile layout,
   deliberately unchanged. */
@media (max-width: 720px) {
  body { display: block; }
  .app-header { position: fixed; top: 0; left: 0; right: 0; }
  .header-logo { width: auto; min-width: 0; border-right: none; padding: 0 18px; justify-content: flex-start; }
  /* The offset belongs to whatever comes first in the flow, and down here that
     is the nav, not the content. The header leaves the flow (`fixed`) while
     the body leaves the grid (`display: block`), so the nav became the first
     in-flow element and sat at y=0 underneath a 66px header: measured, the
     first row of links was covered completely and the second was covered by
     20px of its 42. The 66px the content was reserving did not help it - the
     content is after the nav, so that margin only opened a 66px gap lower down
     the page. One offset, on the element that actually needs it, and both
     symptoms go: the nav clears the header and the gap closes. Every page
     inherits this because every page is this shell; the login screen has no
     header or sidebar at all, so it is unaffected. */
  .sidebar {
    position: static; top: 0; width: 100%; height: auto;
    flex-direction: row; flex-wrap: wrap; align-self: auto; overflow: visible;
    margin-top: var(--header-h);
  }
  .content { margin-left: 0; margin-top: 0; max-width: none; }
  .nav { display: flex; flex-wrap: wrap; padding: 4px; }
  .nav a { border-left: none; border-bottom: 3px solid transparent; padding: 8px 12px; }
  .nav a.active { border-left: none; border-bottom-color: var(--accent); }
  .sidebar-foot { display: none; }
  /* No rail on mobile, whatever the stored preference says. */
  .nav-toggle { display: none; }
  :root.nav-collapsed .nav .label,
  :root.nav-collapsed .brand-name {
    position: static; width: auto; height: auto; margin: 0;
    overflow: visible; clip: auto;
  }
  :root.nav-collapsed .nav a { justify-content: flex-start; padding: 8px 12px; }
  :root.nav-collapsed .header-logo { padding: 0 18px; justify-content: flex-start; }
  :root.nav-collapsed .subnav { display: flex; }
}
