/* HoneyEdge dashboard design system.
   Dark near-black console with the HoneyEdge amber (#ffa929) as the only
   accent. The palette lives entirely in the custom properties below:
   :root carries the dark values, and the prefers-color-scheme: light
   block remaps the same names, so no rule further down this file ever
   names a colour literal. */

:root {
  color-scheme: light dark;

  --bg: #0d0d0f;
  --surface: #17171a;
  --surface-raised: #1e1e22;
  --border: #26262b;
  --border-strong: #34343b;
  --text: #e8e8ea;
  --text-muted: #9a9aa2;

  /* Brand amber. --accent is the fill/border value and never changes;
     --accent-text is the value safe to set as a text colour against the
     current background (see the light-mode block); --on-accent is the
     text colour to use on top of an --accent fill. */
  --accent: #ffa929;
  --accent-hover: #ffc061;
  --accent-text: #ffa929;
  --accent-soft: rgba(255, 169, 41, 0.12);
  --on-accent: #0d0d0f;

  /* Severity: -solid is the marker/border colour, -text is the readable
     label colour on --surface. Never used as the only signal — every
     pill carries its own text. */
  --sev-critical: #dc2626;
  --sev-critical-text: #f87171;
  --sev-high: #f97316;
  --sev-high-text: #fb923c;
  --sev-medium: #eab308;
  --sev-medium-text: #facc15;
  --sev-low: #6b7280;
  --sev-low-text: #a1a1aa;
  --online: #16a34a;
  --online-text: #4ade80;

  --sidebar-width: 14.375rem;
  --gap: 1rem;
  --gap-lg: 1.5rem;
  --radius: 4px;
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f4f4f6;
    --surface: #ffffff;
    --surface-raised: #f7f7f9;
    --border: #dcdce2;
    --border-strong: #c3c3cc;
    --text: #17171a;
    --text-muted: #56565f;

    /* #ffa929 measures 1.92:1 on white, so it stays a fill/border value
       here and text switches to a darkened brand tone. */
    --accent-text: #9a5000;
    --accent-soft: rgba(255, 169, 41, 0.18);

    --sev-critical-text: #b91c1c;
    --sev-high-text: #c2410c;
    --sev-medium-text: #92610a;
    --sev-low-text: #52525b;
    --online-text: #15803d;
  }
}

/* ---------- base ---------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: 0.9375rem;
  line-height: 1.5;
  background: var(--bg);
  color: var(--text);
  display: grid;
  /* auto, not a fixed track: the login page renders no sidebar, and an
     empty auto track collapses to zero instead of leaving a gutter. */
  grid-template-columns: auto 1fr;
  /* The auth pages put their header on its own row, and rows stretch by
     default to fill min-height — which gave the login page a wordmark bar
     a third of the viewport tall. Pack the rows at the top instead; the
     sidebar sets its own 100vh and is unaffected. */
  align-content: start;
  min-height: 100vh;
}

h1,
h2,
h3 {
  margin: 0;
  line-height: 1.25;
  font-weight: 600;
}

h1 {
  font-size: 1.25rem;
}

h2 {
  font-size: 0.9375rem;
}

h3 {
  font-size: 0.875rem;
}

p {
  margin: 0 0 var(--gap);
}

a {
  color: var(--accent-text);
}

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

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
}

.skip-link:focus {
  left: 0;
  padding: 0.5rem 1rem;
  background: var(--accent);
  color: var(--on-accent);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- sidebar ---------- */

.sidebar {
  grid-column: 1;
  /* Sticky rather than position: fixed so it stays a grid track and the
     main column never needs a matching magic margin. */
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
  width: var(--sidebar-width);
  padding: var(--gap-lg) 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
}

.wordmark {
  display: block;
  margin: 0;
  padding: 0 var(--gap-lg);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-decoration: none;
  color: var(--text);
}

.wordmark b {
  color: var(--accent-text);
  font-weight: inherit;
}

.brand {
  display: flex;
  flex-direction: column;
  padding-bottom: var(--gap-lg);
}

.version-tag {
  padding: 0 var(--gap-lg);
  font-size: 0.6875rem;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--text-muted);
}

.nav-toggle {
  display: none;
}

.sidebar nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The visible name of a nav group. Not a heading: it labels its list
   through aria-labelledby instead, so the sidebar contributes structure
   without putting an h2 ahead of the page's own h1. */
