/* Muster - carried over from the wireframe canvas. Color is information here:
   status, priority and coverage gaps are the only things allowed to be bright. */

:root {
  /* Aliases, by reference, so they follow the dark and print redefinitions
     of the tokens they stand for. Twelve rules consumed these and nothing
     declared them, which computed to transparent: the selected incident
     category was white on white in the light theme. */
  --accent: var(--info);
  --card: var(--panel);
 color-scheme: light;
 --ink: #1d1c19; --mut: #6d6a62; --faint: #97948c;
 --paper: #f6f5f1; --panel: #fff; --fill: #efede7; --fill2: #e4e1d9;
 --line: #dcd8d0; --line2: #bab6ac;
 --ok: #2f7d52; --info: #2b6ca3; --warn: #b0741c; --hot: #c25e12; --crit: #ad2f2f;
 --purp: #75499a; --teal: #2c7a76; --off: #8b877f;

 /* ---- six more map colours ----------------------------------------------
    The eight above were never a map palette; they are a STATUS palette that a
    map borrowed, and it shows. Measured in CIELAB they occupy L* 39.4..53.8
    (ink excepted) -- seven colours inside fourteen units of lightness -- and
    three of them, crit, hot and warn, are inside 42 degrees of hue. Cycle66
    has six routes, thirteen posts and an area to draw and only eight keys to
    do it with, so the same orange ends up on a route and on the high traffic
    area, which is the report that started this.

    These six are chosen against the eight in LCh, and they buy LIGHTNESS as
    well as hue: three hue families the eight leave open, each in a light and a
    dark version. Lightness is the one cue that survives a colour-blind reader
    (a deuteranope simulation shifts L* by at most 3.3) and a photocopier
    (which keeps it exactly), and the eight spent none of it.

    Print values live in src/lib/event-map.ts PRINT_HEX and are these, so the
    sheet in somebody's hand matches the map on the wall.                    */
 --plum: #7e3958; --pink: #c3769c;    /* the 78 deg gap above purple, dark/light */
 --olive: #5b4600; --sage: #739f66;   /* the 81 deg gap between amber and green  */
 --navy: #193e6b; --sky: #039cc8;     /* blue, below and above info              */

 /* ---- personnel status, one definition ----------------------------------
    Four different files used to decide what color a status is: the dot in
    layout.tsx, the pill rules below, the marker palette in map.js and the big
    board. They had already drifted, so out of service was purple in three
    places and meant the same thing as on break.

    Named per status rather than reused from the semantic scale, because these
    are read as a set: somebody scanning a roster is comparing green against
    blue against red, not asking what "warn" means. The tokens are the source
    of truth and map.js reads them out of the stylesheet at runtime.

    Read on a phone in daylight and on a laptop in a dark vehicle, so each has
    to hold its identity at both ends of the theme.                          */
 --s-registered:     #8b877f;  /* on the roster, not yet coming            */
 --s-mobilizing:     #2b8fa8;  /* answering a callout, before check-in     */
 --s-en_route:       #1f9e8c;  /* on duty and moving: teal                 */
 --s-checked_in:     #c9971c;  /* here, not yet working: gold              */
 --s-available:      #2f7d52;  /* green                                    */
 --s-at_post:        #2b6ca3;  /* blue                                     */
 --s-assigned:       #c25e12;  /* off post with a task: orange             */
 --s-break:          #75499a;  /* purple                                   */
 --s-out_of_service: #ad2f2f;  /* red                                      */
 --s-off_duty:       #55524c;  /* dark gray                                */
 --s-released:       #97948c;  /* lighter than off duty: gone, not resting */

 /* The unit dispatch ladder is its own scale. It answers a different question
    from a person's status and the two appear side by side on the board. */
 --u-dispatched:  #c9971c;
 --u-enroute:     #c25e12;
 --u-on_scene:    #ad2f2f;
 --u-transporting:#75499a;
 --u-cleared:     #2f7d52;

 /* Text that sits on a solid --ink or --crit fill. */
 --on-ink: #fff;
 --ink-hover: #000;
 --link-hover: #1d4f7a;

 /* Status tints, named rather than inlined so a theme can restate them. At
    night these have to become dark surfaces with light text, not pale washes. */
 --ok-bg: #eaf2ea;   --ok-line: #b6d0bd;   --ok-ink: #2b5c3e;
 --warn-bg: #fbf6ea; --warn-line: #e2cf9f; --warn-ink: #7a6218;
 --crit-bg: #fbeded; --crit-line: #e2b8b8; --crit-ink: #8a3f3f;
 --info-bg: #e9f0f6; --info-line: #a8c4dc; --info-ink: #1d4f7a;
 --rej-bg: #fbeceb;  --rej-line: #dbb0ac;  --rej-ink: #8a2b22;
 --purp-bg: #f5eff8;  --purp-line: #c9aed6;
 --hatch-a: #f7dede; --hatch-b: #fceded;

 /* OpenStreetMap ships light tiles. Dark mode filters them rather than
    swapping providers, which would be a second network dependency. */
 --tile-filter: none;

 --sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
 --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* ---- dark ----------------------------------------------------------------
   A command post at 0300 is a laptop in a vehicle, and a white screen there
   ruins the night vision of everyone in it.

   Not an inversion or a filter over the light theme: color is information in
   this app, so the status palette is restated at the saturation a dark surface
   needs instead of being dimmed into mud.

   The theme is chosen server-side from a cookie and stamped on <html>, so the
   page arrives already dark. A toggle that runs after paint flashes white,
   which is the exact thing this exists to prevent.
------------------------------------------------------------------------- */
:root[data-theme="dark"] {
 color-scheme: dark;
 --ink: #e8e6e1; --mut: #a09c93; --faint: #8f8b82;
 --paper: #14140f; --panel: #1e1e19; --fill: #262620; --fill2: #32322a;
 --line: #34342c; --line2: #4a4a40;
 --ok: #6fbf8d; --info: #7ab3e0; --warn: #d9a34e; --hot: #e08a4a; --crit: #e2726b;
 --purp: #b08fd0; --teal: #5fb5b0; --off: #85817a;

 /* The six map colours, lifted the same way: HUE HELD EXACTLY, L* raised to
    the tier. Hue is held rather than re-optimised because the whole point of
    storing a token instead of a hex is that a pin means the same thing in
    both themes -- a `navy` that renders periwinkle on screen is a token that
    lies about itself. The light-tier three go to L* 74-80 and the dark-tier
    three to 58, which keeps the light-against-dark pairing the print palette
    is built on; lifting all six to one L* collapsed plum and pink to 1.7.
    Known and accepted: on this surface sky sits 3.6 dE00 from info for a
    deuteranope, because the dark info token drifts to h 256.7. On paper,
    where it matters, that pair is 15.4 apart.                             */
 --plum: #c07594; --pink: #ee9ec5;
 --olive: #a48844; --sage: #a5d397;
 --navy: #718cc0; --sky: #2acaff;

 /* The same set, lifted for a dark surface. Same hues, more light, so a status
    means the same thing at 0300 as it did at noon. */
 --s-registered:     #85817a;
 --s-mobilizing:     #5fc0da;
 --s-en_route:       #4fc4b0;
 --s-checked_in:     #e0bd5a;
 --s-available:      #6fbf8d;
 --s-at_post:        #7ab3e0;
 --s-assigned:       #e08a4a;
 --s-break:          #b08fd0;
 --s-out_of_service: #e2726b;
 --s-off_duty:       #9b968e;
 --s-released:       #726e67;

 --u-dispatched:  #e0bd5a;
 --u-enroute:     #e08a4a;
 --u-on_scene:    #e2726b;
 --u-transporting:#b08fd0;
 --u-cleared:     #6fbf8d;

 --on-ink: #14140f;
 --ink-hover: #ffffff;
 --link-hover: #a8ccea;

 --ok-bg: #1a2a1f;   --ok-line: #35543f;   --ok-ink: #8fd0a6;
 --warn-bg: #2b2416; --warn-line: #56492c; --warn-ink: #e0bd7a;
 --crit-bg: #2e1a19; --crit-line: #5c3230; --crit-ink: #f0a09a;
 --info-bg: #172532; --info-line: #2f4a63; --info-ink: #9cc8ea;
 --rej-bg: #2e1a19;  --rej-line: #5c3230;  --rej-ink: #f0a09a;
 --purp-bg: #241d2c;  --purp-line: #4a3a58;
 --hatch-a: #3a1f1e; --hatch-b: #2a1817;

 --tile-filter: invert(1) hue-rotate(180deg) brightness(.86) contrast(1.06) saturate(.7);
}

/* No cookie yet: follow the device. An explicit choice always wins over it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
  --ink: #e8e6e1; --mut: #a09c93; --faint: #8f8b82;
  --paper: #14140f; --panel: #1e1e19; --fill: #262620; --fill2: #32322a;
  --line: #34342c; --line2: #4a4a40;
  --ok: #6fbf8d; --info: #7ab3e0; --warn: #d9a34e; --hot: #e08a4a; --crit: #e2726b;
  --purp: #b08fd0; --teal: #5fb5b0; --off: #85817a;

  /* The same six, same rule. See the note in the explicit dark block. */
  --plum: #c07594; --pink: #ee9ec5;
  --olive: #a48844; --sage: #a5d397;
  --navy: #718cc0; --sky: #2acaff;

 /* The same set, lifted for a dark surface. Same hues, more light, so a status
    means the same thing at 0300 as it did at noon. */
 --s-registered:     #85817a;
 --s-mobilizing:     #5fc0da;
 --s-en_route:       #4fc4b0;
 --s-checked_in:     #e0bd5a;
 --s-available:      #6fbf8d;
 --s-at_post:        #7ab3e0;
 --s-assigned:       #e08a4a;
 --s-break:          #b08fd0;
 --s-out_of_service: #e2726b;
 --s-off_duty:       #9b968e;
 --s-released:       #726e67;

 --u-dispatched:  #e0bd5a;
 --u-enroute:     #e08a4a;
 --u-on_scene:    #e2726b;
 --u-transporting:#b08fd0;
 --u-cleared:     #6fbf8d;
 
  --on-ink: #14140f;
  --ink-hover: #ffffff;
  --link-hover: #a8ccea;
 
  --ok-bg: #1a2a1f;   --ok-line: #35543f;   --ok-ink: #8fd0a6;
  --warn-bg: #2b2416; --warn-line: #56492c; --warn-ink: #e0bd7a;
  --crit-bg: #2e1a19; --crit-line: #5c3230; --crit-ink: #f0a09a;
  --info-bg: #172532; --info-line: #2f4a63; --info-ink: #9cc8ea;
  --rej-bg: #2e1a19;  --rej-line: #5c3230;  --rej-ink: #f0a09a;
  --purp-bg: #241d2c;  --purp-line: #4a3a58;
  --hatch-a: #3a1f1e; --hatch-b: #2a1817;
 
  --tile-filter: invert(1) hue-rotate(180deg) brightness(.86) contrast(1.06) saturate(.7);
  }
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--paper); color: var(--ink);
  font: 400 14px/1.5 var(--sans);
 -webkit-font-smoothing: antialiased;
 -webkit-text-size-adjust: 100%;
}
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
a { color: var(--info); text-decoration: none; }
a:hover { color: var(--link-hover); text-decoration: underline; }
h1, h2, h3, p { margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; }

.lbl { font-size: 10px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--faint); }
.mut { color: var(--mut); }
.fnt { color: var(--faint); }
.row { display: flex; align-items: center; gap: 10px; }
/* A form wrapping one button is a submit mechanism, not a box.
   .btn is inline-flex, so a block form around it opens a line box and the
   font's descender leaves a few pixels of leading under the button. The form
   is therefore taller than the button it contains, and align-items:center
   centers the FORM, floating the button above the plain <a class="btn"> next
   to it. Every Edit / Stand down pair on the assignments page sat out of line
   by exactly that much. */
:where(.row) > form { display: flex; align-items: center; }
/* The exception this used to need is gone. .row > form outranked a lone
   class and put every status button back in a box of its own, so a patch
   had to say "except .sbtn-form". :where(.row) contributes nothing, so
   .sbtn-form's own display:contents beats it the way a class beats a bare
   tag, with nothing here to remember. That patch was a fourth :not()
   wearing a different hat. */
.grow { flex: 1; min-width: 0; }
.wrap { max-width: 1440px; margin: 0 auto; }

/* ---- chrome ---- */
.top {
  display: flex; align-items: center; gap: 16px; padding: 0 16px; height: 56px;
  background: var(--panel); border-bottom: 1px solid var(--line2);
  /* Above Leaflet, which goes to 1000.
     .leaflet-container is position:relative with no z-index, so it creates no
     stacking context and its panes compete in the ROOT one. At z-index 10 this
     bar lost to a map tile pane at 400, and the Planning menu opened behind the
     common operating picture. */
  position: sticky; top: 0; z-index: 1200;
}

/* And pen Leaflet in, so it cannot reach the root context again.
   isolation:isolate makes a stacking context without touching how the map
   paints inside itself, so panes, popups and controls keep their own order and
   none of them can climb over the page furniture. */
.leaflet-container { isolation: isolate; }
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: .15em; font-size: 12px; }
.vr { width: 1px; height: 26px; background: var(--line); }
.nav { display: flex; gap: 2px; overflow-x: auto; }
:where(.nav) a {
  padding: 7px 11px; font-size: 12.5px; font-weight: 500; color: var(--mut);
  border-radius: 3px; white-space: nowrap; text-decoration: none;
}
.nav a:hover { background: var(--fill); text-decoration: none; }
.nav a[aria-current] { background: var(--ink); color: var(--on-ink); font-weight: 600; }

/* The break between tab groups. Same hairline the bar already uses between the
   brand and the tabs, so the bar has one vocabulary rather than two.
   It stays visible below 820px where the nav becomes a thumb-scrollable strip.
   That is where it earns most: you are scrolling a strip you cannot see the
   ends of, and the rules are what tell you which part of it you are in. */
/* A group holds together or moves as one. Without this the bar wrapped
   mid-group, so "Comms Radio log" sat on one row and "Alerts" on the next, and
   a group that can be split in half is not a group. */
.nav-group { display: flex; align-items: center; gap: 2px; }

/* The break belongs to the group it opens, drawn as that group's left edge
   rather than as a separate element between two groups. A standalone rule is
   an item the bar can wrap after, and it did: it dangled at the end of a row
   with nothing behind it. A border cannot be orphaned from what it marks. */
/* A section's pages behind its name. <details> so it opens with no JavaScript,
   which is how every other disclosure in this app works. */
.nav-menu { position: relative; }
.nav-menu-label {
  display: flex; align-items: center; gap: 5px; cursor: pointer;
  padding: 7px 11px; font-size: 12.5px; font-weight: 500; color: var(--mut);
  border-radius: 3px; white-space: nowrap; list-style: none;
}
.nav-menu-label::-webkit-details-marker { display: none; }
/* The chevron turns over when the menu is open, so the trigger says which it
   is without anybody having to look at what is underneath it. */
