/* NovaBase Management Portal (formerly Nova Solis Field Audit) - admin styling.
 *
 * Clean, calm and iOS-influenced: system typography, grouped-list cards,
 * hairline separators, generous corner radii, one accent colour, and neutral
 * surfaces throughout. The brand appears as the logo and nowhere else - an
 * operations console is not a brochure, and colour on a surface here should
 * mean something rather than say whose product it is.
 *
 * The accent is #0a6ed6, not Apple's #007aff. That is deliberate: system blue
 * measures **4.02:1** on white, which fails the 4.5:1 needed for body-sized
 * text, and this accent is used for links. #0a6ed6 reads as the same blue and
 * measures 4.98:1. Dark mode uses #0a84ff, which is 4.66:1 on the dark panel.
 *
 * Three rules are load-bearing and predate this file's current form:
 *
 *   1. --danger / --warn / --ok carry meaning and are used for NOTHING else.
 *      The accent is the only decorative colour. To make something stand out
 *      that is not a state, use weight or size, never a new hue.
 *   2. Tables stay dense. Row padding is deliberately tighter than an iOS
 *      settings list: an audit trail is scanned, not browsed.
 *   3. .killbar is loud on purpose. A global stop must never read as calm.
 */

:root {
  color-scheme: light dark;

  /* Neutrals - iOS systemGroupedBackground family */
  --bg:        #f2f2f7;
  --panel:     #ffffff;
  --panel-2:   #f2f2f7;
  --line:      rgba(60, 60, 67, 0.14);
  --line-2:    rgba(60, 60, 67, 0.08);
  --text:      #1c1c1e;
  --muted:     #6b6b70;

  /* The page ground behind the console photograph. Neutral slate rather than
     a brand colour: at 72% over the photo's brightest band (#d29053) the
     worst-case ground is #49392b, where headings measure 11.03:1, subtitles
     8.32:1 and a white panel 11.03:1 against it. */
  --page-scrim:  rgba(20, 23, 28, .72);
  --on-bg:       #ffffff;
  --on-bg-muted: rgba(255, 255, 255, .84);
  --panel-edge:  var(--line-2);

  /* Semantic - reserved, never decorative */
  --accent:    #0a6ed6;   /* iOS blue, darkened until text passes AA (4.98:1) */
  --accent-sf: rgba(10, 110, 214, 0.10);
  --ok:        #248a3d;
  --ok-sf:     rgba(52, 199, 89, 0.14);
  --warn:      #a05a00;
  --warn-sf:   rgba(255, 149, 0, 0.16);
  --danger:    #d70015;
  --danger-sf: rgba(255, 59, 48, 0.12);
  --danger-bg: #ffe5e3;

  /* Chart marks. Named separately from --accent so a chart never silently
     changes meaning when the interface accent is retuned. Each is >= 3:1 on
     the panel it is drawn on, checked rather than eyeballed. */
  --chart-accent: #0a6ed6;   /* 4.98:1 on white - site visits */
  --chart-danger: #d70015;   /* 5.38:1 - refused sign-ins, the series to watch */
  --chart-muted:  #8e949c;   /* 3.06:1 - context only, never the story */
  --chart-grid:   rgba(60, 60, 67, 0.13);

  /* The lift under the one floating accent control. A token because it has
     to be re-tinted whenever the accent moves, and as a literal it was
     missed: it stayed the old brand green under a blue button for the whole
     of the blue accent's life. */
  --accent-glow: rgba(10, 110, 214, .32);

  --shadow:    0 1px 2px rgba(0,0,0,.05), 0 4px 14px rgba(0,0,0,.05);
  --r-card:    14px;
  --r-ctl:     10px;
}

/* --- Dark mode -----------------------------------------------------------
 * Three states, the same three the operating system offers: follow the system
 * (the default, and what the portal did until now), or pin light, or pin dark.
 * An explicit choice is stored per browser and applied by a `data-theme`
 * attribute on <html> before the first paint - see the inline script in
 * base.html, which has to run in <head> or the page flashes the wrong theme.
 *
 * **The dark values are written once.** They live as `--d-*` on :root, unused
 * until one of the two selectors below adopts them. Both selectors then only
 * alias, so the two paths into dark mode cannot drift apart in what they
 * actually paint - which is the failure mode of duplicating a palette.
 *
 * Dark is chosen, not flipped: each step is re-picked against the dark panel
 * and re-checked, which is why the accent lightens while the chart grey
 * darkens rather than both inverting.
 */

:root {
  --d-bg:        #000000;
  --d-panel:     #1c1c1e;
  --d-panel-2:   #2c2c2e;
  --d-line:      rgba(84, 84, 88, 0.42);
  --d-line-2:    rgba(84, 84, 88, 0.24);
  --d-text:      #f5f5f7;
  --d-muted:     #98989f;

  --d-accent:      #0a84ff;   /* 4.66:1 on the dark panel */
  --d-accent-glow: rgba(10, 132, 255, .34);
  --d-accent-sf: rgba(10, 132, 255, 0.18);
  --d-ok:        #30d158;
  --d-ok-sf:     rgba(48, 209, 88, 0.16);
  --d-warn:      #ff9f0a;
  --d-warn-sf:   rgba(255, 159, 10, 0.16);
  --d-danger:    #ff453a;
  --d-danger-sf: rgba(255, 69, 58, 0.18);
  --d-danger-bg: #3a1a18;

  --d-chart-accent: #4aa3ff;   /* 6.46:1 on #1c1c1e */
  --d-chart-danger: #ff453a;   /* 4.99:1 */
  --d-chart-muted:  #6b6b73;   /* 3.22:1 on the dark panel */
  --d-chart-grid:   rgba(84, 84, 88, 0.34);

  /* Dark mode cannot separate a dark panel from a dark ground by lightness -
     every scrim value lands between 1.0 and 1.2:1. So the ground goes darker
     (the photo becomes texture rather than picture) and the panels separate
     by EDGE instead: a visible border and a shadow. That is a real limit of
     putting a photograph behind a dark interface, not an oversight. */
  --d-page-scrim: rgba(8, 10, 13, .86);
  --d-panel-edge: rgba(255, 255, 255, .14);
}

/* Adopting the dark palette. Kept as one declaration list applied by two
   selectors, so there is exactly one place to add a token to. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: var(--d-bg); --panel: var(--d-panel); --panel-2: var(--d-panel-2);
    --line: var(--d-line); --line-2: var(--d-line-2);
    --text: var(--d-text); --muted: var(--d-muted);
    --accent: var(--d-accent); --accent-sf: var(--d-accent-sf);
    --accent-glow: var(--d-accent-glow);
    --ok: var(--d-ok); --ok-sf: var(--d-ok-sf);
    --warn: var(--d-warn); --warn-sf: var(--d-warn-sf);
    --danger: var(--d-danger); --danger-sf: var(--d-danger-sf);
    --danger-bg: var(--d-danger-bg);
    --chart-accent: var(--d-chart-accent); --chart-danger: var(--d-chart-danger);
    --chart-muted: var(--d-chart-muted); --chart-grid: var(--d-chart-grid);
    --page-scrim: var(--d-page-scrim); --panel-edge: var(--d-panel-edge);
    --shadow: none;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: var(--d-bg); --panel: var(--d-panel); --panel-2: var(--d-panel-2);
  --line: var(--d-line); --line-2: var(--d-line-2);
  --text: var(--d-text); --muted: var(--d-muted);
  --accent: var(--d-accent); --accent-sf: var(--d-accent-sf);
  --accent-glow: var(--d-accent-glow);
  --ok: var(--d-ok); --ok-sf: var(--d-ok-sf);
  --warn: var(--d-warn); --warn-sf: var(--d-warn-sf);
  --danger: var(--d-danger); --danger-sf: var(--d-danger-sf);
  --danger-bg: var(--d-danger-bg);
  --chart-accent: var(--d-chart-accent); --chart-danger: var(--d-chart-danger);
  --chart-muted: var(--d-chart-muted); --chart-grid: var(--d-chart-grid);
    --page-scrim: var(--d-page-scrim); --panel-edge: var(--d-panel-edge);
  --shadow: none;
  color-scheme: dark;
}

/* Pinning light must also tell the browser, or form controls and scrollbars
   stay dark while the page turns light. */
:root[data-theme="light"] { color-scheme: light; }

* { box-sizing: border-box; }