.nav-group {
  margin: var(--gap-lg) 0 0.25rem;
  padding: 0 var(--gap-lg);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.sidebar nav a {
  display: block;
  padding: 0.5rem var(--gap-lg);
  border-left: 3px solid transparent;
  color: var(--text-muted);
  text-decoration: none;
}

.sidebar nav a:hover {
  color: var(--text);
  background: var(--surface-raised);
}

.sidebar nav a[aria-current="page"] {
  border-left-color: var(--accent);
  background: var(--surface-raised);
  color: var(--accent-text);
  font-weight: 600;
}

/* End of the sidebar, unobtrusive: it changes a display preference, not
   something an operator reaches for often. */
.sidebar .locale-switch {
  margin-top: var(--gap-lg);
  padding: 0 var(--gap-lg);
}

/* ---------- main column ---------- */

main {
  grid-column: 2;
  min-width: 0;
}

.topbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
  align-items: center;
  justify-content: space-between;
  padding: var(--gap) var(--gap-lg);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

/* Three page titles carry a device id or an event id, which is a long
   unbreakable token. Without this it pushes the top bar — and with it the
   whole document — wider than the viewport. */
.topbar h1 {
  min-width: 0;
  overflow-wrap: anywhere;
}

.page {
  padding: var(--gap-lg);
  max-width: 78rem;
}

.auth-header {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: var(--gap) var(--gap-lg);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

.auth-header .wordmark {
  padding: 0;
}

.auth-header .version-tag {
  padding: 0;
}

.auth-header-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap);
}

/* Quiet, like the forgot-password link this sits beside: login is what
   most visitors here are here to do. */
.auth-enroll-link {
  color: var(--text-muted);
  font-size: 0.8125rem;
  text-decoration: none;
}

.auth-enroll-link:hover,
.auth-enroll-link:focus-visible {
  color: var(--accent-text);
}

/* The manual language override. Two short links (or, for the active
   locale, a plain span so it never links to itself) separated by a
   muted slash -- a compact toggle rather than a select, since there are
   only ever two options. */
.locale-switch {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.8125rem;
}

.locale-switch a {
  color: var(--text-muted);
  text-decoration: none;
}

.locale-switch a:hover,
.locale-switch a:focus-visible {
  color: var(--accent-text);
}

.locale-switch [aria-current="true"] {
  color: var(--accent-text);
  font-weight: 600;
}

.locale-switch__sep {
  color: var(--text-muted);
}

.auth-card {
  max-width: 22rem;
  margin: 3rem auto;
  padding: var(--gap-lg);
}

/* The title keeps its intro sentence close and the form further away:
   the two margins collapse, so a card with no intro still gets the
   larger gap under its heading. */
.auth-card h1 {
  margin-bottom: var(--gap);
}

.auth-card form {
  margin-top: var(--gap-lg);
}

/* Stacked fields, one per line: nothing else on these pages puts two on a
   row, and without this the password label sits directly against the box
   above it. */
.auth-card .field + .field {
  margin-top: var(--gap);
}

/* One action, and it is the whole point of the page — so it takes the
   width of the fields above it rather than sitting as a small tab under
   their left edge. */
.auth-card .actions button {
  flex: 1 1 100%;
}

/* ---------- cards ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
  margin-bottom: var(--gap-lg);
}

.card > h2 {
  margin-bottom: var(--gap);
}

.card-foot {
  display: flex;
  justify-content: flex-end;
  margin: var(--gap) 0 0;
}

/* Several components below set a display value, which would otherwise
   beat the user-agent rule behind the hidden attribute and reveal
   confirmation prompts and error banners that are meant to stay closed.
   This is the one place !important is load-bearing rather than lazy. */
[hidden] {
  display: none !important;
}

/* ---------- callouts ---------- */

/* Styled by role, not by class: every error banner and destructive
   confirmation on the dashboard already carries role="alert", and
   several of them are asserted verbatim by tests. */
[role="alert"] {
  padding: 0.625rem var(--gap);
  border-left: 3px solid var(--sev-critical);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--sev-critical-text);
}

