/* Egbin CMMS — shared design system */

/* `:root` and `[data-theme="light"]` share one block on purpose. The second
   selector lets any element opt into the light palette — which is what makes the
   theme previews on the Appearance page show the *real* colours instead of a
   hand-copied approximation that drifts the first time a token changes. */
:root,
[data-theme="light"] {
  /* ---- Egbin house colours -------------------------------------------------
     The logo is burgundy, orange, charcoal and white, with #6E1414 the key
     colour. Burgundy carries the interface: 11.84:1 on white, and the same with
     white on it, so it works as both a link colour and a button fill.

     The brand orange is deliberately NOT used for text, nor as a fill behind
     white text: #E8751A manages only 3.00:1 against white, which fails AA for
     anything but large display type. It appears as an accent with dark ink on
     it, and --color-accent-strong (#C2410C, 5.18:1) is the version used where
     white text has to sit on it. */
  --color-bg: #f4f6f8;
  --color-surface: #ffffff;
  /* A second surface one step off the first: dropdown panels, the chart plot
     area, sticky headers. Barely visible in light mode; in dark mode it is what
     stops a popup from disappearing into the card behind it. */
  --color-surface-2: #f7f9fb;
  --color-border: #dde3e8;
  --color-text: #1c2530;
  --color-text-muted: #5b6b79;
  --color-primary: #6e1414;
  --color-primary-dark: #4e0e0e;
  --color-primary-light: #f6eaea;
  --color-accent: #e8751a;
  --color-accent-strong: #c2410c;
  --color-charcoal: #2e3338;
  --sidebar-width: 240px;
  /* The rail: wide enough for an icon and its focus ring, no wider. */
  --sidebar-rail-width: 62px;
  /* The sidebar has its own ground and its own ink, and they do not move with
     the theme. It used --color-primary-dark as a fill behind a literal #fff,
     and the dark palettes *lighten* the primary — correct for text and accents
     on a dark page, exactly wrong as a plate under white writing. The active
     nav link came out at 3.17:1 on Egbin dark and 1.86:1 on Teal dark: white
     on a pale fill, which is the same bug as the white-on-white buttons.

     A sidebar is dark chrome in both themes, so one pair serves both: each
     scheme sets its own ground below, every one of them checked against white
     (7.58:1 at worst). Nothing here is redefined for dark, and that is the
     point rather than an omission. */
  --sidebar-bg: #4e0e0e;
  --sidebar-ink: #ffffff;
  --sidebar-active-bg: #6e1414;
  --sidebar-active-ink: #ffffff;
  --radius: 8px;

  /* ---- Component tokens ----------------------------------------------------
     Each of these was a hard-coded #fff or a fixed pastel before, and each was
     invisible or glaring once a dark theme existed — a white button under light
     grey text, a white dropdown panel on a dark card. They are variables now,
     so the dark block redefines what they mean instead of restyling components
     one at a time and missing some. */
  --btn-secondary-bg: #ffffff;
  --btn-secondary-ink: #1c2530;
  --btn-secondary-hover-bg: #f0f3f6;
  --control-bg: #ffffff;
  /* THE CARD/PANEL TITLE BAR, AND THE TABS, TAKE THE SCHEME'S OWN COLOUR.
     A light wash of the theme rather than the near-black they started as: the
     bar has to separate a card's title from its fields, which a tint does, and
     five near-black bars down a page made every screen look the same whichever
     scheme the reader had chosen. Every scheme redefines these three below, in
     both themes, with the ink checked against its own ground — see
     TokenPairContrastTests, which resolves all ten combinations. */
  --panel-header-bg: #eee3e3;
  --panel-header-ink: #4e0e0e;
  /* The tab strip's underline, the active tab's edge and the board around a
     tab's content: one step stronger than the bar, so the outline reads at a
     glance without competing with what it encloses. */
  --panel-accent-border: #ccadad;

  /* ---- Card geometry -------------------------------------------------------
     One set of numbers for a card's padding, so anything that has to line up
     with a card's content can ask for the same value instead of repeating it.
     The title bar is pulled out to the card's edges with the negative of these,
     which is what keeps its text on the same left edge as the fields below it:
     when the two were written out separately the bar sat 0.25rem off at the
     default density and 0.4rem off under COMPACT, which is what made the header
     look bolted on rather than part of the card.

     The density blocks at the bottom of this file override these four, and
     that is now all density has to do to a card. */
  --card-pad-y: 1.1rem;
  --card-pad-x: 1.25rem;
  --card-gap: 1.5rem;
  --grid-gap: 1.4rem;

  --tint-danger-bg: #fdecea;
  --tint-danger-border: #f0928a;
  --tint-danger-ink: #7a1f17;
  --tint-warning-bg: #fff6e5;
  --tint-warning-border: #f0c36d;
  --tint-warning-ink: #7a5200;
  /* The soft tint behind a pill, an info alert, a table header. It had been
     --color-primary-light with --color-primary-dark on it, which is a *pair of
     dark values* in dark mode: near-black text on near-black, which is what made
     the roster's staff names and the asset register's criticality invisible.
     A background and the ink that sits on it have to move together, so they are
     one pair. */
  /* Success, completing the tint family. These were a hard-coded #eaf7ec plate
     with #1e6b2a text, which stayed pale on a dark page while every other
     alert had learned to move with the theme. */
  --tint-success-bg: #eaf7ec;
  --tint-success-border: #a9dcb2;
  --tint-success-ink: #1e6b2a;
  /* The plate behind a sheet's row labels and its computed totals — a shade off
     the page so the grid reads, and no more. Was #fbfcfd, a literal near-white
     that in dark mode became a white cell with near-white text in it: the
     Plant Status Log's row labels and totals were unreadable. */
  --sheet-label-bg: #fbfcfd;
  --tint-soft-bg: #f6eaea;
  --tint-soft-ink: #4e0e0e;
  /* Ink for the warm priority fills — orange, amber, green. White fails AA on
     all three (3.45-3.58:1) and did so unnoticed because the priority palette
     was never contrast-checked; this near-black clears it in both themes. */
  --ink-on-warm: #10161d;

  /* Charts read these through static/js/chart-theme.js, so a Chart.js canvas
     follows the theme without any page repeating a colour. */
  /* The neutral heavy line on a chart — the "Total" series on the generation
     log. It was hard-coded #1c2530 in that page's script, which reads 1.02:1
     on the dark plot: the line was drawn and could not be seen. */
  --chart-total: #1c2530;
  --chart-grid: #eef1f5;
  --chart-ink: #5b6b79;
  --chart-plot-bg: transparent;

  /* Comfortable spacing — the values the density blocks below override. */
  --gap-field: 1rem;
  --h-textarea: 90px;
  --pad-input-h: 2.1rem;

  /* Status palette. One distinct hue per status so the dashboard strips read
     as a colour-coded pipeline rather than a row of greys. Every value is
     checked to clear WCAG AA (4.5:1) against the white text the tiles use —
     the lightest, --status-draft, sits at 4.76:1. */
  --status-draft: #64748b;
  --status-planned: #4f46e5;
  --status-awaiting-permit: #b45309;
  --status-approved: #0f766e;
  --status-released: #0369a1;
  --status-in-progress: #1d4ed8;
  --status-suspended: #b91c1c;
  --status-completed: #15803d;
  --status-accepted: #047857;
  --status-written-off: #7c3aed;
  --status-closed: #334155;
  --status-new: #6d28d9;
  --status-rejected: #be123c;
  --status-duplicate: #78716c;
  /* Alarm severity for an excursion *below* the band, kept cool where the
     over-range severities are warm. "Too hot" and "too cold" call for opposite
     actions at the panel, and the badges have to say which without being read
     — they were both amber and both red, so the two directions were
     indistinguishable at a glance in exactly the column that exists to say. */
  --status-severity-low: #0369a1;
  --status-severity-critical-low: #5b21b6;

  --priority-1: #c8102e;
  --priority-2: #e8590c;
  --priority-3: #e0a800;
  --priority-4: #2f9e44;
  --priority-5: #6b7280;

  /* The text colour that sits on **any** saturated fill — a status badge, a
     priority chip, the primary button, the active hour on the plant log. White
     on the dark light-mode hues; near-black on the lightened dark-mode ones.

     Wider than its name suggests, and deliberately so: thirteen rules painted a
     literal #fff on a var() fill, which is correct in light mode and unreadable
     in dark, because the dark palettes lift those hues to read against a dark
     page. .btn-primary was 1.86:1 on Teal dark. One ink that flips with the
     theme fixes all of them and keeps them checkable — a rule that hard-codes
     white on a themed fill will fail TokenPairContrastTests.

     The exception is the warm fills (orange, amber, green), where white fails in
     *both* themes: those take --ink-on-warm. */
  --status-ink: #ffffff;
}

* { box-sizing: border-box; }

/* TEXT SIZE.
   Everything in this stylesheet is sized in rem, which is a multiple of the
   *root* font size — so one number on <html> scales the whole interface, and
   no component needs a rule of its own. --font-scale is set by the
   [data-font-size] blocks near the density ones; 1 is the 14px the CMMS has
   always used.

   Scaling the root rather than restyling components is what keeps the layout
   intact at every size: padding, control heights and gaps are rem too, so a
   larger text size grows the boxes around the text with it instead of
   overflowing them. */
html {
  font-size: calc(14px * var(--font-scale, 1));
}
html, body {
  margin: 0;
  padding: 0;
  font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
}

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

h1, h2, h3, h4 { margin: 0 0 .5rem 0; font-weight: 600; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.05rem; }

.app-shell { display: flex; min-height: 100vh; }

/* ---------- Sidebar ---------- */
.sidebar {
  width: var(--sidebar-width);
  flex-shrink: 0;
  background: var(--sidebar-bg);
  color: var(--sidebar-ink);
  display: flex;
  flex-direction: column;
}
.sidebar .brand {
  padding: 1.1rem 1.2rem;
  font-weight: 700;
  font-size: 1.05rem;
  border-bottom: 1px solid rgba(255,255,255,.12);
  display: flex;
  align-items: center;
  gap: .5rem;
}
.sidebar .brand-mark { height: 28px; flex: none; }
.sidebar .brand-words { flex: 1; min-width: 0; }

/* The control that collapses the menu to a rail. Hidden below the drawer
   breakpoint, where the sidebar is already a drawer and a second collapse would
   be two ways to do one thing. */
.nav-rail-toggle {
  background: none; border: 0; padding: .15rem .3rem; cursor: pointer; flex: none;
  color: var(--sidebar-ink); opacity: .55; font-size: .95rem; line-height: 1; border-radius: 4px;
}
.nav-rail-toggle:hover { opacity: 1; background: rgba(255,255,255,.1); }
.nav-rail-chevron { display: inline-block; transition: transform .15s ease; }

/* ---------- The collapsed rail ----------
   Narrower, never poorer: every link keeps its icon, its title attribute and
   its accessible name, so the menu loses its words and nothing else. The logo
   and the EGBIN CMMS wordmark stay, because a rail with nothing at the top
   reads as a fragment of a page rather than as a menu. */
:root.nav-rail .sidebar { width: var(--sidebar-rail-width); }
:root.nav-rail .sidebar .brand {
  flex-direction: column; gap: .3rem; padding: .9rem .4rem; text-align: center;
}
:root.nav-rail .sidebar .brand-words {
  font-size: .62rem; line-height: 1.15; letter-spacing: .04em; text-transform: uppercase;
}
/* The strapline goes; the name does not. */
:root.nav-rail .sidebar .brand-words small { display: none; }
:root.nav-rail .nav-rail-chevron { transform: rotate(180deg); }

:root.nav-rail .sidebar .nav-section { display: none; }
:root.nav-rail .sidebar .nav-chevron,
:root.nav-rail .sidebar .nav-link .dot { display: none; }
/* THE LABEL IS HIDDEN, NOT REMOVED.
   display:none takes an element out of the accessibility tree, and the icon
   beside it is aria-hidden — so a display:none'd label left every link in the
   collapsed rail with no accessible name at all. Clipped instead: invisible,
   still announced, and still there for the tooltip to read. */
:root.nav-rail .sidebar .nav-label {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}
:root.nav-rail .sidebar a.nav-link,
:root.nav-rail .sidebar .nav-group-summary {
  justify-content: center; padding-left: 0; padding-right: 0; gap: 0;
}
:root.nav-rail .sidebar .nav-icon { opacity: .9; font-size: 1.05rem; }
/* A group's children stay reachable: the group opens in place and its own
   links show as icons, which is the whole point of keeping the icons. */
:root.nav-rail .sidebar details.nav-group > summary { padding: .45rem 0; }

/* The rail's tooltip. Positioned by nav-rail-tip.js rather than by CSS, because
   the menu scrolls: `.sidebar nav` has overflow-y, which clips an absolutely
   positioned child on both axes, and a tooltip that disappears into the edge of
   the rail is worse than none. One fixed element, moved to whichever item the
   pointer or the keyboard is on. */
.nav-rail-tip {
  position: fixed; z-index: 60; pointer-events: none;
  background: var(--sidebar-bg); color: var(--sidebar-ink);
  border: 1px solid rgba(255, 255, 255, .18); border-radius: 5px;
  padding: .3rem .55rem; font-size: .78rem; font-weight: 600; white-space: nowrap;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .35);
  opacity: 0; transform: translateX(-4px); transition: opacity .1s ease, transform .1s ease;
}
.nav-rail-tip.is-on { opacity: 1; transform: translateX(0); }

@media (max-width: 900px) {
  /* Below here the sidebar is a drawer, so the rail is neither available nor
     meaningful — and a rail applied to a drawer would be a 64px drawer. */
  .nav-rail-toggle { display: none; }
  :root.nav-rail .sidebar { width: var(--sidebar-width); }
  :root.nav-rail .sidebar .nav-label,
  :root.nav-rail .sidebar .nav-chevron,
  :root.nav-rail .sidebar .nav-section { display: revert; }
  :root.nav-rail .sidebar .brand { flex-direction: row; text-align: left; padding: 1.1rem 1.2rem; }
  :root.nav-rail .sidebar .brand-words { font-size: 1.05rem; text-transform: none; letter-spacing: normal; }
  :root.nav-rail .sidebar .brand-words small { display: block; }
  :root.nav-rail .sidebar a.nav-link,
  :root.nav-rail .sidebar .nav-group-summary { justify-content: flex-start; }
}
.sidebar .brand small {
  display: block;
  font-weight: 400;
  font-size: .7rem;
  opacity: .7;
}
.sidebar nav { flex: 1; overflow-y: auto; padding: .5rem 0; }
.sidebar .nav-section {
  text-transform: uppercase;
  font-size: .68rem;
  letter-spacing: .05em;
  opacity: .55;
  padding: .9rem 1.2rem .3rem;
}
.sidebar a.nav-link {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .5rem 1.2rem;
  color: rgba(255,255,255,.85);
  font-size: .88rem;
}
.sidebar a.nav-link:hover { background: rgba(255,255,255,.08); text-decoration: none; }
.sidebar a.nav-link.active { background: var(--sidebar-active-bg); color: var(--sidebar-active-ink); font-weight: 600; }
.sidebar .nav-link .dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--color-accent); flex-shrink: 0;
}
/* One fixed-width column for the icons, so the labels start on the same x
   whatever the glyph's own width — emoji are not a monospaced set, and ragged
   labels are harder to scan than no icons at all. Decorative: every icon is
   aria-hidden and the label beside it is the real name. */
.sidebar .nav-icon {
  flex-shrink: 0; width: 1.25rem; text-align: center;
  font-size: .95rem; line-height: 1;
  /* Slightly dimmed at rest so the icon column reads as texture rather than
     competing with the words; full strength on the page you are on. */
  opacity: .85;
}
.sidebar a.nav-link:hover .nav-icon, .sidebar a.nav-link.active .nav-icon { opacity: 1; }

/* ---------- Submenus ---------- */
/* A <details> per group, so it opens with no JavaScript and answers to the
   keyboard for free. The list marker is removed on both spellings because
   Safari still uses the WebKit pseudo-element. */
.sidebar details.nav-group > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.sidebar details.nav-group > summary::-webkit-details-marker { display: none; }
.sidebar details.nav-group > summary::marker { content: ""; }
.sidebar .nav-group-summary {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .5rem 1.2rem;
  color: rgba(255,255,255,.85);
  font-size: .88rem;
}
.sidebar .nav-group-summary:hover { background: rgba(255,255,255,.08); }
.sidebar .nav-label { flex: 1; min-width: 0; }
/* The chevron turns to point down when the group is open. A rotation rather
   than swapping the glyph, so it animates and so there is one character to get
   right instead of two. */
.sidebar .nav-chevron {
  flex-shrink: 0;
  opacity: .6;
  font-size: 1rem;
  line-height: 1;
  transition: transform .15s ease;
}
.sidebar details.nav-group[open] > summary .nav-chevron { transform: rotate(90deg); }
/* A group holding the current page reads as active even while shut. */
.sidebar details.nav-group:not([open]):has(.nav-link.active) > summary {
  background: rgba(255,255,255,.06);
  font-weight: 600;
}
.sidebar .nav-children { background: rgba(0,0,0,.16); padding: .15rem 0; }
/* Indented to the icon column above, so a child lines up under its parent's
   label rather than under its parent's icon. */
.sidebar a.nav-child { padding-left: 2.2rem; font-size: .84rem; }

/* ---------- The phone header ---------- */
/* Hidden on a desktop, where the sidebar is always there. Shown below the
   breakpoint, where it is the only chrome above the content. */
.mobile-bar { display: none; }
.nav-backdrop { display: none; }

/* ---------- Main column ---------- */
.main-column { flex: 1; display: flex; flex-direction: column; min-width: 0; }

.topbar {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: .75rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.topbar-titles { min-width: 0; flex: 0 1 auto; }
.topbar .page-title { font-size: 1.1rem; font-weight: 600; }
.topbar .topbar-right { display: flex; align-items: center; gap: .6rem; flex: none; }
.topbar .bell { position: relative; font-size: 1.1rem; text-decoration: none; }
.topbar .bell .badge {
  position: absolute; top: -6px; right: -10px;
  background: var(--color-accent); color: var(--ink-on-warm);
  font-size: .65rem; padding: 1px 5px; border-radius: 10px;
}

/* ---------- Keyboard focus ---------- */
/* One visible focus ring for the whole application.
   There was none, so keyboard focus was whatever each browser draws by default
   — which on a burgundy sidebar link and on a saturated tile is often nothing
   anybody can see. `:focus-visible` rather than `:focus`, so a mouse click
   does not leave a ring behind on a button somebody has already used.

   Declared before the components below, so a control that needs a different
   ring — white over a saturated tile, inset inside a menu row — can say so and
   win on specificity rather than by fighting an !important. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 3px;
}
/* On the sidebar's own dark ground the primary burgundy disappears. */
.sidebar a:focus-visible,
.sidebar summary:focus-visible,
.mobile-bar button:focus-visible {
  outline-color: var(--sidebar-ink);
}

/* ---------- Skip link ---------- */
/* Off-screen until it has focus. Without it, reaching the content of any page
   from the keyboard means tabbing past twenty-odd sidebar links. */
.skip-link {
  position: absolute; left: .5rem; top: -3rem; z-index: 200;
  padding: .5rem .9rem; border-radius: 0 0 var(--radius) var(--radius);
  background: var(--color-primary); color: var(--status-ink);
  font-size: .85rem; font-weight: 600; text-decoration: none;
  transition: top .12s ease;
}
.skip-link:focus { top: 0; text-decoration: none; }

/* ---------- The header's search box ---------- */
/* Given the middle of the bar and allowed to take the slack, because it is the
   control that replaces three navigations: knowing which register a record
   lives in, opening it, and filtering it. */
.topbar-search {
  position: relative; flex: 1 1 22rem; max-width: 34rem; min-width: 0; margin: 0;
  display: flex; align-items: center;
}
/* THE ICON SITS BESIDE THE TEXT — see the comment in base.html.

   Everything here is flex. `.search-shell` is what is drawn to look like the
   field: it carries the border, the radius, the fill and the padding. The
   <input> inside it is stripped bare and simply grows. The distance between the
   icon and the first character is `gap`, which is not a number either side has
   to agree about — it is the space between two boxes that are laid out in a
   row, and it cannot be collapsed by a font metric, a root font size or a
   browser's own idea of what a search field looks like. */
.topbar-search-icon {
  flex: 0 0 auto;
  width: 1rem; height: 1rem;
  color: var(--color-text-muted); opacity: .85; pointer-events: none;
}
.search-shell {
  display: flex; align-items: center; gap: .55rem;
  width: 100%; min-width: 0;
  padding: .42rem .9rem;
  border: 1px solid var(--color-border); border-radius: 999px;
  background: var(--color-surface-2);
}
.topbar-search input[type="search"] {
  /* Stripped: the shell above is the control now. `appearance: none` also stops
     WebKit adding its own inner decoration beside the icon. */
  appearance: none; -webkit-appearance: none;
  flex: 1 1 auto; min-width: 0;
  padding: 0; margin: 0; border: 0; outline: none; background: transparent;
  color: var(--color-text);
  font: inherit; font-size: .82rem; line-height: 1.4;
}
/* The ring belongs to the thing that looks like the control, not to the bare
   input inside it. */
.search-shell:focus-within {
  border-color: var(--color-primary);
  background: var(--color-surface);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
/* Safari's own in-field decorations would sit in the gap the icon occupies. The
   clear button is left alone — it is useful, and it is on the other side. */
.topbar-search input[type="search"]::-webkit-search-decoration,
.topbar-search input[type="search"]::-webkit-search-results-button,
.topbar-search input[type="search"]::-webkit-search-results-decoration {
  -webkit-appearance: none; display: none;
}
.topbar-search input[type="search"]::placeholder { color: var(--color-text-muted); }


.search-suggest {
  position: absolute; top: calc(100% + .35rem); left: 0; right: 0; z-index: 120;
  margin: 0; padding: .3rem 0; list-style: none;
  max-height: 24rem; overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .16);
}
.search-suggest-group {
  padding: .35rem .75rem .15rem; font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; color: var(--color-text-muted);
}
.search-suggest-hit { padding: 0; }
.search-suggest-hit a {
  display: block; padding: .3rem .75rem; font-size: .82rem;
  color: var(--color-text); text-decoration: none;
}
.search-suggest-hit .search-suggest-sub {
  display: block; padding: 0 .75rem .25rem; font-size: .72rem; color: var(--color-text-muted);
}
.search-suggest-hit:hover,
.search-suggest-hit.is-active { background: var(--color-surface-2); }
.search-suggest-hit.is-active a { color: var(--color-primary); }
.search-suggest-all {
  margin-top: .25rem; padding: .4rem .75rem .2rem;
  border-top: 1px solid var(--color-border); font-size: .76rem;
}

/* The full results page: the same box, given room. */
.search-page-form .search-page-row { display: flex; gap: .5rem; align-items: center; }
.search-page-form input[type="search"] { flex: 1 1 auto; }
.search-hits { margin: .6rem 0 0; padding-left: 1.1rem; font-size: .85rem; }
.search-hits li { margin-bottom: .3rem; }

/* ---------- Header disclosure menus ---------- */
/* <details>, so they open from the keyboard and with no JavaScript at all —
   static/js/header.js only adds closing them on Escape and on a click
   elsewhere. The marker is removed because the summary is styled as a button
   and a disclosure triangle beside "+ New" reads as a second control. */
.menu { position: relative; }
.menu > summary { list-style: none; cursor: pointer; }
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::marker { content: ""; }
.menu-summary { display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap; }
.menu-summary:focus-visible {
  outline: 2px solid var(--color-primary); outline-offset: 2px;
}
.menu-panel {
  position: absolute; top: calc(100% + .4rem); left: 0; z-index: 130;
  min-width: 13rem; padding: .3rem 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .16);
}
/* The account menu is at the right edge; a panel dropping left of it would go
   off the window. */