.nav-menu-label::after {
  content: ''; width: 0; height: 0; margin-top: 1px;
  border: 3.5px solid transparent; border-top-color: currentColor;
}
.nav-menu[open] .nav-menu-label::after { margin-top: -3px; transform: rotate(180deg); }
.nav-menu-label:hover { background: var(--fill); }
/* The section you are inside reads like the tab you are on, because it IS the
   tab you are on. Without this, opening a page from a menu leaves the whole bar
   looking as though nothing is selected. */
.nav-menu.here .nav-menu-label { background: var(--ink); color: var(--on-ink); font-weight: 600; }
.nav-menu-items {
  position: absolute; left: 0; top: calc(100% + 5px); z-index: 30;
  min-width: 190px; padding: 4px; display: flex; flex-direction: column; gap: 1px;
  background: var(--panel); border: 1px solid var(--line2); border-radius: 4px;
  box-shadow: 0 8px 24px rgb(0 0 0 / .22);
}
.nav-menu-item { padding: 9px 10px; font-size: 13px; color: var(--ink); }

.nav-cut {
  margin-left: 7px; padding-left: 7px;
  border-left: 1px solid var(--line);
}

/* Above the two-row breakpoint the bar was one 56px row with the tab strip
   scrolling inside it. A console user gets up to fourteen tabs, which stops
   fitting around 1400px, and the tabs that did not fit simply were not there:
   no scrollbar on a trackpad, no edge, nothing to suggest the strip moves. The
   tab you cannot see is the one you do not know exists, and Comms and Forms sit
   at the end of the strip.

   Wrapping instead of scrolling, so the bar grows a row when it has to. It
   adapts to the tab count, which varies by role and cannot be expressed as a
   breakpoint. Below 820px the strip stays a thumb-scrollable single row, where
   sideways scrolling is the expected gesture and vertical space is scarce. */
@media (min-width: 821px) {
  .top { height: auto; min-height: 56px; flex-wrap: wrap; row-gap: 4px; padding-top: 6px; padding-bottom: 6px; }
  .nav { flex-wrap: wrap; overflow-x: visible; row-gap: 3px; }
}

/* ---- surfaces ---- */
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 3px; }
.ph {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 12px; border-bottom: 1px solid var(--line); background: var(--fill);
  font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
}
.pb { padding: 12px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 3px; padding: 11px 12px; }
.stack { display: flex; flex-direction: column; gap: 11px; }
/* The hidden attribute has to win.
 *
 * A browser hides [hidden] through its own stylesheet, which ANY author rule
 * setting display beats. Every one of these layout helpers sets display, so
 * marking a .cols block hidden did nothing at all: the persons-involved form
 * said nought people and drew all five of them anyway. One rule, here, rather
 * than remembering it at each call site. */
[hidden] { display: none !important; }

.cols { display: grid; gap: 12px; }
@media (min-width: 900px) { .cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* The board is not three equal things.
 *
 * Roster is a short list of counts and reads fine narrow. Assignments is the
 * densest panel on the page -- every post and wagon, its staffing, its crew,
 * their named positions and now their status buttons -- and it was being given
 * the same width as a column of five numbers. Its rows wrapped while the
 * roster had space to spare. */
@media (min-width: 900px) {
  .board-cols { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.56fr) minmax(0, 1fr); }
}
@media (min-width: 900px) { .cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ---- bits ---- */
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: 0 0 auto; }
.chip {
  display: inline-flex; align-items: center; gap: 5px; height: 20px; padding: 0 8px;
  border-radius: 2px; font-size: 10px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; border: 1px solid var(--line2); background: var(--fill);
  color: var(--mut); white-space: nowrap;
}
.pill {
  display: inline-flex; align-items: center; gap: 5px; height: 21px; padding: 0 9px;
  border-radius: 11px; font-size: 10px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--on-ink); white-space: nowrap;
}
.s-not_arrived    { background: var(--s-registered); }
.s-mobilizing     { background: var(--s-mobilizing); }
.s-en_route       { background: var(--s-en_route); }
.s-checked_in     { background: var(--s-checked_in); }
.s-available      { background: var(--s-available); }
.s-at_post        { background: var(--s-at_post); }
.s-assigned       { background: var(--s-assigned); }
.s-break          { background: var(--s-break); }
.s-out_of_service { background: var(--s-out_of_service); }
.s-off_duty       { background: var(--s-off_duty); }
.s-released       { background: var(--s-released); }

.s-dispatched   { background: var(--u-dispatched); }
.s-enroute      { background: var(--u-enroute); }
.s-on_scene     { background: var(--u-on_scene); }
.s-transporting { background: var(--u-transporting); }
.s-cleared      { background: var(--u-cleared); }
/* Red is a life, orange is urgent, blue is routine, grey can wait. P3 was
   amber here and blue on the big board, so one priority had two colours
   depending on which screen you were looking at. */
.p1 { background: var(--crit); } .p2 { background: var(--hot); }
.p3 { background: var(--info); } .p4 { background: var(--off); }

/* ---- controls ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 34px; padding: 0 14px; border: 1px solid var(--line2); border-radius: 3px;
  background: var(--panel); font: 600 12.5px/1 var(--sans); color: var(--ink);
  cursor: pointer; white-space: nowrap; text-decoration: none;
}
.btn:hover { background: var(--fill); text-decoration: none; }
.btn.pri { background: var(--ink); border-color: var(--ink); color: var(--on-ink); }
.btn.pri:hover { background: var(--ink-hover); }
.btn.dgr { background: var(--crit); border-color: var(--crit); color: var(--on-ink); }
.btn.sm { min-height: 26px; padding: 0 10px; font-size: 11.5px; }
.btn.big { min-height: 54px; font-size: 16px; font-weight: 700; letter-spacing: .04em; width: 100%; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }

.field { display: block; margin-bottom: 12px; }
.field > .lbl { display: block; margin-bottom: 5px; }
/* A field stacked under another needs the margin. A field on a row does not:
   the row's gap already spaces it, and the margin instead makes the field's
   MARGIN BOX 12px taller than the button beside it. align-items aligns margin
   boxes, not borders, so on an align-items:flex-end row the button hangs 12px
   below the inputs it belongs to. Two live rows on the roster page. */
.row > .field { margin-bottom: 0; }
.inp {
  display: block; width: 100%; min-height: 38px; padding: 8px 10px;
  border: 1px solid var(--line2); border-radius: 3px; background: var(--panel);
  font-size: 14px;
}
.inp:focus { outline: 2px solid var(--ink); outline-offset: -1px; }
textarea.inp { min-height: 84px; resize: vertical; }

table { border-collapse: collapse; width: 100%; }
th {
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  color: var(--faint); text-align: left; padding: 8px 10px;
  border-bottom: 1px solid var(--line2); background: var(--fill); white-space: nowrap;
}
td { padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 13px; vertical-align: middle; }
.tbl-wrap { overflow-x: auto; }
.units .cnt.open, .units .open { color: var(--crit); font-weight: 600; }

.notice { padding: 11px 13px; border-radius: 3px; font-size: 13px; line-height: 1.5; }
.notice.warn { background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn-ink); }
.notice.crit { background: var(--crit-bg); border: 1px solid var(--crit-line); color: var(--crit-ink); }
.notice.info { background: var(--info-bg); border: 1px solid var(--info-line); color: var(--info-ink); }
.notice.ok   { background: var(--ok-bg); border: 1px solid var(--ok-line); color: var(--ok-ink); }
.hatch { background: repeating-linear-gradient(45deg, var(--hatch-a), var(--hatch-a) 5px, var(--hatch-b) 5px, var(--hatch-b) 10px); }

/* ---- field app (phone-first) ---- */
.app { display: flex; flex-direction: column; min-height: 100dvh; }
.app-main { flex: 1; padding: 14px 16px 22px; }
.tabs {
  display: flex; border-top: 1px solid var(--line2); background: var(--panel);
  position: sticky; bottom: 0; padding-bottom: env(safe-area-inset-bottom);
}
:where(.tabs) a {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; min-height: 58px; font-size: 9.5px; font-weight: 600; letter-spacing: .05em;
  color: var(--faint); text-decoration: none;
}
.tabs a[aria-current] { color: var(--ink); }
.tabs a:hover { text-decoration: none; }

/* One per row, full width. Two columns meant a 150px target next to another
   150px target on a phone held in one hand, and the current status sat there
   as a button you could press to no effect. */
.status-list { display: flex; flex-direction: column; gap: 9px; }
:where(.status-list) form { display: block; }
.status-btn {
  width: 100%; min-height: 56px; display: flex; align-items: center; gap: 10px;
  padding: 0 14px; border-radius: 5px; background: var(--panel);
  border: 1.5px solid var(--line2); font-size: 15px; font-weight: 700;
  color: var(--ink); text-decoration: none; cursor: pointer; text-align: left;
}
.status-btn:hover { border-color: var(--faint); text-decoration: none; }
.status-why { font-size: 11.5px; font-weight: 500; white-space: nowrap; }

/* Out of service opens rather than fires, because it asks for a reason. */
.oos { border: 1.5px solid var(--line2); border-radius: 5px; background: var(--panel); margin-top: 9px; }
:where(.oos) > summary {
  display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 0 14px;
  font-size: 15px; font-weight: 700; cursor: pointer; list-style: none;
}
/* THE GROUND, AND IT HAS TO OUTRANK THE SWATCH.
   The status classes are swatches: `.s-out_of_service` fills whatever carries
   it, which is right for a dot and wrong for a row. This summary wears one to
   take its left bar and icon color from the same source as everywhere else, so
   it must state its own background or the whole control comes out solid red.

   NOT inside the :where() above. :where() contributes zero specificity, so
   that rule scores as the bare element `summary` (0,0,1) and loses to
   `.s-out_of_service` (0,1,0) -- the declaration was present, correct, and
   overruled, which is how this bug came back wearing the comment that warned
   about it. A real class puts it at (0,1,1), which wins. */
.oos > summary { background: var(--panel); color: var(--ink); }
.oos > summary::-webkit-details-marker { display: none; }
.oos[open] > summary { border-bottom: 1px solid var(--line); }

@media (max-width: 380px) { .status-why { display: none; } }

/* Offline banner, driven by public/app.js */
#offline-bar { display: none; }
body[data-offline] #offline-bar { display: flex; }
body[data-offline] #sync-chip { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn-ink); }
/* A write the server refused. Louder than "waiting", because nothing is coming
   to fix it and the volunteer has to know their entry did not stick. */
#sync-chip:has(#sync-label[data-rejected]) { background: var(--rej-bg); border-color: var(--rej-line); color: var(--rej-ink); }
.sync-chip {
  display: inline-flex; align-items: center; gap: 5px; padding: 4px 9px;
  background: var(--ok-bg); border: 1px solid var(--ok-line); border-radius: 11px;
  font: 600 10.5px/1 var(--mono); color: var(--ok-ink);
}

/* ------------------------------------------------------ ICS vest colours --
 *
 * The colours responders have trained in: Operations red, Planning blue,
 * Logistics yellow, Finance green, Command white. A CONVENTION, not doctrine
 * -- FEMA's NIMS says nothing about vests -- so nothing in the UI calls these
 * the NIMS colours. No Pantone or hex is published anywhere for them; these
 * are renderings.
 *
 * THEIR OWN TOKEN FAMILY, deliberately. This app already speaks red: a gap
 * chip, a critical incident, an out-of-service unit. Reusing --crit for
 * Operations would put two red languages in one row and make an identity look
 * like an alarm. These are held a step back from the alarm colours -- less
 * saturated, lighter fills -- so --crit stays the loudest red on any screen.
 *
 * COMMAND HAS NO HUE, so it is not given one. White is the absence of a
 * colour, and a white fill is invisible on a light theme and the loudest thing
 * on a dark one -- exactly backwards for the section that should read as
 * authoritative rather than shouting. Instead it takes the strongest
 * achromatic treatment: the page's own ink, at the heaviest border weight.
 * That is how a legend renders "no colour", and it inverts correctly for free.
 */
:root {
  --ics-ops:  #b02020; --ics-ops-line:  #e8a0a0; --ics-ops-bg:  #fceaea;
  --ics-plan: #1b4fa8; --ics-plan-line: #a8c4e8; --ics-plan-bg: #e7effb;
  --ics-log:  #7a5a05; --ics-log-line:  #dfc96a; --ics-log-bg:  #fbf3d8;
  --ics-fin:  #1a6b38; --ics-fin-line:  #93cba6; --ics-fin-bg:  #e6f5ea;
}
:root[data-theme="dark"] {
  --ics-ops:  #f0a9a3; --ics-ops-line:  #8e4a44; --ics-ops-bg:  #3a1f1d;
  --ics-plan: #a8cbee; --ics-plan-line: #4a6e96; --ics-plan-bg: #1b2a3d;
  --ics-log:  #e3c97a; --ics-log-line:  #8a7333; --ics-log-bg:  #332b14;
  --ics-fin:  #96d3aa; --ics-fin-line:  #437a55; --ics-fin-bg:  #17301f;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ics-ops:  #f0a9a3; --ics-ops-line:  #8e4a44; --ics-ops-bg:  #3a1f1d;
    --ics-plan: #a8cbee; --ics-plan-line: #4a6e96; --ics-plan-bg: #1b2a3d;
    --ics-log:  #e3c97a; --ics-log-line:  #8a7333; --ics-log-bg:  #332b14;
    --ics-fin:  #96d3aa; --ics-fin-line:  #437a55; --ics-fin-bg:  #17301f;
  }
}

/* Worn by a chip carrying an ICS position. Not by .chip itself: .chip is one
   shared component used for about thirty unrelated things, and colouring it by
   position would repaint affiliations, staffing counts and form states too. */
.chip.sec-operations { color: var(--ics-ops);  border-color: var(--ics-ops-line);  background: var(--ics-ops-bg); }
.chip.sec-planning   { color: var(--ics-plan); border-color: var(--ics-plan-line); background: var(--ics-plan-bg); }
.chip.sec-logistics  { color: var(--ics-log);  border-color: var(--ics-log-line);  background: var(--ics-log-bg); }
.chip.sec-finance    { color: var(--ics-fin);  border-color: var(--ics-fin-line);  background: var(--ics-fin-bg); }
/* Command wears no hue: the page's own ink, at the heaviest border weight.
   No font-weight change -- .chip is already 600 and bolding it made the one
   chip that should read as calm authority the loudest thing in the row. */
.chip.sec-command    { color: var(--ink); border-color: var(--ink); border-width: 2px; background: transparent; }

/* On the narrowest phones the word goes and the dot stays.
 *
 * The field header carries a home mark, the event name, the operational
 * period, a Command button for command staff, this chip and the theme toggle.
 * At 375px that is over budget and the name is what gets squeezed. The dot is
 * the whole signal anyway -- green is synced, amber is queued, red is refused
 * -- and the word is a gloss on it, so the word is what can go.
 *
 * The label is hidden rather than removed so app.js keeps writing to it and
 * screen readers keep reading it. */