.callout {
  padding: var(--gap);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

.callout code {
  display: block;
  margin-top: 0.5rem;
  overflow-wrap: anywhere;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

.token-value-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.token-value-row code {
  display: block;
  margin-top: 0;
  flex: 1 1 auto;
  min-width: 12rem;
}

/* The scan-to-fill enrollment QR, on both the owner-created-token page
   (#new-token-qr) and the self-enroll confirmation page
   (#selfenrollconfirm-qr) -- one rule, since it is the same image in
   the same role on both. */
#new-token-qr,
#selfenrollconfirm-qr {
  display: block;
  margin-top: 0.5rem;
  width: 160px;
  height: 160px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.5rem;
  background: #fff;
}

.help {
  display: block;
  margin: 0.25rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.muted {
  color: var(--text-muted);
}

/* A card whose contents cannot be undone. Marked by more than colour:
   it carries its own heading saying so.

   It also follows the Save button at the end of the device configuration
   form, where the trailing .actions row leaves it sitting directly under
   the one control on the page an operator presses routinely. Double the
   section rhythm above it, so a thumb aiming at Save cannot land on
   Revoke. */
.card--danger {
  margin-top: calc(var(--gap-lg) * 2);
  border-color: var(--sev-critical);
}

.confirm {
  margin-top: var(--gap);
}

/* The per-row confirmation inside a table: prompt first, then its two
   buttons, wrapping onto their own line. */
.revoke-confirm,
.row-confirm {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin-top: 0.5rem;
}

.revoke-confirm [role="alert"],
.row-confirm [role="alert"] {
  flex-basis: 100%;
}

/* The device-wipe warning and its opt-in checkbox each get the row's
   own full width, the same as the alert text above them -- squeezed
   next to the delete button, either reads as an afterthought instead
   of the thing this confirmation step exists to surface. */
.wipe-account-data-warning,
.wipe-account-data-label {
  flex-basis: 100%;
}

.wipe-account-data-warning {
  color: var(--sev-high-text);
}

/* Changing a role happens in the row it belongs to, on the same
   reveal-then-act step as delete. One column at every width: the actions
   column is the narrowest on the page, and a role and the account it
   moves somebody into are read in that order, not side by side. */
.role-form {
  display: grid;
  gap: 0.5rem;
  max-width: 20rem;
  margin-top: 0.5rem;
  padding: var(--gap);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

.role-form__legend {
  margin: 0;
  font-weight: 600;
}

.role-form .actions {
  margin: 0;
}

/* Declarations are commitments, not bullet points. */
.checklist {
  margin: 0;
  padding: 0;
  list-style: none;
}

.checklist li {
  position: relative;
  padding: 0 0 0 1.5rem;
  margin-bottom: 0.5rem;
}

.checklist li::before {
  content: "";
  position: absolute;
  left: 0.25rem;
  top: 0.45em;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
}

.listener-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.listener-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem var(--gap);
  align-items: center;
  padding: 0.375rem 0;
  border-bottom: 1px solid var(--border);
}

.listener-list li:last-child {
  border-bottom: 0;
}

.listener-list__profile {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* ---------- stat cards ---------- */

.stats {
  display: grid;
  /* min() caps the track floor at the container width: below ~13rem of
     usable space a bare minmax() track keeps its minimum and overflows
     the page instead of shrinking. Same guard on every auto-fit grid
     here. */
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  gap: var(--gap);
  margin: 0 0 var(--gap-lg);
  padding: 0;
  list-style: none;
}

.stat {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
}

.stat__body {
  min-width: 0;
}

.stat__label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.stat__value {
  display: block;
  font-size: 2rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.stat__note {
  display: block;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.stat__icon {
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  color: var(--text-muted);
}

.stat--total .stat__icon {
  color: var(--accent-text);
}

.stat--high .stat__icon {
  color: var(--sev-high-text);
}

.stat--medium .stat__icon {
  color: var(--sev-medium-text);
}

.stat--critical .stat__icon {
  color: var(--sev-critical-text);
}

.stat--online .stat__icon {
  color: var(--online-text);
}

.stat__note--critical {
  color: var(--sev-critical-text);
  font-weight: 600;
}

/* ---------- charts ---------- */

.charts {
  display: grid;
  gap: var(--gap);
  margin-bottom: var(--gap-lg);
}

.charts > .card {
  margin-bottom: 0;
}

.chart {
  margin: 0;
}

/* Equal-height chart cards, desktop only — below this width they stack,
   where matching heights would only add dead space. Expressed as a
   min-width block (the rest of this file is desktop-first) because it is
   genuinely an enhancement for the two-column layout, not something to
   undo three rules at a time on small screens.

   The row height comes from whichever card is naturally taller, which
   changes with the data: a legend of seven classifications is far taller
   than one of two. Nothing here sets a height, so that stays true. The
   figure absorbs the slack and centres its graphic, so the shorter card
   gains whitespace rather than a stretched chart — both SVGs keep their
   aspect-ratio and are never distorted. */
@media (min-width: 48em) {
  .charts {
    grid-template-columns: 2fr 1fr;
  }

  .charts > .card {
    display: flex;
    flex-direction: column;
  }

  .charts .chart,
  .charts > .card > .empty {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin: 0;
  }
}

/* height is reserved by the aspect ratio, so the panel does not jump
   once the SVG is laid out. */
.chart__line-svg {
  display: block;
  width: 100%;
  aspect-ratio: 640 / 180;
  /* The y-axis labels are anchored at x=28 and run leftwards, so a
     four-digit peak or a numeric zone offset ("+05:30") is clipped by the
     viewBox edge. The card's own padding has room for them. */
  overflow: visible;
}

.chart__donut {
  display: block;
  width: 100%;
  max-width: 11rem;
  margin: 0 auto;
  aspect-ratio: 1;
}

.chart figcaption {
  margin-top: 0.5rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

.chart__axis,
.chart__grid {
  stroke: var(--border-strong);
  stroke-width: 1;
}

.chart__grid {
  stroke: var(--border);
  stroke-dasharray: 3 3;
}

.chart__area {
  fill: var(--accent);
  fill-opacity: 0.14;
}

.chart__line {
  fill: none;
  /* The line is the meaningful graphic, so it uses the text-safe brand
     token: #ffa929 in dark mode as specified, darkened in light mode,
     where the raw amber is 1.92:1 on white and fails 1.4.11. */
  stroke: var(--accent-text);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.chart__tick {
  fill: var(--text-muted);
  font-family: var(--font);
  font-size: 11px;
  text-anchor: middle;
}

.chart__tick--y {
  text-anchor: end;
}

/* The display zone names the units of the x axis; it is not a value on
   it, so it sits on its own row and stays the smaller of the two. */
.chart__tick--zone {
  text-anchor: start;
}

.chart__donut-total,
.chart__donut-label {
  text-anchor: middle;
  fill: var(--text);
  font-family: var(--font);
}

.chart__donut-total {
  font-size: 22px;
  font-weight: 600;
}

.chart__donut-label {
  fill: var(--text-muted);
  font-size: 10px;
}

/* The donut palette reuses the accent and severity tokens rather than
   introducing a second one, in their text-safe variants so the arcs keep
   3:1 against the surface in light mode too. Colour is never the only
   cue: the legend states each name, count and percentage. */
.slice--a {
  stroke: var(--accent-text);
}

.slice--b {
  stroke: var(--sev-critical-text);
}

.slice--c {
  stroke: var(--sev-high-text);
}

.slice--d {
  stroke: var(--sev-medium-text);
}

.slice--e {
  stroke: var(--online-text);
}

/* The sixth slice takes the body text colour rather than the second
   grey: --sev-low-text and --text-muted are nearly the same tone, and
   two indistinguishable greys next to each other is worse than a
   neutral that is obviously not the muted "other". */
.slice--f {
  stroke: var(--text);
}

.slice--other {
  stroke: var(--text-muted);
}

.legend {
  margin: var(--gap) 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.8125rem;
}

.legend li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.1875rem 0;
}

.legend__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.legend__meta {
  flex: none;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.swatch {
  flex: none;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 2px;
  align-self: center;
}

.swatch--a {
  background: var(--accent-text);
}

.swatch--b {
  background: var(--sev-critical-text);
}

.swatch--c {
  background: var(--sev-high-text);
}

.swatch--d {
  background: var(--sev-medium-text);
}

.swatch--e {
  background: var(--online-text);
}

.swatch--f {
  background: var(--text);
}

.swatch--other {
  background: var(--text-muted);
}

/* ---------- tables ---------- */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

th,
td {
  padding: 0.5rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

thead th {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

tbody tr:last-child > * {
  border-bottom: 0;
}

/* Each recent-events row carries exactly one link, so highlight the whole
   row when that link is hovered or focused: the row is the target, even
   though only the classification is clickable. */
tbody tr:hover,
tbody tr:focus-within {
  background: var(--surface-raised);
}

td time,
.mono {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

/* Grouping cue for a notice folded into the incident above it: DOM order
   alone does not survive the stacked mobile layout, where every <tr>
   becomes its own equal-weight bordered card (see .stacked tr below), so
   a child needs its own visible signal. The rail is indentation plus a
   border, not colour alone -- the glyph and its visually-hidden label in
   notices.html carry the same relationship for anyone who cannot see
   either. .stacked overrides this per-cell rule with a per-card one below. */
.notice-child-row > td:first-child {
  padding-left: 1.75rem;
  border-left: 3px solid var(--border-strong);
}

.notice-child-marker {
  color: var(--text-muted);
  margin-right: 0.125rem;
}

/* Table furniture: a caption-like footer for the paging summary, sitting
   inside the same card as the table it describes. */
.table-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem var(--gap);
  justify-content: space-between;
  align-items: center;
  margin: var(--gap) 0 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* Cells holding a control rather than a value should not inherit the
   spacing meant to separate a form section from the next one. */
td .actions {
  margin: 0;
}

/* A cell that holds several lines — a value and the detail under it —
   keeps them in one box. On mobile the cell itself becomes a flex row of
   label and value, so without this every line would sit side by side
   instead of stacking. */
.cell-stack {
  display: grid;
  /* Grid items stretch by default, which turns a pill — a shape sized by
     its own label — into a full-width bar. */
  justify-items: start;
  gap: 0.125rem;
  min-width: 0;
}

/* The outcome of a per-rule delivery test. Never colour alone: the text
   states whether the notification was delivered. */
.test-result {
  margin: 0.375rem 0 0;
  font-size: 0.8125rem;
}

.test-result--ok {
  color: var(--online-text);
}

.test-result--fail {
  color: var(--sev-critical-text);
}

/* Confirmation that an action completed, shown beside the button that
   performed it.

   Saving used to reload the page and say nothing: the screen blinked
   and an admin was left inferring success from the absence of an error,
   which is indistinguishable from a click that never registered. Same
   rule as .test-result above -- the words carry the meaning, the colour
   only reinforces it. */
.action-note {
  margin: 0;
  align-self: center;
  color: var(--online-text);
  font-size: 0.8125rem;
}

.table-foot p {
  margin: 0;
}

.pager {
  display: flex;
  align-items: center;
  gap: var(--gap);
}

.pager a {
  padding: 0.375rem 0.5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  text-decoration: none;
  white-space: nowrap;
}

.pager a:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* The first and last page render their spent direction as plain text
   rather than a link that goes nowhere. */
.pager__end {
  padding: 0.375rem 0.5rem;
  border: 1px solid transparent;
  color: var(--text-muted);
  white-space: nowrap;
}

.pager__status {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- severity pills ---------- */

.pill {
  display: inline-block;
  padding: 0.05rem 0.5rem;
  border: 1px solid currentColor;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--sev-low-text);
}

.pill--critical {
  color: var(--sev-critical-text);
}

.pill--high {
  color: var(--sev-high-text);
}

.pill--medium {
  color: var(--sev-medium-text);
}

.pill--low,
.pill--info {
  color: var(--sev-low-text);
}

/* Lifecycle states, not severities: same component so a status reads the
   same everywhere, with its own words rather than a borrowed severity.
   --online is the same green under the name the device table uses; a pill
   modifier that names no colour silently inherits the neutral grey, which
   is how an online device came to look identical to an offline one. */
.pill--ok,
.pill--online {
  color: var(--online-text);
}

.pill--warn {
  color: var(--sev-medium-text);
}

.pill--danger {
  color: var(--sev-critical-text);
}

.pill--muted {
  color: var(--text-muted);
}

/* ---------- status dot ---------- */

.status {
  font-weight: 600;
}

.status::before {
  content: "";
  display: inline-block;
  width: 0.6em;
  height: 0.6em;
  border-radius: 50%;
  margin-right: 0.4em;
  background: currentColor;
}

.status--online {
  color: var(--online-text);
}

.status--offline {
  color: var(--text-muted);
}

/* ---------- controls ---------- */

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

button {
  min-height: 2.25rem;
  padding: 0.375rem 0.875rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  cursor: pointer;
}

button:hover {
  border-color: var(--accent-hover);
  background: var(--accent-hover);
}

.btn-quiet {
  border-color: var(--border-strong);
  background: transparent;
  color: var(--text);
}

.btn-quiet:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-text);
}

/* Destructive actions are never amber: revoke and delete must not look
   like the button an operator is being encouraged to press. */
.btn-danger {
  border-color: var(--sev-critical);
  background: transparent;
  color: var(--sev-critical-text);
}

.btn-danger:hover {
  border-color: var(--sev-critical);
  background: var(--sev-critical);
  color: #fff;
}

input,
select,
textarea {
  width: 100%;
  min-height: 2.25rem;
  padding: 0.375rem 0.5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

/* The `color: inherit` above overrides the browser's own disabled-field
   dimming, so without this a disabled control is pixel-identical to an
   editable one until the user tries to change it. */
input:disabled,
select:disabled,
textarea:disabled {
  color: var(--text-muted);
  border-color: var(--border);
  background: var(--surface);
  cursor: not-allowed;
}

/* Same treatment as :disabled above, for a field whose value still
   submits with the form (a disabled field's does not) -- the
   self-enroll page's device id, filled in from a scanned per-device QR
   code rather than typed, is the first user of this. */
input:read-only {
  color: var(--text-muted);
  border-color: var(--border);
  background: var(--surface);
}

/* Both are sized by their own box, not by the full-width rule above,
   which exists for text inputs and selects. */
input[type="checkbox"],
input[type="radio"] {
  /* 24px square: WCAG 2.2 2.5.8 (AA) minimum target size, and the
     smallest of these that is comfortable to hit with a thumb. */
  width: 1.5rem;
  min-height: 1.5rem;
  height: 1.5rem;
  accent-color: var(--accent);
}

label {
  display: block;
  margin-bottom: 0.25rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* ---------- forms ---------- */

/* One field: its label above its control. Everything else is just how
   many of these sit on a row. */
.field {
  min-width: 0;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--gap);
  align-items: end;
  max-width: 60rem;
}

.form-grid--narrow {
  max-width: 34rem;
}

/* Two grids stacked directly in the same card (one field forced onto
   its own row below another, rather than sharing a row) would otherwise
   touch with no gap -- .form-grid itself carries no margin, since most
   callers are the only grid in their card. */
.form-grid + .form-grid {
  margin-top: var(--gap);
}

/* Fields that each carry help text under the control. Bottom alignment
   would line up the help lines and leave the inputs at different heights,
   which is exactly backwards. */
.form-grid--help {
  align-items: start;
}

/* A checkbox reads as "control, then what it does", which is the reverse
   of every other field here. */
.field--check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.field--check label {
  margin: 0;
  color: var(--text);
}

.field--check input {
  flex: none;
  order: -1;
}

.field--wide {
  grid-column: 1 / -1;
}

/* A group of fields that belong to one another (quiet hours: a switch and
   the window it applies to). The browser default border and padding are
   removed — the card already draws the box — but the fieldset and its
   legend stay, because they are what tells assistive technology the
   grouping exists. */
.fieldset {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.fieldset legend {
  padding: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-muted);
}

.fieldset .field--check {
  margin: 0.375rem 0 0;
}

.fieldset > .form-grid {
  margin-top: var(--gap);
}

/* The classification checkboxes: one column per group, side by side when
   there is room and stacked when there is not. */
.kind-choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: var(--gap);
  margin-top: var(--gap);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin-top: var(--gap);
}

/* A link that sits in a row of buttons. It stays an <a> because it
   navigates; it only needs to line up with them. */
.btn-link {
  padding: 0.375rem 0.25rem;
  font-size: 0.875rem;
}

/* The primary way off a page. Still a link — it navigates, so
   middle-click, right-click and copy-link all have to keep working — but
   weighted like the primary button so it is obviously the way forward. */
.cta {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 2.25rem;
  padding: 0.375rem 0.875rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  text-decoration: none;
}

.cta:hover {
  border-color: var(--accent-hover);
  background: var(--accent-hover);
}

/* A text input with a button attached to it, sharing one line. */
.input-group {
  display: flex;
  gap: 0.5rem;
}

/* An input's intrinsic minimum is about twenty characters wide, which
   with the button beside it overflows a narrow column. */
.input-group input {
  min-width: 0;
}

.input-group button {
  flex: none;
}

.filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  gap: var(--gap);
  align-items: end;
}

.filters .actions {
  margin: 0;
}

/* One long field and its button, rather than the even columns a
   multi-field filter bar wants. Flex, not grid: a two-track grid cannot
   wrap, so on a phone the button sat outside the viewport instead of
   dropping under the box it belongs to. */
.filters--search {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
  align-items: end;
}

.filters--search .field {
  flex: 1 1 20rem;
}

.filters--search .help {
  flex: 1 1 100%;
  margin: 0;
}

/* ---------- definition lists ---------- */

/* The detail pages are specification sheets: a column of names and a
   column of values, aligned, rather than a stack of sentences. */
.dl {
  display: grid;
  grid-template-columns: minmax(8rem, 14rem) 1fr;
  gap: 0.375rem var(--gap);
  margin: 0;
  font-size: 0.875rem;
}

/* Two or more lists in one card are groups of one specification sheet, so
   a rule between them is all the separation they need — no wrapper
   element and no modifier class. The device page uses it to keep faults,
   settings and history apart inside "Set on the device"; lists in
   separate cards are not siblings and are unaffected. */
.dl + .dl {
  margin-top: var(--gap);
  padding-top: var(--gap);
  border-top: 1px solid var(--border);
}

.dl dt {
  color: var(--text-muted);
}

.dl dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.dl code {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

.empty {
  margin: 0;
  padding: var(--gap);
  color: var(--text-muted);
  text-align: center;
}

/* An empty state that has something to explain rather than one line to
   state. Left-aligned and measure-limited, because two sentences centred
   are harder to read than they are decorative. */
.empty--explained {
  max-width: 46rem;
  margin: 0 auto;
  text-align: left;
}

.no-margin {
  margin: 0;
}

.break-all {
  overflow-wrap: anywhere;
}

/* A clipped value that can be opened, for cases where the full text used
   to live in a title attribute -- hover-only, so unreachable on a phone.
   The marker is the affordance that something is hidden, and the summary
   keeps the clipped text visible when closed. */
.reveal > summary {
  cursor: pointer;
}

.reveal > summary::marker {
  color: var(--text-muted);
}

/* The account switcher in the top bar, for a tenant acting for one of
   their customers. Sits between the page title and sign-out; on a phone
   the top bar already wraps, so it needs no separate rule beyond not
   forcing the bar wider than the viewport. */
.account-switch {
  display: flex;
  align-items: center;
  min-width: 0;
}

.account-switch select {
  max-width: 14rem;
}

/* The quiet escape hatch under the sign-in button. Deliberately not a
   button and deliberately smaller than the form's own controls: it is
   what you need when the main path has already failed. */
.auth-aside {
  margin: var(--gap) 0 0;
  font-size: 0.8125rem;
}

/* Muted rather than amber, so it does not compete with the submit button
   for the eye. It keeps its full underline, which is what marks it as a
   link once the accent colour is gone, and takes the accent back on hover
   and focus. */
.auth-aside a {
  color: var(--text-muted);
}

.auth-aside a:hover,
.auth-aside a:focus-visible {
  color: var(--accent-text);
}

/* ---------- responsive ---------- */

@media (max-width: 47.99em) {
  body {
    grid-template-columns: 1fr;
  }

  main {
    grid-column: 1;
  }

  .sidebar {
    position: static;
    height: auto;
    overflow: visible;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    width: auto;
    padding: 0.5rem var(--gap);
    border-right: 0;
    border-bottom: 1px solid var(--border);
  }

  .wordmark {
    padding: 0;
  }

  .version-tag {
    padding: 0;
  }

  /* The desktop gap-lg spacing below .brand (now its own padding, not
     .wordmark's) doesn't apply once the sidebar collapses into a
     single row — .brand sits inline next to the toggle/nav instead of
     stacked above them, so it needs no bottom padding here either. */
  .brand {
    padding-bottom: 0;
  }

  /* The disclosure button only exists once the script has added
     aria-expanded, so a script-less browser keeps the full nav open
     rather than hiding it behind a control it cannot operate. */
  .nav-toggle {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    min-width: 2.75rem;
    justify-content: center;
  }

  .nav-toggle:not([aria-expanded]) {
    display: none;
  }

  .sidebar nav {
    flex-basis: 100%;
  }

  .nav-toggle[aria-expanded] + nav {
    display: none;
  }

  .nav-toggle[aria-expanded="true"] + nav {
    display: block;
  }

  /* Grouping costs three extra lines of height, which behind a
     disclosure button turns a short menu into a scroll. Two columns per
     group buys that back and keeps every target a comfortable size. */
  .sidebar nav ul {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }

  .sidebar nav a {
    padding: 0.75rem var(--gap);
  }

  .nav-group {
    margin: 0.75rem 0 0.125rem;
    padding: 0 var(--gap);
  }

  /* The sidebar's own padding shrinks from --gap-lg to --gap at this
     width; match it here rather than inherit the desktop value, and let
     it sit on its own row alongside the wordmark and nav toggle. */
  .sidebar .locale-switch {
    margin-top: 0;
    padding: 0 var(--gap);
  }

  .page {
    padding: var(--gap);
  }

  /* 3rem of empty space above the card costs a phone most of what is left
     after the browser chrome. */
  .auth-card {
    margin: var(--gap-lg) auto;
  }

  .topbar {
    padding: var(--gap);
  }

  /* ---- touch sizing ---- */

  /* 44px, the comfortable thumb target. 2.25rem clears WCAG 2.2 2.5.8
     but is a fiddly thing to hit while holding a phone one-handed. */
  button,
  .cta {
    min-height: 2.75rem;
  }

  /* A control under 16px makes iOS Safari zoom the page on focus, and a
     zoomed page scrolls sideways for the rest of the visit. */
  input,
  select,
  textarea {
    min-height: 2.75rem;
    font-size: 1rem;
  }

  /* The forgot-password and back-to-login links. No padding-inline, unlike
     the list links below: the aside sits at the card's text edge and
     indenting it would break that alignment. */
  .auth-aside a,
  .auth-enroll-link,
  .locale-switch a,
  .locale-switch span {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }

  /* Links that act as the way off a page or through a list are the only
     targets left below 44px once the buttons are sized. */
  .pager a,
  .pager__end,
  .btn-link,
  .card-foot a {
    display: inline-flex;
    align-items: center;
    /* inline-flex drops the whitespace that separated the arrow glyph
       from its word, so the gap has to be restated. */
    gap: 0.25rem;
    min-height: 2.75rem;
    padding-inline: 0.75rem;
  }

  /* Full width, so the two directions sit at the edges of the card rather
     than crowding the row summary. */
  .table-foot .pager {
    flex: 1 1 100%;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0.5rem;
  }

  /* The viewBox is fixed at 640 units wide, so an 11px tick renders at
     about 5px once the SVG is scaled to a phone. The chart is useless
     without readable axis labels. */
  .chart__tick {
    font-size: 20px;
  }

  .chart__tick--zone {
    font-size: 15px;
  }

  /* The two-column definition list cannot survive here: the name track
     takes its 14rem maximum before the value track is sized, which leaves
     a SHA-256 or an address wrapping one character per line. Name above
     value, labelled like a stacked table cell. */
  .dl {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .dl dt {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }

  .dl dd {
    margin-bottom: 0.625rem;
  }

  .dl dd:last-child {
    margin-bottom: 0;
  }

  /* Stacked-card tables. The <table> markup is untouched so assistive
     technology still gets a real table; only the visual box tree
     changes, and each cell grows a header label from data-label. */
  .stacked thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .stacked,
  .stacked tbody,
  .stacked tr,
  .stacked td {
    display: block;
  }

  .stacked tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 0.5rem;
    padding: 0.25rem 0;
  }

  /* Each stacked <tr> is otherwise its own equal-weight card, so a child
     notice would look like an unrelated finding next to its incident.
     Pulled in and up to sit under the parent card, with the same
     left-rail rule the desktop table uses on the cell -- moved to the
     whole card here since that is the unit a reader scans on mobile. */
  .stacked .notice-child-row {
    margin-left: 1rem;
    margin-top: -0.25rem;
    border-left: 3px solid var(--border-strong);
  }

  .stacked .notice-child-row > td:first-child {
    padding-left: 0.75rem;
    border-left: 0;
  }

  .stacked td {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--gap);
    justify-content: space-between;
    align-items: baseline;
    border-bottom: 0;
    padding: 0.25rem 0.75rem;
    /* A device id, an event id or a token is one unbreakable word. As a
       flex item beside its label it cannot shrink below that word, so it
       pushes the row — and the whole document — wider than the viewport.
       anywhere is what makes the item's minimum small enough to fit; wrap
       then lets a value too long for the remainder take its own line.
       Pills set white-space: nowrap and are unaffected. */
    overflow-wrap: anywhere;
  }

  /* An editable cell (the listener table) still has to fit beside its
     label once the row becomes a card. Checkboxes are excluded: they are
     sized by their own box, and this rule would stretch one across the
     cell. */
  .stacked td > input:not([type="checkbox"]):not([type="radio"]),
  .stacked td > select {
    flex: 1 1 auto;
    min-width: 0;
    width: auto;
  }

  /* Multi-line cell contents and per-row confirmations take the line
     under the label rather than competing with it for the same one. */
  .stacked td > .cell-stack,
  .stacked td > .revoke-confirm,
  .stacked td > .row-confirm {
    flex: 1 1 100%;
  }

  .stacked td::before {
    content: attr(data-label);
    flex: none;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