/* --- Reduced motion -----------------------------------------------------
 * Nothing here is elaborate - hover washes, a 1px lift on the help button,
 * a fade on chart columns - which is exactly why honouring the preference
 * costs nothing. Durations are near-zeroed rather than removed so anything
 * relying on a transitionend still fires.
 *
 * The transform is named separately: a duration of 0 makes it instant, not
 * absent, and the point of the preference is that the element does not move
 * at all. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .help-drawer { transition: none; }
}

/* The hidden attribute must win over any layout rule.
 *
 * An author stylesheet always beats the browser's built-in
 * [hidden]{display:none}, so any component styled with `display:` and toggled
 * by `.hidden = true` stays on screen - which is how the help drawer shipped
 * pinned open over every page. Stated once, globally, so the next component
 * toggled this way cannot repeat it. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text",
               "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  font-size: 15px;
  line-height: 1.47;
  -webkit-font-smoothing: antialiased;
}

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

/* --- Focus --------------------------------------------------------------
 * Inputs had a considered ring; everything you can actually press had
 * whatever the browser drew by default, which on a filled .btn.primary or
 * .btn.danger is close to invisible. Every mutating action in this portal
 * requires a typed reason precisely because it is consequential, so a
 * keyboard user has to be able to see which one is about to fire.
 *
 * :focus-visible rather than :focus, so a mouse click does not leave a ring
 * behind. An outline rather than a box-shadow, because outline follows
 * border-radius, cannot be clipped by an ancestor's overflow, and survives
 * on the pill controls and the round help buttons unchanged.
 *
 * The 2px offset is what makes it work on a filled button: the ring lands on
 * the panel outside the shape rather than on an accent field where an accent
 * ring would disappear. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- Skipping the rail --------------------------------------------------
 * Nine navigation destinations are re-rendered on every page, so a keyboard
 * or screen-reader user tabbed through all nine before reaching the audit
 * table - on every single page load.
 *
 * Hidden by clipping rather than display:none or visibility:hidden, because
 * both of those remove it from the focus order and a skip link that cannot
 * be focused is not a skip link. */
.skip {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.skip:focus {
  position: fixed;
  top: 12px; left: 12px;
  z-index: 100;
  width: auto; height: auto;
  margin: 0; padding: 10px 16px;
  overflow: visible;
  clip-path: none;
  border-radius: var(--r-ctl);
  background: var(--accent);
  color: #fff;
  font-weight: 600;
  box-shadow: var(--shadow);
}
/* main takes focus when the link is followed, so the next Tab continues from
   the content rather than jumping back to the top of the document. It is only
   ever focused programmatically, so it gets no ring of its own. */
main:focus { outline: none; }

/* --- Sidebar ------------------------------------------------------------
 * The navigation is a left rail. There are eight destinations and the list
 * will grow, which a horizontal bar handles badly - it either wraps or it
 * scrolls sideways, and both hide the item you wanted. Vertically there is
 * room for all of them and for the labels to stay whole words.
 *
 * It is sticky and full height so the rail does not scroll away from a long
 * audit table, and the signed-in identity sits at the bottom rather than the
 * top: it is the thing you look at least and click least. */

.shell { display: flex; align-items: flex-start; min-height: 100vh; }

.sidebar {
  flex: 0 0 236px;
  width: 236px;
  align-self: stretch;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 20px 14px 16px;
  background: var(--panel);
  border-right: 1px solid var(--line-2);
  z-index: 20;
}

.sidebar .brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 8px;
  color: var(--text);
}
.sidebar .brand:hover { text-decoration: none; }
.sidebar .brand img { display: block; flex: none; }
.sidebar .brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.sidebar .brand-text strong { display: block; line-height: 0; }

/* The NovaBase wordmark (1.79.0) is the client's lettering, so it is artwork,
 * in two colours: black for the light rail, white for the dark one. Exactly
 * one is shown. The rules follow the theme tokens' own pattern - the system
 * preference unless the browser has an explicit choice stored, then the
 * explicit choice - so the wordmark can never disagree with the rail behind
 * it. Shown at 16px tall from a 48px-tall file, so it stays sharp at 3x. */
.sidebar .brand-wordmark { width: 111px; height: 16px; margin: 1px 0 3px; }
/* `img.` here is load-bearing: `.sidebar .brand img { display: block }` above
   is more specific than a bare class, and would show both wordmarks. */
.sidebar .brand img.brand-wordmark-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sidebar .brand-wordmark-light { display: none; }
  :root:not([data-theme="light"]) .sidebar .brand-wordmark-dark { display: block; }
}
:root[data-theme="dark"] .sidebar .brand-wordmark-light { display: none; }
:root[data-theme="dark"] .sidebar .brand-wordmark-dark { display: block; }
/* "Management Portal" is longer than the "Field Audit" it replaced, and the
   row also holds the collapse button, so at 12px it broke onto two lines.
   One line at 11px fits the space under the wordmark exactly. */
.sidebar .brand-text em {
  font-style: normal;
  font-size: 11px;
  font-weight: 500;
  white-space: nowrap;
  color: var(--muted);
}

.sidebar nav { flex: 1; }
.sidebar ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.sidebar ul a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 8px;
  color: var(--muted);
  font-size: 14px;
  font-weight: 500;
  transition: background .15s ease, color .15s ease;
}

/* The icon strokes in currentColor, so the active item tints its icon with the
   same rule that tints its label and there is no second state to keep in step.
   It never shrinks: flex-basis is fixed, so a long label wraps or clips before
   the icon is squeezed into an unreadable smear. */
.nav-icon { flex: 0 0 20px; width: 20px; height: 20px; }

/* --- The collapsed rail --------------------------------------------------
 * Narrowed to icons, never hidden. Every destination is still on screen and
 * still one click away; only the words go.
 *
 * **This does not contradict the note above the shell in base.html.** That
 * note says a navigation depending on JavaScript is one that can fail to
 * appear, and it still holds: the rail is served expanded and fully working
 * with no script, and script can only ever narrow it. There is no state in
 * which a destination is missing, and a browser that never runs the toggle
 * gets precisely what was there before.
 *
 * Applied from an attribute on <html>, set by the head script before paint -
 * on .content or via a class added later, the rail renders 236px wide and
 * then visibly snaps narrow on every load. */
:root[data-rail="collapsed"] .sidebar { flex-basis: 64px; width: 64px; padding: 20px 10px 16px; }
:root[data-rail="collapsed"] .sidebar .brand { justify-content: center; }
:root[data-rail="collapsed"] .sidebar .brand-text { display: none; }
:root[data-rail="collapsed"] .sidebar ul a { justify-content: center; padding: 9px 0; }

/* Clipped rather than display:none. The icon is aria-hidden, so the label is
   the link's only accessible name - hiding it outright would leave a screen
   reader ten unnamed links. */
:root[data-rail="collapsed"] .sidebar ul a span {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.rail-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  background: var(--panel);
  color: var(--muted);
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.rail-toggle:hover { color: var(--text); border-color: var(--line); }
/* The brand and the collapse control share the rail's top row. */
.rail-head { display: flex; align-items: center; gap: 8px; }
.rail-head .brand { flex: 1 1 auto; min-width: 0; }

/* Collapsed, the row stacks so both stay inside 64px and both stay centred.
   Side by side at that width the button would push the mark off the edge. */
:root[data-rail="collapsed"] .rail-head { flex-direction: column; gap: 10px; }
:root[data-rail="collapsed"] .rail-head .brand { flex: 0 0 auto; }
.sidebar ul a:hover { color: var(--text); background: var(--panel-2); text-decoration: none; }
/* The accent on a wash of itself. The rail is a white panel, so the accent
   reads cleanly on it and weight carries the rest. (This once described a
   green rail the accent had to avoid; there is no green rail any more.) */
.sidebar ul a.on { color: var(--accent); background: var(--accent-sf); font-weight: 600; }

/* The build stamp at the foot of the rail. Deliberately quiet - it is
   reference, not navigation, and it is read once a month. */
.rail-version {
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid var(--line-2);
  font-size: 11px;
  line-height: 1.5;
  color: var(--muted);
  display: flex;
  flex-direction: column;
}
.rail-build { font-size: 10.5px; opacity: .8; }
/* Collapsed, the hash goes and the version stays. Seven hex characters in a
   64px column is a smear; the version is the half anybody glances at. */
:root[data-rail="collapsed"] .rail-version { align-items: center; text-align: center; }
:root[data-rail="collapsed"] .rail-build { display: none; }

/* Help is navigation, but it is not one of the seven things this portal is
   for. Separated by a hairline so it does not compete with them. */
.sidebar ul.minor { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line-2); }

button.link {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  font-size: 14px;
  padding: 0;
}
button.link:hover { text-decoration: underline; }

/* --- Layout ------------------------------------------------------------ */

.content { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; min-height: 100vh; }

/* Wider than a reading measure on purpose. This is a console read during an
 * incident, not an article: the audit trail is six columns and the session
 * page's register tables are wider again, and at 1180px minus the 236px rail
 * they had under 950px to work in - which is why .scroll had to exist at all.
 * Prose that would sprawl at this width is capped where it sits, below. */