@media (max-width: 380px) {
  .sync-chip { padding: 4px 6px; }
  .sync-chip #sync-label {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}

.queued { opacity: .72; }
.queued td:first-child, .queued .mono:first-child { color: var(--warn); }

@media print {
  .top, .tabs, .btn, #offline-bar { display: none !important; }
  :root, :root[data-theme="dark"] {
    color-scheme: light;
    --ink: #1d1c19; --mut: #6d6a62; --faint: #97948c;
    --paper: #fff; --panel: #fff; --fill: #efede7; --fill2: #e4e1d9;
    --line: #dcd8d0; --line2: #bab6ac;
    --ok: #2f7d52; --info: #2b6ca3; --warn: #b0741c; --hot: #c25e12; --crit: #ad2f2f;
    --on-ink: #fff; --tile-filter: none;
    --ok-bg: #eaf2ea;   --ok-line: #b6d0bd;   --ok-ink: #2b5c3e;
    --warn-bg: #fbf6ea; --warn-line: #e2cf9f; --warn-ink: #7a6218;
    --crit-bg: #fbeded; --crit-line: #e2b8b8; --crit-ink: #8a3f3f;
    --info-bg: #e9f0f6; --info-line: #a8c4dc; --info-ink: #1d4f7a;
    --purp-bg: #f5eff8; --purp-line: #c9aed6;
    --hatch-a: #f7dede; --hatch-b: #fceded;
  }
  body { background: #fff; }
  .panel { border-color: #888; }
}

/* ---- map tiles ---- */
/* Only the raster tiles are filtered. Markers, post polygons and the incident
   pins are ours and are already themed, so inverting them would undo the work. */
.leaflet-tile-pane { filter: var(--tile-filter); }

/* The name written across a watch or warning shape.
   Flat and quiet: it labels ground rather than marking a point, so it gets no
   arrow, no box and no shadow -- the polygon under it is already the object.
   Its own pane means dark mode's tile inversion never reaches it. */
.leaflet-tooltip.wx-label {
  background: none; border: 0; box-shadow: none; padding: 0;
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  color: var(--ink); text-shadow:
    0 0 3px var(--paper), 0 0 3px var(--paper), 0 0 3px var(--paper), 0 0 3px var(--paper);
  white-space: nowrap; pointer-events: none;
}
.leaflet-tooltip.wx-label::before { display: none; }
/* Weather that does not reach this event: still named, visibly quieter. */
.leaflet-tooltip.wx-label-far { font-weight: 600; opacity: .72; font-size: 9px; }

/* ---- theme toggle ---- */
.theme-form { display: inline-flex; margin: 0; }
.theme-btn {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px;
  background: var(--fill); border: 1px solid var(--line2); border-radius: 7px;
  color: var(--mut); font: 600 11px/1 var(--sans); cursor: pointer;
}
.theme-btn:hover { color: var(--ink); border-color: var(--faint); }

/* ---- the map input on the post form ---- */
.geo { border: 1px solid var(--line); border-radius: 3px; background: var(--panel); }
:where(.geo) > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 12px; cursor: pointer;
  font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
}
.geo[open] > summary { border-bottom: 1px solid var(--line); background: var(--fill); }
/* Taller than it was. Placing a pin accurately in a 260px window is two
   minutes of panning per post, and the map is the whole point of the panel. */
.post-map { height: 380px; border: 1px solid var(--line); border-radius: 3px; background: var(--fill); }
.post-map-fallback { display: flex; align-items: center; justify-content: center; height: 100%; padding: 12px; text-align: center; font-size: 12px; }
/* The rail is 340px on a laptop and full width on a phone, where the map is the
   whole point: a supervisor standing at a crossing is placing a post by tapping
   where they are standing. */
@media (max-width: 899px) { .post-map { height: 340px; } }

/* ---- touch mode ----------------------------------------------------------
   A command post laptop is often a touchscreen, and the console is drawn for a
   mouse: 20px chips, 26px small buttons, dense table rows. This grows the hit
   targets toward the 44px everyone agrees on, without changing what is on the
   screen or reflowing the layout.

   Chosen rather than detected. The same machine gets used with a trackpad and
   with fingers in one shift, and a media query cannot tell which is happening
   right now. Set server-side from a cookie so the page arrives the right size.
-------------------------------------------------------------------------- */
/* One switch, --tap, rather than one selector.
 *
 * Touch sizing has two triggers and they must not be written out twice: the
 * explicit toggle, and a screen that is narrow AND driven by a finger. A media
 * query cannot be ORed with an attribute selector in one rule, so instead of
 * duplicating twenty declarations (which drift) the two triggers both set a
 * number, and every size is calculated from it.
 *
 * The toggle still wins in both directions. A convertible laptop gets used
 * with a trackpad and with fingers in one shift and no query can tell which is
 * happening right now, which is why the choice exists; but a 400-pixel phone
 * with a coarse pointer is not ambiguous, and making somebody find a toggle
 * before the buttons are usable is worse than guessing right.
 */
:root { --tap: 0; }
@media (max-width: 820px) and (pointer: coarse) {
  :root:not([data-touch="0"]) { --tap: 1; }
}
:root[data-touch="1"] { --tap: 1; }
:root[data-touch="0"] { --tap: 0; }

.btn { min-height: calc(34px + var(--tap) * 8px); padding: 0 calc(14px + var(--tap) * 2px);
       font-size: calc(12.5px + var(--tap) * 1.5px); }
.btn.sm { min-height: calc(26px + var(--tap) * 12px); padding: 0 calc(10px + var(--tap) * 3px);
          font-size: calc(11.5px + var(--tap) * 1.5px); }
.btn.big { min-height: calc(54px + var(--tap) * 6px); font-size: calc(16px + var(--tap) * 1px); }

/* A button that sits on a form row beside an input, and has to line up with it.
   .inp is calc(38px + var(--tap) * 4px) and .btn is calc(34px + var(--tap) * 8px),
   so they agree at 42px on a phone and differ by 4px on a mouse. The comms log
   solved that with an inline min-height:38px, which made mouse mode pixel
   perfect and touch mode 4px out: the same bug, moved. This follows --tap the
   way the input does, so they match in both. */
.btn.with-input { min-height: calc(38px + var(--tap) * 4px); }

/* A button that is only a glyph. Square, so a row of them is a row rather than
   a ragged edge, and sized off --tap like every other control so it grows on a
   phone. The label lives in title and aria-label: an icon-only control that
   says nothing to a screen reader is a control that is not there. */
.btn.icon-only {
  padding: 0; width: calc(26px + var(--tap) * 12px);
  min-height: calc(26px + var(--tap) * 12px);
}
.btn.icon-only svg { width: 15px; height: 15px; }

/* A drag handle, and the row while it is being dragged. */
.reorder-handle {
  cursor: grab; touch-action: none; color: var(--faint);
  background: transparent; border: 0; padding: 0 4px; line-height: 1;
  font-size: 15px;
}
.reorder-handle:hover { color: var(--mut); }
.reorder-handle:active { cursor: grabbing; }
.reorder-handle:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; border-radius: 3px; }
/* touch-action:none on the handle only, never the row: the list still scrolls
   under a finger, and only the grip starts a drag. */
.reorder-lifted { background: var(--fill); opacity: .75; }
[data-reorder][data-saving="failed"] { outline: 2px solid var(--crit); outline-offset: 2px; }

/* !important because several controls carry an inline height, set where they
   are cramped into a table cell, and an inline style beats any selector. This
   is the one place in the stylesheet that needs it: the alternative is hunting
   inline heights across a dozen views and hoping the next one remembers. */
/* select.inp and input.inp, NOT bare .inp. The bare selector also matched
   textarea.inp, whose own 84px floor at the top of this file is 0,1,1 with no
   !important and therefore lost to this one: seventeen textareas were floored
   at 38px instead of 84. Only inputs, selects and textareas ever carry .inp,
   so naming the two this guard is about costs nothing. */
select.inp, input.inp {
  min-height: calc(38px + var(--tap) * 4px) !important;
  font-size: calc(14px + var(--tap) * 1px);
}
/* muster:align-ok not a size. This clears an inline height so the min-height
   floor above can take effect; it has nothing to grow and therefore wants no
   coarse-pointer twin. */
:root[data-touch="1"] .inp,
:root[data-touch="1"] select.inp,
:root[data-touch="1"] input.inp { height: auto !important; }
textarea.inp { font-size: calc(14px + var(--tap) * 1px); }

/* Row height is where a finger actually misses. */
table td { padding-top: calc(7px + var(--tap) * 4px); padding-bottom: calc(7px + var(--tap) * 4px); }
table th { padding-top: calc(6px + var(--tap) * 4px); padding-bottom: calc(6px + var(--tap) * 4px); }

/* Anything in a table cell that is really a control: smaller than a standalone
   one, because the row is the target and the cell is already generous. */
table .btn.sm { min-height: calc(26px + var(--tap) * 10px); }
table .inp { min-height: calc(30px + var(--tap) * 6px) !important; }

:where(.nav) a { padding: calc(7px + var(--tap) * 5px) calc(11px + var(--tap) * 3px);
         font-size: calc(12.5px + var(--tap) * 1px); }
/* min-height, not height. The bar wraps to a second row of tabs above 821px,
   and with a fixed height that row hung outside the box while the bar's
   z-index still put it over the page: on Assignments, "Setup" sat on top of
   the Assign button and took the click. */
.top { min-height: calc(56px + var(--tap) * 10px); }
.chip { height: calc(20px + var(--tap) * 6px); padding: 0 calc(8px + var(--tap) * 3px);
        font-size: calc(10px + var(--tap) * 1px); }
.theme-btn { min-height: calc(30px + var(--tap) * 8px); min-width: calc(30px + var(--tap) * 8px);
             justify-content: center; }

/* A checkbox is the worst target on the page by default. */
input[type="checkbox"], input[type="radio"] {
  width: calc(15px + var(--tap) * 5px); height: calc(15px + var(--tap) * 5px);
}

/* Links inside dense lists get a bigger box without moving anything. The
   padding is a calc so it follows --tap like everything else rather than
   needing its own rule for each trigger.

   THE EXCLUSION LIST IS THE WHOLE RULE. This is a bare-link rule, and
   .tbl-wrap a outranks a lone class, so anything that is really a control but
   happens to be an <a> loses display:inline-flex to inline-block. That takes
   align-items:center with it, so the label sits on the top edge, and the `0`
   horizontal padding overrides the control's own, so the text runs out both
   sides.

   It caught .btn first: every Edit and Retire button in every table on the
   site, and it read as a button problem rather than a link rule. It caught
   .pill the day the roster's incident status became clickable, because that
   turned a <span class="pill"> into an <a class="pill"> and the list did not
   know about pills. The same two symptoms exactly: DISPATCHED sat against the
   top of its pill and the incident number ran off the end of it.

   So the list is now every box-shaped class here that sets inline-flex, not
   only the one that got caught. Chained :not() rather than :not(.btn, .pill,
   .chip), because this is read on whatever phone a volunteer turned up with.
   */
:where(.tbl-wrap) a { padding: calc(var(--tap) * 4px) 0; display: inline-block; }

.theme-btn[data-on] { border-color: var(--ink); color: var(--ink); background: var(--panel); }

/* ---- overflow menu on a table row ---- */
.more { position: relative; display: inline-block; }
.more > summary { list-style: none; cursor: pointer; }
.more > summary::-webkit-details-marker { display: none; }
.more-menu {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 20;
  min-width: 170px; padding: 4px; display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line2); border-radius: 4px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .18);
}
.more-item {
  text-align: left; padding: 9px 10px; border: 0; border-radius: 3px;
  background: transparent; color: var(--ink); font-size: 13px; cursor: pointer;
}
.more-item:hover { background: var(--fill); }
:root[data-touch] .more-item { padding: 12px 12px; font-size: 14px; }

/* ---- org chart ----------------------------------------------------------
   An indented outline with connector lines, not boxes. Boxes look like the
   ICS 207 and then fall apart at 30 people in a division on a 1440px screen;
   an outline just gets taller, which a command post laptop can scroll.
------------------------------------------------------------------------- */
.org-tree, .org-branch { list-style: none; margin: 0; padding: 0; }
.org-branch { margin-left: 13px; padding-left: 15px; border-left: 1.5px solid var(--line2); }
.org-tree > li, .org-branch > li { position: relative; padding: 3px 0; }
.org-branch > li::before {
  content: ''; position: absolute; left: -15px; top: 21px;
  width: 11px; height: 1.5px; background: var(--line2);
}
.org-node {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 7px 9px; border: 1px solid var(--line); border-radius: 3px;
  background: var(--panel);
}
.org-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; min-width: 0; flex: 1; }
.org-name { font-weight: 600; font-size: 13.5px; }
.org-call { font-size: 11.5px; }
.org-count { font-size: 11px; }
.org-ic { border-color: var(--ink); color: var(--ink); font-weight: 700; }
.org-div { border-color: var(--info-line); background: var(--info-bg); color: var(--info-ink); }
.org-warn { border-color: var(--warn-line); background: var(--warn-bg); color: var(--warn-ink); }
.org-edit { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* !important because the global floor above is !important, and only that beats
   it. Without this the split decision was the worst of both: the 12px type and
   3px padding landed and the 30px did not, so the org editor drew small text
   adrift in a 38px box beside a 26px button. A compact inline editor is the
   point of this rule; it should either happen or not exist. */
.org-edit .inp { min-height: 30px !important; font-size: 12px; padding: 3px 7px; }
.org-edit select.inp { max-width: 210px; }
.org-edit input.inp { max-width: 190px; }
@media (max-width: 899px) {
  .org-node { flex-direction: column; align-items: stretch; }
  .org-edit .inp, .org-edit select.inp, .org-edit input.inp { max-width: none; flex: 1; }
}

/* ---- the big board -------------------------------------------------------
   Read at ten feet by someone who cannot touch it. Everything sizes from
   the viewport so one layout serves a 1080p monitor and a 4K panel without a
   breakpoint: at 3840px the same rules simply produce bigger type.

   Gaps are the loudest thing here on purpose. A wallboard exists to be read
   from across a room, and the only thing worth reading from there is what is
   wrong.
------------------------------------------------------------------------- */
body.wall {
  height: 100vh; margin: 0; padding: 1.2vh 1.2vw; overflow: hidden;
  display: flex; flex-direction: column; gap: 1.1vh;
  background: var(--paper); color: var(--ink);
}
.wall-top {
  display: flex; align-items: baseline; gap: 1.5vw;
  padding-bottom: .8vh; border-bottom: 2px solid var(--line2);
}
.wall-title { font-size: clamp(20px, 2.1vw, 62px); font-weight: 700; }
.wall-op { font-size: clamp(13px, 1.1vw, 32px); color: var(--mut); font-family: var(--mono); }
/* Two clocks: the event's own time and UTC, each with its zone against it.
 *
 * A grid rather than two lines of text so the zone labels sit in their own
 * column and the digits stay aligned; the board is read at a distance and a
 * ragged right edge on a clock reads as a wobble. UTC is smaller because the
 * local time is the one somebody is acting on, but it is present because a
 * partner agency on the radio is working in UTC and a bare time invites
 * everybody to assume it is theirs. */
.wall-clock {
  margin-left: auto; font-family: var(--mono); font-weight: 700;
  line-height: 1; font-variant-numeric: tabular-nums;
  display: grid; grid-template-columns: auto auto; align-items: baseline;
  column-gap: .35em; row-gap: .12em; justify-items: end;
}
/* The day, across both columns above the time. Quiet, because it changes once
   and the time changes every minute. */
.wall-date {
  grid-column: 1 / -1; justify-self: end;
  font-size: clamp(11px, .9vw, 26px); font-weight: 600; letter-spacing: .06em;
  color: var(--mut);
}
.wall-now { font-size: clamp(26px, 3vw, 92px); }
.wall-utc { font-size: clamp(15px, 1.5vw, 44px); color: var(--mut); }
.wall-zone {
  font-size: clamp(10px, .78vw, 22px); font-weight: 600; letter-spacing: .08em;
  color: var(--mut); justify-self: start;
}
/* The tactical call sign beside the post's name, quieter than the name. */
.wall-tac { color: var(--mut); font-weight: 600; }

/* Two narrow columns with the map between them.
 *
 * The board used to be three columns of lists, and the third was people counts
 * and unplaced agency units: both are glanceable in a line, and neither needed
 * a column. The middle is the map now, which is the thing a room full of people
 * actually looks at, and the lists either side stay narrow enough to read at a
 * distance without becoming the whole board. */
/* ---- the incident page ---------------------------------------------------
   A dispatcher works a live call from this screen, so it is laid out by how
   often a thing is touched rather than by how much of it there is.

   WORK is the status ladder and the dispatch list: clicked every ninety
   seconds, and it gets the widest column. MAP and LOG answer "where" and
   "what happened", side by side under each other in the middle. The RAIL is
   reference -- outside units, the persons table, the category, the close
   button -- and it is narrow because none of it is touched twice a minute.

   Two equal columns put the close button on the same footing as the ladder,
   and left a third of the right-hand column empty under the outside-unit form
   while the log ran out of room beside it.

   Placed by NAME. nth-child would break the first time somebody adds a panel,
   and this page is one people add panels to. */
.inc-cols {
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.25fr) minmax(0, .9fr);
  /* The rail is two areas, not one shared by two panels: grid PLACES both on
     the same named area, one over the other, rather than flowing them. */
  grid-template-areas: 'work map agency' 'work log rail';
  align-items: start;
}
.inc-a-work   { grid-area: work; }
.inc-a-map    { grid-area: map; }
.inc-a-log    { grid-area: log; }
.inc-a-agency { grid-area: agency; }
.inc-a-rail   { grid-area: rail; }