.menu-panel-right { left: auto; right: 0; }
.menu-heading {
  margin: 0; padding: .4rem .75rem .3rem;
  font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--color-text-muted);
}
.menu-heading .menu-sub {
  display: block; margin-top: .15rem; font-weight: 500;
  text-transform: none; letter-spacing: 0; font-size: .72rem;
}
.menu-item {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  padding: .4rem .75rem; font-size: .82rem;
  color: var(--color-text); text-decoration: none;
  background: none; border: 0; text-align: left; cursor: pointer;
  font-family: inherit;
}
.menu-item:hover { background: var(--color-surface-2); text-decoration: none; }
.menu-item:focus-visible {
  outline: 2px solid var(--color-primary); outline-offset: -2px;
}
.menu-item .menu-icon { flex: none; font-size: .95rem; }
.menu-item .badge { margin-left: auto; }
.menu-caret { font-size: .6rem; opacity: .6; }

/* ---- The tasks popover ----------------------------------------------
   Wider than the other two panels because its rows carry a reference and a
   subject rather than a single word, and capped in height so a busy queue
   scrolls inside the panel instead of running off the bottom of the window.
   Past a handful it stops and points at the page — see core.tasks.POPOVER_LIMIT. */
.menu-tasks > summary { cursor: pointer; }
.tasks-panel { min-width: 20rem; max-width: 24rem; max-height: 70vh; overflow-y: auto; }
.tasks-pop-item {
  display: block; padding: .45rem .75rem; text-decoration: none; color: var(--color-text);
  border-left: 3px solid transparent;
}
.tasks-pop-item:hover {
  background: var(--color-surface-2); text-decoration: none;
  border-left-color: var(--color-primary);
}
.tasks-pop-title { display: block; font-size: .8rem; font-weight: 600; }
.tasks-pop-meta { display: block; font-size: .7rem; color: var(--color-text-muted); margin-top: .1rem; }
.tasks-pop-more { margin: 0; padding: .2rem .75rem .45rem; font-size: .7rem; color: var(--color-text-muted); }
.tasks-pop-empty, .tasks-pop-loading {
  margin: 0; padding: .9rem .75rem; font-size: .8rem; color: var(--color-text-muted); text-align: center;
}
/* Always the last thing in the panel, and always visible when it scrolls. */
.tasks-pop-all {
  position: sticky; bottom: 0; display: block; padding: .5rem .75rem;
  border-top: 1px solid var(--color-border); background: var(--color-surface);
  font-size: .78rem; font-weight: 600; color: var(--color-primary); text-decoration: none;
}
.tasks-pop-all:hover { background: var(--color-surface-2); text-decoration: none; }

/* The name in the bar, now a menu rather than a link to the settings page:
   an initial disc so it still reads as an account control when the window is
   too narrow for the name itself. */
.topbar .user-chip {
  font-size: .82rem; color: var(--color-text-muted); text-decoration: none;
  padding: .2rem .35rem .2rem .2rem; border-radius: 999px;
}
.topbar .user-chip:hover { background: var(--color-surface-2); }
.user-initials {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.55rem; height: 1.55rem; border-radius: 50%;
  background: var(--color-primary); color: var(--status-ink);
  font-size: .72rem; font-weight: 700; flex: none;
}
.user-chip-name { max-width: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Breadcrumbs ---------- */
/* Under the page title, not beside it: the title is what the page is and the
   trail is where it sits, and putting them on one line makes the reader parse
   which half is which. */
.breadcrumbs {
  margin: .15rem 0 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap; align-items: center; gap: .3rem;
  font-size: .74rem; color: var(--color-text-muted);
}
.breadcrumbs li { display: flex; align-items: center; gap: .3rem; }
.breadcrumbs li + li::before { content: "›"; opacity: .6; }
.breadcrumbs a { color: var(--color-text-muted); }
.breadcrumbs a:hover { color: var(--color-primary); }
.breadcrumbs [aria-current="page"] { color: var(--color-text); font-weight: 600; }

/* ---------- Footer ---------- */
/* One line of small print and three links. This is an application, not a
   website: a footer that repeats the sidebar is a second menu nobody reads. */
.app-footer {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: .5rem 1.5rem;
  padding: .85rem 1.5rem;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
  font-size: .74rem; color: var(--color-text-muted);
}
.app-footer-line { margin: 0; }
.app-footer-links { display: flex; flex-wrap: wrap; gap: 1rem; }
.app-footer-links a { color: var(--color-text-muted); }
.app-footer-links a:hover { color: var(--color-primary); }

.content { padding: 1.5rem; flex: 1; }

/* ---------- Cards / grids ---------- */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--card-pad-y) var(--card-pad-x);
  margin-bottom: var(--card-gap);
}
.card > h2, .card > h3 { margin-bottom: .85rem; }

/* THE CARD'S TITLE IS A HEADER BAR, NOT A BIGGER LINE OF TEXT.
   A page of eight cards with plain headings reads as one long column — the eye
   has nothing to catch on, and on a form page you cannot see where one card's
   fields end and the next card's begin. The dashboard's panels already solved
   this with a dark title bar; this gives every card the same, from the same two
   tokens, so light and dark both follow.

   `:first-child` only: a card whose second heading is an <h3> subsection keeps
   that as an ordinary heading, which is what a subsection should look like. The
   negative margins pull the bar out to the card's edges, past its padding. */
.card > h2:first-child,
.card > h3:first-child,
.card > form > h2:first-child {
  margin: calc(-1 * var(--card-pad-y)) calc(-1 * var(--card-pad-x)) .95rem;
  padding: .5rem var(--card-pad-x);
  background: var(--panel-header-bg);
  color: var(--panel-header-ink);
  border-radius: var(--radius) var(--radius) 0 0;
  font-size: .85rem;
  font-weight: 600;
  line-height: 1.35;
}
.card > h2:first-child a,
.card > h3:first-child a { color: var(--panel-header-ink); }
/* A card that is only a heading and nothing else would end with a bare bar. */
.card > h2:only-child, .card > h3:only-child { margin-bottom: calc(-1 * var(--card-pad-y)); border-radius: var(--radius); }

/* Opted out where the heading is the record's own name rather than a section
   label — a work order's title is content, not chrome. */
.card > h2.plain-title:first-child {
  margin: 0 0 .85rem; padding: 0; background: none;
  color: var(--color-text); font-size: 1.15rem; border-radius: 0;
}

/* A heading with a button beside it — a dozen cards open with the heading
   wrapped in a .flex-between so an action can sit on the right. Those cards
   were the only ones left with a plain heading, which is what made the header
   bars look applied at random rather than to every card. `:has()` is how the
   rule can tell a wrapped heading from any other first child; where it is not
   supported the card simply keeps the plain heading it had before. */
.card > .flex-between:first-child:has(> h2),
.card > .flex-between:first-child:has(> h3) {
  margin: calc(-1 * var(--card-pad-y)) calc(-1 * var(--card-pad-x)) .95rem;
  padding: .4rem var(--card-pad-x);
  background: var(--panel-header-bg);
  color: var(--panel-header-ink);
  border-radius: var(--radius) var(--radius) 0 0;
  min-height: 2.1rem;
}
.card > .flex-between:first-child:has(> h2) > h2,
.card > .flex-between:first-child:has(> h3) > h3 {
  margin: 0; padding: 0; background: none; border-radius: 0;
  color: var(--panel-header-ink); font-size: .85rem; font-weight: 600; line-height: 1.35;
}
.card > .flex-between:first-child:has(> h2) > a:not(.btn),
.card > .flex-between:first-child:has(> h3) > a:not(.btn) { color: var(--panel-header-ink); }

.grid { display: grid; gap: var(--grid-gap); }
/* A grid item's automatic minimum size is its min-content width, and a <select>
   reports the width of its LONGEST OPTION as that. One person picker holding
   "Olanrewaju Ajiboye — Chemistry Operations (reports to …)" was therefore
   enough to push a three-column row wider than the card containing it, which is
   what "the dropdowns are too wide and distort the page" was. min-width:0 lets
   the column be the width the grid says it is; the control then truncates its
   own text instead of the page growing a horizontal scrollbar. */
.grid > * { min-width: 0; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); } }

.stat-tile { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 1rem 1.15rem; }
.stat-tile .stat-value { font-size: 1.8rem; font-weight: 700; line-height: 1.1; }
.stat-tile .stat-label { font-size: .78rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: .04em; margin-top: .25rem; }
.stat-tile.warn .stat-value { color: var(--priority-2); }
.stat-tile.danger .stat-value { color: var(--priority-1); }
.stat-tile.good .stat-value { color: var(--priority-4); }

/* ---------- The asset page's head and its band ----------
   A figure that opens the register behind it. The tile is the link, not a word
   inside it: the whole card is the target, which is what makes a band of six
   usable with a trackpad and on a tablet in the plant. */
.asset-head {
  display: flex; flex-wrap: wrap; gap: .75rem;
  align-items: center; justify-content: space-between;
}
.asset-head .asset-where { font-size: .82rem; }
.asset-head-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* Six across, on one line. The tiles are narrow at that width, so the figure
   and the label shrink with them and the note wraps rather than the row
   breaking — the point of the band is that the whole state of the asset is one
   glance, and two rows is two glances. Below 1200px it falls to three and then
   to two, because six columns of 90px is not a glance either. */
