/* Bootstrap 5.3 owns the palette. Nothing here redefines a colour token: its
   light-mode text colours clear WCAG AA far more comfortably than the values
   they replaced (--bs-primary-text-emphasis is 13.51:1 against white), and a
   half-overridden palette is how a design system stops being one.
   Use --bs-*-text-emphasis for text, the base --bs-* colour for fills, and
   --bs-*-bg-subtle with --bs-*-border-subtle for tinted panels. The base
   colours are backgrounds: #ffc107 as text is 1.63:1 and illegible.

   Two overrides, both about identity rather than colour. */
:root {
  /* Bootstrap's 0.375rem rounding is most of what makes a page read as stock
     Bootstrap, and this is a dense operator table, not a marketing site. */
  --bs-border-radius: 4px;
  --bs-border-radius-sm: 3px;
  --bs-border-radius-lg: 6px;
  /* Bootstrap's default stack names Roboto and Segoe UI before the generic
     fallback. This project ships no fonts and takes whatever the OS offers. */
  --bs-body-font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  --bs-font-monospace: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

:root{
  color-scheme:light;

  /* fix-f retired the local colour palette that used to live here: 13
     primitives (--gray-000.., --ink-900.., --blue, --green, --amber, --red)
     and 15 semantic aliases over them (--ink, --rule, --signal, --ok and the
     rest), 28 custom properties in all. Every rule below now reaches for a
     --bs-* custom property directly, so this file defines no colour of its
     own; Bootstrap's vendored stylesheet is the one place a colour value is
     written. What stays local is layout, not palette: spacing, radius and
     font keep their own scale because Bootstrap's differs and swapping it
     was out of scope for this change. */
  --font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,
    Arial,sans-serif;
  --font-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;

  --s1:.25rem; --s2:.5rem; --s3:.75rem; --s4:1rem; --s5:1.5rem;
  --s6:2rem; --s7:3rem;
  --radius:6px; --radius-lg:8px;
  --measure:64ch;

  /* Type scale. Before this the scale had a hole in it: h1 was whatever
     Bootstrap's responsive default resolved to (up to 40px, a size chosen
     for marketing pages), h2 was 16.8px, and h3 was 13.1px - smaller than
     the 14px body text it sat above, so a section heading was quieter than
     its own paragraph. Nothing lived between 40px and 16.8px.
     Four steps now, each a real decision: 30px is large enough to head a
     page without spending the emphasis budget the status chips need, and
     every step from there down is a clear ratio off the one above.
     --t-sm and --t-xs are below body deliberately and are for table
     furniture, captions and chips - never for reading copy. */
  --t-h1:1.875rem;    /* 30px */
  --t-h2:1.1875rem;   /* 19px */
  --t-h3:1rem;        /* 16px */
  --t-body:.9375rem;  /* 15px */
  --t-sm:.8125rem;    /* 13px */
  --t-xs:.75rem;      /* 12px */
}

*,*::before,*::after{box-sizing:border-box}
body{margin:0;background:var(--bs-body-bg);color:var(--bs-body-color);
  font-family:var(--font-ui);
  font-size:var(--t-body);line-height:1.55;font-variant-numeric:tabular-nums;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
/* .wrap, .masthead, .brand h1/svg and .brand-sub used to live here. All
   four described markup that no longer exists: base.html has used
   Bootstrap's .container and a plain <a class="brand"> since the template
   migration, so every one of them was styling nothing. */
/* The wordmark had no rule of its own beyond this layout, so it inherited
   the global link colour and the browser's default underline: the product's
   own name rendered as blue underlined text, which reads as a link somebody
   forgot to style rather than as the name of the thing you are looking at.
   It is still a link to the fleet page; it just stops pretending to be a
   sentence with a hyperlink in it. Colour returns on hover, where it means
   "this is clickable" instead of "this is a URL". */
.brand{display:flex;align-items:center;gap:var(--s3);min-width:0;
  font-weight:600;font-size:var(--t-h3);letter-spacing:-.01em;
  color:var(--bs-emphasis-color);text-decoration:none}
.brand:hover{color:var(--bs-link-hover-color)}
nav{display:flex;align-items:center;gap:var(--s4);font-size:var(--t-sm);
  flex-wrap:wrap}
nav form{margin:0}
/* A two-word label breaks inside itself on a narrow screen otherwise, so
   the nav wraps between items rather than through one. */
nav a{white-space:nowrap;
  color:var(--bs-secondary-color);text-decoration:none;padding-bottom:2px;
  border-bottom:1px solid transparent;transition:color .15s,border-color .15s}
nav a:hover{color:var(--bs-link-hover-color);border-bottom-color:var(--bs-link-hover-color)}
nav a.active{font-weight:600;color:var(--bs-primary-text-emphasis);border-bottom-color:var(--bs-primary-text-emphasis)}

/* No bare h1 rule existed: every page's largest text was Bootstrap's
   responsive default, which is a size nobody here chose. */
h1{font-size:var(--t-h1);font-weight:600;line-height:1.2;
  color:var(--bs-emphasis-color);margin:0 0 var(--s5)}
/* The section rule is the card's own top border now (see .card below), so
   h2 carries no border of its own; it used to draw one because sections
   sat directly on the page with nothing else to separate them. */
h2{font-weight:600;font-size:var(--t-h2);color:var(--bs-emphasis-color);
  margin:0 0 var(--s3);line-height:1.3}
h3{font-size:var(--t-h3);font-weight:600;color:var(--bs-body-color);
  margin:0 0 var(--s2);line-height:1.35}
p{margin:var(--s2) 0}
a{color:var(--bs-link-hover-color)}
code{font-family:var(--font-mono);font-size:.9em;color:var(--bs-body-color);
  background:var(--bs-tertiary-bg);
  border:1px solid color-mix(in oklab,var(--bs-border-color) 60%,var(--bs-body-bg));
  border-radius:3px;padding:.05rem .3rem}

/* tables: hairlines only. No boxes, no zebra, no vertical rules. */
table{width:100%;border-collapse:collapse;margin:0 0 var(--s5)}
thead th{font-size:.74rem;font-weight:600;color:var(--bs-secondary-color);
  text-align:left;padding:0 var(--s4) var(--s2) 0;white-space:nowrap;
  border-bottom:1px solid var(--bs-border-color)}
tbody td,tbody th{padding:var(--s3) var(--s4) var(--s3) 0;
  border-bottom:1px solid color-mix(in oklab,var(--bs-border-color) 60%,var(--bs-body-bg))}
tbody tr:last-child td,tbody tr:last-child th{border-bottom:0}
tbody tr{transition:background .12s}
tbody tr:hover{background:var(--bs-tertiary-bg)}
/* Identical left/right padding on th and td keeps the header rule sitting
   exactly over the column it names. */
th:last-child,td:last-child{padding-right:0}
/* fix-d-brief.md D2 made the first cell of every body row a <th scope="row">
   for screen reader table navigation. The browser default for th is bold,
   centred text - undoing both here keeps it a plain-looking cell, matching
   every td around it, rather than announcing itself as a structural change
   nobody asked to see. */
tbody th{font-weight:inherit;text-align:inherit}
/* Fleet rows centre on each other: the lamp, the name and the meter figure
   should sit on one optical line even when the actions cell wraps to two.
   Run rows hang from the top instead, because a failure reason grows
   downwards out of the status cell. */
.fleet tbody td,.fleet tbody th{vertical-align:middle}
.runs tbody td,.runs tbody th{vertical-align:top}
/* Bootstrap's .table sets margin-bottom:1rem; the old .scroller wrapper
   zeroed it (.scroller table{margin:0}) so each .legend caption sits close
   to the table it describes, per the spacing .legend's own rule is tuned
   against. .table-responsive has no such reset, so restore it here now
   that .table-responsive is the wrapper. */
.table-responsive .fleet,.table-responsive .runs{margin-bottom:0}
/* Magnitude is the point of a duration, so the digits line up right. */
.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
.name{font-family:var(--font-mono);font-size:.82rem;font-weight:500;
  color:var(--bs-emphasis-color);white-space:nowrap}
.mono{font-family:var(--font-mono);font-size:.76rem;color:var(--bs-secondary-color);
  white-space:nowrap}
.dim{color:var(--bs-secondary-color)}
.empty{color:var(--bs-secondary-color);font-style:italic}
time{white-space:nowrap}

/* status: a chip that always spells its state, so it survives colourblindness
   and a greyscale print - colour is never the only cue.
   Steady, per the house rule: the live indicator used to blink, on the
   reasoning that ALIVE means the unit was heard from within twice its
   check-in interval (a fact about the past, not a live connection), and a
   pulsing dot reads as streaming and overstates how fresh that is - at the
   default hourly cadence the news can be nearly two hours old.
   Coloured from Bootstrap's own tokens (-text-emphasis for the word,
   -bg-subtle/-border-subtle for the tint) rather than the --ok/--warn/--crit
   trio the dot indicator used, so this widget rides the same palette as the
   rest of the Bootstrap-era page. */
.chip{display:inline-flex;align-items:center;font-size:var(--t-xs);
  font-weight:600;white-space:nowrap;line-height:1.4;
  padding:.15rem .55rem;border-radius:var(--radius);border:1px solid transparent}
/* Three tones, not one class per state: a state's tone is chosen once, in
   _macros.html's _CHIP map, so the fleet table and the settings table
   cannot drift into disagreeing about what green means. */
.chip-ok{color:var(--bs-success-text-emphasis);
  background:var(--bs-success-bg-subtle);border-color:var(--bs-success-border-subtle)}
.chip-bad{color:var(--bs-danger-text-emphasis);
  background:var(--bs-danger-bg-subtle);border-color:var(--bs-danger-border-subtle)}
.chip-warn{color:var(--bs-warning-text-emphasis);
  background:var(--bs-warning-bg-subtle);border-color:var(--bs-warning-border-subtle)}

/* SIM budget meter. These SIMs get 500MB for life, so the scarcest thing in
   the system gets the only chart on the page: a native <meter>, not a
   hand-rolled bar - it carries an implicit ARIA role and needs no JS. The
   severity class lives on the <td> (not this file's markup) so it passes
   --level down to .meter-num by inheritance; ok/warn/crit are the same three
   classes the old percent-based cell used, now sourced from Bootstrap tokens
   instead of --ok/--warn/--crit so severity rides the same palette as the
   rest of the page. */
.ok{color:var(--bs-success-text-emphasis);--level:var(--bs-success)}
.warn{color:var(--bs-warning-text-emphasis);--level:var(--bs-warning)}
.crit{color:var(--bs-danger-text-emphasis);--level:var(--bs-danger)}
.meter{display:grid;gap:6px;min-width:8.5rem}
.meter-num{font-family:var(--font-mono);font-size:.78rem;font-weight:500;
  color:var(--level,var(--bs-body-color));line-height:1}
.meter meter{width:100%;height:6px}

/* Next check-in is also the latency of every control in this row: Tunnel,
   Send and the pause flag are all delivered on the device's next beat. So
   the countdown runs on the cadence the HERMIT is really keeping, and the
   line under it names which cadence that is and how we know.
   No colour: the row already spends its colour on ALIVE/STALE health, and
   how dear a device is to reach is not health. */
.cadence{display:grid;gap:1px;min-width:8.5rem}
.cadence-k{font-family:var(--font-ui);font-size:.68rem;
  color:var(--bs-secondary-color);white-space:nowrap}
/* An inferred cadence wears the same italic the console already uses for
   "no data" (.empty), so the guess survives colour being switched off. */
.cadence-k[data-src="inferred"]{font-style:italic}

/* run status */
.sent,.pending,.failed{font-size:.74rem;font-weight:600;white-space:nowrap}
.sent{color:var(--bs-success-text-emphasis)}
.pending{color:var(--bs-secondary-color)}
.failed{color:var(--bs-danger-text-emphasis)}
/* Used both as a footnote under a table and as a lead-in above one, so it
   needs breathing room on both sides - with margin-bottom:0 the lead-in
   collided with the table header directly beneath it. */
.legend{font-size:var(--t-sm);color:var(--bs-secondary-color);margin:.5rem 0 var(--s4);
max-width:78ch;line-height:1.5}
/* Two phase badges per run, each a fixed two-column grid: the key and status
   columns are the same width on every badge, so the status words line up
   with each other inside a row and the removal badge starts at the same x on
   every row. Spacing between badges comes from the cellstack gap alone -
   stacking a min-width and a margin on top of it is what left the ragged
   hole between insert and remove. */
.phase{display:inline-grid;grid-template-columns:3.4rem 3.4rem;
  column-gap:.5ch;align-items:baseline}
/* Right-aligned key: the pair reads tight ("insert pending") while both
   columns stay fixed, so the slack hides at the badge's left edge instead
   of opening a hole inside the pair. */
.phase-k{font-size:.68rem;color:var(--bs-secondary-color);text-align:right}
/* Idle removal (run still open): same metrics as a real status word so it
   aligns with the badges, but faint to read as "not yet". */
.phase-idle{font-size:.74rem;font-weight:600;color:var(--bs-secondary-color);
  white-space:nowrap}
.cellstack{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap}
.why{font-size:.74rem;line-height:1.45;color:var(--bs-danger-text-emphasis);max-width:26rem;
  margin-top:var(--s2);word-break:break-word}

/* controls */
button{font:inherit;font-size:.78rem;line-height:1.4;cursor:pointer;
  color:var(--bs-body-color);background:var(--bs-white);border:1px solid var(--bs-border-color);
  border-radius:var(--radius);padding:.3rem .65rem;white-space:nowrap;
  transition:color .15s,border-color .15s,background .15s}
button:hover{border-color:var(--bs-gray-500);background:var(--bs-tertiary-bg)}
/* Fill role: button.primary is a coloured button face, not text, so it takes
   Bootstrap's base --bs-primary (its own .btn-primary uses the same colour
   at the same 4.50:1-with-white pairing) rather than a -text-emphasis tone,
   which is for text sitting on the page background, not white sitting on a
   coloured fill. :hover darkens to --bs-link-hover-color, comfortably clear
   of AA (6.44:1) and a real step down from the default fill. */
button.primary{color:var(--bs-white);background:var(--bs-primary);
  border-color:var(--bs-primary);font-weight:600}
button.primary:hover{color:var(--bs-white);background:var(--bs-link-hover-color);
  border-color:var(--bs-link-hover-color)}
/* Destructive actions look destructive at rest, not only under the pointer.
   stop SIM usage takes a field unit off the network until someone presses
   it again, and forget drops a pinned host key so the next connection
   trusts whatever answers; both used to be indistinguishable from Console
   and Tunnel until the pointer was already on them, which is too late to
   learn it. The label takes the danger tone and the border stays neutral:
   seven rows of red-outlined buttons would turn a monitoring table into a
   warning, and the words already say what they do.
   color is text (the label), border-color is a fill-role accent (like the
   meter's --level above): text gets -text-emphasis, the accent gets the
   base swatch. */
button.danger{color:var(--bs-danger-text-emphasis)}
button.danger:hover{color:var(--bs-danger-text-emphasis);
  border-color:var(--bs-danger);background:var(--bs-danger-bg-subtle)}
button.send:hover{color:var(--bs-success-text-emphasis);border-color:var(--bs-success)}
button.link,button.clear{border:0;background:none;padding:0;
  text-decoration:underline;text-underline-offset:2px;color:var(--bs-secondary-color)}
button.link:hover,button.clear:hover{background:none;border:0;
  color:var(--bs-danger-text-emphasis)}
nav button.link{font-size:.78rem;text-decoration:none;
  border-bottom:1px solid transparent;padding-bottom:2px;color:var(--bs-secondary-color)}
nav button.link:hover{color:var(--bs-danger-text-emphasis);border-bottom-color:var(--bs-danger-text-emphasis)}
.port{font-family:var(--font-mono);color:var(--bs-secondary-color)}
input{font:inherit;font-size:.8rem;color:var(--bs-body-color);background:var(--bs-body-bg);
  border:1px solid var(--bs-border-color);border-radius:var(--radius);padding:.3rem .5rem;
  transition:border-color .15s,box-shadow .15s}
input::placeholder{color:var(--bs-secondary-color)}
input:focus{outline:none;border-color:var(--bs-link-hover-color);
  box-shadow:0 0 0 3px color-mix(in oklab,var(--bs-link-hover-color) 16%,transparent)}
:focus-visible{outline:2px solid var(--bs-link-hover-color);outline-offset:2px}
/* One fixed-width column of controls, so every button and the message field
   share the same left and right edge instead of wrapping into a ragged stack.
   A lone button fills its row; the message field grows and Send stays its own
   width, and the two together still end on that shared right edge. */
.actions{display:grid;gap:var(--s2);width:13rem}
.actions form{display:flex;gap:var(--s2);margin:0}
.actions>form>button{flex:1}
.actions .msgform input{flex:1;min-width:0}
.actions .msgform button{flex:none}
/* Console is a link (it navigates to a page), not a form post, but it sits
   in the same button column as Tunnel/stop/Send, so it wears the same
   chrome as <button> rather than reading as a bare underlined link. */
.actions .btn{display:block;text-align:center;text-decoration:none;
  font:inherit;font-size:.73rem;line-height:1.4;color:var(--bs-secondary-color);
  background:transparent;border:1px solid var(--bs-border-color);
  border-radius:var(--radius);padding:.3rem .6rem;white-space:nowrap;
  transition:color .15s,border-color .15s,background .15s}
/* --bs-gray-500 is the same darker-than-default-border grey button:hover
   above already uses; fix-b-brief.md B3 is the reason this isn't the local
   --slate-600 the pre-Bootstrap version of this rule carried, which was
   never defined anywhere and so silently did nothing. */
.actions .btn:hover{color:var(--bs-emphasis-color);border-color:var(--bs-gray-500);
  background:var(--bs-secondary-bg)}

/* a message currently burning pixels on a HERMIT's screen.
   Was --warn text on a 12% tint of --warn itself: --warn alone clears AA
   against --bg (4.87:1) but the tint it sits on does not (4.17:1, worse yet
   over a hovered row's --surface) - a text colour measured against the
   wrong background is exactly the bug fix-b-brief.md is about. Bootstrap's
   warning trio is designed as one unit and is already what the status chips
   use, so this also removes an inconsistency rather than adding one; its
   background is opaque so there is no tint-over-anything to get wrong. */
.msg{display:inline-flex;align-items:center;gap:.6ch;font-family:var(--font-mono);
  font-size:.7rem;color:var(--bs-warning-text-emphasis);border-radius:var(--radius);
  padding:.12rem .45rem;
  background:var(--bs-warning-bg-subtle);
  border:1px solid var(--bs-warning-border-subtle)}
.clear{margin-left:var(--s2);font-size:.68rem}

/* THE CONTAINER CONVENTION
   Every section of every page is
     <section class="card mb-4"><div class="card-body"><h2>..</h2> .. </div>
   and nothing else draws a box. Before this there were four: settings had
   .panel (tertiary background, 8px radius), the forwarding switch had .fwd
   (same idea, its own rule), the map wrapped Leaflet in a bare border, the
   keys page had no container at all and login had neither container nor
   card. Four inventions of one idea is the single strongest reason those
   pages read as four separate products.
   Bootstrap's .card is the convention because it is already vendored, it is
   what the rest of this file's Bootstrap-era rules assume, and a local
   fifth box class would have been the exact mistake being undone.
   A section whose content is one full-bleed element (the map, the terminal,
   the preview readout) takes no .card-body: the card's own border is the
   frame that element used to draw for itself. */
/* Outcome banners (_macros.html's notice macro). The local .err and
   .note-ok pair that used to be here re-derived Bootstrap's own alert
   colours by hand; the macro uses .alert-success/.alert-danger directly, so
   all that is left to say is how wide the thing should be.
   Which was the visible half of the problem: a one-line "Saved." stretched
   across a 1320px container reads as an unfinished stripe rather than a
   sentence. 62rem is the same reading measure the panels this file caps
   already use, and it is far wider than any of these messages needs. */
.notice{max-width:62rem;font-size:var(--t-sm)}
/* A focused banner should show that it has focus. It takes focus after a
   redirect (see the notice macro) and is not otherwise interactive, so
   without this the page looks unchanged while the keyboard is somewhere
   the operator cannot see. */
.notice:focus-visible{outline:2px solid var(--bs-link-hover-color);
  outline-offset:2px}
.hintline{color:var(--bs-secondary-color);font-size:var(--t-sm);line-height:1.5;
  max-width:var(--measure);margin:0}
.foot-note{color:var(--bs-secondary-color);font-size:var(--t-xs);margin-top:var(--s6);
  padding-top:var(--s3);border-top:1px solid color-mix(in oklab,var(--bs-border-color) 60%,var(--bs-body-bg))}

/* the forwarding switch: the one control here that writes to the lab's
   production LabKey, so it states its own state in plain words rather than
   hiding in a row of buttons. .fwd's own box and .fwd-state's own colours
   are gone: the card supplies the first and status_chip the second. */
.fwd-head{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap;
  margin-bottom:var(--s3)}
.fwd-count{color:var(--bs-secondary-color);font-size:var(--t-sm)}
.fwd-acts{display:flex;align-items:center;gap:var(--s3);flex-wrap:wrap}
/* On a phone the Preview link wraps onto its own row and lands directly on
   top of the paragraph below it, which .hintline's margin:0 cannot fix
   from its own side. */
.fwd-acts+.hintline{margin-top:var(--s3)}
.fwd-acts form{margin:0}
.fwd-acts a{font-size:var(--t-sm);color:var(--bs-secondary-color)}
.fwd-acts a:hover{color:var(--bs-link-hover-color)}

/* tunnel instructions. The box is the card; what stays here is the copy. */
.connect-panel p{color:var(--bs-secondary-color);font-size:var(--t-sm);margin:var(--s2) 0}
.connect-panel b{color:var(--bs-body-color);font-weight:600}
.connect-panel .hint{color:var(--bs-secondary-color);font-size:var(--t-xs);
  max-width:var(--measure)}
pre{font-family:var(--font-mono);font-size:.78rem;color:var(--bs-body-color);
  background:var(--bs-tertiary-bg);border:1px solid var(--bs-border-color);
  border-radius:var(--radius);padding:var(--s3) var(--s4);overflow-x:auto;
  user-select:all;margin:var(--s3) 0}

@media (max-width:60rem){
  /* Bootstrap's .table-responsive only scrolls once its content overflows;
     without a floor the runs table would just shrink its columns instead,
     so this forces the same "scroll, don't squash" behaviour the old
     .scroller class gave every wide table. Runs rows are one line each and
     read fine that way; the fleet table does not, and gets the treatment
     below instead. */
  .table-responsive .runs{min-width:52rem}

  /* ONE CARD PER HERMIT.
     Nine columns and a stack of four controls per row: on a phone that was
     seven rows roughly 130px tall each, of which about 110px was empty,
     because the Controls column set the row height from off-screen where
     nobody could see or reach it. An operator opening this on a phone is
     asking "is anything wrong, and with which unit", and was getting a
     column of whitespace.
     Restacked, every row is a card: the HERMIT name and its status lead it
     on one line, and each remaining cell prints its own column name beside
     its value, taken from the data-label the template writes. Nothing is
     hidden - the alternative the brief offered was a narrower mobile column
     set, which answers the same question by dropping data, and the SIM
     budget in particular is the one number nobody should have to switch
     device to see.
     display:grid on <tr> is what makes this possible and it also strips the
     browser's implicit table semantics; dashboard.html carries explicit
     ARIA roles so the row headers and table navigation survive it. */
  .table-responsive .fleet{min-width:0}
  .fleet,.fleet tbody,.fleet tr,.fleet td,.fleet th{display:block}
  /* The column names move onto the cells, so the header row would only
     repeat them once at the top of a stack it no longer heads. */
  .fleet thead{display:none}
  .fleet tbody tr{display:grid;grid-template-columns:1fr auto;
    column-gap:var(--s3);align-items:center;
    border:1px solid var(--bs-border-color);border-radius:var(--radius);
    padding:var(--s3);margin-bottom:var(--s3)}
  .fleet tbody tr:last-child{margin-bottom:0}
  .fleet tbody td,.fleet tbody th{border-bottom:0;padding:var(--s1) 0}
  /* Name and status are the card's title: identity on the left, health on
     the right, on the line an eye lands on first. */
  .fleet tbody th[scope="row"]{grid-column:1;font-size:var(--t-h3)}
  .fleet tbody .cell-status{grid-column:2;justify-self:end}
  /* Everything else runs full width as a label/value pair. */
  .fleet tbody td[data-label]{grid-column:1/-1;display:grid;
    grid-template-columns:8rem 1fr;column-gap:var(--s3);align-items:baseline}
  .fleet tbody td[data-label]::before{content:attr(data-label);
    font-size:var(--t-xs);font-weight:600;color:var(--bs-secondary-color)}
  /* The controls need the full width of the card, not the value column, and
     they wrap across it rather than stacking: .actions' fixed 13rem column
     is what makes each button its own line, which on a phone spends about
     150px of card on four controls that fit comfortably in two rows.
     The message field keeps a line to itself - it is the one control here
     that needs width to be usable. */
  .fleet tbody td[data-label="Controls"]{grid-template-columns:1fr;
    row-gap:var(--s2);padding-top:var(--s3)}
  .fleet tbody td[data-label="Controls"] .actions{width:auto;
    display:flex;flex-wrap:wrap;align-items:center}
  .fleet tbody td[data-label="Controls"] .actions .msgform{flex:1 0 100%}
  .fleet tbody .empty{grid-column:1/-1}
}

/* Login is the one narrow, centred page; everything else wants the full
   width for tables. Bootstrap's form-label/form-control/btn/alert classes
   cover the rest of what LOGIN_CSS used to hand-roll.
   It is also the one page with no site header, so its <main> is the whole
   viewport and the card sits in the middle of it. It used to be pinned to
   the top with roughly 700px of white below it on a laptop.
   100dvh, with 100vh first for browsers that do not know the dynamic unit:
   on a phone the viewport height changes as the toolbar hides, and a card
   centred against the static height drifts off-centre when it does. */
.login-main{display:grid;align-content:center;min-height:100vh;
  min-height:100dvh}
.login-wrap{max-width:22rem;margin:0 auto;width:100%}
.login-wrap h1{text-align:center;font-size:var(--t-h2);margin:0 0 var(--s4)}

/* Bootstrap has no way to cap a scrolling block's own height; without one, a
   large upload-preview JSON dump makes the page absurdly tall. Everything
   else PREVIEW_CSS and SETTINGS_CSS used to hand-roll (layout, colour) is
   covered by Bootstrap utility classes in the templates themselves.
   The readout is its card's only content, so it drops the frame every other
   <pre> draws rather than putting a second box inside the first. */
.preview-readout{max-height:60vh;overflow-y:auto;
  border:0;border-radius:0;background:transparent;margin:0}

/* The console gives a real shell on a field device, so the grid size it
   reports upstream is real: anything full-screen an operator runs there
   (top, vi, less) renders to whatever size the browser claims. Without a
   height here the box just shrinks to whatever xterm's default 80x24 grid
   draws; console.html measures this box after giving it one, then fits
   xterm's cols/rows to it. Same fixed-box-then-fill shape as #map below. */
#terminal{height:min(72vh,44rem);overflow:hidden}

/* The map frame, and the theming Leaflet needs to look like part of this
   console rather than a stock embed. Ported verbatim from the old MAP_CSS
   Python constant; Bootstrap has no opinion on a third-party mapping
   library, so all of it stays. The !important rules override Leaflet's own
   inline-specificity styles for its attribution control, zoom bar and
   popups; dropping !important here would silently let Leaflet's defaults
   win again. */
#map{height:min(72vh,44rem);overflow:hidden;background:var(--bs-tertiary-bg)}
.leaflet-container{background:var(--bs-tertiary-bg);font-family:var(--font-ui)}
.leaflet-control-attribution{background:rgba(255,255,255,.85)!important;
  color:var(--bs-secondary-color)!important;font-size:.62rem!important}
.leaflet-control-attribution a{color:var(--bs-secondary-color)!important}
.leaflet-bar a{background:var(--bs-body-bg)!important;color:var(--bs-body-color)!important;
  border-color:var(--bs-border-color)!important}
.leaflet-bar a:hover{background:var(--bs-tertiary-bg)!important}
.leaflet-popup-content-wrapper,.leaflet-popup-tip{
  background:var(--bs-body-bg)!important;color:var(--bs-body-color)!important;
  border-radius:var(--radius)!important;
  box-shadow:0 8px 24px rgba(31,35,40,.18)!important}
.leaflet-popup-content{font-size:.78rem;line-height:1.55;margin:.6rem .8rem}
.leaflet-popup-content b{font-family:var(--font-mono);color:var(--bs-emphasis-color)}
.leaflet-popup-close-button{color:var(--bs-secondary-color)!important}
.note{color:var(--bs-secondary-color);font-size:var(--t-sm);margin-top:var(--s3)}