/* The map FILLS its row rather than sitting at a fixed height in it.
   A grid row is as tall as its tallest cell, so a fixed-height map beside a
   busy outside-agencies panel left three hundred pixels of nothing under
   itself -- the same dead space this layout exists to remove, moved one
   column over. The floor keeps it usable on a quiet call; the ceiling stops a
   long agency list from turning it into a wall of tiles. */
.inc-a-map { align-self: stretch; display: flex; flex-direction: column; }
.inc-map {
  flex: 1; min-height: 280px; max-height: 620px; border-radius: 0 0 4px 4px;
}
.inc-map .leaflet-container { background: var(--fill); border-radius: 0 0 4px 4px; }

@media (max-width: 1500px) {
  /* A laptop. The rail goes under the map rather than squeezing three columns
     into something none of them can use. */
  .inc-cols {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: 'work map' 'work log' 'rail rail';
  }
  .inc-a-agency, .inc-a-rail { grid-area: auto; }
  .inc-a-agency { grid-column: 1; }
  .inc-a-rail { grid-column: 2; }
}
@media (max-width: 900px) {
  .inc-cols { grid-template-columns: minmax(0, 1fr); grid-template-areas: none; }
  .inc-a-work, .inc-a-map, .inc-a-log, .inc-a-agency, .inc-a-rail {
    grid-area: auto; grid-column: 1;
  }
  /* In reading order rather than source order: units, then where it is, then
     what happened, then the reference. Source order puts the outside-agency
     list between the ladder and the map, which is a lot of scrolling past on
     a phone to answer "where am I sending them". */
  .inc-a-work { order: 1; } .inc-a-map { order: 2; } .inc-a-log { order: 3; }
  .inc-a-agency { order: 4; } .inc-a-rail { order: 5; }
  /* Shorter here. A phone screen is 812 tall and the map is not what somebody
     opened the call to read. */
  .inc-map { min-height: 200px; max-height: 220px; }
}

/* ---- density -------------------------------------------------------------
   Every unit on a call carries a five-stamp ladder and a row of four buttons,
   and three units of that filled the screen before the dispatch list started.
   Scoped to this page: the same card elsewhere is read, not worked. */
:where(.inc-a-work) .card { padding: 8px 10px; }
:where(.inc-a-work) .stack { gap: 8px; }
.inc-stamps { gap: 10px !important; margin-top: 5px !important; }
.inc-stamps .lbl { font-size: 8px; letter-spacing: .06em; }
.inc-acts { margin-top: 6px !important; }

/* A division heading inside the dashboard's assignments panel. Sticky, because
   the panel scrolls and the group you are reading is the answer to "whose gap
   is this": it must not scroll away above the rows it names. */
.asg-div-head {
  position: sticky; top: 0; z-index: 1;
  padding: 5px 12px; gap: 8px;
  background: var(--fill); border-bottom: 1px solid var(--line2);
  font-size: 10.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.asg-div-head .mono { font-size: 11px; color: var(--mut); }

.wall-grid {
  flex: 1; min-height: 0; display: grid; gap: 1.1vh 1.1vw;
  grid-template-columns: 1fr 2.1fr 1fr;
  /* Incidents own the whole right column. It was split with an "agency units,
     unplaced" card that was almost always empty, while the list somebody walks
     up to this board to read ran out of room above it. */
  grid-template-areas: 'staff map inc' 'staff map inc';
}
/* And the schedule takes the bottom of that column back, but only on an event
   that has one. Most incidents do not: a wildland fire has no 0700 wave start,
   and on those the corner stays with incidents. */
.wall-grid.has-sched { grid-template-areas: 'staff map inc' 'staff map sched'; }
/* And it takes width from the map when it has a lot to carry. A map on 40%
   of a wall is still a map; a staffing list with nothing showing is not a
   staffing list. */
.wall-grid:has([data-rows='some']) { grid-template-columns: 1.5fr 1.75fr 1fr; }
.wall-grid:has([data-rows='many']) { grid-template-columns: 2fr 1.4fr 1fr; }
.wall-card {
  min-height: 0; display: flex; flex-direction: column;
  border: 1px solid var(--line2); border-radius: 4px; background: var(--panel);
  padding: .9vh 1vw; overflow: hidden;
}
/* Placed by name, not by position. nth-child was fine while the set of cards
   was fixed forever, and the schedule card is exactly the thing that breaks
   that: it appears only on an event that has a schedule, and under the old
   rules its arrival would have shifted the map into the incidents slot. */
.wall-a-staff { grid-area: staff; }
.wall-a-map   { grid-area: map; }
.wall-a-inc   { grid-area: inc; }
.wall-a-sched { grid-area: sched; }

/* The map fills its card: no heading, no padding. The frame is the card. */
.wall-map-card { padding: 0; }
#wall-map { width: 100%; height: 100%; min-height: 0; border-radius: 4px; }
/* Leaflet paints its own background, which is white and would flare on a dark
   board before the tiles land. */
#wall-map .leaflet-container { background: var(--fill); }

/* Status counts as a strip under staffing: read as proportions at a distance,
   not looked up one at a time. */
.wall-statuses {
  display: flex; flex-wrap: wrap; gap: .5vh .9vw;
  padding-top: .8vh; margin-top: auto;
  border-top: 1px solid var(--line2);
}
.wall-stat { display: inline-flex; align-items: baseline; gap: .35em; white-space: nowrap; }
/* .dot, not i. This rule exists to resize the status dot on the wall from
   its 9px to .62em of the figure beside it, so it is targeting a control on
   purpose rather than catching one by accident. Naming the class says that
   and stops it being a bare-tag rule that outranks what it hits. */
.wall-stat .dot {
  width: .62em; height: .62em; border-radius: 50%; align-self: center; flex: none;
}
.wall-stat b { font-size: clamp(13px, 1.15vw, 30px); font-weight: 700; }
.wall-stat span { font-size: clamp(10px, .72vw, 19px); color: var(--mut); }
.wall-card h2 {
  margin: 0 0 .7vh; font-size: clamp(11px, .78vw, 24px); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--faint);
}

/* The row count beside a card heading: same size, not shouting. */
.wall-h2n { font-family: var(--mono); color: var(--mut); letter-spacing: 0; }

.wall-figure-row { display: flex; gap: 1.4vw; margin-bottom: 1vh; }
.wall-figure { display: flex; flex-direction: column; line-height: 1; }
.wall-figure b { font-size: clamp(24px, 2.6vw, 78px); font-weight: 700; font-family: var(--mono); }
:where(.wall-figure) span { font-size: clamp(10px, .72vw, 21px); color: var(--mut); margin-top: .4vh; }
.wall-figure.bad b { color: var(--crit); }
.wall-figure.good b { color: var(--ok); }

.wall-list { list-style: none; margin: 0; padding: 0; overflow: hidden; flex: 1; min-height: 0; }
:where(.wall-list) li {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .3vw 1vw;
  align-items: baseline; padding: .45vh 0; border-bottom: 1px solid var(--line);
}
.wall-key { font-size: clamp(13px, 1.05vw, 31px); font-weight: 600; }
.wall-val {
  font-family: var(--mono); font-weight: 700; font-variant-numeric: tabular-nums;
  font-size: clamp(13px, 1.05vw, 31px);
}
.wall-sub {
  grid-column: 1 / -1; font-family: var(--mono); color: var(--mut);
  font-size: clamp(10px, .72vw, 21px);
}
.wall-list li.bad .wall-key, .wall-list li.bad .wall-val { color: var(--crit); }
.wall-list li.bad { border-bottom-color: var(--crit-line); }
/* ---- the staffing card, when there is a lot to fit -----------------------
   The list is the card's whole point and it was the last thing to get space:
   flex:1 against siblings that could not shrink below their content, so on a
   29-row event the list box came out 33px tall and the wall showed three big
   numbers and nothing else. min-height:0 lets the secondary blocks give way,
   and their own headings carry a count so a shrunk one still says how many. */
/* THE LIST GETS THE CARD'S HEIGHT FIRST.
   These blocks are secondary -- who is not yet placed, and the yards that are
   places rather than positions -- and they could not shrink below their own
   content, so on a busy event they took two thirds of the card and the list
   they sit under got 33 pixels. They shrink first now, and hard once the card
   is dense: each one's heading carries its count, so a block cut short still
   says how many are in it, which the silently-clipped list did not. */
:where(.wall-a-staff) .wall-unassigned {
  min-height: 8vh; max-height: 26%; overflow: hidden; flex-shrink: 8;
}
/* Capped, not crushed. A shrink factor alone took these to nine pixels, which
   showed nothing at all -- and that is how somebody standing in a staging yard
   leaves the wall entirely, the exact failure the two lists exist to prevent.
   A floor keeps the heading and a row or two; the heading carries the count,
   so what is cut off still says how much was cut. */
[data-rows='many'] .wall-unassigned { max-height: 20%; min-height: 7vh; }
/* And the word under an empty post, which the red row and the 0/1 both already
   say. Sixteen of them cost more height than an entire division needs. */
[data-rows='some'] .wall-unstaffed, [data-rows='many'] .wall-unstaffed { display: none; }
[data-rows='some'] .wall-sub, [data-rows='many'] .wall-sub { line-height: 1.15; }
[data-rows='some'] .wall-sub:not(:has(*)), [data-rows='many'] .wall-sub:not(:has(*)),
[data-rows='some'] .wall-sub:has(> .wall-unstaffed:only-child),
[data-rows='many'] .wall-sub:has(> .wall-unstaffed:only-child) { display: none; }
/* And the list stops asking for leftovers with a basis of zero: on a full card
   there are none, so it was only ever given what the blocks below did not
   want. It asks for most of the card and gives back what it does not need. */
[data-rows='some'] .wall-cols { flex: 1 1 62%; }
[data-rows='many'] .wall-cols { flex: 1 1 65%; }
/* Two columns from the first step, not the second. A dozen assignments in one
   narrow column already outruns the card; the density steps only decide how
   hard to squeeze, not whether to. */
.wall-cols { column-count: 2; column-gap: 1.2vw; column-fill: balance; }
/* A real basis, not a zero one. With flex-basis:0 the list was only ever given
   space nothing else wanted, and on a full card there is none -- which is how a
   list of 29 assignments came to be rendered into a box 33 pixels tall. */
.wall-cols { flex: 1 1 55%; min-height: 0; overflow: hidden; }
/* A division MAY run over a column break -- a newspaper does it and it reads
   fine -- but a heading may not be the last thing in a column with its rows
   starting the next one. Keeping whole groups together instead was worse: SAG
   Division's ten wagons are taller than a column, so the group could not be
   placed anywhere and the whole tail of the list fell off the card. */
.wall-cols > .wall-list { overflow: visible; flex: none; }
.wall-list li.wall-div { break-after: avoid; break-inside: avoid; }

/* A division heading: the group name and how much of it is covered. */
.wall-list li.wall-div {
  border-bottom: none; padding: .8vh 0 .2vh;
  align-items: baseline;
}
.wall-list li.wall-div:first-child { padding-top: 0; }
.wall-list li.wall-div .wall-key {
  font-size: clamp(9px, .62vw, 17px); font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--faint);
}
.wall-list li.wall-div .wall-val { font-size: clamp(10px, .68vw, 19px); color: var(--mut); }

/* Two steps of density, chosen server-side from the row count -- the server
   knows how many rows there are and a media query does not.
   The thresholds are low on purpose. A card about 660px tall holds roughly
   nine of these rows in one column once the figures, the two secondary blocks
   and the status strip have taken their share, so "few" has to mean genuinely
   few: at thirteen rows the old single column was already dropping five of
   them, silently, before any of this was grouped. */
[data-rows='some'] .wall-cols, [data-rows='many'] .wall-cols {
  column-count: 2; column-gap: 1.4vw; column-fill: balance;
}
[data-rows='many'] .wall-cols { column-count: 3; }
[data-rows='some'] .wall-key, [data-rows='some'] .wall-val,
[data-rows='many'] .wall-key, [data-rows='many'] .wall-val {
  font-size: clamp(11px, .8vw, 23px);
}
[data-rows='many'] .wall-key, [data-rows='many'] .wall-val {
  font-size: clamp(10px, .68vw, 19px);
}
[data-rows='some'] :where(.wall-list) li { padding: .25vh 0; }
[data-rows='many'] :where(.wall-list) li { padding: .16vh 0; }
/* The three big figures give up a little too. They are read first and from
   furthest away, so they stay the largest thing on the card -- but the last
   division was falling off the third column for want of about this much, and
   a number nobody can act on is worth less than the row it displaced. */
[data-rows='many'] .wall-figure b { font-size: clamp(20px, 1.95vw, 56px); }
[data-rows='many'] .wall-figure-row { margin-bottom: .5vh; }
/* The kind suffix and the UNSTAFFED word both go: a red row already says it is
   short, and "· vehicle" after every wagon costs a column's width to repeat
   what the dot in front of it means. */