.asset-insight { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.asset-insight .stat-tile { display: block; padding: .7rem .8rem; min-width: 0; }
.asset-insight .stat-value { font-size: 1.3rem; }
.asset-insight .stat-label { font-size: .7rem; }
.asset-insight .stat-note {
  font-size: .7rem; color: var(--color-text-muted); margin-top: .25rem; line-height: 1.3;
}
@media (max-width: 1200px) { .asset-insight { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 760px) { .asset-insight { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
a.stat-tile.tile-link { color: inherit; text-decoration: none; }
a.stat-tile.tile-link:hover,
a.stat-tile.tile-link:focus-visible {
  border-color: var(--color-primary); text-decoration: none;
}
/* The label carries the underline on hover rather than the whole card: a block
   of underlined text the size of a tile reads as a mistake. */
a.stat-tile.tile-link:hover .stat-label { text-decoration: underline; }

/* ---------- Picking an asset by where it stands ----------
   The memo form offers the tag search first and the tree second, because the
   search is quicker when the tag is known and useless when it is not.

   The tree opens in a dialog over the form. Inline it pushed the rest of the
   form off the screen the moment a unit was opened — the hierarchy is tens of
   thousands of nodes deep, and by the time you reach a tag the field the choice
   lands in is nowhere in sight. */
.raise-card { max-width: 46rem; }
.tree-dialog-open { margin: 0 0 .9rem; }
.tree-dialog {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  padding: 1rem 1.2rem 1.1rem;
  width: min(46rem, calc(100% - 2rem));
  /* Tall, because depth is the whole point: a short box means scrolling the
     tree in a letterbox. */
  max-height: min(82vh, 48rem);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
}
.tree-dialog::backdrop { background: rgba(0, 0, 0, .45); }
.tree-dialog-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.tree-dialog-head h2 { margin: 0 0 .2rem; font-size: 1.02rem; }
.tree-dialog-close {
  background: none; border: 0; font-size: 1.5rem; line-height: 1;
  color: var(--color-text-muted); cursor: pointer; padding: 0 .2rem;
}
.tree-dialog-close:hover { color: var(--color-primary); }
.tree-dialog-body {
  margin-top: .6rem; max-height: 58vh; overflow: auto;
  border: 1px solid var(--color-border); border-radius: var(--radius); padding: .6rem .8rem;
}
.tree-dialog-foot { display: flex; justify-content: flex-end; margin-top: .8rem; }
/* A leaf in picking mode is a button, and must not look like one — it sits in a
   list of names and reads as one. */
.tree-pick {
  background: none; border: 0; padding: 0; font: inherit; color: var(--color-text);
  cursor: pointer; text-align: left;
}
.tree-pick:hover, .tree-pick:focus-visible { color: var(--color-primary); text-decoration: underline; }
.pick-note:empty { display: none; }
.pick-note { color: var(--priority-4); font-weight: 600; }

/* ---------- The Maintenance KPI dashboard ----------
   Laid out after the station's own CM Analysis workbook, because the monthly
   review is already read off that and nobody should have to learn a second
   shape for the same figures.

   Every colour is a token. The workbook is red on grey and this is not: a
   dashboard that ignored the theme would be the one page in the CMMS that
   cannot be read in the control room at three in the morning. */
.kpi-board {
  display: flex; gap: 1rem; align-items: flex-start; justify-content: space-between;
  flex-wrap: wrap; margin-bottom: .9rem;
}
.kpi-strips { display: flex; flex-direction: column; gap: .4rem; flex: 1 1 32rem; min-width: 0; }
/* Each strip is one horizontal run of chips that wraps rather than scrolls —
   twelve months and six units have to be reachable on a tablet in the plant. */
.kpi-strip { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.kpi-strip-label {
  font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--color-text-muted); min-width: 4.2rem;
}
.kpi-strip-sep { width: .8rem; }
.kpi-chip {
  display: inline-block; padding: .22rem .6rem; border-radius: 4px;
  border: 1px solid var(--color-border); background: var(--color-surface);
  color: var(--color-text); font-size: .76rem; font-weight: 600; text-decoration: none;
  line-height: 1.5;
}
.kpi-chip-sm { padding: .22rem .45rem; min-width: 2.3rem; text-align: center; }
.kpi-chip:hover { border-color: var(--color-primary); text-decoration: none; }
.kpi-chip.is-on {
  background: var(--color-primary); border-color: var(--color-primary); color: var(--status-ink);
}
.kpi-period {
  font-size: 1.15rem; font-weight: 700; letter-spacing: .04em;
  padding: .4rem .9rem; border-radius: 6px;
  background: var(--tint-warning-bg); color: var(--tint-warning-ink);
  border: 1px solid var(--color-border); white-space: nowrap;
}
.kpi-secondary-filters { margin-bottom: 1rem; }

/* The seven headline figures with the subject in the middle, as on the sheet. */
.kpi-headline {
  display: grid; gap: .6rem; margin-bottom: .9rem;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
.kpi-fig {
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius); padding: .75rem .8rem; min-width: 0;
}
.kpi-fig-value { font-size: 1.7rem; font-weight: 700; line-height: 1.05; color: var(--color-primary); }
.kpi-fig-value small { font-size: .95rem; font-weight: 600; }
.kpi-fig.is-warn .kpi-fig-value { color: var(--priority-2); }
.kpi-fig-label {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  margin-top: .2rem;
}
.kpi-fig-note { font-size: .69rem; color: var(--color-text-muted); margin-top: .25rem; line-height: 1.35; }
.kpi-subject {
  background: var(--tint-warning-bg); color: var(--tint-warning-ink);
  border: 1px solid var(--color-border); border-radius: var(--radius);
  padding: .75rem .8rem; display: flex; flex-direction: column; justify-content: center;
  text-align: center; min-width: 0;
}
.kpi-subject-name { font-size: 1.1rem; font-weight: 800; line-height: 1.15; }
.kpi-subject-scope { font-size: .68rem; margin-top: .25rem; opacity: .85; }

.kpi-mid {
  display: grid; gap: var(--grid-gap); margin-bottom: 1rem;
  grid-template-columns: 1.4fr 1fr 1fr 1fr;
}
.kpi-mid > .card { margin-bottom: 0; }
.kpi-mid-gauge, .kpi-mid-clock { display: flex; flex-direction: column; justify-content: center; }
.kpi-canvas { position: relative; height: 230px; }
.kpi-mid-chart .kpi-canvas { height: 200px; }

.gauge { margin: 0; text-align: center; }
.gauge svg { width: 100%; max-width: 15rem; height: auto; }
.gauge figcaption {
  font-size: .74rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  margin-top: .2rem;
}
.kpi-clock-label {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-text-muted);
}
.kpi-clock { font-size: 2rem; font-weight: 800; color: var(--color-primary); line-height: 1.1; margin: .3rem 0; }

/* Below tablet the seven-across headline is unreadable, and below phone width
   so is four-across. Both fall back rather than scrolling sideways. */
@media (max-width: 1200px) {
  .kpi-headline { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .kpi-mid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .kpi-headline { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .kpi-mid { grid-template-columns: minmax(0, 1fr); }
  .kpi-strip-label { min-width: 0; }
}

/* A reading outside its limits, on the PM round sheet. Tinted rather than
   coloured text, because the number itself must stay readable — the row is the
   warning, not the figure. */
.row-alarm > td { background: var(--tint-danger-bg); }

/* ---------- The PM programme, as a plan ----------
   A discipline per accordion, open where something is owed. The rows carry a
   left edge in the colour of their horizon so a scan down the page reads as
   "late, late, today, next week" without anybody parsing a date. */
/* ONE ROW, WHATEVER THE COUNT. Six figures across rather than a four-column
   grid wrapping onto two rows: the position is meant to be taken in at a
   glance, and a glance does not wrap. Each tile is roughly half the height of a
   report tile — the figure still leads, the note is one line. Below tablet it
   falls to three and then two, because six columns of 70px is not a glance. */
.pm-summary {
  display: grid; gap: .5rem; margin-bottom: .5rem;
  grid-template-columns: repeat(6, minmax(0, 1fr));
}
.pm-summary .stat-tile { padding: .5rem .6rem; min-width: 0; }
.pm-summary .stat-value { font-size: 1.25rem; line-height: 1.1; }
.pm-summary .stat-label {
  font-size: .66rem; letter-spacing: .03em; margin-top: .1rem;
}
.pm-summary .stat-note {
  font-size: .64rem; line-height: 1.25; margin-top: .15rem;
  /* Two lines at most: a note that grows makes the row ragged, and the tile is
     a link to the thing it describes anyway. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
@media (max-width: 1100px) { .pm-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px) { .pm-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.pm-horizon-bar {
  display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; margin: 1rem 0 .8rem;
}
.pm-horizon-label {
  font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--color-text-muted); margin-right: .2rem;
}
.pm-horizon-count { font-size: .78rem; margin-left: auto; }

.pm-group {
  background: var(--color-surface); border: 1px solid var(--color-border);
  border-radius: var(--radius); margin-bottom: .6rem;
}
.pm-group > summary {
  display: flex; align-items: center; gap: .8rem; justify-content: space-between;
  padding: .7rem 1rem; cursor: pointer; list-style: none;
}
.pm-group > summary::-webkit-details-marker { display: none; }
.pm-group > summary::before {
  content: "▸"; color: var(--color-text-muted); flex: none; font-size: .8rem;
}
.pm-group[open] > summary::before { content: "▾"; }
.pm-group[open] > summary { border-bottom: 1px solid var(--color-border); }
.pm-group-name { font-weight: 700; font-size: .95rem; flex: 1; min-width: 0; }
.pm-group-counts { display: flex; gap: .35rem; align-items: center; flex-wrap: wrap; font-size: .8rem; }
.pm-group-body { padding: 0 1rem 1rem; }
.pm-group-body .data-table { margin-top: .6rem; }

.pm-due { white-space: nowrap; }
.pm-due-note { display: block; font-size: .7rem; }
/* The horizon, as a colour on the row's own edge. */
.pm-row > td:first-child { border-left: 3px solid transparent; padding-left: .55rem; }
.pm-row-overdue > td:first-child { border-left-color: var(--priority-1); }
.pm-row-today > td:first-child { border-left-color: var(--priority-2); }
.pm-row-week > td:first-child { border-left-color: var(--priority-3); }

/* A list of components or parts is a list, not a bulleted paragraph. */
.plain-list { margin: 0; padding-left: 1.1rem; }
.plain-list li { margin-bottom: .2rem; }

/* ---------- Tables ---------- */
table.data-table { width: 100%; border-collapse: collapse; font-size: .87rem; }
table.data-table th, table.data-table td { padding: .55rem .7rem; border-bottom: 1px solid var(--color-border); text-align: left; }
table.data-table th { color: var(--color-text-muted); font-weight: 600; font-size: .75rem; text-transform: uppercase; letter-spacing: .03em; }
table.data-table tr:hover td { background: var(--color-primary-light); }
table.data-table .row-link { display: block; }

/* ---------- Badges ---------- */
.badge { display: inline-block; padding: .18rem .55rem; border-radius: 20px; font-size: .72rem; font-weight: 600; color: var(--status-ink); white-space: nowrap; }
.badge-priority-1 { background: var(--priority-1); }
.badge-priority-2 { background: var(--priority-2); color: var(--ink-on-warm); }
.badge-priority-3 { background: var(--priority-3); color: var(--ink-on-warm); }
.badge-priority-4 { background: var(--priority-4); color: var(--ink-on-warm); }
.badge-priority-5 { background: var(--priority-5); }

.badge-status-draft { background: var(--status-draft); }
.badge-status-planned { background: var(--status-planned); }
.badge-status-awaiting_permit { background: var(--status-awaiting-permit); }
.badge-status-approved { background: var(--status-approved); }
.badge-status-released { background: var(--status-released); }
.badge-status-in_progress { background: var(--status-in-progress); }
.badge-status-suspended { background: var(--status-suspended); }
.badge-status-completed { background: var(--status-completed); }
.badge-status-accepted { background: var(--status-accepted); }
.badge-status-written_off { background: var(--status-written-off); }
.badge-status-closed { background: var(--status-closed); }

/* Job Ticket statuses (distinct string set from Work Order statuses above) */
.badge-status-new { background: var(--status-new); }
.badge-status-under_review { background: var(--status-awaiting-permit); }
.badge-status-converted { background: var(--status-completed); }
.badge-status-ops_confirmed { background: var(--status-accepted); }
.badge-status-finalised { background: var(--status-written-off); }
.badge-status-rejected { background: var(--status-rejected); }
.badge-status-duplicate { background: var(--status-duplicate); }

.badge-outline { background: transparent; border: 1px solid var(--color-border); color: var(--color-text-muted); }
.badge-good { background: var(--priority-4); color: var(--ink-on-warm); }
.badge-bad { background: var(--priority-1); }

/* Risk assessment bands and operation-event severities share one scale, so the
   same colour carries the same level of concern on both screens. */
.badge-risk-low { background: var(--priority-4); color: var(--ink-on-warm); }
.badge-risk-medium { background: var(--priority-3); color: var(--ink-on-warm); }
.badge-risk-high { background: var(--priority-2); color: var(--ink-on-warm); }
.badge-risk-extreme { background: var(--priority-1); }
.badge-risk-critical { background: var(--priority-1); }
.badge-risk-unrated { background: transparent; border: 1px solid var(--color-border); color: var(--color-text-muted); }

/* Unit operating states */
.badge-state-running { background: var(--status-completed); }
.badge-state-standby { background: var(--status-approved); }
.badge-state-stopped { background: var(--status-closed); }
.badge-state-forced_outage { background: var(--priority-1); }
.badge-state-planned_outage { background: var(--status-awaiting-permit); }
.badge-state-unknown { background: transparent; border: 1px solid var(--color-border); color: var(--color-text-muted); }

/* ---------- Forms ---------- */
form .field { margin-bottom: 1rem; }
form label { display: block; font-weight: 600; font-size: .82rem; margin-bottom: .3rem; }
form .help-text { font-size: .76rem; color: var(--color-text-muted); margin-top: .25rem; }
/* Checkbox and radio are excluded: stretching them to 100% width leaves the
   control on the left of a full-width box and throws its label to the far
   right of the form, which is what made the triage options and the
   "requires risk assessment" tick look unaligned. */
input:not([type="checkbox"]):not([type="radio"]), select, textarea {
  width: 100%; padding: .5rem .6rem; border: 1px solid var(--color-border); border-radius: 6px;
  font-size: .88rem; font-family: inherit; background: var(--control-bg); color: var(--color-text);
}
input[type="checkbox"], input[type="radio"] {
  width: auto; margin: 0; flex: none; accent-color: var(--color-primary); cursor: pointer;
}
/* A label wrapping a tick or radio sits inline with it, not above it. */
label.choice, .choice-list label {
  display: flex; align-items: center; gap: .5rem;
  font-weight: 400; margin-bottom: .35rem; cursor: pointer;
}
.choice-list { display: flex; flex-direction: column; gap: .1rem; margin-bottom: .6rem; }
textarea { min-height: 90px; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--color-primary); }
.errorlist { color: var(--priority-1); font-size: .8rem; padding-left: 0; margin: .2rem 0; list-style: none; }
.form-errors { background: var(--tint-danger-bg); border: 1px solid var(--priority-1); border-radius: 6px; padding: .6rem .8rem; margin-bottom: 1rem; color: var(--tint-danger-ink); font-size: .85rem; }

/* ---------- Buttons ---------- */
.btn { display: inline-block; padding: .5rem 1rem; border-radius: 6px; border: 1px solid transparent; font-size: .85rem; font-weight: 600; cursor: pointer; text-align: center; }
.btn-primary { background: var(--color-primary); color: var(--status-ink); }
.btn-primary:hover { background: var(--color-primary-dark); text-decoration: none; }
.btn-secondary { background: var(--btn-secondary-bg); border-color: var(--color-border); color: var(--btn-secondary-ink); }
.btn-secondary:hover { background: var(--btn-secondary-hover-bg); text-decoration: none; }
.btn-danger { background: var(--priority-1); color: var(--status-ink); }
.btn-sm { padding: .32rem .7rem; font-size: .78rem; }
.btn-group { display: flex; gap: .5rem; flex-wrap: wrap; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ---------- Alerts / gate messages ---------- */
.alert { border-radius: 6px; padding: .7rem .9rem; margin-bottom: 1rem; font-size: .87rem; }
.alert-warning { background: var(--tint-warning-bg); border: 1px solid var(--tint-warning-border); color: var(--tint-warning-ink); }
.alert-danger { background: var(--tint-danger-bg); border: 1px solid var(--tint-danger-border); color: var(--tint-danger-ink); }
.alert-success { background: var(--tint-success-bg); border: 1px solid var(--tint-success-border); color: var(--tint-success-ink); }
.alert-info { background: var(--tint-soft-bg); border: 1px solid var(--color-border); color: var(--tint-soft-ink); }

.gate-checklist { list-style: none; padding: 0; margin: 0; }
.gate-checklist li { padding: .35rem 0; display: flex; align-items: center; gap: .5rem; font-size: .87rem; }
.gate-checklist .tick { color: var(--priority-4); font-weight: 700; }
.gate-checklist .cross { color: var(--priority-1); font-weight: 700; }

/* ---------- Misc ---------- */
.muted { color: var(--color-text-muted); }
.text-right { text-align: right; }
.mt-1 { margin-top: .5rem; } .mt-2 { margin-top: 1rem; } .mb-0 { margin-bottom: 0; }
.flex-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.tag-list { display: flex; gap: .4rem; flex-wrap: wrap; }
.pill { background: var(--tint-soft-bg); color: var(--tint-soft-ink); padding: .15rem .55rem; border-radius: 20px; font-size: .72rem; font-weight: 600; }
/* The one variant: a pill that is bad news. Same tints the danger alert uses,
   so it needs no contrast sweep of its own. */
.pill-bad { background: var(--tint-danger-bg); color: var(--tint-danger-ink); }
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--color-text-muted); }
.messages { list-style: none; padding: 0; margin: 0 0 1rem 0; }
.messages li { padding: .6rem .9rem; border-radius: 6px; margin-bottom: .5rem; font-size: .87rem; }
.messages .success { background: var(--tint-success-bg); border: 1px solid var(--tint-success-border); color: var(--tint-success-ink); }
.messages .error { background: var(--tint-danger-bg); border: 1px solid var(--tint-danger-border); color: var(--tint-danger-ink); }
.messages .warning { background: var(--tint-warning-bg); border: 1px solid var(--tint-warning-border); color: var(--tint-warning-ink); }
.messages .info { background: var(--tint-soft-bg); border: 1px solid var(--color-border); color: var(--tint-soft-ink); }

/* ---------- Login page ---------- */
/* ---------- The sign-in page ----------
   The background is a layer of its own behind the card rather than a
   background-image on the page, because a video cannot be a background-image.
   Same stacking either way, so the still and the moving version need no
   separate layout. See core.models.SignInPageSettings. */
.login-page {
  position: relative; min-height: 100vh; display: flex;
  align-items: center; justify-content: center; padding: 1.5rem;
  background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
}
.login-bg { position: absolute; inset: 0; overflow: hidden; z-index: 0; }
.login-bg-media {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* The wash. Its opacity is set inline from the admin setting — this rule owns
   the colour, that one owns the strength. */
.login-bg-wash {
  position: absolute; inset: 0;
  background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
}
.login-card {
  position: relative; z-index: 1;
  background: var(--color-surface); border-radius: var(--radius);
  padding: 2.2rem 2rem; width: 340px; max-width: 100%;
  box-shadow: 0 10px 40px rgba(0, 0, 0, .35);
}

/* Logo, heading and subtitle as one centred stack. The form below stays
   left-aligned: labels reading down a left edge is what makes a form scannable,
   and centring those would be centring for its own sake. */
.login-brand { text-align: center; margin-bottom: 1.3rem; }
.login-logo { height: 44px; margin: 0 auto .75rem; display: block; }
.login-card h1 { font-size: 1.25rem; margin-bottom: .25rem; }
.login-card p.subtitle { color: var(--color-text-muted); font-size: .82rem; margin: 0; }

/* MICROSOFT'S OWN COLOURS, AND ITS OWN MARK.
   Microsoft's brand guidance for the sign-in button is a white face, a grey
   border and the four-square mark at full colour — which is also the version
   people recognise. The dark theme gets Microsoft's dark variant rather than an
   inverted white one, for the same reason. */
.btn-microsoft {
  width: 100%; display: flex; align-items: center; justify-content: center;
  gap: .6rem; background: #ffffff; color: #5e5e5e;
  border: 1px solid #8c8c8c; font-weight: 600;
}
.btn-microsoft:hover { background: #f3f3f3; color: #2f2f2f; text-decoration: none; }
.btn-microsoft .ms-mark { flex: 0 0 auto; display: block; }
:root[data-theme="dark"] .btn-microsoft,
:root:not([data-theme="light"]) .btn-microsoft {
  background: #2f2f2f; color: #ffffff; border-color: #8c8c8c;
}
:root[data-theme="dark"] .btn-microsoft:hover,
:root:not([data-theme="light"]) .btn-microsoft:hover { background: #3d3d3d; color: #ffffff; }
/* The sign-in page never carries data-theme (it is rendered before any
   preference is known), so the light face is what it actually shows. The dark
   rules above are for a device set to dark by its own system. */

.login-or {
  display: flex; align-items: center; gap: .6rem; margin: 1.1rem 0;
  color: var(--color-text-muted); font-size: .78rem;
}
.login-or::before, .login-or::after {
  content: ""; flex: 1; border-top: 1px solid var(--color-border);
}

/* A looping video behind a sign-in box is decoration, and decoration is the
   first thing to drop for somebody who has asked their device for less motion.
   The poster frame is already on the element, so it simply stops moving. */
@media (prefers-reduced-motion: reduce) {
  .login-bg-video { display: none; }
}

/* ---------- Detail key/value ---------- */
.kv-grid { display: grid; grid-template-columns: 160px 1fr; gap: .4rem .8rem; font-size: .87rem; }
.kv-grid dt { color: var(--color-text-muted); }
.kv-grid dd { margin: 0; }

.filter-bar { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: 1rem; align-items: flex-end; }
/* The action block is a field-shaped box, but it is as wide as its buttons —
   the 160px floor is for controls that have to hold a value. */
.filter-bar .field { margin-bottom: 0; min-width: 160px; }
.filter-bar .field.filter-actions { min-width: 0; }
/* A caption that explains the row it belongs to, on its own line inside the same
   card. Stacking it as a separate paragraph put a band of prose between the
   controls and the first figure on the page. */
.filter-bar .filter-caption { flex: 1 0 100%; margin: .3rem 0 0; }
/* The date boxes are a fixed size whatever the locale's date format, so the
   filter bar does not reflow when a range is picked. */
.filter-bar .date-custom { min-width: 9.5rem; }

/* The headline figures and the line that says what they were counted over. The
   two belong together, so the tiles keep only enough space beneath them to
   carry the sentence. */
.list-insight { margin-bottom: .5rem; }
.list-insight-scope { margin: 0 0 1.1rem; }

/* "Showing only the requests you raised." Tinted rather than styled as a
   warning: it is a statement of what the page is, not something gone wrong. */
.scope-note {
  margin: 0 0 .9rem;
  padding: .5rem .75rem;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-surface-2);
  color: var(--color-text-muted);
  font-size: .82rem;
}

/* ---------- Homepage: card header bar (matches the classic CMMS-portal look) ---------- */
.panel { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); margin-bottom: 1.25rem; overflow: hidden; }
.panel-header {
  background: var(--panel-header-bg); color: var(--panel-header-ink); padding: .5rem var(--card-pad-x);
  font-size: .85rem; font-weight: 600;
  display: flex; align-items: center; justify-content: space-between;
}
.panel-header a { color: var(--panel-header-ink); opacity: .8; font-size: .8rem; }
.panel-body { padding: var(--card-pad-y) var(--card-pad-x); }

/* ---------- Dashboard hero: station image beside the live plant monitor ------
   Both halves share one height so the row reads as a single band. It is taller
   than the old image-only hero because the embedded Grafana view needs the room
   to be worth looking at. ---------------------------------------------------- */
.hero-row { --hero-height: 290px; margin-bottom: 1.25rem; }
.hero-row.hero-split { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.1rem; align-items: stretch; }
.hero-row > * { margin-bottom: 0; }

.hero-image { padding: 0; overflow: hidden; position: relative; }
.hero-image img { width: 100%; height: var(--hero-height); object-fit: cover; display: block; }
.hero-caption {
  position: absolute; inset: 0; display: flex; align-items: center; color: #fff;
  background: linear-gradient(90deg, rgba(8, 64, 95, .75), transparent 65%);
}
.hero-caption > div { padding: 0 1.5rem; }
.hero-caption h1 { margin: 0; font-size: 1.3rem; }
.hero-caption p { margin: .2rem 0 0; opacity: .9; font-size: .85rem; }

.hero-monitor { display: flex; flex-direction: column; }
/* The header eats into the shared height, so the frame takes what is left. */
.hero-monitor-frame {
  /* Grafana has no zoom of its own, so give the iframe a viewport 1/zoom the size
     of the frame in both directions and scale the render back down. The panels
     therefore lay out as they would on a large screen and then shrink to fit,
     instead of collapsing into one narrow column. One knob: change this only. */
  --monitor-zoom: .4;
  position: relative; flex: 1; min-height: 0; overflow: hidden; background: var(--color-surface-2);
}
.hero-monitor-frame iframe {
  width: calc(100% / var(--monitor-zoom));
  height: calc(100% / var(--monitor-zoom));
  transform: scale(var(--monitor-zoom));
  transform-origin: top left;
  border: 0; display: block; position: relative;
}
.hero-monitor-status {
  position: absolute; inset: 0; z-index: 1; margin: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: .8rem; color: var(--color-text-muted); text-align: center; padding: 1rem;
}

@media (max-width: 900px) {
  /* Side by side, neither half is wide enough to read — stack them instead. */
  .hero-row.hero-split { grid-template-columns: 1fr; }
  .hero-row > .card, .hero-row > .panel { margin-bottom: 1.1rem; }
}

/* The "no event type" tile links at those rows, so it needs a tile's look
   without a link's underline. */
a.strip-tile { text-decoration: none; }
a.strip-tile:hover { border-color: var(--color-primary); }

/* Two labelled status strips inside one card (job tickets above work orders):
   the label row names each strip so the counts are not ambiguous. */
.strip-group-label {
  display: flex; align-items: baseline; justify-content: space-between;
  font-size: .74rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-text-muted); margin-bottom: .35rem;
}
.strip-group-label a { font-size: .72rem; font-weight: 600; text-transform: none; letter-spacing: 0; }

/* Dashboard outage-insight charts. Heights are fixed rather than left to an
   aspect ratio so the three charts keep a consistent bar thickness. */
.outage-filter { display: flex; flex-wrap: wrap; gap: .7rem; align-items: end; margin-bottom: .3rem; }
.outage-filter .field select { min-width: 9rem; }
.outage-filter-actions { display: flex; gap: .4rem; align-items: flex-end; }
/* Apply and Clear line up with the Year and Month selects at both edges. Two
   things are needed and only two. First the margin: these filter rows are
   <form> elements, so `form .field { margin-bottom: 1rem }` applies to every
   control in them, and with `align-items: end` it is the MARGIN box that gets
   bottom-aligned — leaving the selects sitting a full 1rem above the buttons.
   Second, one shared control height, so the boxes that now share a baseline are
   also the same size and the top edges meet too.

   An earlier attempt added a label-height ::before spacer here. It could not
   work: this is a flex row, so the pseudo-element became a sibling flex item
   beside the buttons rather than a block above them, and its only effect was an
   extra gap. */
.outage-filter .field { margin-bottom: 0; }

/* ---------- Filter rows: one aligned shape for all of them ----------
   The recurring bug: a filter row is a flex line of `.field` blocks, each a
   label stacked over a control, and the Apply button was a bare <button> beside
   them. Bottom-aligning could not fix that, because the button has no label
   above it — so it sat a label's height lower than everything else. Matching
   heights could not fix it either: a <select>, an <input type=date> and a
   <button> are three different intrinsic heights in every browser, and
   type=date carries a picker icon that changes it again.

   So the button gets a label of its own — an empty one — and becomes a `.field`
   like its neighbours. Alignment then follows from the elements having the same
   structure rather than from any measurement, which is why it holds at every
   density setting and with any mix of control types. */
.filter-actions { display: flex; flex-direction: column; flex: 0 0 auto; }

/* WHY THE SPACER IS A <label> AND NOT A <span>.
   This row has been misaligned three times. Each time the cause was the same:
   the spacer's box was *described* to match a field label — same font size, same
   margin — and then something moved one and not the other. The density blocks
   were the last culprit: `form label { margin-bottom: .18rem }` at Compact hit
   every real label and missed the <span>, so the button sat two pixels low at
   the default density.

   So the spacer is now a real <label> holding a non-breaking space. Every rule
   that targets a label in this row — the four filter-row rules below, the global
   `form label`, and both density overrides — applies to it identically, because
   it *is* one. There is nothing left to keep in step by hand, which is the only
   kind of fix that stops this recurring. `visibility: hidden` keeps its box while
   hiding the space, and aria-hidden keeps it off the accessibility tree. */
.filter-actions > label.filter-actions-spacer { visibility: hidden; }

/* One definition for every filter row's labels, including the spacer above.
   Previously four near-identical rules at .72rem, .74rem and .82rem — any of
   which could be edited alone and pull the button out of line. */
.outage-filter .field > label,
.genlog-filter .field > label,
.report-filters .field > label,
.filter-bar .field > label,
.filter-actions > label {
  display: block;
  font-size: .74rem;
  color: var(--color-text-muted);
  font-weight: 600;
  margin-bottom: .2rem;
}
.filter-actions-row { display: flex; gap: .4rem; align-items: stretch; }
.filter-actions-row .btn { display: inline-flex; align-items: center; }

/* ONE CONTROL HEIGHT ACROSS A FILTER ROW — AND NO WRAPPER REQUIRED.

   This is the fourth time the Apply button has come out of line, and the third
   distinct cause. The previous version of this rule reached the button only
   through `.filter-actions-row .btn`, so alignment depended on the template
   having wrapped it. Fifteen pages carry a filter row; **one** wrapped its
   button. The other fourteen — job tickets, work orders, assets, permits,
   inventory, risk, the calendar and the rest — write the obvious thing:

       <button type="submit" class="btn btn-primary">Filter</button>

   sitting bare in the row. Those buttons matched nothing here, kept their
   padding-derived height, and stood a few pixels taller or shorter than the
   2.3rem controls beside them.

   A rule that only works when the markup is written a particular way is not a
   fix, it is a convention — and a convention one page in fifteen follows is
   already lost. So this matches by *position in a filter row*, not by wrapper:
   any select, text-like input or button anywhere inside one of these rows. The
   obvious markup is now the correct markup, and FilterRowAlignmentTests fails
   if this selector starts requiring a wrapper again.

   Checkboxes and radios are excluded: they are square controls whose size is
   the browser's business, and stretching one to 2.3rem makes a tick box the
   size of a button. */
:is(.outage-filter, .genlog-filter, .report-filters, .filter-bar)
  :is(select, input:not([type="checkbox"]):not([type="radio"]), .btn, button) {
  height: 2.3rem;
  box-sizing: border-box;
}

/* A FIXED HEIGHT IS ONLY HALF OF ALIGNMENT — THE TEXT HAS TO BE CENTRED IN IT.
   This is the third distinct cause of the Apply button looking wrong, and the
   subtlest. `.btn` is `display: inline-block`; giving it `height: 2.3rem` above
   sizes the box correctly, and the box does line up. What does not line up is
   the writing: an inline-block lays its line box out from the *top* of the
   content area, so with .5rem padding and a .85rem font the word "Filter" sits
   about 4px above the text in the selects beside it. The boxes agree and the
   labels visibly do not, which is what a reader actually sees.

   `.filter-actions-row .btn` already carried inline-flex for this reason — but
   only one page in fifteen wraps its button in that, which is why the other
   fourteen still looked wrong after the height was fixed. Centring belongs with
   the height, on the same selector, so the two cannot be applied apart. */
:is(.outage-filter, .genlog-filter, .report-filters, .filter-bar) :is(.btn, button) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
/* A bare button carries no label above it, so it has to be pushed down to the
   controls' line rather than floating at the top of the row. The rows are all
   align-items: end, which does this for a one-element item — but a button that
   the browser has stretched (align-self: stretch inherited from a grid row)
   would fill the row's height instead. */
:is(.outage-filter, .genlog-filter, .report-filters, .filter-bar) > :is(.btn, button) {
  align-self: end;
}

/* The Generation Log's filter repeated immediately above the readings table.
   The panel body around the table has no padding of its own — the table runs to
   the panel's edges — so the filter carries its own, and a rule under it marks
   where the controls stop and the readings begin. */
.genlog-table-filter {
  padding: .85rem var(--card-pad-x) .35rem;
  border-bottom: 1px solid var(--color-border);
}
.genlog-table-filter .genlog-filter { margin-bottom: .5rem; }

.insight-chart { position: relative; height: 190px; }
/* The compact row: the four charts side by side rather than stacked, so the whole
   of Outage Insight is one screenful and the four cuts of the same events can be
   read against each other instead of by scrolling between them. */
.insight-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: .9rem; align-items: start; }
/* Taller than the dashboard's other charts: two of the four are pies whose
   legend sits underneath, and the plot needs room left over to be worth drawing. */
.insight-row .insight-chart { height: 235px; }
@media (max-width: 1400px) { .insight-row { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px) { .insight-row { grid-template-columns: 1fr; } }
.chart-heading {
  font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-text-muted); margin: 0 0 .4rem;
}

/* ---------- Report catalogue ---------- */
.report-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: .7rem; }
.report-card {
  display: flex; flex-direction: column; gap: .3rem; padding: .8rem .9rem; text-decoration: none;
  border: 1px solid var(--color-border); border-radius: 6px; background: var(--color-surface);
}
.report-card:hover { border-color: var(--color-primary); background: var(--color-primary-light); }
.report-card-title { font-weight: 600; font-size: .88rem; color: var(--color-primary); }
.report-card-desc { font-size: .78rem; color: var(--color-text-muted); line-height: 1.4; }

/* ---------- Reports: the rail and the pane ----------
   Two dozen reports with long names. A horizontal tab strip either wraps into
   four rows or truncates the words that tell one report from another, so the
   tabs run down the side and each name gets a line. Below 900px the rail
   becomes a scrolling row above the pane — a 12rem column on a phone leaves
   nothing for the table. */
.report-layout { display: grid; grid-template-columns: 15rem 1fr; gap: 1.1rem; align-items: start; }
.report-rail {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: .5rem;
  position: sticky;
  top: 1rem;
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
}
.report-rail-group {
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--color-text-muted); padding: .6rem .55rem .25rem;
}
.report-rail-item {
  display: block; padding: .4rem .55rem; border-radius: 6px; font-size: .82rem;
  color: var(--color-text); text-decoration: none; line-height: 1.35;
}
.report-rail-item:hover { background: var(--color-primary-light); text-decoration: none; }
.report-rail-item.active {
  background: var(--panel-header-bg); color: var(--panel-header-ink); font-weight: 600;
}
.report-pane { min-width: 0; }
@media (max-width: 900px) {
  .report-layout { grid-template-columns: 1fr; }
  .report-rail { position: static; max-height: 15rem; }
}

.report-filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: .7rem; align-items: end; }
/* Same inherited-margin fix as the two filter rows above. */
.report-filters .field { margin-bottom: 0; }
/* Sizes the filter *controls* — a text box, a date box, a select. Checkboxes
   and radios are excluded, and the exclusion is the point: this rule used to
   catch them too, and the field picker's forty checkboxes each became a
   full-width 2.3rem slab that shoved its own label off the row. All anybody
   could see was a column of enormous empty boxes. */