main { width: 100%; max-width: 1400px; margin: 0 auto; padding: 30px 28px 60px; }

footer {
  width: 100%;
  max-width: 1400px;
  margin: auto auto 0;   /* pinned to the bottom of a short page */
  padding: 0 28px 30px;
  color: var(--muted);
  font-size: 12px;
}

h1 { font-size: 27px; margin: 0 0 4px; font-weight: 700; letter-spacing: -.5px; }
.login-wrap h1 { color: var(--muted); }
h2 { font-size: 19px; margin: 34px 0 12px; font-weight: 650; letter-spacing: -.3px; }
h3 { font-size: 12px; margin: 0 0 12px; font-weight: 600; text-transform: uppercase;
     letter-spacing: .5px; color: var(--muted); }

/* The one piece of running prose at the top of every page, so it gets a
   measure of its own rather than following the table width. */
.sub { color: var(--muted); margin: 0 0 24px; max-width: 74ch; }

.panel {
  background: var(--panel);
  border: 1px solid var(--line-2);
  border-radius: var(--r-card);
  padding: 20px 22px;
  margin-bottom: 18px;
  box-shadow: var(--shadow);
}

.grid { display: grid; gap: 18px; }
.grid.two { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
.grid.three { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

/* --- Statistics -------------------------------------------------------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.stat {
  background: var(--panel);
  border: 1px solid var(--line-2);
  border-radius: var(--r-card);
  padding: 14px 18px 16px;
  box-shadow: var(--shadow);
}
/* A tile reporting a problem still says so; a healthy one is just a card. */
.stat.attention { border-top: 3px solid var(--warn); }
.stat.alert     { border-top: 3px solid var(--danger); }
.stat .n { font-size: 30px; font-weight: 700; line-height: 1.15; letter-spacing: -.8px;
           font-variant-numeric: tabular-nums; }
.stat .l { color: var(--muted); font-size: 13px; margin-top: 2px; }
.stat.alert { border-color: var(--danger);
              background: linear-gradient(var(--danger-sf), var(--danger-sf)), var(--panel); }
.stat.alert .n { color: var(--danger); }
.stat.attention .n { color: var(--warn); }

/* --- Tables ------------------------------------------------------------
 * Deliberately tighter than an iOS list. This is scanned during an incident. */

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line-2); }
/* Muted, not accent. `a` is the accent, and in a console that is mostly
 * dense tables an accent-coloured column header reads as a link - which is
 * the first thing somebody reaches for during an incident, looking to sort.
 * It also broke rule 1 in the header: the accent is the only decorative
 * colour, and a header is not a state.
 *
 * Nothing is lost by it. The header is already uppercase, letterspaced, 600
 * and 11px against 15px body - four signals without spending a hue. And the
 * contrast improves: --muted measures 5.30:1 on the light panel and 5.94:1
 * on the dark one, against the accent's 4.98:1 and 4.66:1. */
th {
  color: var(--muted);
  background: var(--panel);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .5px;
  font-weight: 600;
}
th:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
th:last-child  { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
tbody tr { transition: background .12s ease; }
tbody tr:hover { background: var(--panel-2); }
tbody tr:last-child td { border-bottom: none; }
/* The header has to move with the cell. Without the th rule the column
   heading stayed left while its values right-aligned, so a single digit sat
   alone at the far edge of a wide column looking like a mistake - which is
   exactly what it looked like. */
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* A machine value - a register address, a hash, a mask. Muted and a size
   down, so it reads as a reference rather than as prose. There was no rule
   for this element at all, so it fell back to the browser default. */
code {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: .92em;
  color: var(--muted);
}
.mono { font-family: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
        font-size: 12.5px; }
.empty { color: var(--muted); padding: 34px 10px; text-align: center; }

/* The description column on the register reference and the settings table.
   Given a minimum width and allowed to wrap, because it is the only column
   holding a sentence: without this the browser hands the space to the short
   columns and squeezes the prose into a two-word ribbon down the page. The
   max stops it running to a line length nobody can track back from. */
.reg-what {
  min-width: 260px;
  max-width: 520px;
  white-space: normal;
  color: var(--muted);
}

/* --- Sorting and filtering -----------------------------------------------
 * A sortable heading is a link, and it has to keep looking like a heading:
 * these sit in a row of them, and one styled as body-text blue would read as
 * the odd one out rather than as the interactive one. So it inherits the
 * heading's colour and size, and earns its emphasis only when it is the
 * column actually in force.
 */
th .sortlink {
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}
/* Underlined on hover. It is a link, and a header that gives back no cue at
   all until something moves is a header nobody tries. */
th .sortlink:hover { color: var(--text); text-decoration: underline; }
th .sortlink.on { color: var(--accent); }
.sortarrow { font-size: 9px; line-height: 1; }
/* The affordance, not the state: faint enough to read as "this can be
   sorted" rather than as "this is sorted", which is what the single arrow on
   the active column says. It lifts on hover so the header answers the
   pointer before the click. */
.hint-arrow { font-size: 10px; opacity: .35; }
th .sortlink:hover .hint-arrow { opacity: .8; }

/* A row that stands for one record, and opens it.

   The attribute is set by script on the tbody, never in the markup, so the
   pointer never promises a click that a page with no script cannot honour -
   the row's own anchor is what works there. */
tbody[data-rows-live] tr { cursor: pointer; }
/* The in-page version is a <button>, built by script, and has to be dressed
   back down to look like the heading it replaced. */
th button.sortlink {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-align: inherit;
}

/* The in-page filter. Sits directly above its table rather than with the
   server-side filters, because it does something different and conflating the
   two teaches people that one of them does not work. */
.quickfilter {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}
.quickfilter label { margin: 0; font-size: 13px; color: var(--muted); }
.quickfilter input { width: 260px; max-width: 100%; margin: 0; }
.quickfilter-count { font-size: 13px; color: var(--muted); }

/* --- Key/value tables ---------------------------------------------------
 * A fact list that happens to be a table: one row per fact, the label in a
 * row header. The visit, the wizard answers, the snapshots and the relay
 * board on a session page are all built this way.
 *
 * **It needs its own rule, and shipped without one for four uses.** The
 * generic table styling above is for data scanned IN COLUMNS, and every part
 * of it is wrong applied to a fact list: accent-blue uppercase labels read as
 * clickable column headings, an even split gives "Started" the same width as
 * the timestamp beside it, and a hover band sweeps a row that has nothing to
 * scan across. Third time this has happened - see .quiet and .scroll above -
 * so the check is worth keeping: a class named in a template and absent here
 * does not fail, it silently inherits something built for another job.
 *
 * Same shape as dl.facts, which is the right component when the markup is a
 * real definition list. A table earns its place here because these rows are
 * built conditionally, and one <tr> is easier to wrap in an {% if %} than a
 * dt/dd pair that must not be separated.
 */
table.kv th {
  width: 1%;                 /* shrink-to-fit: as wide as the longest label */
  white-space: nowrap;
  padding-left: 0;
  background: none;
  color: var(--muted);
  font-size: 13px;
  font-weight: 500;
  text-transform: none;
  letter-spacing: normal;
  border-radius: 0;
  vertical-align: baseline;
}
table.kv td { vertical-align: baseline; }
table.kv th, table.kv td { border-bottom: 1px solid var(--line-2); }
table.kv tbody tr:last-child th,
table.kv tbody tr:last-child td { border-bottom: none; }
/* Nothing to scan across, so nothing to track with. */
table.kv tbody tr:hover { background: none; }

/* --- Badges ------------------------------------------------------------ */

.badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  border: 1px solid transparent;
  white-space: nowrap;
}
.badge.ok      { color: var(--ok);     background: var(--ok-sf); }
.badge.warn    { color: var(--warn);   background: var(--warn-sf); }
.badge.danger  { color: var(--danger); background: var(--danger-sf); }
.badge.quiet   { color: var(--muted);  background: var(--panel-2); border-color: var(--line-2); }

/* --- Forms ------------------------------------------------------------- */

label { display: block; margin: 16px 0 6px; color: var(--muted); font-size: 13px; font-weight: 500; }

input[type=text], input[type=email], input[type=password], input[type=number],
select, textarea {
  width: 100%;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3.5px var(--accent-sf);
}
textarea { min-height: 74px; resize: vertical; }

.check { display: flex; align-items: center; gap: 10px; margin: 14px 0; }
.check input { width: auto; accent-color: var(--accent); }
.check label { margin: 0; color: var(--text); font-size: 15px; font-weight: 400; }

.btn {
  display: inline-block;
  padding: 9px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  transition: filter .15s ease, background .15s ease, border-color .15s ease;
}
.btn:hover { background: var(--panel-2); text-decoration: none; }
.btn:active { filter: brightness(.94); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent); filter: brightness(1.08); }
.btn.danger  { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn.danger:hover { background: var(--danger); filter: brightness(1.08); }

.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }

/* A button sized for a table row, so an action column does not set the row
   height. */
.btn.small { padding: 4px 11px; font-size: 13px; }

/* Controls that belong to the table directly beneath them. `.push` sends
   one to the far end of the row. */
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 10px; }
.toolbar .push { margin-left: auto; }

/* Dense rows for a list read at a glance. Desktop only in effect: the phone
   card layout sets its own cell padding with a more specific rule. */
table.compact th,
table.compact td { padding-top: 6px; padding-bottom: 6px; }
table.compact td .btn.small + .btn.small { margin-left: 4px; }

/* A centred popup. Same scrim depth as the help drawer, and opaque on its
   own panel, so it reads the same on the photograph and in both themes. */
.modal-scrim {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: rgba(0,0,0,.34);
}
.modal {
  position: fixed;
  z-index: 50;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(520px, calc(100vw - 32px));
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 18px 50px rgba(0,0,0,.24);
}
.modal header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 18px 20px 12px;
  border-bottom: 1px solid var(--line-2);
}
.modal header h2 { font-size: 19px; margin: 0; font-weight: 650; }
.modal-body { padding: 14px 20px 20px; }
.modal-body .actions { justify-content: flex-end; }

.hint { color: var(--muted); font-size: 13px; margin: 7px 0 0; }

/* Absent data, inline. Used wherever a value is missing on purpose - "never",
 * "not given", "not recorded", an em dash in a table cell - and it must not
 * read as a value. Distinct from .hint, which is a block note beneath a
 * control and carries a top margin; this sits inside a sentence or a cell.
 *
 * Was referenced by 15 spans across the templates with no rule behind it, so
 * every one rendered at full contrast: "no serial link recorded" looked
 * exactly as authoritative as a port number would have. */
.quiet { color: var(--muted); }

/* Wide tables scroll inside their own box rather than pushing the whole page
 * sideways. Also referenced with no rule behind it, which is why the widest
 * tables simply overflowed their panel. */
.scroll { overflow-x: auto; }

/* A column worth having on a desk and not worth scrolling to on a phone.
 *
 * **The rule for applying it:** a column may only carry this if its absence
 * changes nothing about what the row *means*. "Laptop" and "Last beat" can
 * go from the service records list; "State" cannot, because a row reading
 * `abandoned` and one reading `closed` are different facts and hiding the
 * column would make them look identical. Everything hidden is still on the
 * record the row opens, which is one tap away.
 *
 * It has no rule of its own on a desk - the column simply shows. The whole
 * behaviour is the one declaration in the touch block below. */

/* --- Notices ----------------------------------------------------------- */

.notice {
  border: 1px solid var(--line-2);
  border-left-width: 3px;
  border-radius: var(--r-ctl);
  padding: 13px 16px;
  margin-bottom: 18px;
}
/* **A `*-sf` token is a TINT, not a background.** Each is a 10-18% wash, so a
 * surface painted with one alone is ~90% transparent. On an opaque panel that
 * reads exactly as intended, which is where they were all designed and where
 * they were all looked at.
 *
 * Directly on `body.app` they do not. The page ground is a dark photograph, so
 * a notice sitting straight on it kept light mode's near-black `--text` over a
 * near-black picture and **the message could not be read at all** - reported
 * on /admin/machines, where the banner explaining that these controls are not
 * a security boundary was invisible. Dark mode hid the bug rather than
 * escaping it: `--text` is near-white there, so it looked fine.
 *
 * The fix is to layer the wash over an opaque panel rather than over whatever
 * happens to be behind. Stated for every tinted surface that can sit on the
 * ground, not just the one that was reported, because the next one added will
 * inherit the fix rather than the bug. */
.notice         { background: var(--panel); }
.notice.error   { border-left-color: var(--danger);
                  background: linear-gradient(var(--danger-sf), var(--danger-sf)), var(--panel); }
.notice.warn    { border-left-color: var(--warn);
                  background: linear-gradient(var(--warn-sf), var(--warn-sf)), var(--panel); }
.notice.info    { border-left-color: var(--accent);
                  background: linear-gradient(var(--accent-sf), var(--accent-sf)), var(--panel); }
.notice.ok      { border-left-color: var(--ok);
                  background: linear-gradient(var(--ok-sf), var(--ok-sf)), var(--panel); }