[data-rows='some'] .wall-kind, [data-rows='many'] .wall-kind { display: none; }
[data-rows='many'] .wall-sub:empty { display: none; }

.wall-empty { color: var(--mut); font-size: clamp(13px, 1.05vw, 31px); padding: 1vh 0; }

.wall-foot {
  display: flex; align-items: center; gap: 1vw;
  font-family: var(--mono); font-size: clamp(10px, .72vw, 21px); color: var(--faint);
}
.wall-foot .wall-code { margin-left: auto; }
/* The console's own staleness chip, on the wall's scale so a dispatcher
   glancing between the two screens does not have to learn two of them. */
.chip.stale-warn { border-color: var(--warn-line); color: var(--warn); background: var(--warn-bg); }
.chip.stale-bad  { border-color: var(--crit-line); color: var(--crit); background: var(--crit-bg); font-weight: 700; }
.wall-foot .stale-warn { color: var(--warn); font-weight: 700; }
.wall-foot .stale-bad { color: var(--crit); font-weight: 700; }
/* A board nobody can trust should look like one from across the room. */
body.wall[data-stale] { outline: .5vh solid var(--crit); outline-offset: -.5vh; }

@media (max-width: 1100px) {
  .wall-grid, .wall-grid.has-sched {
    grid-template-columns: 1fr; grid-template-areas: 'staff' 'map' 'inc' 'sched'; overflow: auto;
  }
  .wall-map-card { min-height: 46vh; }
  body.wall { height: auto; overflow: auto; }
}

/* Controls on the big board: present when someone is at the machine, gone
   otherwise. A wallboard with a button parked on it is a wallboard with a
   button on it for the next twelve hours. */
.wall-controls {
  position: fixed; right: 1vw; bottom: 1vh; z-index: 30;
  opacity: 0; pointer-events: none; transition: opacity .18s ease;
}
body.wall[data-hands-on] .wall-controls { opacity: 1; pointer-events: auto; }
:where(.wall-controls) button {
  padding: 10px 16px; border-radius: 5px; cursor: pointer;
  background: var(--panel); border: 1px solid var(--line2); color: var(--ink);
  font: 600 clamp(11px, .75vw, 20px)/1 var(--sans);
}
.wall-controls button:hover { border-color: var(--faint); }

/* ---- ICS 207 layout on screen -------------------------------------------
   The connector geometry is the whole trick. Each tier drops a stem from its
   parent, runs a horizontal bar across its children, and each child raises a
   stub to meet it. The bar has to start and end at the CENTER of the first and
   last box, not at the edge of the row, so it is inset by half a child: with n
   equal children that is 100%/(2n), which is what --n carries.
------------------------------------------------------------------------- */
.ics-scroll { overflow-x: auto; }
.ics-chart { min-width: 760px; padding: 4px 2px 2px; }

.ics-tier {
  position: relative; display: flex; justify-content: center; align-items: flex-start;
  gap: 12px; padding-top: 22px;
}
.ics-tier:first-child { padding-top: 0; }
/* Stem down from the tier above. */
.ics-tier:not(:first-child)::before {
  content: ''; position: absolute; top: 0; left: 50%;
  width: 2px; height: 11px; background: var(--line2);
}
/* Bar across the children, center of first to center of last. */
.ics-tier:not(:first-child)::after {
  content: ''; position: absolute; top: 11px;
  left: calc(100% / (2 * var(--n, 1))); right: calc(100% / (2 * var(--n, 1)));
  height: 2px; background: var(--line2);
}
.ics-tier[style*="--n:1"]::after { display: none; }

.ics-tier > * { position: relative; flex: 1 1 0; min-width: 0; max-width: 300px; }
/* Stub up from each child to the bar. */
.ics-tier:not(:first-child) > *::before {
  content: ''; position: absolute; top: -11px; left: 50%;
  width: 2px; height: 11px; background: var(--line2);
}

.ics-box {
  border: 1.5px solid var(--line2); border-radius: 3px; background: var(--panel);
  padding: 6px 9px; min-height: 46px;
}
.ics-box.strong { border-color: var(--ink); border-width: 2px; }
.ics-box.vacant { border-style: dashed; background: transparent; }
/* A division is an Operations unit, and wears Operations. This was --info
   blue, which is the Planning family, so the one part of the chart that is
   unambiguously Operations was the part drawn in another section's colour. */
.ics-box.division { border-color: var(--ics-ops-line); background: var(--ics-ops-bg); }
.ics-box.division .ics-slot { color: var(--ics-ops); }

/* The vest colour of the slot.
 *
 * BEFORE .strong and .vacant on purpose, so both still win: a section chief
 * keeps the heavier ink border that marks the top tier, and a vacant box stays
 * dashed and unfilled. A vacant box that took a solid section fill would read
 * as staffed, which is the one thing this chart exists to make obvious.
 *
 * The slot title carries the colour too, because at 9.5px in --faint it is the
 * part a reader scans and it was the least legible thing in the box. */
.ics-box.sec-operations { border-color: var(--ics-ops-line);  background: var(--ics-ops-bg); }
.ics-box.sec-planning   { border-color: var(--ics-plan-line); background: var(--ics-plan-bg); }
.ics-box.sec-logistics  { border-color: var(--ics-log-line);  background: var(--ics-log-bg); }
.ics-box.sec-finance    { border-color: var(--ics-fin-line);  background: var(--ics-fin-bg); }
.ics-box.sec-operations .ics-slot { color: var(--ics-ops); }
.ics-box.sec-planning   .ics-slot { color: var(--ics-plan); }
.ics-box.sec-logistics  .ics-slot { color: var(--ics-log); }
.ics-box.sec-finance    .ics-slot { color: var(--ics-fin); }
/* Command wears no hue, the same decision the chips make. */
.ics-box.sec-command .ics-slot { color: var(--ink); }
.ics-box.vacant { background: transparent; }
.ics-slot {
  font-size: 9.5px; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.ics-who { display: block; font-size: 13px; font-weight: 600; margin-top: 2px; color: var(--ink); }
.ics-who.vacant { color: var(--faint); font-weight: 500; font-style: italic; }
.ics-call { font-size: 11px; font-weight: 500; color: var(--mut); }
.ics-sub { font-size: 11px; color: var(--mut); margin-top: 2px; }

/* A slot somebody covers alongside their own post, and the dotted lines into
 * it. Both are drawn under the box rather than as lines across the grid: a 207
 * gets printed, and a routed line does not survive the page break. */
.ics-second {
  display: inline-block; margin-left: 5px; padding: 0 4px; border-radius: 2px;
  font-size: 9px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--mut); border: 1px dashed var(--line2); vertical-align: middle;
}
.ics-dotted { margin-top: 5px; padding-top: 4px; border-top: 1px dashed var(--line2); }
.ics-dotted-row { font-size: 11px; color: var(--mut); line-height: 1.45; }
.ics-dotted-role {
  display: block; font-size: 9.5px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--faint);
}

/* A box you can fill. The whole box is the target, including a vacant one:
   the empty slot is the one that needs doing something about, and it used to
   be the only box on the chart that could not be clicked. Kept quiet at rest
   so the chart still reads as a 207 rather than as a page of buttons. */
.ics-box.ics-link {
  display: block; text-decoration: none; color: inherit; cursor: pointer;
  transition: border-color .12s, background-color .12s;
}
.ics-box.ics-link:hover, .ics-box.ics-link:focus-visible {
  border-color: var(--info); background: var(--info-bg); text-decoration: none;
}
.ics-box.ics-link:hover .ics-who.vacant { color: var(--info); font-style: normal; }

/* The command block: incident commander on the left with an optional deputy
   beneath, and the command staff stacked on their own stem to the right. This
   is the form's geometry and the form means something by it. Safety, PIO and
   Liaison advise the IC and sit in nobody's chain, which is what a stem to the
   side says and what a tier underneath does not. */
/* A three-column grid, not a centered flex row: the IC has to sit on the
   chart's center line so the sections bar below hangs from it. A flex row
   centers the IC and the staff column together, which leans the whole chart
   to the left by half the staff width. */
.ics-command {
  position: relative; display: grid; align-items: start; padding-bottom: 4px;
  grid-template-columns: minmax(0, 1fr) min(300px, 34%) minmax(0, 1fr);
}
.ics-cmd-stack { grid-column: 2; display: flex; flex-direction: column; }
.ics-deputy { position: relative; margin-top: 16px; }
.ics-deputy::before {
  content: ''; position: absolute; top: -16px; left: 50%;
  width: 2px; height: 16px; background: var(--line2);
}

/* The staff column hangs off a horizontal stem from the IC box. The stem is
   drawn on the column so it lands at the height of the first staff box, which
   is where the form puts it. */
.ics-staff {
  grid-column: 3; justify-self: start;
  position: relative; display: flex; flex-direction: column; gap: 8px;
  width: min(260px, 100%); margin-left: 48px; margin-top: 4px;
}
.ics-staff::before {
  content: ''; position: absolute; top: 22px; left: -48px;
  width: 48px; height: 2px; background: var(--line2);
}
/* Down the left edge of the staff column, so the second and third boxes are
   visibly on the same stem rather than floating. */
.ics-staff::after {
  content: ''; position: absolute; left: -1px; top: 22px; bottom: 22px;
  width: 2px; background: var(--line2);
}
.ics-staff > * { position: relative; }
.ics-staff > * + *::before {
  content: ''; position: absolute; top: 22px; left: -9px;
  width: 9px; height: 2px; background: var(--line2);
}

/* The sections bar hangs from the IC, not from the middle of the whole block,
   because the staff column is off to one side and the chart would lean. */
.ics-sections { padding-top: 26px; }

.ics-col { display: flex; flex-direction: column; gap: 0; }
/* Units hang under their chief in a column, each with its own short stem. */
/* THE UNITS UNDER A SECTION CHIEF ARE SIBLINGS, NOT A CHAIN.
 *
 * They stacked with a short stem between each pair, center to center, which
 * draws a line from the bottom of one box into the top of the next -- and a
 * line from one box to another on an org chart means exactly one thing.
 * Reported from the field: "the way these are stacked makes it look like D2
 * reports to D1". Route Division 2 does not report to Route Division 1; every
 * one of them reports to Operations.
 *
 * So: one spine hanging off the chief, and an elbow into each box. The chief
 * above is full width so its stem leaves from the center and runs left to the
 * spine, which then descends and STOPS at the last elbow rather than running
 * on into nothing. It reads correctly at any number of units, and it is drawn
 * with borders rather than routed lines, so it survives being printed.
 */
.ics-units {
  position: relative; display: flex; flex-direction: column;
  gap: 14px; margin-top: 20px; padding-left: var(--arm, 22px);
}
/* Down from the chief... */
.ics-units::before {
  content: ''; position: absolute; top: -20px; left: 50%;
  width: 2px; height: 10px; background: var(--line2);
}
/* ...and across to where the spine hangs. */
.ics-units::after {
  content: ''; position: absolute; top: -10px; left: 0;
  width: 50%; height: 2px; background: var(--line2);
}
.ics-units > * { position: relative; }
/* The spine, in per-child segments so it needs nobody's height in advance.
   Each runs from the gap above it down through its own box and meets the next
   one exactly at the box edge. */
.ics-units > *::before {
  content: ''; position: absolute; left: calc(var(--arm, 22px) * -1); top: -14px;
  width: 2px; height: calc(100% + 14px); background: var(--line2);
}
.ics-units > *:first-child::before { top: -10px; height: calc(100% + 10px); }
/* The last one stops at its own elbow: a spine that runs past the last box is
   a line to something that is not there. */
.ics-units > *:last-child::before { height: calc(22px + 14px); }
.ics-units > *:first-child:last-child::before { height: calc(22px + 10px); }
/* Into the box, level with its title rather than its middle, so a tall box and
   a short one are entered at the same place. */
.ics-units > *::after {
  content: ''; position: absolute; left: calc(var(--arm, 22px) * -1); top: 22px;
  width: var(--arm, 22px); height: 2px; background: var(--line2);
}

/* ------------------------------------------------------------- alerts -- */

/* The tab badge. Positioned on the icon rather than the label so it does not
   move when the label wraps, and sized so a thumb can still hit the tab. */
.ti { position: relative; display: inline-flex }
.tbadge {
  position: absolute; top: -5px; left: 11px;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--accent); color: var(--on-ink);
  font-size: 10.5px; font-weight: 700; line-height: 16px; text-align: center;
  font-variant-numeric: tabular-nums;
}
.tbadge.urg { background: var(--crit) }

/* One alert in the inbox. Unread carries a left rule and a heavier ground;
   read fades back so the eye lands on what has not been dealt with. */
.alert { border: 1px solid var(--line); border-radius: 8px; padding: 11px 13px; margin-bottom: 9px;
         background: var(--card) }
.alert.unread { border-left: 3px solid var(--accent); background: var(--fill) }
.alert.unread.urg { border-left-color: var(--crit) }
.alert.read { opacity: 0.72 }
.alert .ah { display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px }
.alert .at { font-size: 14.5px; font-weight: 650; line-height: 1.25 }
.alert .ab { font-size: 13.5px; line-height: 1.45; color: var(--ink) }
.alert .am { font-size: 11.5px; color: var(--mut); margin-top: 5px;
             display: flex; gap: 10px; flex-wrap: wrap; align-items: center }

/* A list of people to tick. Multi-column on a wide board, one column on a
   phone, and the whole row is the hit target so a gloved thumb can use it. */
.checks {
  display: grid; gap: 2px;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  max-height: 260px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: 8px; padding: 6px;
  background: var(--card);
}
/* Sized off --tap, not off the cookie.
   These were written as html[data-touch="1"] overrides, which is only ONE of
   the two triggers: --tap is also set by (max-width: 820px) and (pointer:
   coarse), so a volunteer opening this on their own phone with no cookie got
   desktop-sized targets while every .btn and table row around them had already
   grown. The stylesheet says this out loud further up and these are the copies
   that drifted.
   The box is also written as an attribute selector so it beats
   input[type="checkbox"] by specificity rather than by appearing later in the
   file. Both were 0,1,1 and source order was quietly deciding, which is not a
   thing anybody can see while reading either one. */
.chk { display: flex; align-items: center; gap: 8px; border-radius: 6px; cursor: pointer;
       padding: calc(6px + var(--tap) * 5px) calc(8px + var(--tap) * 2px);
       font-size: calc(13.5px + var(--tap) * 1.5px) }
.chk:hover { background: var(--fill) }

/* A CONSENT LINE NEVER WRAPS.
   .row picks up flex-wrap: wrap on a narrow viewport, which is right for a
   toolbar and wrong here: it put the tick box on one line and the sentence it
   governs on the next, so the box appeared to belong to nothing. A carrier
   reads this screen as evidence, and a checkbox floating above its own label
   is exactly the ambiguity they rejected the first version for.
   A two-column grid cannot wrap. Declared after .chk so it wins the tie on
   source order, which is the same rule the status swatches live by. */