.report-filters .field input:not([type="checkbox"]):not([type="radio"]),
.report-filters .field select { width: 100%; height: 2.3rem; box-sizing: border-box; }
/* The report filters are a grid rather than a flex row, so the action block
   spans the last line of it — that keeps Apply/Clear out of the middle of the
   grid. Everything else about its alignment comes from .filter-actions, shared
   with the other three filter rows.

   On its own row the label spacer is pure vertical waste: it exists to align a
   button with the fields *beside* it, and there are none. Dropping it and
   tightening the row gap takes about 1.3rem out of the gap between the last
   filter and the button, which is what made the panel look padded out. */
.report-filter-actions { grid-column: 1 / -1; }
.report-filter-actions > label.filter-actions-spacer { display: none; }
.report-filters { row-gap: .45rem; }

/* THE EXPORT LINKS SIT ON A PANEL HEADER, SO THEY TAKE ITS INK.
   This was a literal `#fff`, which was legible only for as long as every panel
   header was near-black. The moment the header became a light wash of the
   theme, CSV/XLSX/PDF went white on near-white and vanished — the links were
   still there and still worked, which is the worst version of this bug.
   LiteralColourTests guards literal *backgrounds*; it now guards colours on a
   panel header too. */
.report-export-links a {
  color: var(--panel-header-ink);
  opacity: .85;
  margin-left: .5rem;
  font-size: .78rem;
  text-decoration: underline;
}
.report-export-links a:hover { opacity: 1; }
.report-applied { margin: 0; padding: .5rem 1rem; font-size: .76rem; color: var(--color-text-muted); border-bottom: 1px solid var(--color-border); }
/* Reports are wide by nature — scroll the table, never the page. */
.report-table-scroll { overflow-x: auto; }
.report-table-scroll table.data-table { font-size: .8rem; white-space: nowrap; }
/* Numeric columns, header and figures alike. Scoped to the table rather than to
   the scrolling wrapper so a data table anywhere gets it — the two were out of
   step wherever a table was rendered without the wrapper.
   Tabular figures on top of that: proportional digits make 1 narrower than 8, so
   a right-aligned column of readings still comes out ragged down its middle. */