/* A global stop must never read as calm. This one ignores the design. */
.killbar {
  background: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--r-card);
  padding: 16px 20px;
  margin-bottom: 22px;
  color: #fff;
  font-weight: 500;
  box-shadow: 0 2px 12px rgba(215, 0, 21, .3);
}
.killbar strong { color: #fff; font-weight: 700; }
.killbar a { color: #fff; text-decoration: underline; }

/* --- Definition lists -------------------------------------------------- */

dl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 9px 22px; margin: 0; }
dl.facts dt { color: var(--muted); font-size: 13px; }
dl.facts dd { margin: 0; }

/* --- Credential display ------------------------------------------------ */

.credential {
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
  font-size: 23px;
  letter-spacing: 2px;
  padding: 20px 22px;
  background: var(--panel-2);
  border: 1px dashed var(--accent);
  border-radius: var(--r-card);
  user-select: all;
  word-break: break-all;
  text-align: center;
}

/* --- Audit before/after ------------------------------------------------ */

details.diff summary { cursor: pointer; color: var(--accent); font-size: 13px; }
details.diff pre {
  background: var(--panel-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-ctl);
  padding: 12px;
  overflow-x: auto;
  font-size: 12.5px;
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
  margin: 10px 0 0;
}
.diffcols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

/* --- Filters ----------------------------------------------------------- */

.filters { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 18px; }
.filters label { margin: 0 0 5px; }
.filters > div { min-width: 150px; }

/* --- Login ------------------------------------------------------------- */

.login-wrap { max-width: 400px; margin: 10vh auto; }
.login-wrap h1 { text-align: center; margin-bottom: 26px; font-weight: 600;
                 letter-spacing: -.2px; color: var(--muted); font-size: 19px; }

/* Intrinsic size is on the tag so the form does not jump when the image
   arrives; height:auto keeps the aspect ratio once CSS takes over. */
.login-logo { display: block; width: 180px; height: auto; margin: 0 auto 10px; }

/* --- Page header ------------------------------------------------------- */

.pagehead { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.pagehead .actions { margin-top: 0; }

/* --- Section sub-navigation --------------------------------------------
 *
 * The NOV-FC1 section is six pages behind one rail tab. This is how you move
 * between them. It sits above the page heading rather than inside it, so it
 * reads as "where you are in this section" rather than as an action on the
 * page - which is how the Dashboard button row it replaced read, and why that
 * row ended up on a page the section has nothing to do with.
 */
.subnav {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin: 0 0 20px;
  border-bottom: 1px solid var(--line-2);
}
.subnav a {
  padding: 8px 14px;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  /* Pulls the active underline onto the container's own border rather than
     stacking a second line beneath it. */
  margin-bottom: -1px;
}
.subnav a:hover { color: var(--text); }
.subnav a.on { color: var(--accent); border-bottom-color: var(--accent); }

/* --- Collapsible explanation -------------------------------------------
 *
 * For prose that earns its place but does not need to be on screen every
 * time somebody opens the page. The Enforcement page is the case that forced
 * it: every control there carried a paragraph explaining itself, which is
 * right the first time you meet it and noise on the twentieth visit.
 *
 * The rule, and the reason this is a named class rather than a bare
 * <details>: **explanation collapses, state and warnings never do.** A kill
 * switch that is on, a floor sitting above the current build, a count of
 * laptops about to be blocked - those are what the page is for, and nobody
 * should have to open a disclosure to find out the fleet is stopped.
 */
.explain { margin: 0 0 14px; }
.explain summary {
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  list-style: none;
  display: inline-flex;
  gap: 7px;
  align-items: baseline;
}
.explain summary::-webkit-details-marker { display: none; }
.explain summary::before { content: "+"; font-weight: 700; width: 10px; }
/* A CSS escape, not the literal character - see the note on .help-topic,
   where a raw U+2212 lost two of its three UTF-8 bytes in transit. */
.explain[open] summary::before { content: "\2212"; }
.explain > *:not(summary) {
  margin: 9px 0 0 17px;
  font-size: 13px;
  color: var(--muted);
  max-width: 76ch;
}

/* --- A panel that starts collapsed --------------------------------------
 *
 * Used for the bench version channel. Both channels are editable and neither
 * is hidden, but the field channel governs RAMAC_Field_Service.exe on real
 * laptops at real sites, and the bench channel governs a build that by
 * definition stays on a bench. Rendering both at full height made the page
 * twice as long for a control most visits never touch.
 */
details.panel > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 650;
  font-size: 15px;
}
details.panel > summary::-webkit-details-marker { display: none; }
details.panel > summary::before {
  content: "+";
  color: var(--accent);
  font-weight: 700;
  width: 12px;
}
details.panel[open] > summary::before { content: "\2212"; }
details.panel[open] > summary { margin-bottom: 14px; }



/* Below 900px the rail becomes a band across the top and the links scroll
   sideways. Deliberately not an off-canvas drawer: that needs script, and a
   navigation that can fail to appear is worse than one that is a bit cramped.
   The desktop collapse toggle does not apply here and hides itself - the band
   is already compact, and there is nothing left to narrow. */
@media (max-width: 900px) {
  .shell { display: block; }

  /* --- The rail becomes a header ----------------------------------------
   * **Two rows, and the second is icons only.**
   *
   * This has been wrong twice, in opposite directions, and the second
   * attempt looked exactly like the fix for the first. It began as one row
   * that **scrolled**, which put three of nine destinations off the right
   * edge with nothing on screen saying they were there. Wrapping was the
   * obvious answer and was worse: the brand sits in the same row, so the
   * navigation got a narrow column to wrap inside and the nine items stacked
   * almost vertically, taking half a phone screen before any content
   * appeared.
   *
   * The measurement that settles it: 390px less padding leaves about 360px,
   * and ten targets in that is 36px each (nine, before NOV-FC1 units joined
   * the rail, was 40). An icon is 20px. A label is not -
   * "Service records" needs about 90 - so **there is no arrangement in which
   * ten labels fit across a phone.** Either something scrolls, something
   * wraps, or the labels go. The labels go.
   *
   * They go from the screen, not from the markup: the span is clipped
   * exactly as the collapsed desktop rail clips it, so every link keeps its
   * accessible name and a screen reader still reads ten named
   * destinations. */
  .rail-toggle { display: none; }

  /* The collapsed desktop state is neutralised, because the band is already
     icon-only - there is nothing left for it to narrow, and a rail collapsed
     on a desk must not follow the same account onto a phone as something
     visibly different. */
  :root[data-rail="collapsed"] .sidebar { flex-basis: auto; width: auto; }

  .sidebar {
    position: static;
    width: auto;
    height: auto;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    padding: 8px 12px 0;
    border-right: none;
    /* The rail's own edge, kept when it becomes a band. Without it the
       navigation and the content share one surface with nothing between
       them - and on a signed-in page the band sits on the photograph, where
       an unbounded panel reads as a rendering fault. */
    border-bottom: 1px solid var(--line-2);
    overflow: visible;
  }

  /* Row one: who this is. */
  .sidebar .rail-head,
  :root[data-rail="collapsed"] .sidebar .rail-head {
    flex-direction: row;
    justify-content: flex-start;
    gap: 8px;
    padding: 0 2px;
  }
  .sidebar .brand,
  :root[data-rail="collapsed"] .sidebar .brand { justify-content: flex-start; }
  .sidebar .brand-text,
  :root[data-rail="collapsed"] .sidebar .brand-text { display: flex; }
  .sidebar .brand-text em { display: none; }

  /* Row two, and it is **fixed to the foot of the screen**.
   *
   * A navigation that scrolls away is one you have to scroll back for, and
   * on a list of two hundred service records that is the length of the page.
   * At the bottom it is also where a thumb already is, which is the reason
   * every application on the device it is being read on puts it there.
   *
   * Taken out of flow, so `main` and the footer have to be told to clear it -
   * see the padding below. `env(safe-area-inset-bottom)` keeps it above the
   * home indicator on a phone that has one and resolves to zero on every
   * phone that does not, so it costs nothing to include.
   *
   * Below the help drawer's scrim at 45, deliberately: the drawer is modal
   * and must cover the navigation rather than leave it live underneath. */
  .sidebar nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    display: flex;
    align-items: stretch;
    padding: 5px 8px calc(5px + env(safe-area-inset-bottom));
    background: var(--panel);
    border-top: 1px solid var(--line-2);
  }
  .sidebar ul {
    flex: 1 1 auto;
    flex-direction: row;
    flex-wrap: nowrap;
    justify-content: space-around;
    gap: 0;
    min-width: 0;
  }
  .sidebar ul.minor {
    flex: 0 0 auto;
    justify-content: flex-end;
    margin: 0;
    padding: 0 0 0 6px;
    border-top: none;
    border-left: 1px solid var(--line-2);
  }
  .sidebar ul a,
  :root[data-rail="collapsed"] .sidebar ul a {
    flex-direction: column;
    justify-content: center;
    padding: 8px 5px;
    border-radius: 8px;
  }

  /* Clipped, not removed. The icon is aria-hidden, so the label is the
     link's only accessible name - dropping it would leave ten unnamed
     links. Same technique as the collapsed rail, same reason. */
  .sidebar ul a span,
  :root[data-rail="collapsed"] .sidebar ul a span {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* Reference, read once a month, and it was floating in the middle of the
     band looking like a rendering fault. It is on the profile page and in
     /health; it does not need to cost a phone a row. */
  .rail-version { display: none; }

  /* Clearance for the fixed bar. Without it the last row of the last table
     sits underneath the navigation, and on a page that ends in a table that
     is a row you cannot read and cannot scroll to - the page is already at
     its end.
     
     **On the column, not on `main` and `footer` separately.** Both of those
     are re-declared with the `padding` shorthand in the 720px block below,
     which comes later in the file and silently resets a `padding-bottom` set
     here - the footer was still tucked under the bar for exactly that
     reason. The column is not touched by either rule. */
  .content {
    min-height: 0;
    padding-bottom: calc(78px + env(safe-area-inset-bottom));
  }
}

/* --- Touch ---------------------------------------------------------------
 * Everything here is about a finger rather than a narrow window, so it keys
 * off the pointer rather than the width alone. `coarse` covers a phone and a
 * tablet; the width bound keeps a touchscreen laptop on the desk layout it
 * has the room for.
 *
 * 44px is the size a person hits reliably. The controls here were about 37 -
 * 9px of padding on a 15px face - which is fine for a mouse and a coin toss
 * for a thumb. */
@media (max-width: 900px), (pointer: coarse) {
  .btn,
  .rail-toggle,
  .help-trigger,
  .account-btn,
  th .sortlink,
  .account-menu a,
  .account-menu button,
  .sidebar ul a {
    min-height: 44px;
  }
  .btn, .help-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* The header stays 11px; the link inside it grows to a target. */
  th .sortlink { align-items: center; }
}

@media (max-width: 720px) {
  main { padding: 22px 16px 50px; }

  /* **16px exactly, and not one pixel less.** Safari on iOS zooms the
     viewport when a field smaller than this takes focus, and does not zoom
     back out afterwards - so signing in on an iPhone left the whole console
     magnified and off-centre, with no way back but a manual pinch. The
     desktop size is 15px and stays 15px; this is the phone's. */
  input[type=text], input[type=email], input[type=password],
  input[type=number], input[type=search], input[type=tel], select, textarea {
    font-size: 16px;
  }

  /* See the note on .col-wide above. It serves the band between 721px and
     900px, where a table is still a table and dropping two columns helps.
     Below 720px the cards take over and give every column back. */
  .col-wide { display: none; }

  /* --- List rows become cards -------------------------------------------
   * **Six columns do not fit 390px, and no amount of padding changes that.**
   * Hiding columns bought room and cost information; tightening cell, panel
   * and badge padding bought a few pixels and still clipped the last badge
   * by a character. Each attempt traded away something somebody needed.
   *
   * So below 720px a row stops being a table row. It becomes a block with
   * one label/value pair per line - which is what `table.kv` already does on
   * the detail pages, so the idea is not new here and neither is the
   * mechanism.
   *
   * **Every column comes back.** A card has vertical room, which is the one
   * thing a phone has plenty of, so `.col-wide` is overridden rather than
   * respected and nothing is hidden any more.
   *
   * The header row is **not** hidden. It is laid out as a wrapped strip of
   * sort links above the cards, so sorting survives - it is server-side, it
   * is the only ordering control on the page, and dropping it on a phone
   * would mean the newest-first default is the only order a phone can ever
   * see. */
  .cardlist,
  .cardlist tbody,
  .cardlist tr,
  .cardlist td { display: block; width: auto; }

  .cardlist thead { display: block; }
  .cardlist thead tr {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 14px;
    padding: 0 2px 10px;
    background: none;
    border: none;
  }
  .cardlist thead th {
    display: block;
    padding: 4px 0;
    background: none;
    border: none;
    border-radius: 0;
  }

  .cardlist tbody tr {
    border: 1px solid var(--line-2);
    border-radius: 10px;
    padding: 8px 12px;
    margin-bottom: 10px;
    background: var(--panel-2);
  }
  .cardlist tbody tr:hover { background: var(--panel-2); }

  .cardlist tbody td {
    display: flex;
    gap: 14px;
    align-items: baseline;
    justify-content: space-between;
    padding: 4px 0;
    border: none;
    text-align: right;
  }
  /* The label comes from the cell, not from a second copy of the heading
     kept in the stylesheet - so a column renamed in the template renames
     itself here. A cell with no `data-label` renders its value full width,
     which is what an actions cell wants. */
  .cardlist tbody td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    color: var(--muted);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .5px;
    text-transform: uppercase;
    text-align: left;
  }
  /* Given back, because the card has room for them. */
  .cardlist tbody td.col-wide { display: flex; }
  /* Nothing overflows a card, so the wrapper has nothing to scroll. */
  .cardlist { min-width: 0; }

  /* 12px each side is a desk measurement. Five columns of it is 120px of
     padding out of the 358 a phone has, which is a third of the screen spent
     on gaps - and it is the difference between the State column being on
     screen and being behind a scroll. */
  th, td { padding: 9px 7px; }

  /* 22px of panel padding either side costs 44px of a 390px screen, on top of
     32px from `main`. A table is the widest thing this console draws and it
     was losing a fifth of the display to margin before it drew a column. */
  .panel { padding: 16px 12px; }

  /* The last few pixels, and they are the ones that decide whether a state
     reads as `abandoned` or as `abandone`. A word clipped by one character
     looks like a rendering fault rather than a narrow screen. */
  .badge { padding: 3px 7px; }
  footer { padding: 0 16px 26px; }
  .diffcols { grid-template-columns: 1fr; }
  dl.facts { grid-template-columns: 1fr; }
  /* Label above value rather than beside it, matching dl.facts. A nowrap
     label in a 1% column has nowhere to go on a phone. */
  table.kv th, table.kv td { display: block; width: auto; white-space: normal; }
  table.kv th { padding-bottom: 0; border-bottom: none; }
  table.kv td { padding-top: 2px; }
}