.consent-line {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: 11px; align-items: start; cursor: pointer;
}
/* muster:align-ok this one has to keep its rank. input[type="checkbox"] is
   also 0,1,1, and the attribute selector here is what makes this beat it by
   specificity instead of by appearing later in the file. Wrapping .chk in
   :where() would drop it to 0,0,1 and hand the tie straight back. */
.chk input[type="checkbox"], .chk input[type="radio"] {
  width: calc(17px + var(--tap) * 5px); height: calc(17px + var(--tap) * 5px); flex: none }

/* ----------------------------------------------- the guest dashboard --

   Shares body.wall's dark ground and viewport-relative type with the big
   board, and nothing else: the big board is a screen in a room you control and
   shows what the command board shows, this one goes to a stranger and shows
   counts. Same pixels, opposite privacy posture, so separate classes.       */

/* body.wall is a fixed-height flex column that never scrolls, which is right
   for a projector and wrong for the tablet a duty officer checks this on. */
body.wall.guest {
  display: block; height: auto; min-height: 100vh; overflow: auto;
  padding: clamp(14px, 1.6vw, 40px);
}

.wtop { display: flex; align-items: flex-start; justify-content: space-between;
        gap: 20px; padding-bottom: clamp(10px, 1vw, 22px);
        border-bottom: 1px solid var(--line); margin-bottom: clamp(14px, 1.4vw, 30px) }
.wname { font-size: clamp(20px, 2.1vw, 60px); font-weight: 700; line-height: 1.1 }
.wsub { font-size: clamp(12px, 1vw, 26px); color: var(--mut); margin-top: 4px }
.wclock { font-family: var(--mono); font-size: clamp(24px, 2.6vw, 76px); font-weight: 700;
          line-height: 1; font-variant-numeric: tabular-nums }

/* The readings. auto-fit rather than a fixed count, so the row fills a 4K
   projector and stacks sanely on the tablet somebody checks it from. */
.wrow { display: grid; gap: clamp(8px, 0.9vw, 20px); margin-bottom: clamp(14px, 1.4vw, 30px);
        grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr)) }
.wstat { border: 1px solid var(--line); border-radius: 10px; background: var(--card);
         padding: clamp(10px, 1vw, 26px) }
.wstat .wn { font-family: var(--mono); font-size: clamp(30px, 3.4vw, 104px); font-weight: 700;
             line-height: 1; font-variant-numeric: tabular-nums }
.wstat .wl { font-size: clamp(11px, 0.95vw, 24px); color: var(--mut); margin-top: 6px;
             text-transform: uppercase; letter-spacing: 0.05em }
.wstat.ok   { border-color: var(--ok-line) }
.wstat.warn { border-color: var(--warn-line) } .wstat.warn .wn { color: var(--warn) }
.wstat.crit { border-color: var(--crit-line) } .wstat.crit .wn { color: var(--crit) }

.wcols { display: grid; gap: clamp(10px, 1vw, 24px);
         grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)) }
.wpanel { border: 1px solid var(--line); border-radius: 10px; background: var(--card);
          padding: clamp(10px, 1vw, 26px) }
.wpanel h2 { font-size: clamp(13px, 1.1vw, 28px); font-weight: 700; margin-bottom: 8px;
             text-transform: uppercase; letter-spacing: 0.05em; color: var(--mut) }
.wempty { font-size: clamp(12px, 1vw, 24px); color: var(--mut); padding: 6px 0 }
.wtbl { width: 100%; border-collapse: collapse; font-size: clamp(13px, 1.15vw, 30px) }
.wtbl td { padding: clamp(4px, 0.4vw, 12px) 0; border-bottom: 1px solid var(--line) }
.wtbl tr:last-child td { border-bottom: 0 }
.wtbl tr.gap td { color: var(--crit); font-weight: 650 }
.wfoot { font-size: clamp(11px, 0.9vw, 22px); color: var(--mut); margin-top: 10px }
.wbot { font-size: clamp(11px, 0.9vw, 22px); color: var(--mut); margin-top: clamp(14px, 1.4vw, 30px);
        padding-top: clamp(8px, 0.8vw, 18px); border-top: 1px solid var(--line) }

/* Stale is the one thing this screen shouts, because nobody is standing next
   to it and often nobody in the room knows who to call when it stops. */
body.wall.guest.stale .wclock { color: var(--crit) }
body.wall.guest.stale .wtop { border-bottom-color: var(--crit-line) }

/* ---------------------------------------------- the full post editor --

   The rail form on the Posts board is right for standing something up in a
   hurry. This is for planning: the map gets half the screen because placing
   the post is the work, and a strip under a fold is not a map.               */

.post-full { display: grid; gap: 16px; align-items: start;
             grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr) }
@media (max-width: 900px) {
  .post-full { grid-template-columns: minmax(0, 1fr) }
  .post-full-map { order: 2 }
}
.post-full-map { position: sticky; top: 12px;
                 border: 1px solid var(--line); border-radius: 10px;
                 background: var(--card); padding: 12px }
.post-map.big { height: min(62vh, 620px); min-height: 380px }

/* Corner handles want a grab cursor: a circle you can drag has to look like
   one before anybody tries. */
.leaflet-interactive.leaflet-marker-draggable,
.post-map .leaflet-interactive { cursor: grab }
.post-map .leaflet-interactive:active { cursor: grabbing }

/* -------------------------------------------------- button pickers --

   Radios that look and hit like buttons. A dropdown on a touch screen is a
   tap, a scroll and a second tap, done one handed in a vehicle while somebody
   is talking on the radio. These are still real radio inputs, so the keyboard
   works, the form submits without JavaScript, and required is enforced by the
   browser rather than by us.                                              */

.pickbtn { border: 0; padding: 0; margin: 0 0 12px; display: block }
.pickbtn legend { padding: 0; margin-bottom: 6px }
:where(.pickbtn) label {
  display: inline-flex; align-items: center; gap: 0; cursor: pointer;
  margin: 0 calc(6px + var(--tap) * 2px) calc(6px + var(--tap) * 2px) 0;
}
/* muster:align-ok not a control box. The radio is visually hidden and the
   <span> beside it is the thing with a size; a zero box is the point. */
.pickbtn input { position: absolute; opacity: 0; width: 0; height: 0 }
.pickbtn span {
  display: inline-block; border-radius: 7px;
  padding: calc(8px + var(--tap) * 5px) calc(12px + var(--tap) * 4px);
  border: 1px solid var(--line2); background: var(--card);
  font-size: calc(13px + var(--tap) * 2px); line-height: 1.2; white-space: nowrap;
}
.pickbtn label:hover span { background: var(--fill) }
.pickbtn input:checked + span {
  border-color: var(--accent); background: var(--accent); color: var(--on-ink); font-weight: 600;
}
/* Visible focus, because these are the controls somebody tabs through at
   speed and an invisible focus ring is a wrong entry waiting to happen. */
.pickbtn input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px }

.pickbtn.pri4 span b { font-family: var(--mono); margin-right: 5px }
.pickbtn .t-crit input:checked + span { border-color: var(--crit); background: var(--crit) }
.pickbtn .t-hot  input:checked + span { border-color: var(--hot);  background: var(--hot) }
.pickbtn .t-warn input:checked + span { border-color: var(--warn); background: var(--warn); color: var(--on-ink) }
.pickbtn .t-off  input:checked + span { border-color: var(--mut);  background: var(--mut) }

/* Folded into the --tap calcs above, so a phone with no cookie gets these
   sizes too. This is the incident report form in the field app. */

/* ------------------------------------------- the command view on a phone --

   The command console assumes a laptop: a sticky bar with twelve tabs, three
   columns, and tables eleven columns wide. On a phone that is a horizontal
   scroll in every direction, and the person holding the phone is usually the
   one who needed it most, standing somewhere, with the laptop in the vehicle.

   Nothing is hidden and nothing becomes a separate page. The same routes, the
   same markup, laid out for the width that is actually there.               */