.data-table th.num, .data-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}
.report-note { margin: 0; padding: .6rem 1rem; font-size: .74rem; font-style: italic; color: var(--color-text-muted); }
.report-pager { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

/* ---------- Quick-launch tile grid ("My favourite menus") ---------- */
/* Deliberately compact. The panel is a launcher, not the point of the page, and
   the tall square tiles it used to carry pushed the asset tree under it off the
   first screen. Icon and label sit on one line, which is what lets the tile be
   a third of the height and still read at a glance. */
.quick-tile-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; }
@media (max-width: 900px) { .quick-tile-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .quick-tile-grid { grid-template-columns: 1fr; } }
.quick-tile {
  display: flex; flex-direction: row; align-items: center; gap: .4rem;
  padding: .45rem .55rem; border-radius: 5px; color: #fff; text-decoration: none;
  font-size: .72rem; font-weight: 600; line-height: 1.15; min-height: 34px;
  transition: transform .1s ease;
  /* The fallback for a module key with no colour of its own below. Without it
     a newly added module renders white text on nothing. */
  background: var(--color-charcoal);
}
.quick-tile:hover { transform: translateY(-1px); text-decoration: none; opacity: .93; }
.quick-tile .tile-icon { font-size: 1rem; flex: none; }
.quick-tile.tile-tickets { background: #c8102e; }
.quick-tile.tile-workorders { background: #e8590c; color: var(--ink-on-warm); }
.quick-tile.tile-pm { background: #a8570b; }
.quick-tile.tile-permits { background: #2f9e44; color: var(--ink-on-warm); }
.quick-tile.tile-workforce { background: #74b816; color: var(--ink-on-warm); }
.quick-tile.tile-inventory { background: #7048e8; }
.quick-tile.tile-procurement { background: #1b77cb; }
.quick-tile.tile-reports { background: #e0a800; color: #3a2c00; }
/* Reporting a hazard: the one favourite that *writes* rather than opens a list,
   which is why it says "Report a Hazard" and not "HSE Observations". Deep teal,
   away from the permit green beside it. */
.quick-tile.tile-observations { background: #0f766e; }
/* Kept for the Grafana tile, which the dashboard no longer offers — the portal
   is embedded in the hero at the top of this page, so a tile beside it was a
   second door into what the reader is already looking at. Any page that adds
   the link back gets the same "off-site" darkness. */
.quick-tile.tile-monitor { background: #0b3d5c; }
/* The remaining functionality keys. The grid is driven by whatever modules a
   department holds (see apps/accounts/modules.py), so every key needs a colour
   — and a key nobody has styled yet falls back to the charcoal set on
   .quick-tile above rather than rendering as white-on-white. Hues are spread
   across the wheel rather than grouped by app, because the grid is scanned for
   a shape and a position, not read as a taxonomy. */
.quick-tile.tile-maintenance { background: #a8570b; }
.quick-tile.tile-planning { background: #0b5b8c; }
.quick-tile.tile-operations { background: #b02a37; }
.quick-tile.tile-risk { background: #8a5a00; }
.quick-tile.tile-monitoring { background: #495057; }
.quick-tile.tile-assets { background: #1f6f5c; }
.quick-tile.tile-oncall { background: #6741d9; }

/* A request module's tile is two controls, not one: the register, and the page
   that raises a new one. "+ New" is the action somebody came to the dashboard
   for, so it sits on the tile rather than a click inside the register. */
.quick-tile-wrap { position: relative; display: flex; }
.quick-tile-wrap .quick-tile { flex: 1 1 auto; padding-right: 3.1rem; }
.quick-tile-new {
  position: absolute; top: 0; right: 0; bottom: 0;
  display: flex; align-items: center; padding: 0 .5rem;
  border-radius: 0 5px 5px 0;
  background: rgba(0, 0, 0, .22); color: inherit;
  font-size: .68rem; font-weight: 700; text-decoration: none; white-space: nowrap;
}
.quick-tile-new:hover { background: rgba(0, 0, 0, .35); text-decoration: none; }
.quick-tile-new:focus-visible { outline: 2px solid #fff; outline-offset: -3px; }

/* The department name beside a panel heading — "My Functions · Chemistry
   Operations". Quiet: it says why the panel below contains what it does. */
.panel-header-note {
  font-weight: 500; font-size: .72rem; color: var(--color-text-muted);
  text-transform: none; letter-spacing: 0;
}

/* ---------- Tree widget (asset location hierarchy) ---------- */
.tree-widget { font-size: .85rem; }
.tree-widget .tree-toolbar { display: flex; gap: .4rem; margin-bottom: .6rem; flex-wrap: wrap; }
.tree-widget ul { list-style: none; margin: 0; padding-left: 1.1rem; }
.tree-widget > ul { padding-left: 0; }
.tree-widget li { padding: .2rem 0; }
.tree-widget .tree-node { display: flex; align-items: center; gap: .4rem; }
.tree-widget .tree-node a { color: var(--color-text); }
.tree-widget .tree-node a:hover { color: var(--color-primary); }
.tree-widget .tree-icon { opacity: .7; }
.tree-widget .tree-count { color: var(--color-text-muted); font-size: .76rem; }
.tree-widget details > summary { cursor: pointer; list-style: none; }
.tree-widget details > summary::-webkit-details-marker { display: none; }
.tree-widget details > summary .tree-caret::before { content: "▸"; display: inline-block; width: 1em; }
.tree-widget details[open] > summary .tree-caret::before { content: "▾"; }

/* ---------- Status stat strip ---------- */
.stat-strip { display: flex; gap: .6rem; flex-wrap: wrap; }
.stat-strip .strip-tile {
  flex: 1 1 110px; min-width: 110px; text-align: center; padding: .7rem .45rem;
  border-radius: 6px; color: #fff;
}
/* Fallback fill, so a status with no colour rule of its own is never white
   text on a white card. Restricted to tiles that carry no badge-status class:
   as a plain `.stat-strip .strip-tile` rule it outranked every
   `.badge-status-*` colour (two classes to one) and painted the whole strip
   one shade of slate. */
.stat-strip .strip-tile:not([class*="badge-status-"]) {
  background: var(--status-draft);
}
.stat-strip .strip-tile .strip-count { font-size: 1.5rem; font-weight: 700; line-height: 1; }
.stat-strip .strip-tile .strip-label {
  font-size: .7rem; margin-top: .3rem; line-height: 1.25; opacity: .95;
  overflow-wrap: anywhere;
}
/* Empty counts keep their status colour — the strip is a colour-coded pipeline
   and greying out the empty stages drains the colour from it. They are held
   back by fading the count alone, so the tile still reads as "this stage, and
   it is empty" without losing either its hue or its label contrast. */
.stat-strip .strip-tile.dim .strip-count { opacity: .45; font-weight: 600; }
.stat-strip .strip-tile.dim .strip-label { opacity: .9; }
.stat-strip .strip-tile:not(.dim) {
  box-shadow: 0 1px 3px rgba(0,0,0,.18);
}

/* ---------- Agenda / unassigned work list ---------- */
.agenda-list details { border: 1px solid var(--color-border); border-radius: 6px; margin-bottom: .5rem; overflow: hidden; }
.agenda-list summary {
  cursor: pointer; list-style: none; padding: .55rem .8rem; background: var(--color-bg);
  font-size: .82rem; font-weight: 600; display: flex; justify-content: space-between; align-items: center;
}
.agenda-list summary::-webkit-details-marker { display: none; }
.agenda-list .agenda-badge { background: var(--color-accent); color: var(--ink-on-warm); border-radius: 20px; padding: .05rem .5rem; font-size: .72rem; }
.agenda-list .agenda-body { padding: .5rem .8rem; }
.agenda-list .agenda-row { padding: .3rem 0; border-bottom: 1px solid var(--color-border); font-size: .82rem; }
.agenda-list .agenda-row:last-child { border-bottom: none; }

/* ---------- Maintenance calendar ---------- */
.cal-toolbar { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; margin-bottom: .75rem; }
.cal-month { font-weight: 700; font-size: 1rem; }
.cal-legend { display: flex; gap: .8rem; flex-wrap: wrap; font-size: .72rem; color: var(--color-text-muted); }
.cal-legend span.key { display: inline-block; width: .7rem; height: .7rem; border-radius: 2px; margin-right: .3rem; vertical-align: -1px; }

.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; background: var(--color-border); border: 1px solid var(--color-border); border-radius: var(--radius); overflow: hidden; }
.cal-grid .cal-weekday { background: var(--color-bg); padding: .4rem .5rem; font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--color-text-muted); text-align: center; }
.cal-cell { background: var(--color-surface); min-height: 96px; padding: .35rem .4rem; display: flex; flex-direction: column; }
.cal-cell.out-of-month { background: var(--color-bg); opacity: .55; }
.cal-cell.today { outline: 2px solid var(--color-primary); outline-offset: -2px; }
.cal-daynum { font-size: .74rem; font-weight: 700; color: var(--color-text-muted); margin-bottom: .2rem; }
.cal-cell.today .cal-daynum { color: var(--color-primary); }

/* The chip's ink is a token, not a literal white.
   It was #fff, set here while each kind's fill was set in its own rule below —
   so no single rule paired a background with a colour and the contrast test
   could not see either. On the two warm fills that was 3.45:1 and 3.58:1 in
   *light* mode; in dark it was worse. Each kind now names the ink that suits
   its fill, and every pair below is checked. */
.cal-event { display: block; font-size: .69rem; line-height: 1.25; padding: .16rem .3rem; border-radius: 3px; margin-bottom: .16rem; color: var(--status-ink); text-decoration: none; border-left: 3px solid rgba(0,0,0,.25); }
.cal-event:hover { filter: brightness(1.1); text-decoration: none; }
.cal-event .cal-ref { font-weight: 700; }
.cal-event.kind-pm_due { background: var(--status-planned); color: var(--status-ink); }
/* Green and orange: white fails on both in either theme, so they take the
   near-black ink the priority badges use. */
.cal-event.kind-preventive { background: var(--priority-4); color: var(--ink-on-warm); }
.cal-event.kind-corrective { background: var(--priority-2); color: var(--ink-on-warm); }
/* Assignment and lateness are modifiers, not colours of their own — a
   preventive job still reads as preventive when nobody is assigned to it. */
.cal-event.is-unassigned { outline: 1px dashed rgba(255,255,255,.75); outline-offset: -2px; }
.cal-event.is-overdue { border-left-color: var(--priority-1); border-left-width: 4px; }
.cal-event .cal-flag { float: right; opacity: .85; }
.cal-more { font-size: .68rem; color: var(--color-text-muted); }

/* The day number is a link now — it opens the day. Styled as the number it was,
   so the cell does not sprout a blue underline in every square. */
a.cal-daynum { text-decoration: none; }
a.cal-daynum:hover { text-decoration: underline; }

/* ---------- Grouped day cells ---------- */
/* One chip per kind, its count on the right. The fills are the same tokens the
   individual chips used, so the legend still reads and a cell that used to be
   green for monitoring still is. */
.cal-group {
  display: flex; align-items: baseline; justify-content: space-between; gap: .3rem;
  font-size: .68rem; line-height: 1.3; padding: .14rem .3rem; border-radius: 3px;
  margin-bottom: .16rem; text-decoration: none; color: var(--status-ink);
  border-left: 3px solid rgba(0, 0, 0, .25);
}
.cal-group:hover { filter: brightness(1.1); text-decoration: none; }
.cal-group-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-group-count { font-weight: 700; font-variant-numeric: tabular-nums; }
.cal-group.kind-pm_due { background: var(--status-planned); color: var(--status-ink); }
.cal-group.kind-preventive { background: var(--priority-4); color: var(--ink-on-warm); }
.cal-group.kind-corrective { background: var(--priority-2); color: var(--ink-on-warm); }
/* Amber, but the deep amber of the light palette — so this one takes the
   shared ink (5.02:1) and not --ink-on-warm, which reads 3.62:1 on it. */
.cal-group.kind-outage { background: var(--status-awaiting-permit); color: var(--status-ink); }
.cal-group.kind-plan { background: var(--status-written-off); color: var(--status-ink); }
.cal-group.kind-condition_monitoring { background: var(--status-approved); color: var(--status-ink); }
/* Same two modifiers the individual chips carried: a group holding late work
   keeps the red edge, one holding unowned work keeps the dashed outline. */
.cal-group.has-overdue { border-left-color: var(--priority-1); border-left-width: 4px; }
.cal-group.has-unassigned { outline: 1px dashed rgba(255, 255, 255, .75); outline-offset: -2px; }

/* The dashboard panel shows the same grid at a reduced size. */
.cal-compact .cal-cell { min-height: 62px; padding: .25rem .3rem; }
.cal-compact .cal-event,
.cal-compact .cal-group { font-size: .63rem; padding: .1rem .25rem; }
.cal-compact .cal-daynum { font-size: .68rem; }

/* The tick that sits beside the month arrows on the dashboard panel. */
.cal-inline-filter {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .6rem;
}

/* ---------- One day in full ---------- */
.cal-day-group { margin-bottom: 1.2rem; }
.cal-day-group:last-child { margin-bottom: 0; }
.cal-day-heading {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .5rem; font-size: .95rem;
}
.cal-day-heading .key {
  display: inline-block; width: .8rem; height: .8rem; border-radius: 2px; flex: 0 0 auto;
}

/* ---------- Asset autocomplete widget ---------- */
.asset-autocomplete { position: relative; }
.asset-autocomplete-results {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 20;
  /* The raised surface, not the card's: a popup the same colour as what it
     covers has no edge, which in dark mode made it look like the page had
     simply grown taller. */
  background: var(--color-surface-2); border: 1px solid var(--color-border); border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0,0,0,.12); max-height: 260px; overflow-y: auto; margin-top: .2rem;
}
.asset-autocomplete-item { padding: .5rem .7rem; font-size: .85rem; cursor: pointer; border-bottom: 1px solid var(--color-border); }
.asset-autocomplete-item:last-child { border-bottom: none; }
.asset-autocomplete-item:hover { background: var(--color-primary-light); }
.asset-autocomplete-empty { padding: .6rem .7rem; font-size: .82rem; color: var(--color-text-muted); }

/* ---------- Searchable select (searchable-select.js) ---------- */
/* The native <select> stays in the DOM so the form still submits it, and is
   clipped rather than `display:none`d or made transparent: a `required` select
   the browser cannot focus makes submission fail with "an invalid form control
   is not focusable", which the user sees as a form that silently won't submit.
   Clipping keeps it focusable; tabindex="-1" keeps it out of the tab order. */
.ss-wrap { position: relative; }
.ss-wrap .ss-native {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}
.ss-toggle {
  width: 100%; text-align: left; padding: .5rem 1.6rem .5rem .6rem;
  border: 1px solid var(--color-border); border-radius: 6px; background: var(--control-bg);
  font-size: .88rem; font-family: inherit; color: var(--color-text); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ss-toggle::after {
  content: "▾"; position: absolute; right: .6rem; top: 50%; transform: translateY(-50%);
  color: var(--color-text-muted); pointer-events: none; font-size: .8rem;
}
.ss-toggle:focus { outline: none; border-color: var(--color-primary); }
.ss-toggle.is-placeholder { color: var(--color-text-muted); }

/* A searchable dropdown's list is absolutely positioned, so any ancestor that
   clips its overflow crops it — leaving the hidden options reachable only with
   the arrow keys. .panel clips by default, to crop its coloured header to the
   rounded corners, so a panel holding a dropdown opts out and the header rounds
   its own corners instead. The :has() rule catches any panel; the explicit class
   is there for browsers without :has(). */
.panel.panel-dropdowns,
.panel:has(.ss-wrap) { overflow: visible; }
.panel.panel-dropdowns > .panel-header,
.panel:has(.ss-wrap) > .panel-header { border-radius: var(--radius) var(--radius) 0 0; }

.ss-panel {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 30; margin-top: .2rem;
  background: var(--color-surface-2); border: 1px solid var(--color-border); border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0,0,0,.12); overflow: hidden;
}
.ss-panel .ss-search {
  width: 100%; border: 0; border-bottom: 1px solid var(--color-border); border-radius: 0;
  padding: .5rem .6rem; font-size: .85rem;
}
.ss-panel .ss-search:focus { outline: none; }
.ss-list { list-style: none; margin: 0; padding: 0; max-height: 260px; overflow-y: auto; }
.ss-list:focus { outline: none; }
.ss-option { padding: .45rem .7rem; font-size: .85rem; cursor: pointer; }
.ss-option.is-active { background: var(--color-primary-light); }
.ss-option.is-selected { font-weight: 700; }
.ss-option.is-disabled { color: var(--color-text-muted); cursor: not-allowed; }
.ss-group {
  padding: .35rem .7rem; font-size: .7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--color-text-muted); background: var(--color-bg);
}
.ss-empty { padding: .6rem .7rem; font-size: .82rem; color: var(--color-text-muted); }

/* Truncation notice in the server-backed autocomplete result lists. */
.asset-autocomplete-note {
  padding: .45rem .7rem; font-size: .75rem; color: var(--color-text-muted);
  background: var(--color-bg); border-top: 1px solid var(--color-border);
}

/* Outage jobs on the maintenance calendar — planned against a window rather
   than a date, so they read differently from routine preventive work. */
.cal-event.kind-outage { background: var(--status-awaiting-permit); color: var(--status-ink); }
/* The start and end of a maintenance project window (apps.planning). Marked at
   each end rather than filled across the span — see maintenance_calendar. */
.cal-event.kind-plan { background: var(--status-written-off); color: var(--status-ink); }
/* POPIT conditional monitoring rounds. Teal, so they read as their own kind of
   work beside the preventive green and the corrective orange. */
.cal-event.kind-condition_monitoring { background: var(--status-approved); color: var(--status-ink); }

/* ---------- Completion checklist with per-item evidence ---------- */
.checklist { list-style: none; margin: 0; padding: 0; }
.checklist-item { border: 1px solid var(--color-border); border-radius: 6px; padding: .55rem .7rem; margin-bottom: .5rem; }
.checklist-item.is-done { background: var(--tint-success-bg); border-color: var(--tint-success-border); color: var(--tint-success-ink); }
.checklist-head { display: flex; justify-content: space-between; align-items: center; gap: .75rem; flex-wrap: wrap; }
.checklist-tick { display: flex; align-items: center; gap: .5rem; font-weight: 500; margin: 0; cursor: pointer; }
.checklist-tick input { width: auto; margin: 0; }
.checklist-item.is-done .checklist-tick span { text-decoration: line-through; color: var(--color-text-muted); }
.checklist-evidence { list-style: none; margin: .4rem 0 0; padding: 0 0 0 1.6rem; font-size: .8rem; }
.checklist-evidence li { padding: .12rem 0; }
.checklist-upload { margin: .45rem 0 0 1.6rem; }
.btn-link-danger { background: none; border: 0; color: var(--priority-1); cursor: pointer; font-size: .8rem; padding: 0 .2rem; }

/* Checklist rows on the creation form */
.checklist-row { margin-bottom: .35rem; }

/* ---------- Monitored cells on the plant status log ---------- */
/* A cell under a condition alarm carries the band its reading falls in, right
   where the reading was taken. The band chip's colour comes from the band's own
   configured colour via a custom property, so re-colouring a band in the admin
   re-colours the sheet — there is no second palette here to fall out of step.

   The left edge marks a watched cell whether or not it is in alarm, because
   "this reading is being watched" is worth knowing while typing it. */
.pl-watched { box-shadow: inset 2px 0 0 var(--color-border); }
.pl-alarm { box-shadow: inset 3px 0 0 var(--priority-1); }
/* The band's own colour is a 3px bar, never a fill behind the words.

   Band colours are configured in the admin, so no fill can be contrast-checked
   here: the seeded orange (#E8751A) is 3.00:1 against white and 4.9:1 against
   near-black, and the next colour somebody picks could be worse than either.
   A coloured bar beside text in the page's normal ink is legible whatever hue
   the band is given. Alarm chips take the danger tint, which is checked. */
.pl-band {
  display: block;
  margin: .1rem .15rem .15rem;
  font-size: .62rem;
  line-height: 1.2;
  font-weight: 600;
  text-align: center;
  border-radius: 3px;
  padding: .05rem .2rem;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--pl-band-colour, var(--color-border));
  background: var(--color-surface);
  color: var(--color-text-muted);
  text-decoration: none;
}
/* Only an actionable band is tinted. Colouring the Normal band as well would
   make a sheet where everything is fine look like a wall of alarms. */
.pl-band-alarm {
  background: var(--tint-danger-bg);
  color: var(--tint-danger-ink);
  border-color: var(--tint-danger-border);
  border-left-color: var(--pl-band-colour, var(--priority-1));
}
.pl-band-quiet { border-style: dashed; border-left-style: solid; }
a.pl-band:hover { text-decoration: underline; }

/* ---------- Frequency bands under the generation chart ---------- */
/* The band's own configured colour is a left bar, never a fill behind the
   words — the colours are set in the admin and cannot be contrast-checked
   here, the same reason the plant log's band chips work this way. */
.freq-band-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.freq-band {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .18rem .5rem;
  font-size: .74rem;
  border: 1px solid var(--color-border);
  border-left: 3px solid var(--band-colour, var(--color-border));
  border-radius: 4px;
  background: var(--color-surface);
  color: var(--color-text);
}
.freq-band.is-alarm {
  background: var(--tint-danger-bg);
  color: var(--tint-danger-ink);
  border-color: var(--tint-danger-border);
  border-left-color: var(--band-colour, var(--priority-1));
  font-weight: 600;
}
.freq-band.is-gap { border-style: dashed; border-left-style: solid; }
.freq-band-count { font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---------- Horizontal scroll for wide content ---------- */
/* Wrap a table (or any wide block) that cannot be narrowed further. The page
   body must never scroll sideways — a whole page shifted left because one table
   was 40px too wide is how the site reads as broken on a phone. Scrolling the
   table inside its own box keeps the rest of the layout still.

   -webkit-overflow-scrolling is for iOS, where a nested scroller otherwise
   moves in steps rather than following the finger. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* Room for the scrollbar, so it does not sit on top of the last row. */
  padding-bottom: .15rem;
}
.table-scroll > table { margin-bottom: 0; }

/* ---------- Upload rows ---------- */
/* One class for every "choose a file and send it" control on the site.

   These were five different hand-rolled layouts — two with inline flex styles
   in the template, three stacking the file input above its own button. Stacked,
   one upload costs three rows of vertical space (label, input, button) for a
   single action, and on the work-order page there are two of them. On one line
   it costs one row.

   The button aligns to the *bottom* of the row, not the middle: a field with a
   label is taller than a bare button, and centring it leaves the button
   floating against the label instead of against the input it submits. Every
   filter row on the site aligns the same way and for the same reason. */
.upload-row {
  display: flex;
  align-items: flex-end;
  gap: .5rem;
  flex-wrap: wrap;
}
/* The row is the thing with spacing; the fields inside it must not each add
   their own bottom margin or the button drops out of line. */
.upload-row .field { margin-bottom: 0; flex: 0 0 auto; }
.upload-row .field > input,
.upload-row .field > select { margin-bottom: 0; }
/* A caption or note takes the slack, so the file name stays readable and the
   button stays hard against the right of the controls. */
.upload-row .field.grow { flex: 1 1 12rem; min-width: 0; }
.upload-row .field.grow > input { width: 100%; }
.upload-row input[type="file"] { font-size: .78rem; max-width: 260px; margin-bottom: 0; }
.upload-row input[type="text"] { max-width: 220px; }
.upload-row > .btn,
.upload-row > button { flex: 0 0 auto; }
/* Procurement's "add to inventory" control, inside a table cell on the material
   request. Inline so the part-number box and its button read as one action on
   the line they belong to, rather than as a form that has escaped into a
   table. */
.adopt-row { display: flex; gap: .3rem; align-items: center; flex-wrap: wrap; }
.adopt-row input[type="text"] { width: 9rem; min-width: 0; }

/* A whole form's last line: the attachment control on the left, the form's own
   actions hard right. The separator and the gap are what stop the Save button
   reading as "upload this file" — it still submits the whole form, and the rule
   above it says where the fields end. */
.upload-row.form-upload-actions {
  border-top: 1px solid var(--color-border);
  padding-top: .9rem;
  margin-top: 1.1rem;
}
.upload-row.form-upload-actions > .field + :is(.btn, button) { margin-left: auto; }
@media (max-width: 560px) {
  /* Stacked, the auto margin would leave a gap above the buttons. */
  .upload-row.form-upload-actions > .field + :is(.btn, button) { margin-left: 0; }
}

/* Below this the row is wider than the phone: one line becomes a stack of
   full-width controls rather than a file input squeezed to 40px. */
@media (max-width: 560px) {
  .upload-row { flex-direction: column; align-items: stretch; }
  .upload-row .field,
  .upload-row .field.grow,
  .upload-row input[type="file"],
  .upload-row input[type="text"] { max-width: none; width: 100%; }
  .upload-row > .btn,
  .upload-row > button { width: 100%; }
}
/* The browser's own "Choose file" button is a native control and ignores the
   form-control tokens, so in dark mode it stays a white slab with white text
   unless it is painted explicitly. */
/* A checkbox with its wording beside it, rather than a label stacked above a
   control. Normal weight, because the words are the control's meaning and not a
   field name; the surrounding row keeps it level with its neighbours. */
.check-inline {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-weight: 400;
  margin: 0;
  white-space: nowrap;
  padding-bottom: .35rem;
}
.check-inline input { width: auto; margin: 0; }

input[type="file"]::file-selector-button {
  background: var(--btn-secondary-bg);
  color: var(--btn-secondary-ink);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  padding: .2rem .55rem;
  margin-right: .5rem;
  font: inherit;
  font-size: .76rem;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { border-color: var(--color-primary); }

/* ---------- The on-call memo's staff table ---------- */
/* Eight columns of a paper form on a screen. The widths come from the template
   so the columns cannot collapse to unreadable slivers when the address is
   long; here we only stop the controls fighting the cells. */
.staff-table input,
.staff-table select { font-size: .78rem; width: 100%; min-width: 0; }
.staff-table td { vertical-align: top; }
.staff-table .help-text { margin: .2rem 0 0; font-size: .7rem; }
/* A row the browser filled in from the roster, so the requester can see at a
   glance which lines they typed themselves. */
.staff-table tr[data-staff-filled] td:first-child { box-shadow: inset 2px 0 0 var(--color-accent); }

/* ---------- Editable line-item rows (material request items) ---------- */
/* A repeating row of inputs inside a data-table: the fields have to sit flush
   in their cells, otherwise every row carries the standalone field's margin
   and the table grows a gap between each line. */
.line-items input[type="text"],
.line-items input[type="number"],
.line-items input[type="file"],
.line-items select { margin-bottom: 0; }
.line-items input[type="file"] { font-size: .78rem; max-width: 220px; }
.line-items td { vertical-align: top; }
.line-items .errorlist { margin: .25rem 0 0; }
/* The formset's DELETE box is driven by the row's bin icon, never clicked directly. */
.line-items .line-item-delete-field { display: none; }
.line-item-number { font-size: .8rem; padding-top: .55rem; }

/* Row action icons — add / edit / delete on a line item. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.85rem; height: 1.85rem; padding: 0;
  background: var(--btn-secondary-bg); border: 1px solid var(--color-border); border-radius: 6px;
  font-size: .85rem; line-height: 1; cursor: pointer; color: var(--btn-secondary-ink);
}
.icon-btn:hover { background: var(--color-bg); text-decoration: none; }
.icon-btn:disabled { opacity: .4; cursor: not-allowed; }
.icon-btn-danger:hover:not(:disabled) { border-color: var(--priority-1); background: var(--tint-danger-bg); }
.nowrap { white-space: nowrap; }

/* ---------- Plant Status Log ----------
   A dense, hand-filled sheet rather than a report table: every cell is bordered
   so the shift team can see at a glance which readings are still blank, and the
   recorded values sit in the primary colour to separate them from the fixed
   labels — the convention the PHP sheet used and the operators read by. */
.plant-log-slot { display: flex; align-items: flex-end; gap: .6rem; margin-bottom: .8rem; }
.plant-log-slot .field { margin-bottom: 0; }
.plant-log-slot input[type="date"] { width: 11rem; }

.plant-log-hours { display: flex; flex-wrap: wrap; gap: .3rem; }
.plant-log-hour {
  padding: .3rem .6rem; border: 1px solid var(--color-border); border-radius: 6px;
  font-size: .82rem; font-weight: 600; color: var(--color-text-muted); background: var(--control-bg);
}
.plant-log-hour:hover { border-color: var(--color-primary); text-decoration: none; }
.plant-log-hour.active { background: var(--color-primary); border-color: var(--color-primary); color: var(--status-ink); }

.plant-log-status { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
.plant-log-open { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: .8rem; }
.plant-log-open .choice { margin-bottom: 0; }

.plant-log-wrap { overflow-x: auto; }
table.plant-log-sheet { width: 100%; min-width: 900px; border-collapse: collapse; font-size: .84rem; }
table.plant-log-sheet th, table.plant-log-sheet td {
  border: 1px solid var(--color-border); padding: .35rem .5rem; vertical-align: top;
}
table.plant-log-sheet th {
  background: var(--color-primary-light); color: var(--color-text);
  font-weight: 600; font-size: .78rem; text-align: center;
}
.pl-label { font-weight: 600; background: var(--sheet-label-bg); color: var(--color-text); }
.pl-center { text-align: center; }
.pl-cell { color: var(--color-primary); min-height: 1.3em; }
table.plant-log-sheet:not(.plant-log-readonly) .pl-cell:not(.pl-computed) { cursor: text; }
table.plant-log-sheet:not(.plant-log-readonly) .pl-cell:not(.pl-computed):hover { background: var(--color-primary-light); }
.pl-computed { font-weight: 700; background: var(--sheet-label-bg); color: var(--color-text); }
.pl-cell input, .pl-cell textarea { padding: .2rem .3rem; font-size: .84rem; border-radius: 4px; }
.pl-cell textarea { min-height: 4.5rem; }

.plant-log-savestate { font-size: .78rem; margin-top: .6rem; min-height: 1.1em; }
.plant-log-savestate.error { color: var(--priority-1); font-weight: 600; }

@media print {
  .sidebar, .topbar, .no-print { display: none !important; }
  .content { padding: 0; }
  .card { box-shadow: none; border: none; }
}

/* ---------- Plant Status Log access ---------- */
.access-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; }
.access-picker {
  border: 1px solid var(--color-border); border-radius: 6px; padding: .5rem;
  max-height: 18rem; overflow-y: auto; background: var(--color-surface);
}
.access-picker ul { list-style: none; margin: 0; padding: 0; }
.access-picker li { margin: 0; }
.access-picker label {
  display: flex; align-items: center; gap: .5rem;
  font-weight: 400; font-size: .85rem; margin: 0; padding: .2rem .1rem; cursor: pointer;
}
.access-picker label:hover { background: var(--color-primary-light); }
/* The filter box and its tally stay put while the list scrolls under them. */
.access-filter { position: sticky; top: 0; z-index: 1; margin-bottom: .3rem; }
.access-count { position: sticky; top: 2.3rem; z-index: 1; background: var(--color-surface); font-size: .76rem; padding-bottom: .3rem; }


/* ---------- Generation Log ---------- */
/* Five controls that belong on one line — two dates, two times and a unit — with
   Apply level with them, same rule as the outage filter above. */
/* Flex, not grid: the five controls share the space, and the two buttons take
   only what they need. On a grid track sized for a date input they would be
   squeezed or pushed onto a row of their own — which is the thing this row must
   not do. */
.genlog-filter { display: flex; flex-wrap: wrap; gap: .7rem; align-items: end; }
.genlog-filter .field { flex: 1 1 150px; min-width: 130px; margin-bottom: 0; }
.genlog-filter .field input, .genlog-filter .field select { width: 100%; }

/* Taller than the dashboard's insight charts: a week of 15-minute ticks is a
   long, detailed line, and 190px of height turns every excursion into noise. */
.genlog-chart { position: relative; height: 320px; }

/* ---------- Live unit status source ---------- */
.source-choice ul { list-style: none; margin: .3rem 0 0; padding: 0; }
.source-choice li { margin-bottom: .45rem; }
.source-choice label { font-weight: 600; font-size: .9rem; cursor: pointer; }
.source-choice input[type="radio"] { width: auto; margin-right: .45rem; }
.source-notes {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: .8rem; margin-top: 1rem;
}
.source-note {
  border: 1px solid var(--color-border); border-radius: 6px; padding: .7rem .8rem;
  background: var(--color-bg);
}
.source-note h4 { margin: 0 0 .35rem; font-size: .82rem; }
.source-note p { margin: 0 0 .4rem; font-size: .78rem; line-height: 1.45; }
.source-note p:last-child { margin-bottom: 0; }

/* Overdue in the work-order strip. It is a condition, not a status, so it gets
   the danger colour directly rather than a --status-* token that does not exist,
   and a rule to its left to say it is a different kind of tally from the status
   tiles it follows. */
.stat-strip .strip-tile.strip-tile-overdue {
  background: var(--priority-1);
  box-shadow: inset 3px 0 0 rgba(255, 255, 255, .55);
}

/* ---------- Row action icons (risk action plan) ---------- */
/* One narrow column carrying edit, copy and delete. Each control is a real
   button or link with a title and an aria-label — the icon is the label's
   shorthand, not a replacement for it. */
.data-table th.action-col, .data-table td.action-col { width: 1%; white-space: nowrap; }
.row-actions { display: flex; gap: .15rem; align-items: center; }
.row-actions form { display: inline; margin: 0; }
.row-action {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.85rem; height: 1.85rem; padding: 0;
  border: 1px solid transparent; border-radius: 5px;
  background: none; cursor: pointer; font-size: .9rem; line-height: 1;
  text-decoration: none; color: var(--color-text);
}
.row-action:hover { background: var(--color-primary-light); border-color: var(--color-border); text-decoration: none; }
.row-action-danger:hover { background: var(--tint-danger-bg); border-color: var(--priority-1); }
/* The row being edited, so the form at the foot of the table is visibly about
   this line and not the last one somebody looked at. */
.data-table tr.row-editing td { background: var(--color-primary-light); font-weight: 500; }
.status-select { min-width: 8.5rem; font-size: .8rem; padding: .25rem .4rem; }
.field-error { margin: .25rem 0 0; font-size: .76rem; color: var(--priority-1); }

/* ---------- Picture galleries & lightbox ---------- */
/* Asset and stores records carry photographs of the plant. The gallery shows
   thumbnails only; the full (<=1 MB) copy is fetched by lightbox.js when a
   picture is actually opened. */

.picture-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 10px;
}

.picture-tile {
  position: relative;
  display: block;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--color-bg);
  cursor: zoom-in;
  aspect-ratio: 4 / 3;
}

.picture-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .18s ease;
}

.picture-tile:hover img { transform: scale(1.04); }
.picture-tile:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

.picture-tile .picture-flag {
  position: absolute;
  top: 6px;
  left: 6px;
  font-size: .66rem;
  font-weight: 600;
  padding: 2px 6px;
  border-radius: 10px;
  background: rgba(0, 0, 0, .62);
  color: #fff;
}
.picture-tile .picture-flag.review { background: var(--status-awaiting-permit); }

/* Row thumbnail in the asset and inventory list tables. */
.picture-cell {
  width: 52px;
  height: 40px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--color-border);
  cursor: zoom-in;
  display: block;
}
.picture-cell-empty {
  width: 52px;
  height: 40px;
  border-radius: 4px;
  border: 1px dashed var(--color-border);
  display: grid;
  place-items: center;
  color: var(--color-text-muted);
  font-size: .9rem;
}

body.lightbox-open { overflow: hidden; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(12, 18, 24, .88);
  padding: 3rem 1rem;
}
.lightbox.is-open { display: flex; }

.lightbox-figure {
  margin: 0;
  max-width: 100%;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .6rem;
}
.lightbox-figure img {
  max-width: 100%;
  /* Leave room for the caption line under the picture. */
  max-height: calc(100vh - 9rem);
  object-fit: contain;
  border-radius: var(--radius);
  background: #fff;
}
.lightbox-figure.is-loading img { opacity: .45; }

.lightbox-figure figcaption {
  color: #e8eef3;
  font-size: .85rem;
  text-align: center;
  display: flex;
  gap: .75rem;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
}
.lightbox-counter { color: #9fb1bf; }

.lightbox-close,
.lightbox-nav {
  position: absolute;
  background: rgba(255, 255, 255, .12);
  color: #fff;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  line-height: 1;
}
.lightbox-close {
  top: 1rem;
  right: 1rem;
  width: 38px;
  height: 38px;
  font-size: 1.5rem;
}
.lightbox-nav {
  top: 50%;
  transform: translateY(-50%);
  width: 42px;
  height: 42px;
  font-size: 1.8rem;
  display: none;
}
.lightbox.has-siblings .lightbox-nav { display: block; }
.lightbox-prev { left: 1rem; }
.lightbox-next { right: 1rem; }
.lightbox-close:hover,
.lightbox-nav:hover { background: rgba(255, 255, 255, .26); }

/* Header controls on a panel — sits beside the header's existing right-hand
   link (e.g. the monitor card's refresh, next to "Full screen"). */
.panel-header-actions { display: inline-flex; align-items: center; gap: .6rem; }
.panel-header-btn {
  /* The overlay is mixed from the header's own ink rather than being white:
     the ink is by definition the colour that contrasts with the header, so this
     darkens on a light header and lightens on a dark one. It was white-on-white
     the moment the header stopped being near-black — the same bug as the export
     links, in the one other control that sits up there. */
  background: color-mix(in srgb, var(--panel-header-ink) 14%, transparent);
  color: var(--panel-header-ink); border: 0; border-radius: 4px;
  cursor: pointer; font-size: .95rem; line-height: 1; padding: .2rem .45rem;
}
.panel-header-btn:hover { background: color-mix(in srgb, var(--panel-header-ink) 28%, transparent); }
.panel-header-btn.is-spinning { animation: panel-header-spin .9s linear infinite; }
@keyframes panel-header-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .panel-header-btn.is-spinning { animation: none; opacity: .6; }
}

/* Text for screen readers only — kept out of the visual layout without being
   hidden from assistive technology, which display:none would do. */
.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;
}

/* ---------- Long forms built from a spreadsheet's columns ---------- */
/* The outage entry form carries the workbook's thirty columns. The ten an event
   cannot be logged without sit in a plain grid; the rest fold away behind a
   <details>, so the common case is one screen rather than three. */
.field-wide { grid-column: 1 / -1; }
.field-wide textarea, .field-wide input { width: 100%; }
.req { color: var(--priority-1); }
.details-summary {
  cursor: pointer; font-size: .82rem; font-weight: 600; color: var(--color-primary);
  padding: .4rem 0; user-select: none;
}
.details-summary:hover { color: var(--color-primary); text-decoration: underline; }

/* ---------- Field-level validation messages ---------- */
/* Used by the forms that render fields one at a time (the outage entry sheet,
   the unit operating state box) rather than letting Django print an errorlist. */
.field-error { color: var(--priority-1); font-size: .76rem; margin: .25rem 0 0 0; }

/* The Live unit status table's reason column. Free text an operator typed, so
   it is given room to wrap rather than stretching the row to one long line. */
.state-reason { max-width: 22rem; white-space: normal; font-size: .82rem; }

/* ---------- Dropdowns built from values already on record ---------- */
/* Shutdown Type / Fault Type / Outage Category on the outage entry form: a
   <select> of what the register holds, with the "Other" text box stacked under
   it and hidden until it is needed. */
[data-recorded-value="select"] { width: 100%; }
[data-recorded-value="other"] { width: 100%; margin-top: .3rem; }
[data-recorded-value="other"][hidden] { display: none; }

/* ---------- Maintenance plan Gantt ---------- */
/* The annual plan is issued as a twelve-month chart, so it is drawn as one here
   too. Bar positions are percentages of the year computed server-side
   (planning.services.bar_geometry), which keeps the whole thing CSS: no canvas,
   no chart library, and it prints the way the issued sheet does. */
.gantt { border: 1px solid var(--color-border); border-radius: var(--radius); overflow: hidden; }
.gantt-head,
.gantt-row { display: flex; align-items: stretch; border-bottom: 1px solid var(--color-border); }
.gantt-row:last-child { border-bottom: 0; }
.gantt-head { background: var(--color-bg); font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; color: var(--color-text-muted); }

.gantt-label { flex: 0 0 17rem; min-width: 0; padding: .45rem .6rem; border-right: 1px solid var(--color-border); font-size: .82rem; }
.gantt-label a { font-weight: 600; }
.gantt-sub { display: block; font-size: .7rem; color: var(--color-text-muted); }
.gantt-days { flex: 0 0 3.5rem; padding: .45rem .6rem; text-align: right;
  border-left: 1px solid var(--color-border); font-size: .8rem; }

.gantt-track { position: relative; flex: 1 1 auto; display: flex; min-height: 2.4rem; }
.gantt-head .gantt-track { min-height: 0; }
.gantt-month,
.gantt-cell { border-right: 1px solid var(--color-border); }
.gantt-month { padding: .35rem .2rem; text-align: center; }
.gantt-month:last-child,
.gantt-cell:last-child { border-right: 0; }
.gantt-cell:nth-child(even) { background: rgba(0, 0, 0, .022); }

.gantt-bar {
  position: absolute; top: .5rem; bottom: .5rem;
  border-radius: 3px; color: #fff; font-size: .68rem; line-height: 1;
  display: flex; align-items: center; padding: 0 .35rem; overflow: hidden;
  white-space: nowrap; text-decoration: none;
}
.gantt-bar:hover { filter: brightness(1.12); text-decoration: none; }
/* A track summarises what is under it, so it reads as a rule rather than a
   block — otherwise a parent and its children are four identical bars. */
.gantt-bar.is-track { top: .75rem; bottom: .75rem; opacity: .85; }
.gantt-bar.is-overdue { box-shadow: inset 0 0 0 2px var(--priority-1); }
.gantt-today { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--color-accent); opacity: .65; }

/* THE SEASON BAND — context painted behind the work, never a row of its own.
   Low opacity and no border so it reads as ground rather than as a bar: the
   plan sheet draws it as a wash under everything, and a strip with an edge on
   it would be mistaken for a fifth project. `pointer-events: none` keeps the
   bars underneath it clickable where they overlap. */
.gantt-season {
  position: absolute; top: 0; bottom: 0; z-index: 0;
  background: var(--color-accent); opacity: .10; pointer-events: none;
}
.gantt-season-label {
  position: absolute; top: 0; bottom: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  font-size: .62rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--color-text-muted); background: color-mix(in srgb, var(--color-accent) 12%, transparent);
  overflow: hidden; white-space: nowrap;
}
.gantt-unscheduled { position: absolute; left: .5rem; top: 50%; transform: translateY(-50%);
  font-size: .7rem; color: var(--color-text-muted); }
.gantt-empty { padding: .8rem; color: var(--color-text-muted); font-size: .82rem; }

/* Work breakdown table: the WBS code column and the over-budget figure. */
.wbs-table .wbs { font-variant-numeric: tabular-nums; color: var(--color-text-muted); font-size: .78rem; white-space: nowrap; }
.over-budget { color: var(--priority-1); font-weight: 600; }

/* SLA traffic lights on a project. Same palette as the status strip, so a
   breached plan reads the same red as a suspended work order. */
.badge-sla-on_track { background: var(--status-approved); color: var(--status-ink); }
.badge-sla-at_risk { background: var(--priority-3); color: var(--ink-on-warm); }
.badge-sla-breached { background: var(--priority-1); color: var(--status-ink); }
.badge-sla-met { background: var(--status-completed); color: var(--status-ink); }
.badge-sla-none { background: var(--status-draft); color: var(--status-ink); }

@media (max-width: 900px) {
  .gantt-label { flex-basis: 10rem; }
}

/* ==========================================================================
   Themes, density and colour schemes
   ==========================================================================
   Everything here keys off three attributes stamped on <html> by base.html from
   apps.core.models.effective_ui: data-theme, data-density and data-scheme. No
   page and no component knows anything about theming — they use the variables,
   and these blocks change what the variables mean.

   The variables at the top of this file are the light palette. Dark redefines
   them; it does not restyle components. That is what keeps a new card or badge
   working in both themes without anybody remembering to add a dark rule. */

/* ---------- Dark ---------- */
/* Applied when the user chose dark outright, and when they are following the
   device and the device asks for dark. Surfaces are lifted off pure black —
   #0f151b rather than #000 — because a full-black page under control-room
   lighting makes the white text bloom. */
:root[data-theme="dark"],
:root[data-theme="system"] {
  color-scheme: dark;
}
:root[data-theme="dark"],
[data-theme="dark"] {
  /* Status hues lightened so they clear AA against the dark surface rather
     than the white one they were checked against. */
  --status-draft: #94a3b8;
  /* The one status colour that was never lifted for dark. It stayed at the
     light palette's deep indigo, where the dark ink reads 2.89:1 — every
     "Planned" badge on the site was below AA and nothing noticed, because the
     contrast test only checked the tokens each block redeclares. It layers the
     palettes properly now. */
  --status-planned: #9790f0;
  --status-awaiting-permit: #d99525;
  --status-approved: #2dae9f;
  --status-released: #38a3e0;
  --status-in-progress: #5b8def;
  --status-suspended: #ef5350;
  --status-completed: #43b871;
  --status-accepted: #35b58c;
  --status-written-off: #a78bfa;
  --status-severity-low: #38a3e0;
  --status-severity-critical-low: #a78bfa;
  --status-closed: #7d8fa1;
  --status-new: #a78bfa;
  --status-rejected: #f0637f;
  --status-duplicate: #9c968c;

  --priority-1: #ef5350;
  --priority-2: #f0863c;
  --priority-3: #e0b62e;
  --priority-4: #43b871;
  --priority-5: #94a3b8;

  /* Dark ink on the lightened fills. White on a pastel badge fails AA badly —
     these hues are lifted so they read against the dark page, which is exactly
     what makes white unreadable on them. */
  --status-ink: #10161d;

  --color-bg: #12171c;
  --color-surface: #1b2228;
  --color-surface-2: #232b33;
  --color-border: #333e48;
  --color-text: #e6edf3;
  --color-text-muted: #9dadba;
  /* Burgundy lifted until it reads on the dark surface: #6E1414 manages 1.6:1
     there and is simply not visible. This rose keeps the hue at 5.24:1. */
  --color-primary: #d9736b;
  --color-primary-dark: #4e0e0e;
  --color-primary-light: #33201f;
  --color-accent: #f0913c;
  --color-accent-strong: #e8751a;

  /* The reported bug: .btn-secondary was a hard #fff behind --color-text, which
     in dark mode is near-white — a white button with white writing on it. A
     light grey plate with near-black ink is both visible and clearly distinct
     from the burgundy primary button. 12.60:1. */
  --btn-secondary-bg: #cfd8e0;
  --btn-secondary-ink: #10161d;
  --btn-secondary-hover-bg: #e3eaf0;
  /* Inputs, selects and the custom dropdown toggle. One step lighter than the
     card so the control reads as a control rather than a hole in the page. */
  --control-bg: #232b33;
  --panel-header-bg: #392f33;
  --panel-header-ink: #eab2ae;
  --panel-accent-border: #634141;

  /* Tints rebuilt rather than dimmed: a pale pink alert on a dark page glows,
     and its dark red text on it is unreadable either way round. */
  --tint-danger-bg: #3a1c1f;
  --tint-danger-border: #7d3a3a;
  --tint-danger-ink: #f4b3ad;
  --tint-warning-bg: #382c14;
  --tint-warning-border: #7d6127;
  --tint-warning-ink: #f0d19a;
  /* Rebuilt rather than dimmed, like the other tints: a pale green plate on a
     dark page glows, and dark green text on it is unreadable either way round. */
  --tint-success-bg: #16301c;
  --tint-success-border: #2f6b3c;
  --tint-success-ink: #a8dfb4;
  /* One step *lighter* than the card, not darker: a sheet's row labels have to
     separate from its data cells, and on a dark page that separation reads as
     a lift rather than a recess. */
  --sheet-label-bg: #262f38;
  --tint-soft-bg: #33201f;
  --tint-soft-ink: #f0c6c2;
  /* Ink for the warm priority fills — orange, amber, green. White fails AA on
     all three (3.45-3.58:1) and did so unnoticed because the priority palette
     was never contrast-checked; this near-black clears it in both themes. */
  --ink-on-warm: #10161d;

  /* Charts: a barely-there plate instead of the bright white the canvas
     inherited, a grid faint enough not to compete with the bars, and ink light
     enough to read. */
  /* Light enough to read on the dark plot (10.48:1), still neutral so it does
     not compete with the coloured unit series. */
  --chart-total: #cfd8e0;
  --chart-grid: #2b3540;
  --chart-ink: #9dadba;
  --chart-plot-bg: #1f272e;
}
@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] {
    --status-draft: #94a3b8;
  /* The one status colour that was never lifted for dark. It stayed at the
     light palette's deep indigo, where the dark ink reads 2.89:1 — every
     "Planned" badge on the site was below AA and nothing noticed, because the
     contrast test only checked the tokens each block redeclares. It layers the
     palettes properly now. */
  --status-planned: #9790f0;
    --status-awaiting-permit: #d99525;
    --status-approved: #2dae9f;
    --status-released: #38a3e0;
    --status-in-progress: #5b8def;
    --status-suspended: #ef5350;
    --status-completed: #43b871;
    --status-accepted: #35b58c;
    --status-written-off: #a78bfa;
    --status-severity-low: #38a3e0;
    --status-severity-critical-low: #a78bfa;
    --status-closed: #7d8fa1;
    --status-new: #a78bfa;
    --status-rejected: #f0637f;
    --status-duplicate: #9c968c;
    --priority-1: #ef5350;
    --priority-2: #f0863c;
    --priority-3: #e0b62e;
    --priority-4: #43b871;
    --priority-5: #94a3b8;
    --status-ink: #10161d;
    --color-bg: #12171c;
    --color-surface: #1b2228;
    --color-surface-2: #232b33;
    --color-border: #333e48;
    --color-text: #e6edf3;
    --color-text-muted: #9dadba;
    /* Burgundy lifted until it reads on the dark surface: #6E1414 manages 1.6:1
       there and is simply not visible. This rose keeps the hue at 5.24:1. */
    --color-primary: #d9736b;
    --color-primary-dark: #4e0e0e;
    --color-primary-light: #33201f;
    --color-accent: #f0913c;
    --color-accent-strong: #e8751a;

    /* The reported bug: .btn-secondary was a hard #fff behind --color-text, which
       in dark mode is near-white — a white button with white writing on it. A
       light grey plate with near-black ink is both visible and clearly distinct
       from the burgundy primary button. 12.60:1. */
    --btn-secondary-bg: #cfd8e0;
    --btn-secondary-ink: #10161d;
    --btn-secondary-hover-bg: #e3eaf0;
    /* Inputs, selects and the custom dropdown toggle. One step lighter than the
       card so the control reads as a control rather than a hole in the page. */
    --control-bg: #232b33;
    --panel-header-bg: #392f33;
    --panel-header-ink: #eab2ae;
    --panel-accent-border: #634141;

    /* Tints rebuilt rather than dimmed: a pale pink alert on a dark page glows,
       and its dark red text on it is unreadable either way round. */
    --tint-danger-bg: #3a1c1f;
    --tint-danger-border: #7d3a3a;
    --tint-danger-ink: #f4b3ad;
    --tint-warning-bg: #382c14;
    --tint-warning-border: #7d6127;
    --tint-warning-ink: #f0d19a;
    /* Rebuilt rather than dimmed, like the other tints: a pale green plate on a
     dark page glows, and dark green text on it is unreadable either way round. */
  --tint-success-bg: #16301c;
  --tint-success-border: #2f6b3c;
  --tint-success-ink: #a8dfb4;
  /* One step *lighter* than the card, not darker: a sheet's row labels have to
     separate from its data cells, and on a dark page that separation reads as
     a lift rather than a recess. */
  --sheet-label-bg: #262f38;
  --tint-soft-bg: #33201f;
    --tint-soft-ink: #f0c6c2;
    /* Ink for the warm priority fills — orange, amber, green. White fails AA on
       all three (3.45-3.58:1) and did so unnoticed because the priority palette
       was never contrast-checked; this near-black clears it in both themes. */
    --ink-on-warm: #10161d;

    /* Charts: a barely-there plate instead of the bright white the canvas
       inherited, a grid faint enough not to compete with the bars, and ink light
       enough to read. */
    /* Light enough to read on the dark plot (10.48:1), still neutral so it does
     not compete with the coloured unit series. */
  --chart-total: #cfd8e0;
  --chart-grid: #2b3540;
    --chart-ink: #9dadba;
    --chart-plot-bg: #1f272e;
  }
}

/* The handful of places that hard-coded white before there was a dark theme.
   Listed rather than hunted down one at a time so a missed one is obvious. */
/* Native form controls. The rule at the top of the file now reads --control-bg,
   so these need no override — except <option>, which a browser paints from its
   own defaults unless told otherwise. That is the "dropdowns retain white"
   report: the closed select was already dark, the list that dropped out of it
   was not. `color-scheme` covers the native picker chrome (the date and time
   fields' calendar and spinner) for the same reason. */
:root[data-theme="dark"] option,
:root[data-theme="system"] option { background: var(--control-bg); color: var(--color-text); }
:root[data-theme="dark"] input[type="date"],
:root[data-theme="dark"] input[type="time"],
:root[data-theme="dark"] input[type="datetime-local"],
:root[data-theme="system"] input[type="date"],
:root[data-theme="system"] input[type="time"],
:root[data-theme="system"] input[type="datetime-local"] { color-scheme: dark; }
:root[data-theme="dark"] .cal-event,
:root[data-theme="system"] .cal-event,
:root[data-theme="dark"] .gantt-bar,
:root[data-theme="system"] .gantt-bar { color: var(--status-ink); }
/* These two set a dark ink of their own for a pale amber fill in light mode;
   in dark mode the shared ink is already dark, so the override is redundant and
   overriding it again with a different near-black would look like a mistake. */
/* No dark override needed any more: --ink-on-warm is the same near-black in both
   themes, because the warm fills stay mid-tone in both. */
:root[data-theme="dark"] .gantt-cell:nth-child(even) { background: rgba(255, 255, 255, .03); }
:root[data-theme="dark"] .lightbox-figure img { background: var(--color-surface); }

/* ---------- Colour schemes ---------- */
/* Only the primary family moves. Status and priority hues carry meaning — a
   red P1 must be red whatever the house colour is — so a scheme never touches
   them. */
/* The blue the CMMS shipped with, before the house burgundy. Kept as a scheme
   rather than deleted: people had got used to it, and the values are the
   originals — #0b5b8c reads 5.99:1 on white. */
:root[data-scheme="CLASSIC"],
[data-scheme="CLASSIC"] {
  /* Sidebar: 11.00:1 and 7.27:1 against white. Set here and not in
     the dark block below, so the sidebar stays the same dark chrome in both
     themes — see --sidebar-bg. */
  --sidebar-bg: #08405f; --sidebar-active-bg: #0b5b8c;
  --color-primary: #0b5b8c; --color-primary-dark: #08405f; --color-primary-light: #e7f1f7;
  --panel-header-bg: #e2ebf1; --panel-header-ink: #08405f;
  --panel-accent-border: #aac6d7;
}
:root[data-scheme="CLASSIC"][data-theme="dark"],
[data-scheme="CLASSIC"][data-theme="dark"] {
  --color-primary: #4aa3d8; --color-primary-dark: #08405f; --color-primary-light: #16303f;
  --panel-header-bg: #233744; --panel-header-ink: #9bccea;
  --panel-accent-border: #2d536b;
}
:root[data-scheme="SLATE"],
[data-scheme="SLATE"] {
  /* Sidebar: 10.35:1 and 7.58:1 against white. Set here and not in
     the dark block below, so the sidebar stays the same dark chrome in both
     themes — see --sidebar-bg. */
  --sidebar-bg: #334155; --sidebar-active-bg: #475569;
  --color-primary: #475569; --color-primary-dark: #334155; --color-primary-light: #eef1f5;
  --panel-header-bg: #e9ebed; --panel-header-ink: #334155;
  --panel-accent-border: #bfc4ca;
}
:root[data-scheme="SLATE"][data-theme="dark"],
[data-scheme="SLATE"][data-theme="dark"] {
  --color-primary: #94a3b8; --color-primary-dark: #64748b; --color-primary-light: #232c37;
  --panel-header-bg: #2e373f; --panel-header-ink: #c4ccd8;
  --panel-accent-border: #49535f;
}
:root[data-scheme="TEAL"],
[data-scheme="TEAL"] {
  /* Sidebar: 7.58:1 and 5.47:1 against white. Set here and not in
     the dark block below, so the sidebar stays the same dark chrome in both
     themes — see --sidebar-bg. */
  --sidebar-bg: #115e59; --sidebar-active-bg: #0f766e;
  --color-primary: #0f766e; --color-primary-dark: #115e59; --color-primary-light: #e6f4f2;
  --panel-header-bg: #e2efee; --panel-header-ink: #115e59;
  --panel-accent-border: #abcfcc;
}
:root[data-scheme="TEAL"][data-theme="dark"],
[data-scheme="TEAL"][data-theme="dark"] {
  --color-primary: #2dd4bf; --color-primary-dark: #14b8a6; --color-primary-light: #12302d;
  --panel-header-bg: #1e3e40; --panel-header-ink: #8ce7dc;
  --panel-accent-border: #226661;
}
:root[data-scheme="INDIGO"],
[data-scheme="INDIGO"] {
  /* Sidebar: 9.93:1 and 7.90:1 against white. Set here and not in
     the dark block below, so the sidebar stays the same dark chrome in both
     themes — see --sidebar-bg. */
  --sidebar-bg: #3730a3; --sidebar-active-bg: #4338ca;
  --color-primary: #4338ca; --color-primary-dark: #3730a3; --color-primary-light: #ecebfa;
  --panel-header-bg: #e8e7f9; --panel-header-ink: #3730a3;
  --panel-accent-border: #bdb9ec;
}
:root[data-scheme="INDIGO"][data-theme="dark"],
[data-scheme="INDIGO"][data-theme="dark"] {
  --color-primary: #a5b4fc; --color-primary-dark: #818cf8; --color-primary-light: #211f45;
  --panel-header-bg: #31394a; --panel-header-ink: #ced6fd;
  --panel-accent-border: #4f5979;
}

/* ---------- Density ---------- */
/* Spacing only. Nothing is hidden and no font drops below 12px at any setting —
   a form that fits on screen because it cannot be read is not more usable.

   Compact is the default because of what the forms here actually are: the outage
   entry sheet carries thirty of the workbook's columns and the work order page
   is a dozen cards. The textarea min-height is the single biggest win — 90px is
   four lines of room for fields that mostly hold one. */
/* ---------- Text size ---------- */
/* Reachable by any element, not only :root, so the Appearance page's preview
   tiles can carry data-font-size and show the real thing — the same rule the
   theme and density blocks follow.

   The range is deliberately narrow. Below 0.9 the .62rem labels on the plant
   log fall under 8px, and above 1.25 the fixed-width sheet stops fitting a
   laptop screen. Four steps that all work beat eight where half do not. */
:root[data-font-size="SMALL"],
[data-font-size="SMALL"] { --font-scale: .9; }
:root[data-font-size="NORMAL"],
[data-font-size="NORMAL"] { --font-scale: 1; }
:root[data-font-size="LARGE"],
[data-font-size="LARGE"] { --font-scale: 1.12; }
:root[data-font-size="XLARGE"],
[data-font-size="XLARGE"] { --font-scale: 1.25; }
/* A preview tile sets the scale on itself, where `rem` would still resolve
   against the document root and show nothing. `em` inside the tile follows the
   tile, so the preview text has to be sized in em. */
/* The text-size preview tile. Two lines of real wording rather than lorem —
   a work order reference is what the reader will actually be squinting at. */
.fontsize-tile {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  justify-content: center;
  min-height: 62px;
  padding: .5rem .6rem;
  border-radius: 4px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  overflow: hidden;
}
.fontsize-tile-sample { font-size: calc(1em * var(--font-scale, 1)); font-weight: 600; }
.fontsize-tile-meta {
  font-size: calc(.78em * var(--font-scale, 1));
  color: var(--color-text-muted);
  white-space: nowrap;
}

:root[data-density="COMPACT"],
[data-density="COMPACT"] {
  --card-pad-y: .95rem;
  --card-pad-x: 1.1rem;
  --card-gap: 1.15rem;
  --grid-gap: 1.05rem;
  --pad-cell: .4rem .55rem;
  --gap-field: .6rem;
  --h-textarea: 58px;
  --pad-input: .34rem .5rem;
  /* A single height for the Appearance page density preview. */
  --pad-input-h: 1.55rem;
  /* Relative, so the text-size setting still applies on top of it — an
     absolute px here would silently override the whole scale. */
  --size-base: .964rem;
}
:root[data-density="CONDENSED"],
[data-density="CONDENSED"] {
  --card-pad-y: .7rem;
  --card-pad-x: .85rem;
  --card-gap: .85rem;
  --grid-gap: .8rem;
  --pad-cell: .28rem .45rem;
  --gap-field: .42rem;
  --h-textarea: 46px;
  --pad-input: .26rem .45rem;
  /* A single height for the Appearance page density preview. */
  --pad-input-h: 1.35rem;
  --size-base: .929rem;
}

:root[data-density="COMPACT"] body,
:root[data-density="CONDENSED"] body { font-size: var(--size-base); }

:root[data-density="COMPACT"] form .field,
:root[data-density="CONDENSED"] form .field { margin-bottom: var(--gap-field); }

:root[data-density="COMPACT"] form label,
:root[data-density="CONDENSED"] form label { margin-bottom: .18rem; }

:root[data-density="COMPACT"] input:not([type="checkbox"]):not([type="radio"]),
:root[data-density="COMPACT"] select,
:root[data-density="COMPACT"] textarea,
:root[data-density="CONDENSED"] input:not([type="checkbox"]):not([type="radio"]),
:root[data-density="CONDENSED"] select,
:root[data-density="CONDENSED"] textarea { padding: var(--pad-input); }

/* The big one: a textarea rendered with rows="3" is still four lines tall
   because of the min-height. Density shortens the box; it still grows when the
   user drags it, and the content is untouched. */
:root[data-density="COMPACT"] textarea,
:root[data-density="CONDENSED"] textarea { min-height: var(--h-textarea); line-height: 1.3; }

:root[data-density="COMPACT"] .data-table th,
:root[data-density="COMPACT"] .data-table td,
:root[data-density="CONDENSED"] .data-table th,
:root[data-density="CONDENSED"] .data-table td { padding: var(--pad-cell); }


:root[data-density="COMPACT"] .kv-grid,
:root[data-density="CONDENSED"] .kv-grid { row-gap: .18rem; }

:root[data-density="COMPACT"] h1 { font-size: 1.32rem; }
:root[data-density="COMPACT"] h2 { font-size: 1.06rem; margin-bottom: .35rem; }
:root[data-density="CONDENSED"] h1 { font-size: 1.22rem; }
:root[data-density="CONDENSED"] h2 { font-size: 1rem; margin-bottom: .25rem; }
:root[data-density="CONDENSED"] h3 { font-size: .95rem; }

:root[data-density="CONDENSED"] .content { padding: .75rem; }
:root[data-density="CONDENSED"] .btn { padding: .32rem .7rem; }

/* ---------- The theme switch, on the bar ----------
   A two-state switch that shows the state it is in, rather than a button
   labelled with the state it would move to. Both faces are drawn and the knob
   slides over one of them, which is the idiom every reader already knows from
   their phone — and it means the control is legible at a glance instead of
   needing a word read off it.

   WHAT IT SHOWS IS DECIDED BY CSS, NOT BY THE SERVER. "Follow the device"
   resolves nowhere on the server, so the server cannot know which way to draw
   the knob; the <html> element's data-theme plus prefers-color-scheme can,
   exactly as they decide every colour token in this sheet. */
.theme-switch { display: inline-flex; margin: 0; }
.theme-toggle {
  display: inline-flex; align-items: center;
  padding: 0; border: 0; background: none; cursor: pointer;
}
.theme-toggle-track {
  position: relative; display: inline-flex; align-items: center;
  width: 3.1rem; height: 1.6rem; padding: 0 .28rem;
  border: 1px solid var(--color-border); border-radius: 999px;
  background: var(--color-surface-2);
  justify-content: space-between;
  transition: background .15s ease, border-color .15s ease;
}
.theme-toggle:hover .theme-toggle-track { border-color: var(--color-primary); }
.theme-toggle:focus-visible .theme-toggle-track {
  outline: none; border-color: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
/* The two faces sit still; the knob is what moves. Drawn under it, so the one
   being covered reads as the state not chosen. */
.theme-toggle-face {
  position: relative; z-index: 1;
  font-size: .72rem; line-height: 1; color: var(--color-text-muted);
  transition: color .15s ease;
}
.theme-toggle-knob {
  position: absolute; z-index: 2; top: 50%; left: .18rem;
  width: 1.15rem; height: 1.15rem; margin-top: -0.575rem;
  border-radius: 50%; background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .22);
  transition: transform .16s ease;
}
/* Light: the knob covers the sun, because the sun is where we are. */
:root[data-theme="light"] .theme-toggle-knob { transform: translateX(0); }
:root[data-theme="dark"] .theme-toggle-knob { transform: translateX(1.44rem); }
:root[data-theme="light"] .theme-toggle-moon,
:root[data-theme="dark"] .theme-toggle-sun { color: var(--color-text); }
/* "Follow the device": the media query decides, as it does for every other
   token in the sheet. */
:root[data-theme="system"] .theme-toggle-knob { transform: translateX(0); }
:root[data-theme="system"] .theme-toggle-moon { color: var(--color-text); }
@media (prefers-color-scheme: dark) {
  :root[data-theme="system"] .theme-toggle-knob { transform: translateX(1.44rem); }
  :root[data-theme="system"] .theme-toggle-moon { color: var(--color-text-muted); }
  :root[data-theme="system"] .theme-toggle-sun { color: var(--color-text); }
}
/* A switch that slides is decoration for anyone who asked for less motion; it
   still moves, it simply arrives. */
@media (prefers-reduced-motion: reduce) {
  .theme-toggle-knob, .theme-toggle-track, .theme-toggle-face { transition: none; }
}

/* ---------- Odds and ends the new pages need ---------- */
.nowrap { white-space: nowrap; }
.wrap-cell { white-space: normal; max-width: 30rem; }
.row-current > td { background: color-mix(in srgb, var(--status-completed) 14%, transparent); font-weight: 600; }
.row-past > td { color: var(--color-text-muted); }
.row-inactive > td { opacity: .55; }
.row-standing > td { background: color-mix(in srgb, var(--color-primary) 8%, transparent); }
.reservation-request { border: 1px solid var(--color-border); border-radius: var(--radius); padding: .6rem; margin-bottom: .7rem; }
.reservation-request + .reservation-request { margin-top: .5rem; }

/* A form mid-submit. The button already says "Saving…"; this stops a second
   click landing on a row that is about to be replaced. */
form.is-busy { opacity: .72; pointer-events: none; }

/* ---------- Who an alarm was told to ---------- */
/* The column lists names, so it needs to wrap rather than stretch the table off
   the page — and each channel needs to read as one block. */
.delivery-cell { min-width: 16rem; max-width: 30rem; white-space: normal; }
.delivery-group { display: flex; gap: .4rem; align-items: baseline; margin-bottom: .25rem; }
.delivery-group:last-child { margin-bottom: 0; }
.delivery-names { font-size: .78rem; line-height: 1.4; }
.delivery-sep { color: var(--color-text-muted); margin: 0 .2rem; }
/* A failed delivery is struck through and flagged beside the name. Colouring
   the whole channel red would say nobody was told, when one address bounced. */
.delivery-name.did-not-send { text-decoration: line-through; color: var(--color-text-muted); }
.delivery-flag {
  text-decoration: none; font-size: .68rem; font-weight: 700;
  color: var(--priority-1); text-transform: uppercase; letter-spacing: .03em;
}
/* Switched off is a decision, not a fault — it does not get the alarm colour. */
.delivery-flag.delivery-off { color: var(--color-text-muted); }

/* ---------- The permit page: tabs, the call to action, the gates ---------- */
/* THE STRIP THAT SAYS WHOSE TURN IT IS.
   A permit carries controls belonging to four different people. This is the one
   line that says which of them is outstanding — and it changes colour when it is
   the reader's, because "somebody should do this" and "you should do this" are
   different messages. */
.next-step {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  margin: .9rem 0; padding: .8rem 1rem;
  border: 1px solid var(--color-border); border-left: 4px solid var(--color-text-muted);
  border-radius: var(--radius); background: var(--color-surface);
}
.next-step-yours { border-left-color: var(--priority-1); background: color-mix(in srgb, var(--priority-1) 7%, var(--color-surface)); }
.next-step-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
.next-step-head strong { font-size: 1rem; }
.next-step-detail { font-size: .82rem; color: var(--color-text-muted); }
.next-step-owner { font-size: .82rem; margin-top: .15rem; }

/* The tabs are real links carrying ?tab=, so each is bookmarkable and works
   with JavaScript off — the permit page is opened on a phone at the job. */
/* ONE BORDER COLOUR, ONE BORDER WIDTH, FOR THE WHOLE COMPONENT.

   The strip, the tabs and the board below them were drawn in three different
   ways: the inactive tabs took the neutral --color-border while the strip's
   underline, the open tab and the board took the theme's --panel-accent-border,
   and the underline was 2px against the tabs' 1px. Three greys and two widths
   in one control is what made it look unresolved — every edge now comes from
   the same token at the same width, so the tabs and the board they open read as
   one outline. */
.permit-tabs {
  display: flex; gap: .3rem; flex-wrap: wrap; margin: 1rem 0 0;
  border-bottom: 1px solid var(--panel-accent-border);
}
/* THE OPEN TAB IS THE COLOUR OF WHAT IT OPENS — and the closed ones are not.

   This was the wrong way round. The closed tabs were painted in the board's own
   colour and the open one in a tinted wash, so the tab that was selected looked
   like the odd one out and the ones that were not looked continuous with the
   panel below them. A tab strip reads by one rule: the open tab is part of the
   board and the closed ones are behind it.

   So the open tab now takes --color-bg, which is exactly what .tab-panel is
   painted in, and drops its bottom edge into that same colour so there is no
   line between the tab and the content it belongs to. The closed tabs take the
   card-title wash and sit behind. */
.permit-tab {
  display: block; padding: .5rem .9rem;
  border: 1px solid var(--panel-accent-border); border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0; text-decoration: none;
  background: var(--panel-header-bg); color: var(--panel-header-ink);
  /* Sits its own bottom edge on the strip's line, so the open tab can paint
     over it and join the board. 1px now, matching the line it covers. */
  margin-bottom: -1px;
}
.permit-tab:hover {
  background: var(--color-surface-2); color: var(--color-text); text-decoration: none;
}
.permit-tab.active {
  background: var(--color-bg); color: var(--color-text);
  border-color: var(--panel-accent-border);
  /* The join. Its bottom edge is the board's colour rather than a line, which
     is what makes the open tab and the panel one shape. */
  border-bottom: 1px solid var(--color-bg);
}
/* Weight carries the selection as well as colour, for anyone who cannot tell
   these two backgrounds apart. */
.permit-tab .permit-tab-label { font-weight: 600; }
.permit-tab.active .permit-tab-label { font-weight: 700; }
/* THE TAB IS ITS LABEL. It used to carry a line of description underneath, and
   three of those side by side made the strip wider than the screen — on a phone
   it wrapped into a paragraph with tab borders round it, and on a small laptop
   it pushed the content below the fold. The description was worth keeping and
   is: it opens the panel it describes, as .tab-intro, where only the relevant
   one is shown and it has a full line to be readable on. The tab keeps it as a
   title attribute for the hover. */
.permit-tab-label { display: block; font-weight: 600; font-size: .88rem; white-space: nowrap; }

/* THE BOARD AROUND THE TAB'S CONTENT.
   Without it the tabs looked like three links above a page, not three faces of
   one thing — nothing said where the tab's contents ended. The panel joins the
   active tab (which has no bottom border of its own) and encloses everything
   the tab holds. */
.tab-panel {
  border: 1px solid var(--panel-accent-border);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  background: var(--color-bg);
  padding: 1rem 1.1rem 1.1rem;
  margin-bottom: 1.25rem;
}
/* Inside the board the cards sit on their own ground, so the two surfaces do
   not merge into one grey block. */
.tab-panel > .card:last-child,
.tab-panel > .grid:last-child { margin-bottom: 0; }
.tab-intro { margin: 0 0 .9rem; }
@media (max-width: 700px) {
  /* On a phone the board's side padding costs more than it explains. */
  .tab-panel { padding: .8rem .55rem .6rem; }
}

/* The panels are all in the document and switched by static/js/tabs.js, so the
   hidden ones have to stay hidden however they are laid out. */
.tab-panel[hidden] { display: none; }

/* ---------- Confirmation dialog ----------
   The guard in front of anything that cannot be undone. Rendered by
   static/js/confirm.js; without the script nothing here is used. */
.confirm-dialog {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  padding: 0;
  max-width: 26rem;
  width: calc(100% - 2rem);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .35);
}
.confirm-dialog::backdrop { background: rgba(0, 0, 0, .45); }
.confirm-box { padding: 1.1rem 1.25rem 1.15rem; }
.confirm-box h2 { margin: 0 0 .5rem; font-size: 1.02rem; }
.confirm-detail { margin: 0 0 .9rem; font-size: .85rem; color: var(--color-text-muted); }
.confirm-phrase { display: block; margin-bottom: .9rem; font-size: .82rem; }
.confirm-phrase span { display: block; margin-bottom: .25rem; color: var(--color-text-muted); }
.confirm-phrase input { width: 100%; }
.confirm-actions { display: flex; justify-content: flex-end; gap: .5rem; }

/* ---------- A blocker with the link that clears it ----------
   The release gate lists what is outstanding; each line now carries the page
   that deals with it, because a reader who has been told an HSE permit is
   missing should not then have to find where permits are raised. */
.gate-checklist .gate-action {
  display: inline-block;
  margin-left: .4rem;
  font-size: .8rem;
  font-weight: 600;
  white-space: nowrap;
  /* Inherit the alert's own ink rather than the link colour: the list sits on a
     tinted panel whose ink is chosen against that tint, and a link painted in
     the page's blue can come out unreadable on it in one theme or the other.
     The underline is what marks it as a link instead. */
  color: inherit;
  text-decoration: underline;
}

/* Each gate names the role that holds it, in the same place every time — which
   is what makes "this one is not mine" readable at a glance. */
.approval-gate .gate-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .6rem; }
.approval-gate .gate-head h2 { margin: 0; font-size: 1rem; }
.approval-gate .gate-head > .badge { margin-left: auto; }
.gate-number {
  display: flex; align-items: center; justify-content: center;
  width: 1.7rem; height: 1.7rem; flex: 0 0 auto; border-radius: 50%;
  background: var(--color-primary); color: var(--status-ink);
  font-weight: 700; font-size: .8rem;
}
.gate-owner { display: block; font-size: .74rem; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: .03em; }

/* ---------- The HSE permit and toolbox forms ---------- */
/* The class ticks, and the answer row inside a checklist cell. Both are wrapped
   grids rather than flex rows: six class names and four answers each need to
   break onto a second line on a laptop, and a nowrap row pushed the remark
   column off the card. */
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: .4rem .9rem;
}
.radio-row { display: flex; flex-wrap: wrap; gap: .1rem .7rem; }
.radio-row .check-inline { white-space: nowrap; }

/* The hazard analysis rows. A textarea in a table cell needs a width or it
   takes the browser's default 20 columns and the row grows to five lines. */
.jha-table textarea, .jha-table input[type="text"] { width: 100%; min-width: 8rem; }
.jha-table td { vertical-align: top; }
.jha-table select { min-width: 8rem; }

/* ---------- The linear journey (job ticket, work order) ----------
   A row of steps joined by a line. The connector is drawn on each step's own
   ::before rather than as a background on the list, so it stops at the last
   step instead of running off the end, and it wraps correctly when the row
   breaks onto two lines. */
/* The card is the caller's — both pages already had a progress card with its
   own heading and controls, so the partial renders the list alone. */
.journey {
  display: flex;
  flex-wrap: wrap;
  gap: .2rem 0;
  margin: .4rem 0 1rem;
  padding: 0;
  list-style: none;
  counter-reset: journey;
}

.journey-step {
  position: relative;
  flex: 1 1 8rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .35rem;
  text-align: center;
  padding: 0 .25rem;
}

/* The connector, from this step's marker back to the previous one. Not on the
   first step, which has nothing behind it. */
.journey-step + .journey-step::before {
  content: "";
  position: absolute;
  top: .82rem;
  right: 50%;
  left: -50%;
  height: 2px;
  background: var(--color-border);
  z-index: 0;
}
.journey-step.is-done::before,
.journey-step.is-current::before { background: var(--status-completed); }

.journey-marker {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.65rem;
  height: 1.65rem;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 2px solid var(--color-border);
  background: var(--color-surface);
  font-size: .78rem;
  font-weight: 700;
  color: var(--color-text-muted);
}

.journey-body { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.journey-label { font-size: .78rem; line-height: 1.25; overflow-wrap: anywhere; }
.journey-meta { font-size: .68rem; color: var(--color-text-muted); overflow-wrap: anywhere; }

/* Done: filled, and the label stops competing for attention. */
.journey-step.is-done .journey-marker {
  background: var(--status-completed);
  border-color: var(--status-completed);
  color: var(--status-ink);
}
.journey-step.is-done .journey-label { color: var(--color-text-muted); }

/* Current: the one thing the reader came for, so it is the only ring in the
   theme's own colour and the only bold label. */
.journey-step.is-current .journey-marker {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--status-ink);
  box-shadow: 0 0 0 3px var(--color-primary-light);
}
.journey-step.is-current .journey-label { font-weight: 700; color: var(--color-text); }

/* Ahead: outlined and quiet. */
.journey-step.is-todo .journey-label { color: var(--color-text-muted); }

/* Skipped or branched — a suspended work order, a memo closed without
   conversion. Dashed, because claiming a tidy progression that did not happen
   is worse than admitting the detour. */
.journey-step.is-off-path .journey-marker {
  border-style: dashed;
  background: var(--color-surface);
  color: var(--color-text-muted);
}

@media (max-width: 700px) {
  /* Vertical on a phone: eight steps across a 380px screen is four characters
     each. The connector becomes the left-hand rail. */
  .journey { flex-direction: column; gap: 0; }
  .journey-step {
     flex: 1 1 auto;
     flex-direction: row;
     align-items: flex-start;
     text-align: left;
     gap: .6rem;
     padding: .3rem 0;
  }
  .journey-step + .journey-step::before {
    top: -.3rem; bottom: auto; left: .82rem; right: auto;
    width: 2px; height: 1.1rem;
  }
  .journey-body { align-items: flex-start; }
}

/* ---------- Calendar: year view ----------
   Twelve cells, four across on a desk and two on a phone. The cell is a month
   and it opens that month's grid, so it carries the same chips as a day cell —
   the two views must read the same way or the reader has to learn twice. */
.cal-year {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: .7rem;
  margin-top: .6rem;
}
.cal-year-cell {
  display: block;
  padding: .6rem .7rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  text-decoration: none;
  color: inherit;
  min-height: 6.5rem;
}
.cal-year-cell:hover {
  background: var(--color-surface-2);
  border-color: var(--color-primary);
  text-decoration: none;
}
/* This month, the way `today` is marked in the month grid. */
.cal-year-cell.current { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); }
/* Months already gone are still worth reading — what was planned and what
   slipped — so they are dimmed, not hidden. */
.cal-year-cell.past { opacity: .72; }
.cal-year-head { display: flex; justify-content: space-between; align-items: baseline; gap: .4rem; }
.cal-year-month { font-weight: 700; font-size: .82rem; }
.cal-year-count { font-size: 1.05rem; font-weight: 700; }
/* Share of the busiest month — see maintenance_calendar_year, which scales
   against the real maximum rather than a guessed ceiling. */
.cal-year-bar {
  height: 4px; border-radius: 2px; margin: .35rem 0 .45rem;
  background: var(--color-border); overflow: hidden;
}
.cal-year-bar > span { display: block; height: 100%; background: var(--color-primary); }
.cal-year-groups { display: flex; flex-wrap: wrap; gap: .2rem; }
.cal-year-empty { font-size: .7rem; color: var(--color-text-muted); }
.cal-year-flags { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .4rem; }

@media (max-width: 900px) { .cal-year { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 620px) { .cal-year { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* The month/year switcher. Two links that look like one control, so it reads
   as a choice between views rather than two unrelated buttons. */
.cal-view-switch { display: inline-flex; border: 1px solid var(--color-border); border-radius: var(--radius); overflow: hidden; }
.cal-view-switch a {
  padding: .3rem .8rem; font-size: .78rem; font-weight: 600;
  text-decoration: none; color: var(--color-text-muted); background: var(--color-bg);
}
.cal-view-switch a + a { border-left: 1px solid var(--color-border); }
.cal-view-switch a:hover { background: var(--color-surface-2); color: var(--color-text); text-decoration: none; }
.cal-view-switch a.active { background: var(--panel-header-bg); color: var(--panel-header-ink); }

/* A figure's own footnote — what it is over, under the number it qualifies. */
.stat-note { font-size: .64rem; color: var(--color-text-muted); margin-top: .15rem; line-height: 1.3; }

/* ---------- The work order's transition control, on one line ----------
   Four controls in a row: two short selects sized to their own content, the
   note taking whatever is left, and the button aligned with them. The labels
   sit above, so `align-items: end` is what lines the button up with the boxes
   rather than with the labels.

   It wraps instead of squeezing: below about a tablet the note drops to its own
   line and the selects stay legible, which is better than four controls none of
   which can show their contents. */
.transition-row {
  display: flex; flex-wrap: wrap; gap: .6rem; align-items: end;
}
.transition-row .field { margin-bottom: 0; min-width: 0; }
.transition-row .field > label {
  display: block; font-size: .74rem; color: var(--color-text-muted);
  font-weight: 600; margin-bottom: .2rem; white-space: nowrap;
}
.transition-row select,
.transition-row input[type="text"] { height: 2.3rem; box-sizing: border-box; }
/* The two selects carry the decision; the note is a remark about it. Sizing
   them by content let a short status list shrink "Move to status" to a stub
   while the note ran the width of the card. Both selects now get a floor wide
   enough for their longest real option — "Suspend — awaiting spares" and
   "Return to the raising section" — and the note is what yields. */
.transition-row .field-status { flex: 0 1 15rem; min-width: 11rem; }
.transition-row .field-reason { flex: 0 1 15rem; min-width: 11rem; }
.transition-row .field-status select,
.transition-row .field-reason select { width: 100%; }
.transition-row .field-grow { flex: 1 1 11rem; }
.transition-row .field-grow input[type="text"] { width: 100%; }
.transition-row .field-actions { flex: 0 0 auto; }
.transition-row .field-actions .btn { height: 2.3rem; }
@media (max-width: 760px) {
  .transition-row .field-status,
  .transition-row .field-reason,
  .transition-row .field-grow { flex: 1 1 100%; }
}

/* ---------- A declared form, laid out like its paper original ----------
   The station's permit forms are declared as sections of fields, each field
   carrying how much of a row it takes on the printed sheet. This is that: a
   twelve-column grid where a field spans a quarter, a third, a half or the lot.

   Keeping the widths means the screen and the paper have the same shape, which
   is what lets somebody transcribing a signed form find the next box without
   reading every label. */
.form-grid {
  display: grid; grid-template-columns: repeat(12, 1fr); gap: .6rem .7rem;
  align-items: start;
}
.form-grid > .field { margin-bottom: 0; min-width: 0; grid-column: span 6; }
.form-grid > .field.span-full { grid-column: 1 / -1; }
.form-grid > .field.span-half { grid-column: span 6; }
.form-grid > .field.span-third { grid-column: span 4; }
.form-grid > .field.span-quarter { grid-column: span 3; }
.form-grid > .field > label {
  display: block; font-size: .74rem; color: var(--color-text-muted);
  font-weight: 600; margin-bottom: .2rem;
}
.form-grid input[type="text"], .form-grid input[type="date"],
.form-grid input[type="time"], .form-grid input[type="number"],
.form-grid select, .form-grid textarea { width: 100%; box-sizing: border-box; }
.form-grid .help-text { margin: .15rem 0 0; }
/* A block of tick boxes reads as a block, the way it does on the sheet — not as
   a column of full-width rows each holding one small square. */
.form-grid-dense > .field { grid-column: span 3; }
.form-grid-dense > .field.span-full { grid-column: 1 / -1; }
.form-grid-dense > .field.span-half { grid-column: span 6; }
@media (max-width: 900px) {
  .form-grid > .field,
  .form-grid > .field.span-third,
  .form-grid > .field.span-quarter { grid-column: span 6; }
  .form-grid-dense > .field { grid-column: span 6; }
}
@media (max-width: 560px) {
  .form-grid > .field { grid-column: 1 / -1; }
}

/* ---------- The report field picker ----------
   Forty checkboxes must not push the four filters people actually use off the
   top of the page, so it is a <details> that sits with them and opens only when
   wanted. It opens by itself when a selection is already in force, because a
   closed box is how a reader loses track of why their table looks unfamiliar.

   It spans the whole filter grid rather than sitting in one cell: a column of
   tick boxes 190px wide is unreadable, and the names are the thing being
   chosen. */
.column-picker { grid-column: 1 / -1; min-width: 0; margin: 0; }
.column-picker > summary {
  cursor: pointer; font-size: .78rem; font-weight: 600;
  padding: .3rem 0; color: var(--color-primary); list-style: none;
  display: flex; align-items: center; gap: .35rem;
}
.column-picker > summary::-webkit-details-marker { display: none; }
.column-picker > summary::marker { content: ""; }
.column-picker > summary::before { content: "▸"; font-size: .65rem; }
.column-picker[open] > summary::before { content: "▾"; }
.column-picker > summary:hover { text-decoration: underline; }
.column-picker-panel {
  border: 1px solid var(--color-border); border-radius: var(--radius);
  padding: .6rem .75rem; background: var(--color-surface-2);
  margin-top: .3rem;
}
.column-picker-grid {
  display: grid; gap: 0 1rem;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  max-height: 17rem; overflow-y: auto;
}
/* THE NAME IS THE THING BEING PICKED, so it gets the row and the box gets a
   box. Stated here rather than inherited, because the filter grid's control
   sizing above is exactly what flattened these once already. */
.column-picker-grid label {
  display: flex; align-items: center; gap: .45rem;
  font-size: .78rem; font-weight: 400; color: var(--color-text);
  padding: .2rem .3rem; margin: 0; border-radius: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  cursor: pointer;
}
.column-picker-grid label:hover { background: var(--color-surface); }
.column-picker-grid input[type="checkbox"] {
  flex: 0 0 auto;
  width: 0.95rem; height: 0.95rem; min-width: 0; margin: 0; padding: 0;
  accent-color: var(--color-primary);
}
/* A field that is not in the report's standard set, marked so a reader can tell
   what they are adding from what was already there. */
.column-picker-grid label.is-extra { color: var(--color-text-muted); }
.column-picker-actions {
  display: flex; gap: .6rem; align-items: center;
  margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--color-border);
  font-size: .74rem;
}

/* ---------- Overdue, graded ----------
   Three bands off WorkOrder.overdue_severity. The tints are the ones the alert
   variants already use, so nothing new needs a contrast sweep; what separates
   the bands is the left rail, which is the part read at a glance from across a
   desk. Critical also carries the priority-1 rail at full width. */
.overdue-banner { border-left-width: 4px; border-left-style: solid; }
.overdue-late {
  background: var(--tint-warning-bg);
  color: var(--tint-warning-ink);
  border: 1px solid var(--tint-warning-border);
  border-left: 4px solid var(--priority-2);
}
.overdue-overdue {
  background: var(--tint-danger-bg);
  color: var(--tint-danger-ink);
  border: 1px solid var(--tint-danger-border);
  border-left: 4px solid var(--priority-1);
}
.overdue-critical {
  background: var(--tint-danger-bg);
  color: var(--tint-danger-ink);
  border: 1px solid var(--tint-danger-border);
  border-left: 6px solid var(--priority-1);
  font-weight: 600;
}

/* ---------- Condition monitoring ---------- */
.band-swatch {
  display: inline-block; width: .7rem; height: .7rem; border-radius: 2px;
  vertical-align: -1px; margin-right: .35rem; border: 1px solid rgba(0, 0, 0, .18);
}
/* ALARM SEVERITY: WARM FOR OVER, COOL FOR UNDER.
   The comment here used to claim the two criticals differed while both rules
   pointed at --priority-1, and the two warnings were both --priority-2 — so the
   Band column showed one colour for "too high" and the same colour for "too
   low". Those are opposite actions at the panel: a reader who has to read the
   words has lost the point of a coloured badge.

   The cool pair are --status-* tokens deliberately, because that prefix is what
   StatusColourContrastTests sweeps against each palette's own ink. A colour
   named anything else here would go unchecked in ten palettes. */
.badge-severity-critical_high { background: var(--priority-1); color: var(--status-ink); }
.badge-severity-high_warning { background: var(--priority-2); color: var(--ink-on-warm); }
.badge-severity-normal { background: var(--priority-4); color: var(--ink-on-warm); }
.badge-severity-low_warning { background: var(--status-severity-low); color: var(--status-ink); }
.badge-severity-critical_low { background: var(--status-severity-critical-low); color: var(--status-ink); }
.row-unacked > td { background: color-mix(in srgb, var(--priority-1) 10%, transparent); }

/* ==========================================================================
   Appearance page: previews of each theme, scheme and density
   ==========================================================================
   Every tile below carries data-theme / data-scheme / data-density, and the
   palette blocks at the top of this file match those attributes on any element
   — not only on <html>. So a tile inherits the genuine tokens for the option it
   represents and there is not one colour literal in this section. That is the
   whole point: a swatch painted by hand goes stale the first time a palette
   changes, and a preview that lies about the theme is worse than no preview. */

.appearance-now { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
.appearance-now .theme-tile { flex: 0 0 auto; }
.appearance-now .help-text { flex: 1 1 22rem; }

.option-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .55rem; margin-top: .1rem;
}

/* The tile is the label, so the preview is the click target. */
.option-tile {
  display: flex; flex-direction: column; gap: .25rem; cursor: pointer;
  border: 2px solid var(--color-border); border-radius: var(--radius);
  padding: .45rem; margin: 0; font-weight: 400; background: var(--color-surface);
}
.option-tile:hover { border-color: var(--color-primary); }
.option-tile input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
/* :has() styles the chosen tile live as the radio changes; the server-rendered
   .is-chosen class covers the first paint and browsers without :has(). */
.option-tile.is-chosen,
.option-tile:has(input:checked) {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 1px var(--color-primary) inset;
}
.option-tile:focus-within { outline: 2px solid var(--color-primary); outline-offset: 1px; }
.option-tile-name { font-size: .8rem; font-weight: 600; line-height: 1.25; }
.option-tile-note { font-size: .7rem; color: var(--color-text-muted); line-height: 1.3; }

/* ---- The app-in-miniature tile ---- */
.theme-tile {
  position: relative; display: flex; width: 100%; height: 5.4rem; overflow: hidden;
  border-radius: 5px; border: 1px solid var(--color-border); background: var(--color-bg);
}
.theme-tile-sidebar {
  flex: 0 0 26%; background: var(--color-primary-dark);
  padding: .3rem .25rem; display: flex; flex-direction: column; gap: .18rem;
}
.theme-tile-brand { height: .34rem; border-radius: 1px; background: rgba(255, 255, 255, .85); }
.theme-tile-navline { height: .22rem; border-radius: 1px; background: rgba(255, 255, 255, .3); }
.theme-tile-navline.is-active { background: var(--color-primary); box-shadow: 0 0 0 1px rgba(255,255,255,.25); }
.theme-tile-body { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; }
.theme-tile-topbar {
  height: .85rem; background: var(--color-surface);
  border-bottom: 1px solid var(--color-border); display: flex; align-items: center; padding: 0 .25rem;
}
.theme-tile-title { width: 45%; height: .26rem; border-radius: 1px; background: var(--color-text-muted); opacity: .7; }
.theme-tile-card {
  margin: .25rem; padding: .25rem; flex: 1 1 auto; border-radius: 3px;
  background: var(--color-surface); border: 1px solid var(--color-border);
  display: flex; flex-direction: column; gap: .16rem;
}
.theme-tile-heading { width: 55%; height: .28rem; border-radius: 1px; background: var(--color-primary); }
.theme-tile-text { height: .18rem; border-radius: 1px; background: var(--color-text); opacity: .35; }
.theme-tile-text.short { width: 60%; }
.theme-tile-row { display: flex; gap: .2rem; align-items: center; margin-top: auto; }
.theme-tile-badge {
  width: 1.1rem; height: .38rem; border-radius: 20px; background: var(--status-completed);
}
.theme-tile-btn { width: 1.4rem; height: .42rem; border-radius: 2px; }
/* The two that were the reported dark-mode bug — a preview is the right place
   to be able to see at a glance that the secondary button is still visible. */
.theme-tile-btn.primary { background: var(--color-primary); }
.theme-tile-btn.secondary { background: var(--btn-secondary-bg); border: 1px solid var(--color-border); }
.theme-tile-label {
  position: absolute; right: .2rem; bottom: .2rem; font-size: .58rem;
  padding: 0 .25rem; border-radius: 2px;
  background: var(--color-primary); color: var(--status-ink);
}

/* ---- The density tile ---- */
/* Reads the density tokens, so the rows really are that far apart and the text
   box really is that much shorter. */
.density-tile {
  width: 100%; height: 5.4rem; overflow: hidden; padding: .3rem;
  border-radius: 5px; border: 1px solid var(--color-border); background: var(--color-surface);
  display: flex; flex-direction: column; gap: var(--gap-field, 1rem);
}
.density-tile-label { height: .18rem; width: 40%; border-radius: 1px; background: var(--color-text-muted); opacity: .8; }
.density-tile-input {
  height: calc(var(--pad-input-h, .95rem)); border-radius: 2px;
  background: var(--control-bg); border: 1px solid var(--color-border);
}
.density-tile-textarea {
  /* --h-textarea is 90px at Comfortable and 46px at Condensed; scaled to fit
     the tile while keeping the ratio, which is the thing being compared. */
  height: calc(var(--h-textarea, 90px) * 0.18); min-height: .5rem;
  border-radius: 2px; background: var(--control-bg); border: 1px solid var(--color-border);
}
:root[data-density="COMPACT"] .density-tile,
[data-density="COMPACT"] .density-tile { gap: var(--gap-field); }

@media (max-width: 900px) {
  .option-grid { grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); }
}

/* ==========================================================================
   Small screens
   ==========================================================================
   Everything above is written desktop-first, so this is where the site becomes
   usable on a phone. Two breakpoints, and each one earns its place:

   900px — the sidebar stops fitting beside the content. It becomes a drawer
           behind a ☰ in a new header, and the content takes the full width.
   560px — a phone held upright. Two-column grids become one, the filter rows
           stack, and the tables that cannot be narrowed scroll inside their own
           box rather than pushing the page sideways.

   The rule that matters most: the page body never scrolls horizontally. One
   table 40px too wide shifts the whole layout left, and every heading and
   button with it, which is what makes a site read as broken rather than as
   cramped. Wide things scroll inside themselves. */

@media (max-width: 900px) {
  .app-shell { display: block; }

  /* The phone header: sticky, so the menu is reachable without scrolling back
     up a long work-order page. */
  html.js-nav .mobile-bar {
    display: flex;
    align-items: center;
    gap: .6rem;
    position: sticky;
    top: 0;
    z-index: 40;
    padding: .5rem .7rem;
    background: var(--sidebar-bg);
    color: var(--sidebar-ink);
  }
  .mobile-title { font-weight: 700; font-size: .95rem; }
  .mobile-menu-button {
    background: rgba(255,255,255,.12);
    color: var(--sidebar-ink);
    border: 0;
    border-radius: 6px;
    font-size: 1.15rem;
    line-height: 1;
    padding: .35rem .55rem;
    cursor: pointer;
  }
  .mobile-menu-button:hover { background: rgba(255,255,255,.22); }

  /* The drawer. Off-screen by default and slid in, rather than hidden with
     display:none — a transform animates, and a menu that appears instantly
     feels like the page jumped.

     Gated on html.js-nav, which static/js/nav.js sets as its first act. Without
     the script there is nothing that could slide the drawer back in, so the
     sidebar stays in the normal flow as a block above the content and the ☰
     header is hidden. Long, but every link present and working — which is the
     failure mode to want from a navigation bar. Hiding it unconditionally would
     leave a phone with no script no way to reach any page at all. */
  html.js-nav .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 60;
    width: min(84vw, 20rem);
    transform: translateX(-100%);
    transition: transform .2s ease;
    overflow-y: auto;
  }
  html.js-nav .sidebar.is-open { transform: translateX(0); box-shadow: 0 0 40px rgba(0,0,0,.4); }
  /* THE BACKDROP IS SHOWN BY A CLASS, NEVER BY THE `hidden` ATTRIBUTE.
     This rule used to be `.nav-backdrop { display: block }`, relying on the
     element's `hidden` attribute to keep it out of the way. It does not: the
     UA stylesheet's `[hidden] { display: none }` is an author-rule loss — any
     author rule setting `display` outranks it — so the backdrop was painted
     permanently, inset: 0 at z-index 50, over the whole viewport.

     The mobile bar sits at z-index 40. The result was a menu button that was
     visible, looked enabled, and could not be clicked, because a full-screen
     sheet of rgba(0,0,0,.45) was lying on top of it.

     Keyed off .is-open so its visibility is driven by the same state as the
     drawer, and there is nothing left for a UA default to lose an argument
     with. NavBackdropTests fails the build if a display rule for this element
     stops requiring the class. */
  html.js-nav .nav-backdrop.is-open {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgba(0,0,0,.45);
  }
  /* While the drawer is open the page behind it must not scroll under the
     finger — the drawer is what the finger is on. */
  body.nav-is-open { overflow: hidden; }

  /* The desktop top bar carries the page title and the account chip. On a phone
     it wraps rather than squeezing the title to two words. */
  .topbar { flex-wrap: wrap; gap: .4rem; padding: .5rem .7rem; }
  .topbar-right { flex-wrap: wrap; gap: .4rem; }
  .page-title { font-size: 1rem; }
  .user-chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  .content { padding: .7rem; }
  .card { padding: .8rem; }

  /* Wide content scrolls inside itself. Named individually rather than applied
     to every table, because a table that does fit should stay in the flow and
     keep its sticky headers. */
  .table-scroll,
  .report-table-scroll,
  .plant-log-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

@media (max-width: 560px) {
  /* One column. A two-column grid at 380px gives each column 170px, which is
     narrower than the labels inside it. */
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }

  /* Filter rows stack, and the Apply button spans the width. Its alignment with
     the fields beside it is meaningless once there is nothing beside it. */
  .outage-filter, .genlog-filter, .report-filters, .filter-bar {
    flex-direction: column;
    align-items: stretch;
  }
  .outage-filter .field,
  .genlog-filter .field,
  .report-filters .field,
  .filter-bar .field { width: 100%; }
  .filter-actions > label.filter-actions-spacer { display: none; }
  .filter-actions .btn { width: 100%; }

  /* Buttons in a row become a stack of full-width targets: a 44px tap target is
     the point, not the tidiness of the row. */
  .btn-group { flex-direction: column; align-items: stretch; }
  .btn-group .btn { width: 100%; text-align: center; }
  .flex-between { flex-direction: column; align-items: flex-start; gap: .5rem; }

  /* Tables that hold a record per row read better as stacked blocks than as
     four unreadable columns — but only where the template opts in, because a
     numeric table is still clearer as a table with a scrollbar. */
  .data-table.stack-on-mobile thead { display: none; }
  .data-table.stack-on-mobile tr {
    display: block;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    margin-bottom: .5rem;
    padding: .4rem .6rem;
  }
  .data-table.stack-on-mobile td { display: block; border: 0; padding: .18rem 0; }
  .data-table.stack-on-mobile td[data-label]::before {
    content: attr(data-label) ": ";
    font-weight: 600;
    color: var(--color-text-muted);
    font-size: .74rem;
  }

  h1 { font-size: 1.25rem; }
  h2 { font-size: 1.05rem; }
  .kpi-value { font-size: 1.4rem; }
}

/* ==========================================================================
   The header on a small screen
   ==========================================================================
   Three things want the bar's width and only two fit. The order they give way
   in is the order they matter least:

   900px — the page title's own row. The search box gets a full line of its
           own beneath the title, because a box squeezed between a title and
           three controls is 6rem wide and no use for a fault description.
   560px — the account name. The initial disc stays, which is enough to say
           what the control is, and the name is inside the menu anyway. */

@media (max-width: 900px) {
  .topbar {
    flex-wrap: wrap;
    align-items: flex-start;
    padding: .6rem .9rem;
    row-gap: .5rem;
  }
  .topbar-titles { flex: 1 1 auto; order: 1; }
  .topbar .topbar-right { order: 2; margin-left: auto; }
  /* Full width on its own line, after both of the above. */
  .topbar-search { order: 3; flex: 1 1 100%; max-width: none; }

  .app-footer { padding: .8rem .9rem; }
}

@media (max-width: 560px) {
  .user-chip-name, .menu-caret { display: none; }
  .topbar .user-chip { padding: .2rem; }
  /* Both panels are wider than the buttons that open them, so on a narrow
     window they anchor to the right edge and are allowed to shrink rather than
     hanging off it. */
  .menu-panel { right: 0; left: auto; min-width: 11rem; max-width: calc(100vw - 1.8rem); }
  .app-footer { flex-direction: column; align-items: flex-start; }
}