/* --- capability checkboxes ------------------------------------------------
   Each row is a decision about what somebody may do on live plant, so the
   label carries its consequence rather than just its name. Laid out as a
   grid so the explanation aligns under the title instead of wrapping around
   the checkbox. */
.caps { border: 1px solid var(--line); border-radius: 10px; padding: 1rem 1.1rem; margin: 1rem 0; }
.caps legend { font-weight: 600; padding: 0 .4rem; }
label.check { display: grid; grid-template-columns: auto 1fr; gap: .1rem .7rem; align-items: start; margin: .7rem 0; cursor: pointer; }
label.check input { margin-top: .25rem; }
label.check span { display: block; }
label.check strong { display: block; font-weight: 600; }
label.check em { display: block; font-style: normal; color: var(--muted); font-size: .875rem; margin-top: .15rem; }

/* Marks a mandatory field. Quiet on purpose - it is a note, not an alarm. */
.req { font-size: .75rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-left: .4rem; }


/* --- Help ---------------------------------------------------------------
 * A "?" that follows you down the page, and a drawer explaining the page you
 * are actually on. Contextual rather than a single manual, because the
 * question somebody has is almost always about what is in front of them.
 *
 * The drawer is in the delivered markup and merely hidden. Script unhides it
 * and nothing more, so the help survives a script that fails to run - it is
 * then simply the section at the bottom of /admin/help instead. */

/* The contextual-help trigger: floating, on the right edge, at mid height.
 *
 * It lived in the bottom-right corner once, and that was wrong for a specific
 * reason worth keeping written down: on a console whose widest tables scroll
 * inside their own box, that corner holds the horizontal scrollbar and the
 * last column of a row, so a button offering to explain the page sat on top
 * of the page. Mid height clears both, and on a wide screen it sits in the
 * gutter beside the 1400px content rather than over it.
 *
 * Opaque, never a tint. It floats on body.app - a photograph - where a 10-18%
 * wash is about 90% transparent and the label would be near-invisible in
 * light mode. Same rule as the account control. */
/* --- The help trigger ----------------------------------------------------
 * In the topbar beside the account control, not floating over the page.
 *
 * It floated for two versions and neither position worked. The bottom-right
 * corner is where a wide table's horizontal scrollbar and its last column
 * are, so a control offering to explain the page sat on top of it; mid-height
 * on the right edge cleared the scrollbar but left a button hanging in the
 * margin attached to nothing. A floating control has to earn its float, and
 * this one never did - it is read once per page at most.
 *
 * Opaque, like everything else that can sit over the signed-in ground: a
 * *-sf token is a 10-18% wash, so a surface painted with one alone is about
 * 90% transparent and its label is unreadable in light mode. */
.help-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px 8px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}
.help-trigger:hover { background: var(--panel-2); border-color: var(--line-2); }
.help-trigger-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent-sf);
  color: var(--accent);
  font-size: 12px;
  font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
  .help-trigger { transition: none; }
}

/* Narrow, the words go and the mark stays. It still reads as help, and the
   account control is the thing that must keep its width. */
@media (max-width: 700px) {
  .help-trigger { padding: 8px; }
  .help-trigger-text {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

.help-scrim {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: rgba(0,0,0,.34);
}

.help-drawer {
  position: fixed;
  z-index: 50;
  top: 0;
  right: 0;
  width: 420px;
  max-width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border-left: 1px solid var(--line);
  box-shadow: -8px 0 34px rgba(0,0,0,.16);
}

.help-drawer header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  padding: 20px 22px 16px;
  border-bottom: 1px solid var(--line-2);
}
.help-drawer header h2 { font-size: 20px; margin: 0; font-weight: 650; letter-spacing: -.3px; }
.help-drawer header h3 { margin: 0 0 2px; }

.help-close {
  flex: none;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 50%;
  background: var(--panel-2);
  color: var(--muted);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.help-close:hover { color: var(--text); }

.help-body { flex: 1; overflow-y: auto; padding: 18px 22px 28px; }
.help-body .lead { margin: 0 0 18px; color: var(--muted); font-size: 14px; }

/* Stops the page behind scrolling under the drawer on a touchpad. */
body.help-lock { overflow: hidden; }

/* --- Help topics (shared by the drawer and /admin/help) ----------------- */

.help-topic { border-top: 1px solid var(--line-2); padding: 12px 0; }
.help-topic:last-of-type { border-bottom: 1px solid var(--line-2); }
.help-topic summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 14.5px;
  list-style: none;
  display: flex;
  gap: 10px;
  align-items: baseline;
}
.help-topic summary::-webkit-details-marker { display: none; }
.help-topic summary::before {
  content: "+";
  flex: none;
  width: 14px;
  color: var(--accent);
  font-weight: 700;
}
/* Written as a CSS escape rather than the literal character. It shipped as a
   raw U+2212 that lost two of its three UTF-8 bytes somewhere in transit,
   leaving a control character and the digit 2 - so every open topic read "2"
   where a minus belonged. An escape cannot be mangled by an encoding pass. */
.help-topic[open] summary::before { content: "\2212"; }
.help-topic p { margin: 10px 0 0 24px; font-size: 14px; color: var(--text); }
.help-topic .help-go { margin-top: 12px; }
.help-topic .help-go a { font-weight: 600; font-size: 13.5px; }

.help-more { margin: 22px 0 0; font-size: 13.5px; }

/* --- The full help page ------------------------------------------------- */

.help-contents ul { list-style: none; margin: 0; padding: 0;
                    display: flex; flex-wrap: wrap; gap: 8px 10px; }
.help-contents li a {
  display: block;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--panel-2);
  font-size: 13.5px;
  font-weight: 500;
}
.help-contents li a:hover { background: var(--accent-sf); text-decoration: none; }

.help-section { scroll-margin-top: 20px; }
.help-section h2 { margin-top: 0; }
.help-section .lead { color: var(--muted); margin: 0 0 14px; }

@media (max-width: 720px) {
  .help-drawer { width: 100vw; border-left: none; }
}


/* --- Dashboard: hero and exceptions --------------------------------------
 * One hero figure per view, and nothing else competing with it. The old nine
 * equal tiles made the reader rank them personally on every visit. */

.hero-row { display: grid; grid-template-columns: minmax(230px, 300px) 1fr; gap: 18px; margin-bottom: 18px; }

.hero {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 20px 22px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-card);
  background: var(--panel);
  box-shadow: var(--shadow);
}
/* Tinted only when somebody is actually on site. A hero that is always
   accent-coloured stops meaning anything. */
.hero.live { background: linear-gradient(var(--accent-sf), var(--accent-sf)), var(--panel);
             border-color: transparent; }

.hero-label { color: var(--muted); font-size: 13px; font-weight: 500; }
/* Proportional figures on purpose: tabular-nums gives every digit the width of
   a zero, which reads loose at display sizes. */