@media (max-width: 820px) {
  /* The bar becomes two rows: identity and controls on top, the tab strip
     underneath where a thumb can reach it and scroll it sideways. */
  .top {
    height: auto; min-height: 52px; flex-wrap: wrap; padding: 8px 12px; gap: 10px;
    row-gap: 8px;
  }
  .top .vr { display: none; }
  .nav {
    order: 10; flex-basis: 100%; gap: 4px;
    margin: 0 -12px -8px; padding: 0 12px 6px;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .nav::-webkit-scrollbar { display: none; }
  .brand { font-size: 11px; }

  /* Every multi-column layout on the console is an inline grid-template, set
     per page. One rule beats all of them, which is the only way to do this
     without editing a dozen views and hoping the next one remembers. */
  .cols { grid-template-columns: minmax(0, 1fr) !important; }

  /* Wide tables scroll inside their own box rather than the page. A page that
     scrolls sideways loses the nav and the header with it. */
  .tbl-wrap, .panel .tbl-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tbl-wrap table { min-width: 620px; }

  /* Header rows on every console page are a row of chips and buttons that were
     built assuming they fit. */
  .wrap { padding-left: 12px; padding-right: 12px; }

  /* A control that was next to a table cell is now under it. */
  .more-menu { right: 0; left: auto; min-width: 180px; }
  /* Header rows stop shoving the page sideways.
     Seventy of these are a heading with chips and buttons after it, laid out
     at a width where they fit. Twenty-two had already been patched with an
     inline flex-wrap, one at a time, which is the tell: the missing thing was
     this rule, not those twenty-two rows. */
  .row { flex-wrap: wrap; row-gap: 8px; }
  .row > input, .row > select, .row > textarea { min-width: 0; }
  .ph { flex-wrap: wrap; row-gap: 6px; }

  /* Small caps at 10.5px is right at a desk and gone at arm's length. These
     are labels, so nothing reflows when they grow. */
  .ph { font-size: 11.5px; }
  .lbl { font-size: 11px; }

  /* Safari zooms the entire page when a field smaller than 16px takes focus,
     and it does not zoom back out afterwards. The nav and the header go with
     it, so one tap on a search box costs you the page. */
  .inp, select.inp, input.inp, textarea.inp { font-size: 16px; }

  /* The identity column stays put while the row is swiped.
     Every one of these tables leads with what the row IS: a name, an item, a
     post, a time. A status you cannot attach to a name is not worth reading,
     and at this width the table is always scrolled. The last column is stuck
     for the same reason at the other end.

     Rows carrying their own background are left alone: an opaque cell would
     paint over the tint that says a 214 entry was written by the system. */
  .tbl-wrap table tr:not([style*="background"]) > *:first-child {
    position: sticky; left: 0; background: var(--panel);
    box-shadow: 6px 0 6px -6px rgb(0 0 0 / .18);
  }
  .tbl-wrap table thead tr > *:first-child { background: var(--fill); }
}

/* Below a phone's width the shared post editor stops being two columns and the
   map goes first, because placing the post is what somebody is here to do. */
@media (max-width: 640px) {
  .post-full-map { position: static; }
  .post-map.big { height: 46vh; min-height: 260px; }
}

/* --------------------------------------------- status buttons -----------

   Color AND shape. A board gets read at arm's length in daylight by somebody
   who may be color blind, and green against blue is the pair that goes first.
   The icon carries the meaning; the color makes it quick.

   All of them on screen rather than two and an overflow menu. That menu was
   absolutely positioned inside a horizontally scrolling table, so on a roster
   with one row it was clipped by its own container and the options underneath
   could not be reached at all.                                             */

.sbtns { display: flex; flex-wrap: wrap; gap: 4px; }
.sbtn-form { display: contents; }

.sbtn {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: calc(28px + var(--tap) * 10px);
  padding: 0 calc(8px + var(--tap) * 3px);
  border: 1px solid var(--line2); border-radius: 6px;
  background: var(--panel); color: var(--ink);
  font: 600 calc(11px + var(--tap) * 1.5px)/1 var(--sans);
  white-space: nowrap; cursor: pointer;
}
.sbtn svg { flex: none; color: var(--sc, var(--mut)); }
.sbtn:hover { background: var(--fill); border-color: var(--sc, var(--line2)); }
.sbtn:focus-visible { outline: 2px solid var(--sc, var(--ink)); outline-offset: 1px; }

/* A colored left edge rather than a fully colored button. Nine saturated
   buttons in a row is a Christmas tree, and the thing being looked for is the
   one color, not all of them. */
.sbtn { border-left-width: 4px; border-left-color: var(--sc, var(--line2)); }

.sbtn.big {
  min-height: calc(48px + var(--tap) * 8px); width: 100%; justify-content: flex-start;
  font-size: calc(14px + var(--tap) * 1px); padding: 0 14px; gap: 10px;
}
.sbtn.big svg { width: 20px; height: 20px; }

/* Each button names the color of the status it SETS: somebody hunting for
   "on break" is looking for purple. One custom property, set per class, so the
   border, the icon and the focus ring all follow it. */
.sbtn.s-not_arrived    { --sc: var(--s-registered); }
.sbtn.s-mobilizing     { --sc: var(--s-mobilizing); }
.sbtn.s-en_route       { --sc: var(--s-en_route); }
.sbtn.s-checked_in     { --sc: var(--s-checked_in); }
.sbtn.s-available      { --sc: var(--s-available); }
.sbtn.s-at_post        { --sc: var(--s-at_post); }
.sbtn.s-assigned       { --sc: var(--s-assigned); }
.sbtn.s-break          { --sc: var(--s-break); }
.sbtn.s-out_of_service { --sc: var(--s-out_of_service); }
.sbtn.s-off_duty       { --sc: var(--s-off_duty); }
.sbtn.s-released       { --sc: var(--s-released); }

/* Compact: the icon alone, with the label as the tooltip. For a table cell
   where nine labeled buttons would push the row off the screen.
   
   ON A TOUCHSCREEN THERE IS NO TOOLTIP. A command post laptop is usually a
   touchscreen and touch mode is exactly when a recorder is working the roster
   with a finger, so hiding the label behind a hover meant picking from memory
   among seven glyphs, one of which is Released. In touch mode the label comes
   back and the buttons wrap, which costs a row of height and is worth it. */
.sbtns.compact .sbtn { padding: 0 calc(7px + var(--tap) * 2px); gap: 0; }
/* muster:align-ok the subject is the label text inside a control, not a
   control. It is clipped to a 1px box so the button reads as icon-only,
   and nothing will ever put a control class on it. */
.sbtns.compact .sbtn span { position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

html[data-touch="1"] .sbtns.compact .sbtn { gap: 6px; padding: 0 11px; }
/* muster:align-ok the subject is the label text inside a control, not a
   control. It is clipped to a 1px box so the button reads as icon-only,
   and nothing will ever put a control class on it. */
html[data-touch="1"] .sbtns.compact .sbtn span {
  position: static; width: auto; height: auto; overflow: visible;
  clip-path: none; font-size: 11.5px;
}
@media (max-width: 820px) and (pointer: coarse) {
  html:not([data-touch="0"]) .sbtns.compact .sbtn { gap: 6px; padding: 0 11px; }
  /* muster:align-ok the subject is the label text inside a control, not a
   control. It is clipped to a 1px box so the button reads as icon-only,
   and nothing will ever put a control class on it. */
html:not([data-touch="0"]) .sbtns.compact .sbtn span {
    position: static; width: auto; height: auto; overflow: visible;
    clip-path: none; font-size: 11.5px;
  }
}

/* ICON-ONLY EVEN ON TOUCH, for the one place that cannot afford the labels.
 *
 * .sbtns.compact deliberately brings its labels BACK under touch, and that is
 * right where it is used: the roster offers seven statuses, and picking among
 * seven glyphs from memory with a gloved finger is worse than spending a row
 * of height. The board's wagon cards offer two or three, inside the narrowest
 * column on the page, where the labels are exactly what stops the buttons
 * fitting beside the crew. Same control, different budget.
 *
 * Both touch selectors are overridden, because either can be the one in play:
 * data-touch is the explicit setting, and the coarse-pointer query is the
 * fallback when nothing has been chosen. The label is clipped rather than
 * removed, so it is still the button's accessible name. */
html[data-touch="1"] .sbtns.compact.icons .sbtn {
  gap: 0; padding: 0 calc(7px + var(--tap) * 2px);
}
/* muster:align-ok the subject is the label text inside a control, not a
   control. It is clipped to a 1px box so the button reads as icon-only,
   and nothing will ever put a control class on it. */
html[data-touch="1"] .sbtns.compact.icons .sbtn span {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* The same, for a phone that has never been given a touch cookie. Inside the
   coarse-pointer query rather than beside the rule above, which is where the
   .sbtns.compact pair it is overriding keeps its own fallback -- and where
   check-align looks for the --tap-derived box to be restored. */
@media (max-width: 820px) and (pointer: coarse) {
  html:not([data-touch="0"]) .sbtns.compact.icons .sbtn {
    gap: 0; padding: 0 calc(7px + var(--tap) * 2px);
  }
  /* muster:align-ok the subject is the label text inside a control, not a
     control. It is clipped to a 1px box so the button reads as icon-only,
     and nothing will ever put a control class on it. */
  html:not([data-touch="0"]) .sbtns.compact.icons .sbtn span {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}

/* Controls in a wide table's last column sat past the right edge of a 1366px
   laptop once touch mode widened everything, so the roster's check-in buttons
   were reachable only by scrolling the table sideways, at a net desk, mid
   event. Sticking the last column keeps them on screen wherever the table has
   been scrolled to. */
.tbl-wrap table tr > *:last-child.stick {
  position: sticky; right: 0; background: var(--panel);
  box-shadow: -6px 0 6px -6px rgb(0 0 0 / .18);
}
.tbl-wrap table thead tr > *:last-child.stick { background: var(--fill); }

/* The field's own status list, sharing the board's color vocabulary so a
   volunteer and their supervisor are looking at the same thing. */
.status-btn.s-not_arrived    { --sc: var(--s-registered); }
.status-btn.s-mobilizing     { --sc: var(--s-mobilizing); }
.status-btn.s-en_route       { --sc: var(--s-en_route); }
.status-btn.s-checked_in     { --sc: var(--s-checked_in); }
.status-btn.s-available      { --sc: var(--s-available); }
.status-btn.s-at_post        { --sc: var(--s-at_post); }
.status-btn.s-break          { --sc: var(--s-break); }
.status-btn.s-out_of_service { --sc: var(--s-out_of_service); }
.status-btn.s-off_duty       { --sc: var(--s-off_duty); }
.status-btn.s-released       { --sc: var(--s-released); }

.status-btn[class*="s-"] { border-left: 5px solid var(--sc, var(--line2)); }
.status-btn[class*="s-"] svg { color: var(--sc, var(--mut)); flex: none; }
.oos > summary.s-out_of_service { border-left: 5px solid var(--s-out_of_service); }
.oos > summary.s-out_of_service svg { color: var(--s-out_of_service); flex: none; }

/* An inline-flex box aligns on its own first baseline, so a button in a table
   cell sat on the text baseline of the first line. In a cell whose neighbour
   wraps to two lines, that pins it to the top while everything else is
   centered. Aligning on the middle of the line box is what the cell already
   asked for. */
.btn, .chip, .pill, .sbtn { vertical-align: middle; }

/* A cell holding several controls lines them up rather than letting them sit
   on separate baselines. */
td > .row, td > .sbtns { align-items: center; }

/* An outside unit standing at one of our posts. Shown in the staffing column
   because that is where the room looks to answer "who is at the command post",
   and marked so it reads as somebody else's resource rather than as one of
   ours: it is not in the staffed count and cannot be tasked. */
/* One chip per outside unit. A gap between siblings, because with one chip
   per unit two of them sit next to each other and without it they merge back
   into the single box they used to be. */
.wall-outside {
  display: inline-block; margin-left: .35em; padding: 0 .45em;
  border: 1px solid var(--line2); border-radius: 3px;
  color: var(--mut); font-family: var(--mono); font-size: .84em;
}

/* ------------------------------------------- the shared channel library --

   Grouped the way a field operations guide is: by guide, then zone, then band.
   That is the order somebody building a plan works in, pulling the VHF page and
   then the UHF page, and ninety-two channels in one flat list is a list nobody
   opens twice.                                                              */

.libgroup { border: 1px solid var(--line); border-radius: 7px; margin-bottom: 7px;
            background: var(--card) }
:where(.libgroup) > summary {
  display: flex; align-items: center; gap: 9px; padding: 10px 12px;
  cursor: pointer; list-style: none; font-size: 13px;
}
.libgroup > summary::-webkit-details-marker { display: none }
/* A disclosure triangle that turns, so the row reads as openable without
   depending on the platform's own marker, which the flex layout suppresses. */
.libgroup > summary::before {
  content: ''; flex: none; width: 0; height: 0;
  border-left: 5px solid var(--mut);
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform .12s;
}
.libgroup[open] > summary::before { transform: rotate(90deg) }
.libgroup > summary:hover { background: var(--fill) }
.libname { font-weight: 700 }
.libzone { color: var(--mut); font-size: 12px }

/* The link to the guide the channels were transcribed from. A transcription in
   a database is a convenience; the document is the authority, and somebody
   about to transmit on VTAC11 should be one click from checking it. */
.libdoc {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: calc(22px + var(--tap) * 8px); height: calc(22px + var(--tap) * 8px);
  border-radius: 50%;
  border: 1px solid var(--line2); color: var(--mut);
  font-size: 12px; font-weight: 700; text-decoration: none;
}
.libdoc:hover { border-color: var(--info); color: var(--info); text-decoration: none }
/* Folded into the --tap calc above, so a phone with no cookie gets it too. */

.libgroup .tbl-wrap { padding: 0 4px 8px }
.libgroup table { font-size: 12.5px }

/* -------------------------------------------------- adding a channel --

   Three sources for one question, so they read as three answers rather than
   three competing panels: the organization's own standing channels, the shared
   library, or typed by hand. All collapsed, because the comms page is opened to
   READ the plan far more often than to build it, and a reader should not scroll
   past the work of the few people building.                                 */

.addsrc { border: 1px solid var(--line); border-radius: 7px; background: var(--card) }
.addsrc + .addsrc { margin-top: 2px }
:where(.addsrc) > summary {
  display: flex; align-items: center; gap: 10px; padding: 11px 13px;
  cursor: pointer; list-style: none;
}
.addsrc > summary::-webkit-details-marker { display: none }
.addsrc > summary::before {
  content: ''; flex: none; width: 0; height: 0;
  border-left: 5px solid var(--mut);
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform .12s;
}
.addsrc[open] > summary::before { transform: rotate(90deg) }
.addsrc[open] > summary { border-bottom: 1px solid var(--line) }
.addsrc > summary:hover { background: var(--fill) }
.addsrc-name { font-weight: 650; font-size: 13.5px }

/* The panels that used to stand alone now sit inside a disclosure, so they
   drop their own frame and let the disclosure carry it. */
.addsrc .panel { border: 0; border-radius: 0; background: transparent; margin: 0 !important }
.addsrc .panel > .ph { background: transparent; border-bottom: 0; padding-bottom: 4px }

/* The library's own groups are a second layer inside this one. Indented so the
   nesting is legible rather than guessed at. */
.addsrc .libgroup { margin-left: 10px }

/* The band layer, outside the guide groups.
 *
 * Band is the outer collapse because it is the first thing that narrows the
 * problem: somebody knows what radios their people are carrying before they
 * know which guide a channel came out of. It also keeps the section to four
 * rows rather than nine, which matters now that Oklahoma's 700 MHz template
 * alone is eighty channels. */
.bandgroup { border: 1px solid var(--line2); border-radius: 8px; background: var(--panel);
             margin-bottom: 5px }
:where(.bandgroup) > summary {
  display: flex; align-items: center; gap: 10px; padding: 11px 13px;
  cursor: pointer; list-style: none;
}
.bandgroup > summary::-webkit-details-marker { display: none }
.bandgroup > summary::before {
  content: ''; flex: none; width: 0; height: 0;
  border-left: 6px solid var(--ink);
  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
  transition: transform .12s;
}
.bandgroup[open] > summary::before { transform: rotate(90deg) }
.bandgroup[open] > summary { border-bottom: 1px solid var(--line) }
.bandgroup > summary:hover { background: var(--fill) }
.bandname { font-family: var(--mono); font-weight: 700; font-size: 14px; letter-spacing: .02em }

/* The guide groups sit inside the band, indented and without their own heavy
   frame, so the nesting reads as one tree rather than two lists. */
.bandgroup .libgroup { margin: 4px 8px 4px 22px; border-color: var(--line) }
.bandgroup .libgroup:last-child { margin-bottom: 8px }

/* A division's name, written on its boundary.
 *
 * A boundary with no label is a shape nobody can act on: "move to the edge of
 * the purple one" is not an instruction. Non-interactive and unselectable so it
 * never eats a click meant for a post underneath it, and carrying its own
 * background because it sits over map tiles of any color. */
.div-label {
  background: none;
  border: 0;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
}
:where(.div-label) span {
  display: inline-block;
  transform: translate(-50%, -50%);
  padding: 2px 7px;
  border-radius: 3px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--purp);
  background: color-mix(in srgb, var(--panel) 86%, transparent);
  border: 1px solid color-mix(in srgb, var(--purp) 45%, transparent);
}

/* A post pin.
 *
 * Auto width, because a post identifier is four or five characters ("AID1",
 * "NG2") and the old pin was a fixed 26px square holding three. It also took
 * the LAST three characters, so AID1 drew as "ID1" and threw away the part that
 * says what it is.
 *
 * The color belongs to whoever planned the event. Staffing keeps its own
 * signal, the red ring, so a nicely colored aid station can never hide the
 * fact that nobody is standing on it. */
.post-pin-wrap { background: none; border: 0; }
.post-pin {
  display: inline-flex; align-items: center; gap: 3px;
  transform: translate(-50%, -50%);
  min-width: 26px; height: 24px; padding: 0 6px;
  border: 2px solid var(--pin, var(--ink));
  border-radius: 3px;
  background: var(--panel);
  color: var(--pin, var(--ink));
  font: 600 11px/1 'IBM Plex Mono', ui-monospace, monospace;
  letter-spacing: .02em;
  white-space: nowrap;
}
.post-pin svg {
  width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Short-staffed. A ring rather than a recolour, so it reads on top of whatever
   color the post was given. */
.post-pin.gap {
  box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--crit);
}

/* The same mark in the post form and the legend, at rest. */
.sym-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 3px;
  border: 1.5px solid var(--line2); background: var(--panel); color: var(--mut);
}
.sym-chip svg {
  width: 15px; height: 15px;
  fill: none; stroke: currentColor; stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
}

/* A point of interest: the taco truck, a road closure, a staging area.
 *
 * Quieter than a post pin and carrying its NAME rather than a code, because a
 * feature is read once ("where is the water?") rather than referred to on the
 * radio the way a post is. Rounded, so the two shapes are distinguishable at a
 * glance across a room. */
.poi-pin {
  display: inline-flex; align-items: center; gap: 4px;
  transform: translate(-50%, -50%);
  max-width: 22ch; height: 22px; padding: 0 8px;
  border: 1.5px solid var(--pin, var(--teal));
  border-radius: 11px;
  background: var(--panel);
  color: var(--pin, var(--teal));
  font-size: 11px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.poi-pin svg {
  width: 12px; height: 12px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
}

/* One panel that stands up either a post or a team.
 *
 * Radios and sibling selectors rather than a script: the toggle is
 * presentation, it works before any JavaScript has loaded, and it cannot get
 * out of step with the form it is showing. The radios are visually hidden but
 * still focusable, so the choice is reachable from the keyboard and announced
 * as the radio group it actually is. */
.new-assignment .nk-radio {
  position: absolute;
  width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.nk-tabs { display: flex; gap: 6px; align-items: center; }
.nk-tabs label { cursor: pointer; }

/* Only the chosen one is drawn. */
#nk-post:checked ~ .nk-body-team,
#nk-team:checked ~ .nk-body-post { display: none; }

/* And the chosen tab reads as chosen. Focus is shown on the LABEL, since the
   input it belongs to is the thing that is visually hidden. */
#nk-post:checked ~ .nk-tabs label[for="nk-post"],
#nk-team:checked ~ .nk-tabs label[for="nk-team"] {
  background: var(--ink); color: var(--on-ink); border-color: var(--ink); font-weight: 600;
}
#nk-post:focus-visible ~ .nk-tabs label[for="nk-post"],
#nk-team:focus-visible ~ .nk-tabs label[for="nk-team"] {
  outline: 2px solid var(--info); outline-offset: 2px;
}

/* A team on the map.
 *
 * Rounded, so a body that moves is not read as a place at a glance. Dashed when
 * it is only PARKED at a staging post rather than reporting a position of its
 * own: a wagon waiting at staging and a wagon working that corner are different
 * facts and the map should not state the stronger one. */
.team-pin {
  display: inline-flex; align-items: center; gap: 3px;
  transform: translate(-50%, -50%);
  min-width: 28px; height: 24px; padding: 0 8px;
  border: 2px solid var(--pin, var(--teal));
  border-radius: 12px;
  background: var(--panel);
  color: var(--pin, var(--teal));
  font: 700 11px/1 'IBM Plex Mono', ui-monospace, monospace;
  white-space: nowrap;
}
.team-pin.staged { border-style: dashed; font-weight: 600; }
.team-pin svg {
  width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.1;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ---- weather ------------------------------------------------------------
   Conditions live beside the clock on the big board, because that corner is
   where somebody already looks to answer "what time is it and what is it
   doing". The alert bar sits above everything, because it is the only thing
   on the screen that can end the event. */

.wall-wx { display: flex; align-items: center; gap: 18px; }
.wall-wx-now { display: flex; align-items: center; gap: 10px; }
/* Fahrenheit is the reading and Celsius is the footnote, so the sizes say so.
   Both are present because an IAP and a board go to people who read one or the
   other; only one of them is meant to carry across the room. */
.wall-wx-temps { display: flex; flex-direction: column; align-items: flex-start; line-height: 1; }
.wall-wx-temp { font-size: 34px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.wall-wx-alt { font-size: 11px; color: var(--mut); font-variant-numeric: tabular-nums; margin-top: 2px; }
/* The scale letter beside a temperature, at the size of something you check
   rather than something you read. */
.wall-wx-scale { font-size: 0.62em; font-weight: 600; letter-spacing: 0.02em; }
.wall-wx-side { display: flex; flex-direction: column; gap: 2px; font-size: 12px; color: var(--mut); max-width: 20ch; }
.wall-wx-side span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.wall-wx-hours { display: flex; gap: 14px; }
.wall-wx-hour { display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 46px; }
.wall-wx-at { font-family: var(--mono); font-size: 11px; color: var(--mut); }
.wall-wx-t { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.wall-wx-hour .wall-wx-alt { font-size: 10.5px; margin-top: 0; }
.wall-wx-pop { font-family: var(--mono); font-size: 10.5px; color: var(--info); min-height: 13px; }

/* One bar per alert. Severity decides the treatment: a bar that is always red
   is a bar nobody reads, so only Severe and Extreme get the loud one. */
.wx-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 14px; margin-bottom: 10px; border-radius: 4px;
  border: 1px solid var(--line2); background: var(--fill); color: var(--ink);
  font-size: 14px;
}
.wx-bar-text { flex: 1; min-width: 0; }
.wx-bar-text b { font-weight: 700; }
.wx-bar-when { color: var(--mut); font-size: 12.5px; }
/* The hue is the hazard and the intensity is the level, which is the
   convention people already read off a television: red is a tornado, yellow a
   thunderstorm, green flooding, and a watch is the pale version of the warning
   it would become. Each hazard names three colors and the level picks among
   them, so adding a hazard is one line rather than three rules. */
/* The hazard colours live on :root as well as on the classes, because the map
   needs them too: a Leaflet polygon is drawn from JavaScript and cannot read a
   custom property off a class it is not wearing. One definition, two readers. */
:root {
  --wx-tornado-emergency: #e0489a; --wx-tornado-pds: #7f1010; --wx-tornado: #c62828;
  --wx-svr-considerable: #e2711d;  --wx-svr: #e0b400;
  --wx-flashflood: #5cb800;        --wx-flood: #2e7d32;
  --wx-winter: #3f7fd0;            --wx-heat: #d2601a;
  --wx-wind: #8d6e63;              --wx-special: #c2a878;  --wx-other: #607d8b;
}

.wx-h-tornado-emergency { --wx: var(--wx-tornado-emergency); --wx-pale: #fbe0ef; --wx-ink: #7a1f4c; }
.wx-h-tornado-pds       { --wx: var(--wx-tornado-pds); --wx-pale: #f0d4d4; --wx-ink: #5c0d0d; }
.wx-h-tornado           { --wx: var(--wx-tornado); --wx-pale: #f7dede; --wx-ink: #7f1d1d; }
.wx-h-svr-considerable  { --wx: var(--wx-svr-considerable); --wx-pale: #fce8d5; --wx-ink: #7a3c05; }
.wx-h-svr               { --wx: var(--wx-svr); --wx-pale: #fdf4d0; --wx-ink: #6b5200; }
.wx-h-flashflood        { --wx: var(--wx-flashflood); --wx-pale: #e8f8d6; --wx-ink: #33660a; }
.wx-h-flood             { --wx: var(--wx-flood); --wx-pale: #dff0df; --wx-ink: #1b4d1e; }
.wx-h-winter            { --wx: var(--wx-winter); --wx-pale: #dceafa; --wx-ink: #1d4a80; }
.wx-h-heat              { --wx: var(--wx-heat); --wx-pale: #fbe6d8; --wx-ink: #6f3208; }
.wx-h-wind              { --wx: var(--wx-wind); --wx-pale: #efe6e2; --wx-ink: #4e342e; }
.wx-h-special           { --wx: var(--wx-special); --wx-pale: #f3ece0; --wx-ink: #5e4d2c; }
.wx-h-other             { --wx: var(--wx-other); --wx-pale: #e4eaed; --wx-ink: #2f4550; }

/* A warning is happening, so it is solid. A watch is possible, so it is the
   pale one with the hazard's edge. An advisory is quieter still. */
.wx-l-warning  { background: var(--wx); border-color: var(--wx); color: #fff; }
.wx-l-watch    { background: var(--wx-pale); border-color: var(--wx); color: var(--wx-ink); }
.wx-l-advisory { background: var(--wx-pale); border-color: var(--line2); color: var(--wx-ink); }

/* The pale halves are mixed for paper-white. On the board, which is dark, the
   same tint would glow, so it is rebuilt against the dark ground. */
:root[data-theme="dark"] .wx-l-watch,
:root[data-theme="dark"] .wx-l-advisory,
.wall .wx-l-watch, .wall .wx-l-advisory {
  background: color-mix(in srgb, var(--wx) 22%, var(--panel));
  color: var(--ink);
}

/* The forecaster's own tag, in their own words. */
.wx-tag {
  display: inline-block; margin-left: 8px; padding: 0 6px; border-radius: 3px;
  background: rgba(0, 0, 0, .22); font-size: .78em; font-weight: 700; letter-spacing: .04em;
}

/* New and unacknowledged. It stops the moment somebody says they have seen it,
   because a screen that flashes all day gets a sticky note put over it. */
@keyframes wx-flash { 50% { opacity: .42; } }
.wx-new { animation: wx-flash 1.1s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .wx-new { animation: none; outline: 3px solid var(--wx); outline-offset: 2px; }
}

/* Everything active, on one line, so five alerts do not push the incident
   list off the screen. */
.wx-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.wx-pill {
  padding: 2px 9px; border-radius: 999px; border: 1px solid var(--wx);
  background: var(--wx-pale); color: var(--wx-ink); font-size: 11.5px; font-weight: 600;
}
.wall .wx-pill { font-size: 15px; padding: 3px 12px; }
:root[data-theme="dark"] .wx-pill, .wall .wx-pill {
  background: color-mix(in srgb, var(--wx) 20%, var(--panel)); color: var(--ink);
}

/* On the wall it is read across a room, so it is bigger and it does not wrap
   into three lines of teletype. */
.wall .wx-bar { font-size: 20px; padding: 12px 18px; }
.wall .wx-bar-when { font-size: 16px; }

@media (max-width: 820px) {
  .wall-wx { gap: 10px; }
  .wall-wx-hours { display: none; }
  .wall-wx-side { display: none; }
  /* Under 820px the strip is the whole width and every row costs height. The
     Fahrenheit reading stays; the conversion is the first thing to go. */
  .wall-wx-hour .wall-wx-alt { display: none; }
}

/* Open incidents on the big board, coloured by priority.
   One alarm red for everything cannot be triaged across a room, which is the
   only thing a board is for. Red is a life, orange is urgent, blue is routine,
   grey can wait. The bar carries it rather than the text, so the words stay
   readable at distance. */
/* pri1, NOT p1. The pill that reads "P1" is .p1 and it carries a solid alarm
   background; wearing that name here filled the whole row with it, and the
   subtitle underneath is --mut, so the line that says what the call actually is
   went unreadable. The bar was always the design. */
.wall-list li.pri1, .wall-list li.pri2, .wall-list li.pri3, .wall-list li.pri4 {
  border-left: 5px solid var(--pri);
  padding-left: 9px;
}
.wall-list li.pri1 { --pri: var(--crit); }
.wall-list li.pri2 { --pri: var(--hot); }
.wall-list li.pri3 { --pri: var(--info); }
.wall-list li.pri4 { --pri: var(--off); }
.wall-list li.pri1 .wall-val { color: var(--crit); font-weight: 700; }
.wall-list li.pri2 .wall-val { color: var(--hot); font-weight: 700; }
.wall-list li.pri3 .wall-val { color: var(--info); }
.wall-list li.pri4 .wall-val { color: var(--mut); }

/* Who is on a resource, and who is not.
   Call signs joined by spaces said who was there and nothing about what any of
   them was doing, which is the question the board is being read for. A pill
   each, with the same status dot the rest of the app uses. */
.wall-who {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 1px 8px 1px 6px; margin-right: 5px; border-radius: 999px;
  border: 1px solid var(--line2); background: var(--fill);
  font-family: var(--mono); font-size: .88em; white-space: nowrap;
}
.wall-who-row { display: flex; flex-wrap: wrap; gap: 5px 0; }

/* Everybody the event is tracking who is not on a post. A board that lists
   only the placed cannot answer "who is left", which is the question asked the
   moment a post goes short. */
.wall-unassigned { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; }
.wall-unassigned-head {
  font-size: .8em; letter-spacing: .07em; text-transform: uppercase;
  color: var(--mut); margin-bottom: 6px;
}

/* A status dot sitting immediately against the name it belongs to reads as
   part of the word. */
.wall-key .dot { margin-right: 7px; vertical-align: middle; }
/* The same slot the dot occupies, so posts and teams start on one edge.
   Muted, because it says WHAT the row is, not how it is doing: the colour on
   this board belongs to status, and a place does not have one. */
.wall-pin {
  /* 13 + 3 is the same 16px advance as the dot's 9 + 7, so a post and a wagon
     start their names on one edge. Fixed rather than 1em on purpose: the dot
     beside it is a fixed 9px, and a glyph that grew with the board's clamped
     type would pull away from it on a wall screen, which is the only place
     this is read. */
  width: 13px; height: 13px; margin-right: 3px; vertical-align: middle;
  color: var(--mut); flex: none;
}

/* The people who are not on anything are read as individual units: a safety
   officer, a floater, somebody just arrived. Bigger than a crew pill, because
   a commander dispatches these one at a time. */
.wall-who-solo {
  font-size: 1.05em; padding: 3px 12px 3px 9px; margin-right: 7px;
  border-color: var(--line2); background: var(--panel);
}
.wall-who-solo .dot { width: 11px; height: 11px; }

/* The field map.
   Full height on its own tab, because a map with a header, a tab bar and 200px
   of its own is a map nobody can read a street name on. Small on the
   assignment card, where it answers "where am I going" and nothing else. */
.field-map-full {
  height: calc(100vh - 210px); min-height: 320px;
  border: 1px solid var(--line); border-radius: 4px;
}
.field-map-small {
  height: 150px; margin-top: 11px;
  border: 1px solid var(--line); border-radius: 4px;
}
/* Your own post and your own wagon, outlined, so the one that matters is
   findable among forty. */
.post-pin.mine, .team-pin.mine { outline: 3px solid var(--ink); outline-offset: 1px; }

/* The map on the command board. Tall enough to read a street name on, short
   enough that the lists under it are still on the screen. */
.board-map { height: 300px; }
@media (max-width: 820px) { .board-map { height: 220px; } }

/* Weather on the console. The wall's own blocks, on a light panel: the two
   numbers that decide whether a bike event continues are the wind and the next
   three hours, and a chip in a row of chips is the wrong weight for them. */
.wx-strip {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 10px 14px;
}
.wx-strip .wall-wx-temp { font-size: 28px; }
.wx-strip .wall-wx-hours { gap: 16px; }
@media (max-width: 820px) {
  .wx-strip { gap: 10px; padding: 9px 11px; }
  .wx-strip .wall-wx-side { display: none; }
}

/* The schedule of events, on the console and on the wall.
   -----------------------------------------------------
   A schedule is read for one thing: where in it we are. Three states, and the
   only loud one is the row that is happening. Past rows fade rather than
   disappear, because "did the road actually close" is asked at 0800 about a
   0645 line, and a schedule that hides what it has passed cannot answer it. */
tr.sched-past > * { opacity: .5; }
tr.sched-now > * { background: var(--ok-bg); }
tr.sched-now > *:first-child { box-shadow: inset 3px 0 0 var(--ok); }

/* The wall's copy. Same three states, sized for a room. */
.wall-sched { display: flex; flex-direction: column; gap: .55vh; min-height: 0; overflow: hidden; }
.wall-sched-row {
  display: grid; grid-template-columns: max-content 1fr; gap: 0 .7vw;
  align-items: baseline; padding: .35vh .4vw; border-radius: 3px;
  border-left: 3px solid transparent;
}
.wall-sched-row b {
  font-family: var(--mono); font-size: clamp(13px, 1.35vw, 26px); font-weight: 700;
  letter-spacing: .01em;
}
.wall-sched-row span { font-size: clamp(12px, 1.2vw, 23px); min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.wall-sched-row i {
  grid-column: 2; font-style: normal; font-size: clamp(10px, .9vw, 17px); color: var(--mut);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wall-sched-row.past { opacity: .38; }
.wall-sched-row.now { background: var(--ok-bg); border-left-color: var(--ok); }
.wall-sched-row.now b, .wall-sched-row.now span { color: var(--ok); }

/* The schedule on the command board.
   -----------------------------------
   Three states and one of them is loud. An overdue milestone is either a thing
   that did not happen or a thing nobody recorded, and both are worth walking
   over to ask about, so it gets the bar and the color. Running and upcoming
   are quiet: they are context, not a call to action. */
.sched-line { padding: 8px 12px; border-bottom: 1px solid var(--line); border-left: 3px solid transparent; }
.sched-line:last-child { border-bottom: 0; }
.sched-line.late { border-left-color: var(--crit); background: var(--crit-bg); }
.sched-line.now { border-left-color: var(--ok); background: var(--ok-bg); }
.sched-late-word { color: var(--crit); font-weight: 700; }

/* A card folded down to its heading.
   ---------------------------------
   Everything but the first .ph goes: the shift line, the crew, the add form.
   What is left is the grip, the tick box, the name, the tactical and the
   staffing chip, which is exactly what somebody putting seventeen posts in
   order needs on screen and all they need.

   > :not(.ph) rather than a class on the body, because the card has three
   children and only one of them is the heading. Naming what stays is shorter
   than naming everything that goes, and it keeps working when a card grows a
   fourth part. */
.panel[data-collapsed] > :not(.ph) { display: none; }
.panel[data-collapsed] > .ph { border-bottom: 0; }

.collapse-toggle {
  cursor: pointer; color: var(--faint); background: transparent; border: 0;
  padding: 0 3px; line-height: 1; font-size: 11px; min-width: 16px;
}
.collapse-toggle:hover { color: var(--mut); }
.collapse-toggle:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; border-radius: 3px; }

/* Unified Command on the org chart.
   ---------------------------------
   A labelled frame holding one box per commander, side by side. That is how the
   ICS 207 draws it and it is the point: none of them is above the others, so
   none of them is drawn above the others. A stack would say the first one is in
   charge, which is the arrangement this exists to replace. */
.ics-uc {
  border: 2px solid var(--ink); border-radius: 4px; padding: 6px 8px 8px;
  background: var(--panel);
}
.ics-uc-label {
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--mut); margin-bottom: 6px;
}
.ics-uc-members { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
/* The point of contact, marked inside its own box.
   Three letters beside the call sign rather than three words under the box: the
   chart is titles and names, a sentence hanging off one of them reads as an
   annotation on the whole chart, and it pushed the boxes out of line with each
   other. Green because it is the one to ring. */
.ics-mark {
  margin-left: 6px; font-size: 9px; font-weight: 700; letter-spacing: .08em;
  padding: 1px 4px; border-radius: 2px; vertical-align: middle;
  color: var(--ok); border: 1px solid var(--ok-line); background: var(--ok-bg);
}