.hero-figure { font-size: 54px; line-height: 1.05; font-weight: 700; letter-spacing: -2px; margin: 2px 0 4px; }
.hero.live .hero-figure { color: var(--accent); }
.hero-note { color: var(--muted); font-size: 13px; }

.hero-side { display: flex; align-items: stretch; }
.hero-side .stats { flex: 1; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); }

.exceptions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 18px; }
.exception {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  background: var(--panel);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--text);
}
.exception:hover { text-decoration: none; border-color: var(--line); }
.exception b { font-size: 15px; font-weight: 700; }
.exception.warn   { background: linear-gradient(var(--warn-sf), var(--warn-sf)), var(--panel);
                    border-color: transparent; }
.exception.warn b { color: var(--warn); }
.exception.danger   { background: linear-gradient(var(--danger-sf), var(--danger-sf)), var(--panel);
                      border-color: transparent; }
.exception.danger b { color: var(--danger); }

/* --- Charts --------------------------------------------------------------
 * Server-rendered SVG. No library, because the CSP forbids a CDN and an audit
 * service should not take a JavaScript dependency to draw fourteen rectangles.
 *
 * Fills come from CSS custom properties named in the markup, so the charts
 * follow light and dark with everything else. */

.chart {
  margin: 0;
  padding: 18px 20px 14px;
  background: var(--panel);
  border: 1px solid var(--line-2);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
}
.chart figcaption { margin-bottom: 12px; }
.chart figcaption h3 { margin: 0 0 1px; }
.chart figcaption p { margin: 0; color: var(--muted); font-size: 13px; }

.chart-svg { display: block; width: 100%; height: auto; overflow: visible; }

/* Recessive by design: hairline, solid, one step off the surface. Never dashed
   - dashing adds noise and reads as data. */
.chart-grid { stroke: var(--chart-grid); stroke-width: 1; shape-rendering: crispEdges; }
.chart-ytick, .chart-xtick {
  fill: var(--muted);
  font-size: 10px;
  font-family: inherit;
  font-variant-numeric: tabular-nums;   /* columns of numbers do align */
}

/* The hit target is the whole slot, not the painted bar. */
.chart-hit { fill: transparent; }
.chart-col:hover .chart-hit { fill: var(--panel-2); }
.chart-col path { transition: opacity .12s ease; }
.chart-col:hover path { opacity: .78; }

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
  font-size: 12.5px;
  color: var(--muted);
}
.chart-legend li { display: flex; align-items: center; gap: 6px; }
.chart-legend i { width: 10px; height: 10px; border-radius: 3px; flex: none; }

/* The table is why a native <title> is an adequate hover layer: every value is
   reachable without hovering at all, so the tooltip enhances and never gates. */
.chart-table { margin-top: 10px; }
.chart-table summary { cursor: pointer; color: var(--accent); font-size: 12.5px; }
.chart-table table { margin-top: 10px; }
.chart-table th, .chart-table td { padding: 5px 8px; font-size: 12.5px; }

.chart-empty {
  display: flex;
  flex-direction: column;
  gap: 6px;
  justify-content: center;
  min-height: 150px;
  padding: 18px;
  border: 1px dashed var(--line);
  border-radius: var(--r-ctl);
  color: var(--muted);
  font-size: 13.5px;
}
.chart-empty strong { color: var(--text); font-size: 14.5px; }

@media (max-width: 900px) {
  .hero-row { grid-template-columns: 1fr; }
}


/* --- Theme control -------------------------------------------------------
 * Three explicit states rather than a two-way switch. A single toggle cannot
 * express "follow the system", which is the default and the one most people
 * actually want; offering only light/dark would strand them. */

.theme {
  display: flex;
  gap: 2px;
  padding: 3px;
  margin-top: 12px;
  border-radius: 9px;
  background: var(--panel-2);
}
.theme button {
  flex: 1;
  padding: 5px 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.theme button:hover { color: var(--text); background: var(--bg); }
.theme button[aria-pressed="true"] { color: var(--accent); background: var(--panel); box-shadow: 0 1px 2px rgba(0,0,0,.10); }

/* --- Office ---------------------------------------------------------------
 * A link out rather than an embedded map: the CSP is default-src 'self' with
 * no frame-src, and widening it to embed a third-party map is a poor trade
 * for a pin somebody looks at once. */

.office { display: flex; gap: 14px; align-items: flex-start; }
.office-body { flex: 1; }
.office h4 { margin: 0 0 3px; font-size: 14.5px; font-weight: 650; }
.office p { margin: 0 0 8px; color: var(--muted); font-size: 13.5px; }
.office .pin {
  flex: none;
  width: 38px; height: 38px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 10px;
  background: var(--accent-sf);
  color: var(--accent);
  font-size: 19px;
}


/* --- The sign-in page ----------------------------------------------------
 * A full-bleed hero, modelled on novasolis.co.za - whose own hero is a muted
 * video with no text on it at all.
 *
 * **This reverses what this section used to say.** It read "flat brand ground,
 * no photograph - the picture belongs to the console, by request", and the
 * reason given was sound: the card's contrast was fixed rather than dependent
 * on an image. Requested reversed, and the reasoning is answered rather than
 * discarded - the card keeps its own opaque panel, so its text still sits on a
 * colour this stylesheet chose. Only the ground changed.
 *
 * What does sit on the ground is the heading and the two hints. Measured
 * against the poster's 99th-percentile pixel in the band they cross (#bb8f6c)
 * at the 72% scrim: heading 11.28:1, muted text 8.51:1.
 *
 * **And measured again against a pure white frame**, which is the floor no
 * video can go under: 7.14:1 and 5.63:1, both past AA. That is the useful
 * property - unlike the console ground, this scrim does NOT need re-sampling
 * if the image or the video is ever swapped, because it already holds against
 * the brightest frame that could possibly exist.
 */

body.signed-out .login-wrap { margin-top: 9vh; position: relative; z-index: 1; }
/* On the hero rather than on a panel, so it takes the on-ground colour like
   every heading over the console photograph does. */
body.signed-out .login-wrap h1 { color: var(--on-bg); }
body.signed-out .login-wrap > .hint { color: var(--on-bg-muted); }
body.signed-out footer { text-align: center; color: var(--on-bg-muted); position: relative; z-index: 1; }
body.signed-out footer a { color: var(--on-bg); }

/* Fixed, so the ground does not travel with a page that scrolls, and behind
   everything. The scrim is part of this element rather than a separate
   overlay: one stacking context, and it covers the video and the still
   identically without either needing to know about it. */
.hero-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: linear-gradient(168deg, #1d232c 0%, #171c23 46%, #12161c 100%);
}
.hero-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--page-scrim);
}
.hero-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The video starts invisible and is revealed only once it is actually
   playing. Its first frame is a different photograph from the poster, so
   swapping them the instant a source is set would be a hard cut. */
.hero-video { opacity: 0; transition: opacity .8s ease; }
.hero-video.is-playing { opacity: 1; }
/* The NovaBase stacked logo with white lettering on transparency, so it sits
   on the dark hero unchanged - no tile, no scrim, nothing behind it. 180px
   wide keeps it the height the old Nova Solis mark had (about 144px). */
body.signed-out .login-logo { width: 180px; }


/* --- The console ground --------------------------------------------------
 * The photograph sits behind every signed-in page. The sign-in page now has
 * its own hero, painted by .hero-bg above rather than by this rule - the two
 * grounds share the same image and the same scrim token, and nothing else.
 *
 *   Source : Unsplash, "black towers during sunset" by Mario Caruso
 *   Licence: Unsplash License - free commercially, no attribution required.
 *            Recorded for provenance, not obligation.
 *   Stored : 2560px wide, q76 progressive, 497 KB, from the 3840px original.
 *
 * **Re-sample the scrim if the photograph is ever swapped.** The 72% figure is
 * a property of this image - its 99th-percentile pixel where text crosses is
 * #d29053 - and not a house style.
 * It is fixed, so it does not
 * travel with a long audit table, and every piece of dense content stays on an
 * opaque panel - the image shows in the gaps between cards, never underneath a
 * row of data.
 *
 * The scrim is a token because the two themes need different strengths: see
 * --d-page-scrim for why dark mode has to work harder.
 *
 * The flat gradient underneath is a genuine fallback. If the JPEG 404s the
 * page is still a dark slate ground with light text on it, not light text on
 * white.
 */
body.app {
  background:
    linear-gradient(var(--page-scrim), var(--page-scrim)),
    url("/static/login-bg.jpg") center center / cover no-repeat,
    linear-gradient(168deg, #1d232c 0%, #171c23 46%, #12161c 100%);
  background-attachment: fixed, fixed, fixed;
}

/* Text that sits directly on the ground rather than on a panel. */
body.app > .shell h1,
body.app > .shell h2 { color: var(--on-bg); }
body.app .pagehead .sub,
body.app > .shell > .content > main > .sub,
body.app footer { color: var(--on-bg-muted); }
body.app footer a { color: var(--on-bg); }

/* ...and everything inside a panel goes back to normal. A heading inside a
   card is on the card, not on the photograph. */
body.app .panel h1, body.app .panel h2,
body.app .chart h1, body.app .chart h2,
body.app .hero h1,  body.app .hero h2 { color: var(--text); }
body.app .panel .sub, body.app .panel .lead { color: var(--muted); }

/* Panels lift off the picture. In light mode the white already does it at
   9:1; in dark mode the border and shadow are what do it. */
body.app .panel,
body.app .stat,
body.app .chart,
body.app .hero { border-color: var(--panel-edge); }
body.app .panel, body.app .chart, body.app .stat, body.app .hero {
  box-shadow: 0 1px 2px rgba(0,0,0,.16), 0 8px 26px rgba(0,0,0,.20);
}

/* The hero is painted by .hero-bg, which is fixed and sits underneath. This
   only needs to be a genuine fallback: if the image 404s and the video never
   loads, the page is still a dark slate ground with light text on it - never
   light text on white. */
body.signed-out {
  background: linear-gradient(168deg, #1d232c 0%, #171c23 46%, #12161c 100%);
  min-height: 100vh;
}

/* --- Visit map -----------------------------------------------------------
 * A 16:9 responsive frame. aspect-ratio rather than the old padding-top
 * trick: it is supported everywhere this portal runs and it does not need a
 * wrapper whose only job is to be a spacer. */
.map-frame {
  aspect-ratio: 16 / 9;
  max-height: 420px;
  margin-bottom: 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-ctl);
  overflow: hidden;
  background: var(--panel-2);
}
.map-frame iframe { display: block; width: 100%; height: 100%; border: 0; }

/* Which visit the map is showing. Quiet, but not optional: the map centres on
 * one point and eight are listed under it, so without this the frame is an
 * unlabelled picture of somewhere. */
.map-caption { margin: 0 0 16px; font-size: 12.5px; color: var(--muted); }
.map-caption strong { color: var(--text); font-weight: 600; }

/* The row the map is on. The same accent wash the active rail item uses, on
 * the same surface it uses it on - a table row here sits inside .panel - plus
 * a bar down the leading edge, because a 10% tint alone is easy to miss on a
 * row that also has a hover state. */
.maplist tbody tr[aria-current="true"] {
  background: var(--accent-sf);
  box-shadow: inset 3px 0 0 var(--accent);
}
/* The attribute is set by script, never in the markup, so a page whose script
 * did not run does not offer a pointer over a row that cannot be clicked. */
.maplist[data-map-live] tbody tr { cursor: pointer; }
/* The "Show" button the script puts beside the "Open" link. Same button.link
 * as everywhere else; this only keeps the two off each other. */
.map-show { margin-right: 12px; }


/* --- The account control -------------------------------------------------
 * Top right, because that is where every interface a person already uses puts
 * it, and because the rail is navigation: who you are, and what the interface
 * does for you, are not places to go.
 *
 * The trigger is an OPAQUE panel, never a tint. It sits directly on
 * body.app - a photograph - and a *-sf token is a 10-18% wash, so a surface
 * painted with one alone is about 90% transparent. That is what made the
 * machines notice unreadable in light mode, and this control sits on the same
 * ground. */

.topbar { width: 100%; }
.topbar-inner {
  max-width: 1400px;
  margin: 0 auto;
  padding: 16px 28px 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}

.account { position: relative; }

.account-btn {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 10px 6px 6px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: border-color .15s ease, background .15s ease;
}
.account-btn:hover { border-color: var(--line-2); background: var(--panel-2); }

.avatar {
  flex: none;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1;
}

.account-who {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.25;
  min-width: 0;
}
.account-who strong {
  font-size: 12.5px;
  font-weight: 600;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-who em {
  font-style: normal;
  font-size: 11px;
  color: var(--muted);
  text-transform: capitalize;
}

.account-btn .chev { flex: none; color: var(--muted); }
.account-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }

.account-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 40;
  width: 250px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  /* Opaque, for the same reason the trigger is. */
  background: var(--panel);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
}

.account-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 9px 10px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line-2);
}
.account-head strong { font-size: 13px; font-weight: 600; word-break: break-all; }
.account-head em { font-style: normal; font-size: 11.5px; color: var(--muted); text-transform: capitalize; }

.account-menu a,
.account-signout button {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: var(--r-ctl);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
}
.account-menu a:hover,
.account-signout button:hover {
  background: var(--panel-2);
  text-decoration: none;
}

.account-section { padding: 6px 10px 4px; }
.account-label {
  display: block;
  margin-bottom: 6px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The control was built for the rail, where it needed to clear the nav above
   it. Inside the menu the section already spaces it. */
.account-section .theme { margin-top: 0; }

.account-signout {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid var(--line-2);
}
.account-signout button { color: var(--accent); font-weight: 600; }

@media (max-width: 900px) {
  .topbar-inner { padding: 12px 16px 0; }
}
@media (max-width: 420px) {
  /* The address is the first thing to go: the avatar and the role still say
     who is signed in, and the menu repeats the address in full. */
  .account-who { display: none; }
}


/* --- Site photographs ----------------------------------------------------

   The grid on a session page. These are the only user-supplied images the
   console renders, and they are rendered because looking at them is the whole
   point of collecting them - a filename in a table answers no question about
   whether a relay board was seated correctly.

   Fixed aspect and object-fit: cover, so a page of eight photographs taken in
   portrait and landscape on four different phones still reads as a grid
   rather than as a ransom note. The full frame is one click away.          */

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 14px;
  margin-top: 4px;
}

.photo-card {
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--panel-2);
}

.photo-card img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--panel-2);
}

.photo-card figcaption {
  padding: 8px 10px;
  font-size: 13px;
  border-top: 1px solid var(--line-2);
}

/* A slot with nothing in it. Deliberately not styled as an error: "no second
   aircon at this site" is an answer, and the only thing that distinguishes it
   from a gap is that somebody said so. */
.photo-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  color: var(--muted);
  font-size: 13px;
  text-align: center;
  padding: 12px;
}

.photo-missing { color: var(--danger); }

/* --- Release notes ------------------------------------------------------
   What changed, in the portal and in the field application.

   A list rather than a table: each entry is a sentence, and a sentence in a
   table cell wraps into a shape nobody reads. The left rule on an unread
   entry is the whole "new" treatment beyond the badge - a coloured background
   would make the newest items the hardest to read, which is backwards. */
.releases { list-style: none; margin: 0; padding: 0; }

.release {
  padding: 14px 0 14px 14px;
  border-left: 3px solid transparent;
  border-bottom: 1px solid var(--line);
}

.release:last-child { border-bottom: 0; }

.release.is-new { border-left-color: var(--accent); }

.release-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 13px;
}

.release-headline { margin: 0; }

/* --- Field builds ------------------------------------------------------
   One row per registered build, and the current one has to be findable at a
   glance: it is the answer to "what will an invite hand out", which is the
   only question this page is really asked. A left rule rather than a filled
   background, so the row stays as readable as every other. */
.is-current { border-left: 3px solid var(--ok); }

.is-current td:first-child { padding-left: 9px; }

/* The make-current control sits in its own cell beside the row it acts on.
   A reason is mandatory, so the field and the button travel together and
   wrap together on a narrow screen rather than the button orphaning. */
form.inline {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

form.inline input[type="text"] { width: 12rem; }

/* A retired build: the file is gone, the row is kept because download
   records refer to it. Dimmed rather than hidden when the filter is off,
   so "this existed and was handed out" stays legible. */
.retired { opacity: 0.55; }

.retired .badge { opacity: 0.9; }

/* --- Privacy Control ---------------------------------------------------- */

.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin: 0 0 14px; border-bottom: 1px solid var(--line); }
.tabs a { padding: 8px 14px; color: var(--muted); border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tabs a.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tabs .count { font-size: 12px; color: var(--muted); margin-left: 4px; }
details.guidance > summary { cursor: pointer; font-weight: 600; }
details.guidance[open] > summary { margin-bottom: 10px; }
.bulk-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: 12px;
  background: var(--accent-sf); border: 1px solid var(--line); border-radius: 10px;
}
.bulk-bar[hidden] { display: none; }
table.requests td.oneline { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
table.requests .pick, table.cardlist .pick { width: 1%; }
table.requests td .btn.small, table.cardlist td .btn.small { white-space: nowrap; }
table.requests input.pick-one, table.requests #pick-all,
table.cardlist input.pick-one, table.cardlist #pick-all { width: auto; margin: 0; cursor: pointer; }
