/* Phase 6 first pass (spec/06): dense, legible, "operational" NOC dashboard.
   Deliberately not deeply polished yet -- structure and legibility first,
   look-and-feel iterated after the STOP #2 review. */

:root {
  --bg: #0d1117;
  --panel-bg: #161b22;
  --raised: #1c222b;
  --border: #30363d;
  --border-soft: #262c36;
  --text: #e6edf3;
  --muted: #8b949e;
  --accent: #58a6ff;
  --green: #3fb950;
  --amber: #d29922;
  --red: #f85149;
  --danger: #f85149;

  /* THE BRAND GREEN, AND THE ONE RULE ABOUT IT: it appears in the brand block
     and nowhere else. Sampled from the logo (22,226 pixels carry exactly this
     value) and shared with `scripts/make_logo_assets.py`, which tints the
     icons the same.

     It must not become the interactive accent, tempting as that is. In this
     app green is a STATUS -- `--green` is "live", it is the bottom of the
     utilization ramp, and it is a coverage grade -- and those two greens are
     four points apart in hue. A green button would read as a healthy
     something, and an operator scanning for red would have to learn which
     greens mean nothing. Interactive stays `--accent` blue. */
  --brand: #22c55e;

  /* -- SPACE, TYPE, RADIUS, ELEVATION, MOTION (DECISIONS.md #170) --------
     Color was the only thing this file had ever tokenized. Everything else
     was tuned per element, which is measurable rather than a matter of
     taste: before this block there were ELEVEN font sizes (including 12.5px,
     11.5px and 10.5px -- the fingerprint of nudging one rule at a time),
     EIGHT border radii, and ONE transition in 2,362 lines.

     A half-pixel difference between two labels is invisible on its own and
     is exactly what makes a screen read as assembled rather than designed:
     nothing lines up with anything, because nothing was ever measured
     against the same ruler.

     Use these. A literal px in a new rule should be a deliberate exception
     with a reason beside it, the way `--brand` carries its one rule. */

  /* One 4px scale. `--s-1` is hairline separation; `--s-7` is the gap
     between blocks that are not about each other. */
  --s-1: 2px;
  --s-2: 4px;
  --s-3: 6px;
  --s-4: 8px;
  --s-5: 12px;
  --s-6: 16px;
  --s-7: 24px;
  --s-8: 32px;

  /* Five sizes, and the app is dense on purpose -- an operator reads tables
     of hardware, not an article. `--t-md` is the body: buttons, fields, the
     text in a panel. Anything below `--t-xs` is not a size, it is a mistake. */
  --t-xs: 11px;
  --t-sm: 12px;
  --t-md: 13px;
  --t-lg: 14px;
  --t-xl: 17px;

  /* A SIXTH SIZE THAT IS NOT PART OF THE SCALE, and is declared here rather
     than written inline so #170's guard can still see every size in one
     place (ROADMAP item 74). It is not a view about type: iOS Safari zooms
     the viewport in when a field under 16px takes focus and does not zoom
     back out when it is left, so on a touch screen every field is given
     exactly the threshold. Between `--t-lg` and `--t-xl` by accident, and it
     must not be retuned with either -- it is a number Apple chose. */
  --t-no-zoom: 16px;

  /* Three radii. `--r-sm` for things inside other things (inputs, chips),
     `--r-md` for controls and cards, `--r-pill` for badges that are read as
     a shape rather than as a box. */
  --r-sm: 4px;
  --r-md: 7px;
  --r-pill: 999px;

  /* Elevation says how far something is from the page, and there are only
     three distances here: a control, a panel, a thing floating over the map. */
  --e-1: 0 1px 2px rgba(0, 0, 0, 0.30);
  --e-2: 0 4px 12px rgba(0, 0, 0, 0.35);
  --e-3: 0 12px 32px rgba(0, 0, 0, 0.45);

  /* `--fast` for a color or a border under the cursor; `--med` for anything
     that changes size or position. Both are short enough to feel like the
     control responding rather than like the app thinking. */
  --fast: 90ms ease;
  --med: 160ms ease;
}

/* Motion is a preference, not a decoration: a reader who has asked their
   system for less must get none. Stated once, here, rather than remembered
   at every rule that uses `--fast`. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --fast: 0s;
    --med: 0s;
  }
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: var(--t-lg);
}

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  /* AND THEN THE ONE THAT IS TRUE ON A PHONE (item 74). `100vh` on mobile
     Safari and Chrome is the viewport with the URL bar RETRACTED, so a full
     height layout is taller than the window until you scroll -- the bottom of
     the panel sits under the browser chrome and cannot be reached. `dvh`
     tracks the window as that bar comes and goes. Declared second so browsers
     that do not know the unit keep the line above. */
  height: 100dvh;
}

#topbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s-6);
  /* Too narrow for one row: whole GROUPS move down. The alternative is every
     button wrapping its own text, which is what makes the bar look ragged --
     a row of controls at four different heights. */
  flex-wrap: wrap;
  row-gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--border);
  /* A shade above the panel behind it, so the bar reads as the thing in
     front rather than as a region of the same surface with a line drawn
     across it. */
  background: linear-gradient(180deg, var(--raised), var(--panel-bg));
}

.topbar-group {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

/* THE GAP BETWEEN GROUPS DOES THE WORK, not a rule between every pair. Two
   controls six pixels apart are one thing; two groups sixteen apart are two
   (#170). The bar had a single gap for both, so ten buttons read as one
   undifferentiated run. */
#topbar > .topbar-group + .topbar-group {
  margin-left: var(--s-2);
}

/* Everything that is not about the network itself, pushed to the far end:
   how it is displayed, what you take away, and Reset. `status-line` rides
   with them so the free space opens in ONE place -- two `margin-left: auto`
   siblings would split it and leave the utilities stranded mid-bar. */
.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  margin-left: auto;
}

/* The second vertical rule in the bar, and the last. The first separates who
   this is from what you can do; this one separates everything else from the
   control that throws the network away. */
.topbar-rule {
  width: 1px;
  align-self: stretch;
  min-height: 18px;
  margin: 0 var(--s-2);
  /* `--border`, not `--border-soft`: this one has to be SEEN. The rule after
     the brand only has to be felt, because the brand is already unlike
     everything beside it; this one is the whole reason Reset reads as
     separate from the button next to it. */
  background: var(--border);
}

/* WHILE THE INVENTORY IS OPEN THERE IS NO MAP, and a mode is a claim about
   what the next map CLICK means. The claim is still true and comes back with
   the map, so the state is left alone and only the assertion is withdrawn --
   a lit segment over a screen with no map says the app is in a state it is
   not in. Pressing one still works: it closes the inventory and applies the
   mode, which is what it has always done. */
#topbar.inventory-open .mode-group {
  opacity: 0.45;
}

/* While a full-width view covers the map there IS no current map mode, and a
   blue segment glowing through the dimmed group claimed otherwise -- the bar
   said "Explore" over a screen with no map on it. The mode survives underneath
   (leaving the screen returns to it); only the claim is withdrawn. The screen's
   own button carries `.active` instead, so "where am I" has one answer. */
#topbar.inventory-open .mode-group .mode-btn.active,
#topbar.inventory-open .mode-group .mode-btn.active:hover {
  background: transparent;
  color: var(--text);
  font-weight: 400;
}

/* -- brand ---------------------------------------------------------------
   Identity, not a control. Sized to the button row rather than to itself: at
   26px the mark matches the cap height of the buttons beside it, so the bar
   has one optical baseline instead of a logo sitting proud of everything. */
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding-right: 14px;
  margin-right: 2px;
  /* The one vertical rule in the bar. It separates WHO THIS IS from WHAT YOU
     CAN DO, which is the only division here worth drawing; the groups after
     it are spaced apart instead. */
  border-right: 1px solid var(--border-soft);
  user-select: none;
}

.brand-mark {
  display: block;
  width: 26px;
  height: 26px;
  /* The PNG is already keyed off its black field, so nothing is composited
     here -- this is only the faint lift that keeps a 26px wireframe from
     looking flat against the bar. */
  filter: drop-shadow(0 0 6px rgba(34, 197, 92, 0.28));
}

.brand-name {
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-size: var(--t-lg);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The logo stacks a white NETWORK over a green CRUISR. Set side by side the
   same two weights read as one name rather than as two words. */
.brand-network {
  color: var(--text);
  font-weight: 600;
}

.brand-cruisr {
  color: var(--brand);
  font-weight: 500;
}

/* Below this the bar wraps anyway, and the name is the first thing that can
   go: the mark still identifies the app, and the width buys a row back for
   the controls, which are what the operator came for. */
@media (max-width: 1180px) {
  .brand-name {
    display: none;
  }

  .brand {
    padding-right: 10px;
  }
}

/* Seed picker (#078). Every property the panel's `label select` rule sets is
   re-set here rather than left to specificity: this control lives in the bar,
   not in a form, and it should be impossible for a form rule to restyle it. */
.seed-picker {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
}

.seed-picker-label {
  color: var(--muted);
  font-size: var(--t-sm);
  line-height: 1.35;
  white-space: nowrap;
}

.seed-picker .select {
  display: inline-block;
  width: auto;
  /* Sized to keep the whole bar on one row at a typical laptop width: the
     long seed label truncates (tooltip and the open list carry it in full)
     rather than pushing the status line onto a row of its own. */
  max-width: 200px;
  margin: 0;
  background: #21262d;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 6px 10px;
  font-size: var(--t-md);
  font-family: inherit;
  line-height: 1.35;
  cursor: pointer;
  /* Truncate rather than stretch the bar: the full label is in the tooltip
     and, once open, in the option list. */
  text-overflow: ellipsis;
}

.seed-picker .select:hover {
  border-color: var(--accent);
}

.status-line {
  /* No `margin-left: auto` any more (#176). It was last in `.topbar-right`
     and pushed itself to the end; it is now FIRST, so that margin would
     absorb nothing and only mislead the next reader. What pins this cluster
     to the right edge is `.topbar-right`'s own auto margin, which is also
     what makes a longer status grow leftward instead of shoving Contact us
     out of the corner. */
  color: var(--muted);
  font-family: ui-monospace, monospace;
  font-size: var(--t-sm);
  /* Ancillary text: it gives up width and truncates rather than pushing
     itself onto a row of its own, which is a whole extra bar height spent on
     "network v-2". */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One control height for the whole bar: same padding, same line-height, and
   no text wrapping (a two-line button is a taller button, and one taller
   button is what makes the row look uneven). */
.btn,
.mode-btn {
  background: #21262d;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-5);
  font-size: var(--t-md);
  font-family: inherit;
  line-height: 1.35;
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}

/* THE APP HAD ONE TRANSITION IN 2,362 LINES (#170), so nothing ever
   acknowledged the cursor: a border appeared, fully formed, and vanished the
   same way. Only color and border move here -- never size or position, which
   is what makes an interface feel loose rather than responsive. */
.btn,
.mode-btn,
.select,
input,
textarea {
  transition:
    background-color var(--fast),
    border-color var(--fast),
    color var(--fast),
    opacity var(--fast);
}

.btn:hover,
.mode-btn:hover {
  border-color: var(--accent);
}

/* Every text-ish control is dark AT THE BASE. Each home used to restate the
   palette (`label input`, `.inv-table input`, `.search-input`, …), and any
   input created outside one of those scopes fell through to the native white
   box -- the work queue's as-built field was one. The scoped rules all use
   these same tokens, so they still win where they add sizing; this rule is
   what a bare input gets instead of the browser default. Checkboxes and
   radios are excluded by the type selectors on purpose. */
input[type="text"],
input[type="number"],
input:not([type]),
select,
textarea {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--r-sm);
  padding: 2px 6px;
  font: inherit;
}

/* Keyboard focus was invisible: the only feedback either control had was a
   hover border, which a keyboard never triggers. `:focus-visible` rather than
   `:focus` so a mouse click does not leave a ring behind it. */
.btn:focus-visible,
.mode-btn:focus-visible,
.select:focus-visible,
input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* -- the modes are one control, not five ---------------------------------
   They are mutually exclusive -- picking one un-picks the rest -- and they
   were drawn identically to the tool buttons beside them, which are not
   exclusive at all. Five separate pills say "five things you can do"; a
   segmented control says "the map is in one of these states", which is what
   a mode actually is. Nothing about the markup or the click handlers moves;
   the group draws the border and the children stop drawing their own. */
.mode-group {
  gap: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: #21262d;
}

.mode-group .mode-btn {
  border: 0;
  border-radius: 0;
  background: transparent;
}

.mode-group .mode-btn + .mode-btn {
  border-left: 1px solid var(--border-soft);
}

.mode-group .mode-btn:hover {
  background: #2a313b;
}

.mode-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #0d1117;
  font-weight: 600;
}

/* Specificity: the segment rules above zero the background, and the active
   one has to win over them without `!important`. */
.mode-group .mode-btn.active,
.mode-group .mode-btn.active:hover {
  background: var(--accent);
  color: #0d1117;
}

/* THE ONE CONTROL IN THIS BAR THAT LEAVES THE TOOL (#176), so it is the one
   that does not recede: it keeps an outline when the utilities have given
   theirs up.

   Accent, NOT brand green. It wore `--brand` for a while, and that broke the
   one rule the brand green carries ("the brand block and nowhere else"): on
   a map where green means live and clear, the brightest green in the chrome
   was a mailto link, and an operator scanning for status hit it every time.
   The outline-not-filled treatment is what keeps it from reading as the
   bar's main action -- the filled blue stays the modes' and the forms'. */
.topbar-group .btn.btn-contact,
.topbar-group .btn.btn-contact:not(.btn-danger):not(.btn-primary) {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.topbar-group .btn.btn-contact:hover {
  background: var(--accent);
  border-color: var(--accent);
  /* The page ground, not `--text`: on a filled accent button the label has
     to be the darkest thing available or it sits at 2:1 against the blue. */
  color: var(--bg);
}

/* The tools recede. They are always available and rarely the point, so they
   carry a border only when reached for -- which lets the modes and the brand
   lead the bar instead of fifteen controls competing at one weight. Reset
   keeps its red and Export keeps its link, because both say something about
   themselves that outranks tidiness. */
.topbar-group .btn:not(.btn-danger):not(.btn-primary) {
  background: transparent;
  border-color: transparent;
  color: var(--muted);
}

.topbar-group .btn:not(.btn-danger):not(.btn-primary):hover {
  background: #21262d;
  border-color: var(--border);
  color: var(--text);
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #0d1117;
  font-weight: 600;
}

.btn-danger {
  border-color: var(--red);
  color: var(--red);
}

.btn-link {
  background: none;
  border: none;
  color: var(--accent);
  padding: 2px 4px;
}

/* A link-button sitting INSIDE a sentence rather than on its own line
   (#151): the route rows read "BH-1 -> BH-2 => BS-CORE  show on map". */
.btn-inline {
  font-size: 0.85em;
  padding: 0 4px;
  cursor: pointer;
  white-space: nowrap;
}

/* One warning per row, each already carrying a notice class. The list strips
   its own bullets and spacing so the notices stack as blocks. */
.site-warnings {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-warnings li {
  margin: 0 0 6px 0;
}

.btn-small {
  padding: 2px 8px;
  font-size: var(--t-sm);
}

#main {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
}

#map {
  flex: 1 1 auto;
  min-width: 0;
  position: relative;
}

/* Crosshair while a map click means "put a point here" (check / plan modes),
   normal grab cursor while exploring. */
#map.placing .maplibregl-canvas-container,
#map.placing canvas {
  cursor: crosshair;
}

#map canvas.hover-target {
  cursor: pointer;
}

#divider {
  flex: 0 0 5px;
  cursor: col-resize;
  background: var(--border);
}

#divider:hover,
body.resizing #divider {
  background: var(--accent);
}

body.resizing {
  user-select: none;
  cursor: col-resize;
}

#panel {
  /* `--panel-width` is what the divider drag writes (main.js). A property
     rather than an inline width, so the narrow rules at the end of this file
     can still overrule it -- see the comment there. */
  flex: 0 0 var(--panel-width, 420px);
  overflow-y: auto;
  border-left: 1px solid var(--border);
  background: var(--panel-bg);
  padding: 16px;
}

/* The idle panel. It is 420px wide and the full height of the window, and it
   held one line of gray text against the top of all that -- which reads as a
   panel that failed to load rather than as one waiting to be told what to
   look at.
   
   MATCHED STRUCTURALLY, not by a class the JS sets: `clearPanel` renders
   exactly one `<p class="muted">` and every panel that has anything to show
   renders more than that, so `:only-child` IS the empty state and the
   frontend keeps rendering rather than deciding (#9). "Loading network…"
   passes through the same treatment, which is correct -- it is the same
   nothing-yet.

   The mark is background rather than an `<img>` so it cannot be read out or
   tabbed to: it is wallpaper, and a screen reader announcing the logo in
   place of the instruction would be worse than silence. Its URL is relative
   to this stylesheet, which is served from `/a/<stamp>/css/`, so it is
   cache-stamped along with everything else without app.py knowing. */
#panel-body > p.muted:only-child {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  /* The panel's own padding, so the block centers in what is left. */
  min-height: calc(100vh - 140px);
  margin: 0;
  padding: 0 24px;
  max-width: 30ch;
  margin-inline: auto;
  line-height: 1.6;
}

#panel-body > p.muted:only-child::before {
  content: "";
  width: 108px;
  height: 108px;
  margin-bottom: 18px;
  background: url("../img/mark.png") center / contain no-repeat;
  /* Faint. It is there to stop the space reading as broken, not to be
     looked at -- at full strength a 108px logo becomes the loudest thing on
     a screen whose subject is the map. */
  opacity: 0.16;
}

/* -- the invitation under an answer (ROADMAP item 75) ------------------ */

/* SEPARATED BY A RULE, NOT BY A CARD. This is an aside to the result above
   it, and a raised box with its own border would compete with the verdict
   for the eye -- on the one screen where the verdict is the whole point. The
   space above it is `--s-7`, the gap this scale reserves for blocks that are
   not about each other, because that is exactly what this is. */
.reach-out {
  margin-top: var(--s-7);
  padding-top: var(--s-6);
  border-top: 1px solid var(--border-soft);
}

.reach-out-head {
  margin: 0 0 var(--s-3);
  font-size: var(--t-lg);
}

.reach-out p {
  margin: 0 0 var(--s-5);
  line-height: 1.5;
}

/* -- layer toggles ---------------------------------------------------- */

/* A POPOVER, not a permanent box (#171). Closed until the Layers button is
   pressed, so the map keeps the corner it used to lose -- which on the
   shipped network is where half the sites are.

   Still anchored top-left rather than floated under the button: the button is
   directly above this corner, and a popover that tracks its trigger needs
   position logic on every resize to buy nothing at all here. */
.layer-panel {
  position: absolute;
  top: var(--s-5);
  left: var(--s-5);
  z-index: 2;
  background: rgba(22, 27, 34, 0.96);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-4) var(--s-5) var(--s-5);
  min-width: 190px;
  /* Floating over the map is the furthest thing from the page in this app. */
  box-shadow: var(--e-3);
  /* The legend grows with every overlay added to it, and coverage added two
     sections at once -- past which the bottom rows sat below the window with
     no way to reach them. Bounded to the canvas and scrolled INSIDE, so the
     panel can keep growing without ever running off the screen again. */
  max-height: calc(100% - 24px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.layer-panel h4 {
  margin: 0 0 var(--s-3);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

/* The group headings. Quieter than the panel's own title and separated by
   space rather than by a rule: three rules in a 190px box is a form, and
   this is a list. */
.layer-group {
  margin: var(--s-5) 0 var(--s-2);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--muted);
}

.layer-group:first-of-type {
  margin-top: var(--s-2);
}

/* The bar button, while its popover is open. `.active` is what the mode
   segments already use for "this is the state you are in", and a toggle
   button that looks identical open and closed is a button you press twice.
   `aria-pressed="true"` gets the same treatment: "Detail: brief" is a chosen
   state, and a toggle drawn at the resting weight in both states read as a
   status line rather than as a control. */
#topbar .topbar-group .btn.active,
#topbar .topbar-group .btn[aria-pressed="true"] {
  background: #21262d;
  border-color: var(--border);
  color: var(--text);
}

.layer-toggle {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  padding: 2px 0;
  font-size: var(--t-sm);
  color: var(--text);
  cursor: pointer;
}

.layer-toggle input {
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--accent);
}

.layer-toggle span {
  white-space: nowrap;
}

/* `!important` because hiding is a STATE and must beat any component that
   declares its own display. `.notice` became `display: block` in #175, later
   in this file than this rule, and from then on every hidden notice drew as
   an empty amber box -- the plan form's upstream warning sat on screen empty. */
.hidden {
  display: none !important;
}

#panel h2 {
  margin-top: 0;
  font-size: var(--t-xl);
}

#panel h3 {
  font-size: var(--t-md);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin: 18px 0 8px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 4px;
}

#panel h4 {
  font-size: var(--t-sm);
  color: var(--muted);
  margin: 12px 0 4px;
}

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

.mono {
  font-family: ui-monospace, monospace;
}

.panel-meta {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-bottom: 10px;
}

.panel-actions {
  display: flex;
  gap: 8px;
  margin: 12px 0;
}

#panel a {
  color: var(--accent);
}

/* The first-run panel (#192). No card, no accent bar, no illustration: it
   sits where every other panel's content sits and reads as the app talking,
   rather than as an advert that arrived on top of it. The only styling it
   needs is room to breathe between the three things it suggests -- a dense
   list is the one thing that would stop somebody reading the part of this
   screen that exists to be read. */
.intro-list {
  list-style: none;
  padding: 0;
  margin: 8px 0 12px;
}

.intro-try {
  /* Hanging indent, so the bolded action starts each item flush and the
     sentence after it stays visibly subordinate. */
  margin: 0 0 12px;
  padding-left: 10px;
  border-left: 2px solid var(--border);
  line-height: 1.45;
}

.link-back {
  color: var(--accent);
  font-size: var(--t-sm);
  display: inline-block;
  margin-bottom: 8px;
}

label {
  display: block;
  font-size: var(--t-sm);
  color: var(--muted);
  margin: 8px 0 2px;
}

/* Scoped to text-ish inputs on purpose: a bare `label input` also caught
   checkboxes and radios, which then stretched to the full label width. */
label input[type="text"],
label input[type="number"],
label select {
  display: block;
  width: 100%;
  margin-top: 2px;
  background: #0d1117;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--r-sm);
  padding: 5px 8px;
}

/* -- the "?" shortcuts overlay (main.js) -----------------------------------
   A reference card, not a dialog: nothing on it acts, so it closes the three
   ways anything transient must -- Escape, a backdrop click, or "?" again. */
#shortcuts-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
}

.shortcuts-card {
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--e-3);
  padding: var(--s-6) var(--s-7);
  min-width: 300px;
}

.shortcuts-card h3 {
  margin: 0 0 var(--s-5);
}

.shortcut-row {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-2) 0;
  font-size: var(--t-md);
}

.shortcut-keys {
  display: flex;
  gap: var(--s-2);
  min-width: 72px;
}

.shortcuts-card kbd {
  background: var(--raised);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: var(--r-sm);
  padding: 1px 6px;
  font-family: ui-monospace, monospace;
  font-size: var(--t-sm);
}

/* -- a request in flight (ui.js workingNote) -------------------------------
   The ring is the one animation allowed to run continuously: it exists to
   prove the app has not hung, which nothing static can do. Reduced motion
   stops it -- the text still changes when the wait turns out to be long,
   which is the part that carries meaning. */
.working-spinner {
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: var(--s-3);
  vertical-align: -1px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: working-spin 0.8s linear infinite;
}

@keyframes working-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .working-spinner {
    animation: none;
  }
}

/* The second line arrives only when the wait has earned it. */
.working-slow {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--t-sm);
}

/* THE ENGINE'S OWN CLASSIFICATION, ahead of its sentence (#172). Three
   warnings at the same severity rendered as three identical amber paragraphs,
   and the only way to tell them apart was to read all three. Block-level so it
   sits on its own line: inline, it read as the first two words of the
   sentence, which is exactly what it is not. */
.warning-kind {
  display: block;
  margin-bottom: var(--s-1);
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* -- what a panel shows while it is fetching (#174) ------------------------
   The shape of the answer rather than a sentence about it, so the content
   arrives into a layout the reader is already looking at instead of
   replacing a single gray line with a screenful.

   The shimmer is a background sweep, not a transform: it moves nothing, so
   it costs no layout and cannot nudge the rows beside it. */
.skeleton {
  padding: var(--s-5) 0;
}

.sk-line,
.sk-cell {
  background: linear-gradient(
    90deg,
    var(--raised) 0%,
    var(--border) 50%,
    var(--raised) 100%
  );
  background-size: 200% 100%;
  border-radius: var(--r-sm);
  animation: sk-sweep 1.4s ease-in-out infinite;
}

.sk-line {
  height: 10px;
  margin-bottom: var(--s-4);
}

.sk-title {
  height: 16px;
  width: 55%;
  margin-bottom: var(--s-6);
}

/* Where the stat strip lands (#172), so the figures do not arrive into
   space that was not being held for them. */
.sk-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s-2);
  margin-bottom: var(--s-6);
}

.sk-cell {
  height: 28px;
}

/* Uneven, because rows of text are. A stack of identical bars reads as a
   loading GRAPHIC; ragged ones read as text that has not arrived. */
.sk-row-0 { width: 92%; }
.sk-row-1 { width: 74%; }
.sk-row-2 { width: 83%; }

@keyframes sk-sweep {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

/* A reader who asked their system for less motion gets a still skeleton --
   the layout is the useful half and it survives without the sweep. */
@media (prefers-reduced-motion: reduce) {
  .sk-line,
  .sk-cell {
    animation: none;
  }
}

/* -- the figures you opened the panel for (#172) ---------------------------
   A row of numbers under the title, reading left to right in the order an
   operator asks them. `grid` rather than `flex` so four cells and two cells
   both sit on one line without a stray gap, and so a long value ("shared
   link") does not squeeze the cell beside it. */
.stat-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s-2);
  margin: var(--s-5) 0 var(--s-6);
  padding: var(--s-4) 0;
  border-top: 1px solid var(--border-soft);
  border-bottom: 1px solid var(--border-soft);
}

.stat {
  min-width: 0;
}

/* The figure carries the weight and the label says what it is. The other way
   round is a table, which is what this replaces. */
.stat-value {
  font-size: var(--t-xl);
  line-height: 1.15;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stat-unit {
  margin-left: 3px;
  font-size: var(--t-sm);
  color: var(--muted);
}

.stat-label {
  margin-top: var(--s-1);
  font-size: var(--t-xs);
  color: var(--muted);
}

/* -- badges / chips ------------------------------------------------- */

.badge {
  display: inline-block;
  border-radius: var(--r-pill);
  padding: 1px 8px;
  font-size: var(--t-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  border: 1px solid var(--border);
}

.badge-live,
.badge-active,
.badge-no_change,
.badge-done {
  color: var(--green);
  border-color: var(--green);
}

.badge-planned,
.badge-open {
  color: var(--muted);
  border-color: var(--muted);
}

.badge-pending_install,
.badge-config_change {
  color: var(--accent);
  border-color: var(--accent);
}

.badge-pending_upgrades,
.badge-hardware_add,
/* "not a site yet", "not live yet" (#106) -- a real caveat on an option that
   is otherwise offerable, so it reads amber rather than red. */
.badge-warn {
  color: var(--amber);
  border-color: var(--amber);
}

/* An impact that REMOVES rather than grays (#083) -- the destructive one of
   the two, so it reads in the same red as the delete action itself. */
.badge-danger {
  color: var(--red, #f85149);
  border-color: var(--red, #f85149);
}

.chip {
  display: inline-block;
  border-radius: var(--r-sm);
  padding: 1px 6px;
  font-size: var(--t-xs);
  font-family: ui-monospace, monospace;
}

.chip-green {
  background: rgba(63, 185, 80, 0.15);
  color: var(--green);
}

.chip-amber {
  background: rgba(210, 153, 34, 0.15);
  color: var(--amber);
}

.chip-red {
  background: rgba(248, 81, 73, 0.15);
  color: var(--red);
}

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

/* Live beside committed (#094). The second chip is outlined rather than
   filled so the two never read as one measurement of the same thing, and so
   the FILLED color on the left stays the thing the eye goes to. */
.chip-pair {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.chip-outline {
  background: transparent;
  border: 1px solid currentColor;
  opacity: 0.75;
}

/* What a hop does for the rest of the network (#094) -- set apart from the
   row's own facts, because it is about OTHER sites. */
.network-gains {
  margin-top: 6px;
}

.network-gain-list {
  list-style: none;
  padding: 0;
  margin: 4px 0 0;
  font-size: var(--t-sm);
  color: var(--muted);
}

.network-gain-list li::before {
  content: "↗ ";
  color: var(--green);
}

/* Why one hop is worth choosing over another (#096): the engine's pick, and
   what it does for other sites, on the card where the choice is made. */
.hop-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 6px;
}

.hop-meta .network-gains {
  margin-top: 0;
}

/* A gain that is only a partial one: the backup shares a link with the
   primary (#095). Same arrow, amber, so it reads as progress with a caveat
   rather than as an equal to a disjoint route. */
.network-gain-list li.network-gain-partial::before {
  color: var(--amber);
}

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

.data-table,
.kv-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-sm);
}

.data-table th {
  text-align: left;
  color: var(--muted);
  font-weight: 500;
  border-bottom: 1px solid var(--border);
  padding: 4px 6px;
}

.data-table td {
  padding: 4px 6px;
  border-bottom: 1px solid #21262d;
}

.row-clickable {
  cursor: pointer;
}

.row-clickable:hover {
  background: #1c2129;
}

.kv-key {
  color: var(--muted);
  padding: 3px 8px 3px 0;
  white-space: nowrap;
  vertical-align: top;
}

.kv-val {
  padding: 3px 0;
}

/* -- option cards (verdict panel) -------------------------------------- */

.option-card {
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--r-md);
  padding: 8px 10px;
  margin-bottom: 10px;
}

.option-card.disruption-no_change {
  border-left-color: var(--green);
}

.option-card.disruption-config_change {
  border-left-color: var(--accent);
}

.option-card.disruption-hardware_add {
  border-left-color: var(--amber);
}

.option-card-head {
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: var(--t-md);
}

.option-rank {
  font-weight: 700;
}

.option-strategy {
  font-family: ui-monospace, monospace;
  color: var(--muted);
}

.option-card-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 6px;
}

.option-drilldown {
  margin-top: 8px;
  border-top: 1px solid var(--border);
  padding-top: 6px;
}

.option-drilldown.hidden {
  display: none;
}

.work-plan {
  margin: 0;
  padding-left: 18px;
  font-size: var(--t-sm);
}

/* What to change when a site cannot be built (DECISIONS.md #084, item 1).
   Inside a notice, so it inherits the warning color and reads as part of
   the same statement rather than as unrelated advice below it. */
.remedy-list {
  margin: 6px 0 0;
  padding-left: 18px;
  font-size: var(--t-sm);
}

.remedy-list li {
  margin-top: 3px;
}

.notes {
  color: var(--muted);
  font-style: italic;
  font-size: var(--t-sm);
}

.candidate {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 8px 10px;
  margin-bottom: 8px;
}

.candidate-head {
  font-weight: 600;
  margin-bottom: 4px;
}

.reason-list {
  margin: 0;
  padding-left: 16px;
  font-size: var(--t-sm);
}

.reason-code {
  font-family: ui-monospace, monospace;
  color: var(--amber);
}

.conflict {
  color: var(--red);
  font-size: var(--t-sm);
}

/* -- path profile chart ------------------------------------------------ */

.profile-holder {
  margin: 6px 0;
}

.profile-chart {
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: #0d1117;
}

.profile-chart.profile-clear {
  border-color: var(--green);
}

.profile-chart.profile-blocked {
  border-color: var(--red);
}

.profile-ground {
  fill: #3d444d;
  stroke: none;
}

/* GROUND COVER, BY KIND (ROADMAP item 46). Terrain is the gray mass above;
   these sit on top of it, and the whole point of giving them separate colors
   is that "obstructed by a ridge" and "obstructed by the trees at the end of
   the garden" are different problems with different fixes -- one needs a
   taller mast, the other needs permission.

   Both are lighter than the terrain rather than darker: they are ON the
   ground, and a darker band reads as a hole in it. */
.profile-clutter-vegetation {
  fill: #2f6b43;
  stroke: none;
}

.profile-clutter-building {
  fill: #6e7681;
  stroke: none;
}

/* The provenance line, under the chart rather than inside it -- see
   `pathProfileChart`. It has to be READABLE, because an unverifiable clutter
   figure is worse than none; it is muted rather than small so that it does
   not compete with the verdict while staying on the type scale (#170). */
.profile-figure {
  margin: 0;
}

.profile-source {
  color: var(--muted);
  font-size: var(--t-xs);
  font-family: ui-monospace, monospace;
  line-height: 1.4;
}

.profile-fresnel {
  fill: rgba(88, 166, 255, 0.15);
  stroke: none;
}

.profile-los {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
}

/* The verdict, ABOVE the chart rather than painted inside it (#213). An SVG
   `<text>` cannot wrap, so #205's longer sentence ran off the right edge of a
   320-unit viewBox on every clear path. Here it wraps like any other prose and
   the sentence can be as long as it needs to be. Same size and face as the
   provenance lines below the chart; `--text` rather than `--muted` because it
   is the answer, not the footnote. */
.profile-verdict {
  margin: 0 0 4px;
  color: var(--text);
  font-size: var(--t-xs);
  font-family: ui-monospace, monospace;
  line-height: 1.4;
}

.profile-chart-empty {
  color: var(--muted);
  font-size: var(--t-sm);
  padding: 8px;
}

/* -- forms: place-a-point, branch fields, notices ---------------------- */

.coord-fields {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 10px;
  margin-bottom: 4px;
}

.coord-fields .hint {
  grid-column: 1 / -1;
  margin: 4px 0 0;
  font-size: var(--t-xs);
  color: var(--muted);
}

/* Address search (ROADMAP item 21) spans both coordinate columns: it fills
   them in, so it reads as the thing above them rather than beside one. */
.address-search {
  grid-column: 1 / -1;
}

.address-row {
  display: flex;
  gap: 6px;
}

.address-row input {
  flex: 1 1 auto;
  min-width: 0;
}

.address-results {
  grid-column: 1 / -1;
}

.address-candidates {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
}

.address-candidates li {
  margin: 2px 0;
}

.address-candidates button {
  text-align: left;
  font-size: var(--t-sm);
}

.radio-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0;
  color: var(--text);
  font-size: var(--t-md);
  cursor: pointer;
}

.radio-row input {
  accent-color: var(--accent);
  margin: 0;
}

.radio-row.disabled {
  color: var(--muted);
  cursor: not-allowed;
}

.branch-fields {
  border-left: 2px solid var(--border);
  padding-left: 10px;
  margin: 4px 0 8px;
}

.form-status:empty {
  display: none;
}

/* A field and the button that acts on it, on one line -- "capacity needed"
   next to "Suggest hardware" (#090). Wraps rather than squeezing the input. */
.inline-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
  margin: 4px 0 8px;
}

.inline-fields label {
  flex: 1 1 140px;
  margin: 0;
}

.notice {
  /* A NOTICE IS A BOX, AND HAS TO SAY SO (#175). Everything below assumes a
     block: padding on all four sides, a border round the whole thing, and
     vertical margins.

     On an INLINE element none of that means what it looks like. The border is
     drawn round each line fragment rather than round the notice, and the
     vertical padding overlaps the lines above and below -- so a notice that
     wrapped onto a second line had border edges running through the middle of
     its own text, which reads as a strikethrough. Two of these are rendered
     as a <span>, because they sit inside a <p> where a <div> would be invalid
     HTML; declaring the display here fixes them where the assumption lives
     rather than at the two call sites that happened to trip over it. */
  display: block;
  border-radius: var(--r-md);
  padding: var(--s-4) 10px;
  font-size: var(--t-sm);
  margin: var(--s-4) 0;
  border: 1px solid var(--border);
}

.notice-warn {
  border-color: var(--amber);
  background: rgba(210, 153, 34, 0.12);
  color: #e5c07b;
}

.notice-error {
  border-color: var(--red);
  background: rgba(248, 81, 73, 0.12);
  color: #ffa198;
}

/* GOOD NEWS, and the first notice in the app that carries any (#163). It uses
   the STATUS green rather than the brand one: this is a fact about the
   network -- there is room here -- which is exactly the family of meanings
   `--green` already carries on the map, so the two agree instead of
   introducing a third green with a fourth meaning. */
.notice-ok {
  border-color: var(--green);
  background: rgba(63, 185, 80, 0.12);
  color: #7ee787;
}

/* The request id an operator quotes when reporting a problem (#116, #121).
   Muted, because it is a reference and not part of the sentence -- but always
   present, because an id nobody can see is an id nobody can quote. */
.notice-ref {
  opacity: 0.65;
  font-size: var(--t-xs);
}

.badge-core {
  color: #e3b341;
  border-color: #e3b341;
}

.endpoint {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 8px 10px;
  margin-bottom: 10px;
}

.endpoint h4 {
  margin-top: 0;
  color: var(--text);
  font-size: var(--t-sm);
}

/* Indented keys ("  gain") are a resolved reference's sub-fields. */
.kv-key {
  white-space: pre;
}

.planned-bs-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 12px;
}

/* -- gray soft-state + delete flow (DECISIONS.md #074) ----------------- */

.notice-grey {
  border-color: #6e7681;
  background: rgba(110, 118, 129, 0.14);
  color: #c9d1d9;
}

.grey-chain {
  margin: 8px 0 6px;
  padding-left: 18px;
  font-size: var(--t-sm);
}

.grey-chain li {
  margin-bottom: 4px;
}

.grey-cause {
  font-family: ui-monospace, monospace;
}

.impact-list {
  margin: 8px 0 6px;
  padding-left: 18px;
  font-size: var(--t-sm);
  max-height: 220px;
  overflow-y: auto;
}

.impact-list li {
  margin-bottom: 3px;
}

.delete-flow {
  margin-top: 18px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

/* A grayed row in a list reads the same way a grayed marker does on the map. */
.row-greyed {
  opacity: 0.55;
}

.row-greyed td {
  font-style: italic;
}

/* ---------------------------------------------------------------------------
   Inventory: the catalogs + network tables (DECISIONS.md #076).

   Full-width because these tables run to twenty columns; the map view is
   hidden rather than squeezed. Dense and monospaced-where-it-matters, in the
   same "serious network software" register as the rest.
   --------------------------------------------------------------------------- */

#inventory-view {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}

/* `min-width: 0` at every level: a flex item defaults to `min-width: auto`,
   which refuses to shrink below its content, so without these the twenty-column
   table pushes the PAGE sideways instead of scrolling inside its own box. */
#inventory-body {
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  display: flex;
}

.inv-shell {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
  min-width: 0;
}

.inv-sidebar {
  flex: 0 0 190px;
  border-right: 1px solid var(--border);
  background: var(--panel-bg);
  overflow-y: auto;
  padding: 10px 0;
}

.inv-group h4 {
  margin: 12px 12px 6px;
  font-size: var(--t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.inv-kind {
  display: block;
  width: 100%;
  text-align: left;
  padding: 6px 12px;
  background: none;
  border: 0;
  border-left: 2px solid transparent;
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.inv-kind:hover {
  background: rgba(88, 166, 255, 0.08);
}

.inv-kind.active {
  border-left-color: var(--accent);
  color: var(--accent);
  background: rgba(88, 166, 255, 0.1);
}

.inv-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.inv-header {
  padding: 12px 16px 0;
}

.inv-header h2 {
  margin: 0 0 2px;
  font-size: var(--t-xl);
}

.inv-header p {
  margin: 0 0 6px;
  font-size: var(--t-sm);
}

.inv-toolbar {
  display: flex;
  align-items: center;
  /* WRAPS RATHER THAN CRUSHES (#214). Measured at 390px with the search box
     added: the bar stayed on one line and squeezed the box to 12px wide --
     present, focusable and useless. The buttons have fixed content; the box
     is the only flexible thing on the bar, so it is the one that collapses. */
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border);
}

.inv-status {
  color: var(--muted);
  font-size: var(--t-sm);
}

/* Find anything on any inventory table (#214). Pushed to the right of the
   action buttons and allowed to shrink, so a narrow window loses width from
   the box rather than pushing Save off the bar. */
.inv-search {
  /* Right of the buttons while they fit on one line; its own full-width row
     once they do not. Never narrower than something you can read a site id
     in. */
  margin-left: auto;
  flex: 1 1 200px;
  min-width: 160px;
}

.inv-match-count {
  color: var(--muted);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.inv-empty {
  color: var(--muted);
  padding: 12px 6px;
  white-space: normal;
}

/* Unsaved work must be obvious: the button says how much is staged and stops
   looking neutral the moment there is anything to lose. */
.btn-dirty {
  border-color: var(--amber);
  background: var(--amber);
  color: #1a1204;
}

/* The table scrolls INSIDE its own box, so a twenty-column catalog never
   makes the page itself scroll sideways. */
.inv-table-wrap {
  flex: 1 1 auto;
  overflow: auto;
  min-height: 0;
  min-width: 0;
}

.inv-table {
  border-collapse: collapse;
  font-size: var(--t-sm);
  width: max-content;
  min-width: 100%;
}

.inv-table th,
.inv-table td {
  border: 1px solid var(--border);
  padding: 3px 6px;
  text-align: left;
  vertical-align: middle;
  white-space: nowrap;
}

/* Numeric columns (marked by field kind in inventory.js): right-aligned so
   magnitudes line up under each other, inputs included. */
.inv-table th.col-num,
.inv-table td.col-num {
  text-align: right;
}

.inv-table td.col-num input {
  text-align: right;
}

.inv-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--panel-bg);
  color: var(--muted);
  font-weight: 600;
  font-size: var(--t-xs);
  letter-spacing: 0.03em;
}

/* Every column sorts (#214). The caret holds its space whether or not this
   is the sorted column, so the headings do not jump sideways when the sort
   moves -- the thing that makes a sortable table feel unsteady. */
.inv-table th.inv-sortable {
  cursor: pointer;
  user-select: none;
}

.inv-table th.inv-sortable:hover,
.inv-table th.inv-sortable:focus-visible {
  color: var(--text);
}

.inv-sort-caret {
  display: inline-block;
  width: 1em;
  margin-left: 4px;
  font-size: 0.85em;
  color: var(--accent);
}

.inv-table th.sorted-asc,
.inv-table th.sorted-desc {
  color: var(--text);
}

.inv-table tbody tr:hover td {
  background: rgba(88, 166, 255, 0.05);
}

.inv-table input[type="text"],
.inv-table input[type="number"],
.inv-table select {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--r-sm);
  padding: 2px 4px;
  font: inherit;
  max-width: 220px;
}

.inv-table input[type="number"] {
  width: 90px;
}

.inv-table input:focus,
.inv-table select:focus {
  outline: none;
  border-color: var(--accent);
}

.inv-actions-col {
  width: 34px;
  text-align: center;
}

/* Engine-owned columns are marked, not hidden: an operator who knows the real
   IP should be able to record it, and one who clears it should know the
   engine will take it back. */
.engine-managed {
  background: rgba(139, 148, 158, 0.06);
}

.tag-engine {
  margin-left: 4px;
  font-size: var(--t-xs);
  padding: 0 3px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--muted);
  letter-spacing: 0.04em;
}

.row-edited td {
  box-shadow: inset 0 -2px 0 rgba(210, 153, 34, 0.5);
}

.row-new td {
  box-shadow: inset 0 -2px 0 rgba(63, 185, 80, 0.5);
}

.row-deleting td {
  opacity: 0.5;
  text-decoration: line-through;
}

.inv-impact td {
  white-space: normal;
  background: var(--panel-bg);
}

.inv-impact .notice {
  margin: 4px 0;
}

.cell-checks {
  display: flex;
  gap: 8px;
}

.cell-check {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: var(--t-xs);
  color: var(--muted);
}

.notice-info {
  border-color: var(--accent);
  background: rgba(88, 166, 255, 0.1);
  color: #a5d6ff;
}

.impact-list {
  margin: 6px 0 0;
  padding-left: 18px;
}

/* `.hidden` is a class (0-1-0) and `#main` / `#inventory-view` are ids
   (1-0-0), so the plain `.hidden { display: none }` above LOSES to their own
   `display: flex`. These two rules are what actually hides them -- without
   them the inventory renders with the whole map still stacked underneath it. */
#main.hidden,
#inventory-view.hidden {
  display: none;
}

/* -- the full-width home for a list screen (#173) --------------------------
   Same shape as the inventory view, and a content column rather than edge to
   edge: a table stretched across 1600px puts its first and last cell so far
   apart that reading a row means tracking across the screen. */
#screen-view {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  overflow: auto;
}

#screen-view.hidden {
  display: none;
}

#screen-body {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 1100px;
  padding: var(--s-7) var(--s-8);
}

/* A LIST SCREEN IS THE PAGE, so its title carries the weight the panel's
   could not afford. */
#screen-body > h2 {
  margin: 0 0 var(--s-2);
  font-size: var(--t-xl);
}

#screen-body .data-table {
  width: 100%;
}

/* -- the same table, in a narrow box (#173, generalized by item 74) --------
   `workitems` renders in the 400px panel -- one entity's outstanding work --
   and six columns do not fit however they are styled. Each row becomes a
   stack, with the column name in front of its value from the header row via
   `data-label`, so nothing is dropped and nothing runs off the edge.

   ASKED OF THE BOX, NOT OF THE ID. These rules named `#panel-body`, because
   that was the only narrow place a `.data-table` appeared. Item 74 puts the
   work queue in a 390px `#screen-body`, which needs exactly this and must
   NOT have it on a laptop -- something an id cannot express and a second
   copy of these six rules would only pretend to (DESIGN_PRINCIPLES #12).
   What decides is how wide the container is, so that is what is asked.

   560px, because the panel is ~388px inside its padding on a laptop and has
   always had cards there; the threshold has to sit above that. */
#panel-body,
#screen-body {
  container-type: inline-size;
}

@container (max-width: 560px) {
  .data-table thead {
    display: none;
  }

  .data-table,
  .data-table tbody,
  .data-table tr,
  .data-table td {
    display: block;
    width: auto;
  }

  .data-table tr {
    margin-bottom: var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: var(--raised);
    border: 1px solid var(--border-soft);
    border-radius: var(--r-md);
  }

  .data-table td {
    display: flex;
    gap: var(--s-4);
    align-items: baseline;
    padding: var(--s-1) 0;
    border: 0;
  }

  .data-table td::before {
    content: attr(data-label);
    flex: 0 0 92px;
    color: var(--muted);
    font-size: var(--t-xs);
  }

  /* The "show 100 more" row (#189) is a control, not a record: it carries no
     column and must not be dressed as a card with an empty label in front of
     its button. */
  .data-table tr.list-more {
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
  }

  .data-table tr.list-more td::before {
    content: none;
  }
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Work items listed on a customer, and the queue rows that open them. */
.work-items {
  margin: 6px 0;
  padding-left: 18px;
  font-size: var(--t-sm);
}

.work-items li {
  margin-bottom: 4px;
}

.work-items .chip {
  margin-left: 6px;
}

.work-items-done {
  opacity: 0.7;
}

.dependents {
  margin: 6px 0;
  padding-left: 18px;
}

.row-clickable td a {
  color: var(--accent);
}

.hint {
  color: var(--muted);
  font-size: var(--t-sm);
  margin: 2px 0 8px;
}

/* -- optional form sections (DECISIONS.md #089) -------------------------- */
/* Serials and as-built positions are six-plus fields that most links do not
   need. Collapsed by default so the common case stays a short form. */
.form-details {
  margin: 8px 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 6px 10px;
}

.form-details > summary {
  cursor: pointer;
  font-size: var(--t-sm);
  color: var(--muted);
}

.form-details[open] > summary {
  margin-bottom: 6px;
  color: var(--text);
}

/* A site that LOST redundancy in a backhaul change reads differently from one
   that merely re-routed -- the first is a thing to act on. */
.path-change-lost {
  color: var(--warn, #d29922);
  font-weight: 600;
}

/* How far a route runs, printed ahead of which way it goes (#157). Dimmer
   than the link ids it introduces: it is a label on the chain, not a member
   of it. Over the operator's hop limit it takes the warning color, because a
   route allowed to run long is exactly when the number matters. */
.route-hops {
  color: var(--muted, #8b949e);
}

.route-hops-over {
  color: var(--warn, #d29922);
  font-weight: 600;
}

/* -- basemap picker (DECISIONS.md #088) ---------------------------------- */
/* Bottom left of the map, opposite corner from the layer panel. Sits above
   MapLibre's attribution strip, which must stay readable -- the tile
   providers are used on the condition that they are credited. */
.basemap-picker {
  position: absolute;
  /* BOTTOM RIGHT, clear of the layer panel. On the left it sat under a legend
     that grows with every overlay, so the two fought for the same corner and
     the picker won -- hiding the rows the legend most needed to show.
     Measured, not guessed: MapLibre's `.maplibregl-ctrl-bottom-right` control
     block is 44 px tall, and the attribution inside it is a LICENSING
     requirement for OSM and Esri rather than decoration, so this clears the
     whole block rather than only the visible text. */
  bottom: 52px;
  right: 12px;
  z-index: 2;
  display: flex;
  gap: 2px;
  padding: 3px;
  background: rgba(22, 27, 34, 0.94);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

.basemap-btn {
  background: transparent;
  border: 0;
  border-radius: var(--r-md);
  padding: 4px 10px;
  font-size: var(--t-sm);
  color: var(--muted);
  cursor: pointer;
}

.basemap-btn:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.06);
}

.basemap-btn.active {
  color: var(--text);
  background: rgba(88, 166, 255, 0.18);
}

/* One sub-network for a whole upload, or a column that names it per row
   (DECISIONS.md #088). The control under each radio is indented so it reads
   as belonging to that choice rather than as a third field. */
.subnet-mode + .subnet-mode {
  margin-top: 8px;
}

.subnet-mode > *:not(.radio-row) {
  margin-left: 22px;
}

/* -- the check flow's site list (DECISIONS.md #083) ----------------------- */
/* -- the verdict headline ---------------------------------------------------
   The verdict IS a status, so it wears the status colors -- the one screen
   where green/amber/red text is exactly the vocabulary the operator is
   scanning in. Sticky, because the sites that made the verdict true run to
   several screens of reasons, and "serve with upgrades" scrolling away left
   the reader holding six reason codes and no conclusion. Full-bleed against
   the panel's 16px padding so nothing shows scrolling past its sides. */
.verdict-head {
  position: sticky;
  top: -16px;
  z-index: 3;
  margin: 0 -16px var(--s-4);
  padding: var(--s-5) 16px;
  background: var(--panel-bg);
  border-bottom: 1px solid var(--border-soft);
}

.verdict-head.verdict-serve_as_is {
  color: var(--green);
}

.verdict-head.verdict-serve_with_upgrades {
  color: var(--amber);
}

.verdict-head.verdict-cannot_serve {
  color: var(--red);
}

/* Step 1 is "who can see this address, and how far away" -- a row per site,
   nearest first. A servable row is clickable through to its options; a
   blocked one carries its reasons inline, because the operator is deciding
   about THAT site and the reason belongs where the decision is made. */

.site-row {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 8px 10px;
  margin-bottom: 8px;
  cursor: pointer;
}

.site-row:hover {
  border-color: var(--accent);
}

/* A hop the operator has picked for the ladder (#097). The list is a
   multi-select now -- primary and backup are chosen together, on one screen
   -- so a selected row has to read as selected without being re-ordered. */
.site-row-chosen {
  border-color: var(--accent);
  background: rgba(88, 166, 255, 0.1);
}

.site-row-blocked {
  cursor: default;
  opacity: 0.85;
}

.site-row-blocked:hover {
  border-color: var(--border);
}

/* The verdict's two lists differed by nothing but a badge, and the one site
   that CAN serve is the answer the operator came for. The rail is the same
   4px the option cards already use for disruption, in the same status
   colors the verdict headline wears. Scoped to the wrappers rather than to
   `.site-row` itself: the row is shared, and a hop-picking ladder's rows are
   not verdicts. */
.servable-sites .site-row {
  border-left: 4px solid var(--green);
}

.blocked-sites .site-row {
  border-left: 4px solid var(--red);
}

/* The "cannot build to / cannot serve from" section (#105). Set apart from
   the choices above it rather than merely below them: an operator scanning
   for something to build should be able to stop reading at the heading. */
.blocked-heading {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px dashed var(--border);
  color: var(--muted);
}

.blocked-sites {
  opacity: 0.9;
}

.site-row-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}

.site-distance {
  margin-left: auto;
  color: var(--muted);
  font-family: ui-monospace, monospace;
  font-size: var(--t-sm);
}

/* Bulk-add rejections (DECISIONS.md #087). Numbered by SPREADSHEET line so
   the operator can open the file and go straight to the row. */
.reject-list {
  margin: 6px 0 0;
  padding-left: 18px;
  font-size: var(--t-sm);
}

.reject-list li {
  margin-top: 2px;
}

.notice-info .reject-list {
  color: inherit;
}

/* Unplanned: a candidate location, not part of the network yet (#087).
   Amber, matching the hollow marker on the map so the two read as one state. */
.badge-unplanned {
  color: #f0883e;
  border-color: #f0883e;
}

/* The work plan as a list of decisions (#092): one block per item, collapsed
   to its answer once chosen so what is left to decide stays obvious. */
/* Each decision is a CARD, not a line with a rule down one side (#093).
   With a 2px left border and 8px of margin, a last mile and four backhaul
   upgrades ran together into one column of text -- the operator could not see
   where one choice ended and the next began, which is exactly the thing this
   screen exists to make separable. A real edge, a raised surface and room
   between them do that with no extra words. */
.work-slot {
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--r-md);
  background: rgba(255, 255, 255, 0.02);
  padding: 10px 12px;
  margin: 12px 0;
}

.work-slot-blocked {
  border-left-color: var(--red, #f85149);
}

/* Chosen reads as settled: the accent goes green and the surface drops back,
   so an operator scanning a long plan sees what is still outstanding. */
.work-slot-done {
  border-left-color: var(--green, #3fb950);
  background: transparent;
}

.work-slot-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: var(--t-sm);
}

/* What KIND of thing this slot buys. The last mile and the backhauls were
   indistinguishable once their labels wrapped; this says which is which
   without relying on reading the label. */
.work-slot-kind {
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 1px 5px;
}

.work-slot-options {
  list-style: none;
  padding: 0;
  margin: 4px 0 0;
}

.work-slot-options li {
  padding: 2px 0;
}

.work-slot-options li.chosen {
  font-weight: 600;
}

.work-slot-chosen {
  margin: 4px 0 0;
  font-size: var(--t-sm);
}

/* An upper bound, so the caveat is never separable from the number. */
.free-capacity {
  margin: 6px 0 0;
  font-size: var(--t-sm);
}

.free-capacity .hint {
  display: block;
  margin-top: 2px;
}

/* A slot with its answer in: dimmed heading, so what is LEFT to decide is
   what stands out in a six-item plan (#092). */
.work-slot-done .work-slot-head strong {
  color: var(--muted);
  font-weight: 500;
}

.work-slot-options .select {
  width: 100%;
  margin-top: 4px;
  background: #0d1117;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--r-sm);
  padding: 5px 8px;
  font-family: inherit;
  font-size: var(--t-sm);
}

/* A work plan is now GROUPED (#100): "Last mile" first, then one block per
   hop with a row per end. The group is the visible unit of a purchase --
   without a container the two ends of a hop read as two unrelated items,
   which is exactly the confusion the grouping exists to remove. */
.work-group {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 0.6rem 0.75rem 0.35rem;
  margin: 0.75rem 0;
  background: rgba(255, 255, 255, 0.02);
}
.work-group-title {
  margin: 0 0 0.5rem;
  font-size: 0.95rem;
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  flex-wrap: wrap;
}
/* Rows inside a group carry the group's border instead of their own, so a
   hop reads as one card and not as two stacked ones. */
.work-group .work-slot {
  border: 0;
  border-top: 1px solid var(--border);
  border-radius: 0;
  margin: 0;
  padding: 0.5rem 0;
}
.work-group .work-slot:first-of-type {
  border-top: 0;
}
.work-group.work-slot-blocked {
  border-color: var(--danger, #f85149);
}

/* The legend (#108, item 4). Lives inside the layer panel, collapsed by
   default: a reference you consult rather than a control you use, and long
   enough to push the toggles off screen if it opened by itself. */
.legend {
  margin-top: 10px;
  border-top: 1px solid var(--border);
  padding-top: 8px;
}

.legend summary {
  cursor: pointer;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}

.legend h5 {
  margin: 8px 0 4px;
  font-size: var(--t-xs);
  color: var(--muted);
  font-weight: 600;
}

.legend-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 2px 0;
  font-size: var(--t-sm);
  color: var(--text);
}

/* One swatch element, shaped by its modifier class -- an operator matching
   the legend to the canvas is looking for a dashed line or a hollow ring, so
   a column of identical squares would make them work it out themselves. */
.legend-swatch {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  display: inline-block;
}

.legend-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--swatch);
}

.legend-ring {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 2px solid var(--swatch);
}

.legend-line {
  height: 0;
  border-top: 3px solid var(--swatch);
}

.legend-dashed {
  height: 0;
  border-top: 3px dashed var(--swatch);
}

.legend-bar {
  height: 8px;
  border-radius: 2px;
  background: var(--swatch);
}

/* The sector wedge, as a wedge: a quarter-disc pointing right, which is the
   shape on the canvas rotated to a readable orientation. */
.legend-wedge {
  width: 14px;
  height: 14px;
  background: var(--swatch);
  opacity: 0.55;
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
}

/* A site with nothing upstream (#125). Two crossing bars rather than a glyph,
   so it matches the shape drawn on the canvas and needs no font. */
.legend-cross {
  position: relative;
  width: 12px;
  height: 12px;
}

.legend-cross::before,
.legend-cross::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 2.5px;
  margin-top: -1.25px;
  background: var(--swatch);
}

.legend-cross::before {
  transform: rotate(45deg);
}

.legend-cross::after {
  transform: rotate(-45deg);
}

/* -- settings (#110) ------------------------------------------------------
   One row per tunable: what it is, what it means, and when a change bites.
   The effect badge is the load-bearing part -- "takes effect now" and "next
   decision only" are different promises, and a row that looked the same for
   both would make the screen quietly untrue about half of them. */
.settings-group {
  margin-bottom: 18px;
}

.setting {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 10px 12px;
  margin-bottom: 8px;
}

.setting-staged {
  border-color: var(--accent);
}

.setting-bad {
  border-color: var(--red);
}

.setting-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.setting-label {
  font-weight: 600;
}

.setting-effect {
  flex: none;
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 1px 8px;
  color: var(--muted);
  cursor: help;
}

.effect-now {
  color: var(--green);
  border-color: var(--green);
}

.effect-next {
  color: var(--amber);
  border-color: var(--amber);
}

.setting-control {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 8px 0 6px;
  flex-wrap: wrap;
}

.setting-input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 4px 8px;
  min-width: 140px;
}

.setting-input:focus {
  border-color: var(--accent);
  outline: none;
}

.setting-check {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
}

.setting-unit {
  color: var(--muted);
  font-size: var(--t-sm);
}

.setting-readonly {
  color: var(--muted);
}

.setting-desc {
  margin: 0;
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--muted);
}

.setting-note {
  margin: 6px 0 0;
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--amber);
}

.setting-error {
  margin: 6px 0 0;
  font-size: var(--t-sm);
  color: var(--red);
}

.setting-key {
  margin: 6px 0 0;
  font-size: var(--t-xs);
}

.setting-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.setting-list-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* -- Search (ROADMAP item 22) ------------------------------------------- */
/* An overlay rather than a control that pushes the bar around: the list is
   twenty rows on a wide network, and a top bar that changed height while
   somebody typed would move every button under their cursor. */

.search-box {
  position: relative;
}

.search-input {
  width: 260px;
  padding: 6px 10px;
  font: inherit;
  font-size: var(--t-md);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.search-input:focus {
  outline: none;
  border-color: var(--accent);
  width: 340px;
}

.search-results {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 30;
  width: 380px;
  max-height: 60vh;
  overflow-y: auto;
  padding: 4px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}

.search-note {
  margin: 4px 6px;
  font-size: var(--t-xs);
  color: var(--muted);
}

.search-hit {
  display: block;
  width: 100%;
  padding: 6px 8px;
  text-align: left;
  color: var(--text);
  background: none;
  border: none;
  border-radius: var(--r-sm);
  cursor: pointer;
}

/* Hover and keyboard selection look the SAME. Two highlights on one list is
   the pattern that makes an operator press Enter on the wrong row. */
.search-hit:hover,
.search-hit.active {
  background: rgba(88, 166, 255, 0.15);
}

.search-hit-label {
  display: block;
  font-size: var(--t-md);
}

.search-hit-detail {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-top: 2px;
  font-size: var(--t-xs);
  color: var(--muted);
}

.search-kind,
.search-flag,
.search-work,
.search-distance {
  padding: 0 5px;
  font-size: var(--t-xs);
  line-height: 15px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
}

/* Stranded is a WARNING, not decoration: the entity is excluded from every
   reasoning read (#074), and a row that looked ordinary would leave somebody
   wondering why the thing they just found carries no load. */
.search-flag {
  color: var(--amber);
  border-color: var(--amber);
}

.search-work {
  color: var(--accent);
  border-color: var(--accent);
}

.search-geocode {
  margin: 2px 6px 6px;
  font-size: var(--t-sm);
}

/* -- Rule-chain trace (ROADMAP item 58) --------------------------------- */
/* Deliberately quieter than the verdict above it: this is the working, and
   an operator who trusts the answer should be able to scroll past it. */

.trace {
  margin-top: 18px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
}

.trace-toggle {
  padding: 0;
}

.trace-body {
  margin-top: 8px;
}

.trace-chain {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 6px 0 12px;
}

.trace-chain-rung {
  padding: 1px 7px;
  font-size: var(--t-xs);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}

.trace-site {
  margin: 10px 0;
  padding: 8px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

.trace-site-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font-size: var(--t-md);
}

.trace-attempts {
  margin-top: 6px;
}

.trace-attempt {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  padding: 3px 0;
  font-size: var(--t-xs);
  border-top: 1px solid rgba(255, 255, 255, 0.04);
}

.trace-step {
  min-width: 14px;
  color: var(--muted);
}

.trace-strategy {
  min-width: 96px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.trace-ok {
  color: var(--green);
}

.trace-no {
  color: var(--muted);
}

.trace-option {
  color: var(--accent);
}

/* The reason CODE, with the engine's full sentence on hover. Codes here and
   prose in the tooltip, because a rung-by-rung list is scanned rather than
   read -- the sentences already have a home on the site rows above. */
.trace-reason,
.trace-conflict {
  padding: 0 5px;
  font-size: var(--t-xs);
  line-height: 15px;
  color: var(--amber);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  cursor: help;
}

.trace-refusal {
  margin: 6px 0 0;
  font-size: var(--t-xs);
  color: var(--muted);
}

.trace-rank {
  margin: 10px 0;
  padding: 8px 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

.trace-rank-head {
  font-size: var(--t-md);
}

.trace-terms {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 5px;
}

.trace-term {
  display: flex;
  gap: 4px;
  font-size: var(--t-xs);
}

.trace-term-label {
  color: var(--muted);
}

.trace-links {
  width: 100%;
  margin-top: 8px;
  border-collapse: collapse;
  font-size: var(--t-xs);
}

.trace-links th {
  text-align: left;
  font-weight: normal;
  color: var(--muted);
  padding: 2px 6px 2px 0;
}

.trace-links td {
  padding: 2px 6px 2px 0;
}

/* A hop that cannot take the load is the row the whole table exists for. */
.trace-short td {
  color: var(--amber);
}


/* -- collapsible panel sections (ROADMAP item 4 follow-up, #147) --------- */

/* SEPARATED BY SURFACE, NOT BY A LINE (#172). Every block used to sit on one
   background with a 1px rule between, so a panel was a flat scroll with
   stripes across it and nothing read as a thing you could look at on its own.
   A card that is one shade lighter does the same job and says "this is a
   unit" rather than "something changed here". */
.panel-section {
  margin: 0 0 var(--s-3);
  background: var(--raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-md);
  box-shadow: var(--e-1);
}

.panel-section > summary {
  cursor: pointer;
  padding: var(--s-4) var(--s-5);
  font-size: var(--t-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  list-style: none;
}

.panel-section > summary::-webkit-details-marker {
  display: none;
}

/* The disclosure triangle, drawn rather than inherited so it points the same
   way in every browser and turns with the section. */
.panel-section > summary::before {
  content: "\25b8";
  display: inline-block;
  width: 12px;
  color: var(--muted);
  transition: transform 0.12s ease;
}

.panel-section[open] > summary::before {
  transform: rotate(90deg);
}

.panel-section > summary:hover {
  color: var(--fg);
}

/* A closed section hides its contents but must not hide their severity:
   "WARNINGS (2)" in the same gray as "SITE" is the panel knowing and not
   saying. The tone is set by the caller from the worst severity inside. */
.panel-section-warn > summary,
.panel-section-warn > summary::before {
  color: var(--amber);
}

.panel-section-error > summary,
.panel-section-error > summary::before {
  color: var(--red);
}

.panel-section-body {
  /* Even inside the card now. It used to be indented from the left only,
     which read as a hanging block rather than as contents. */
  padding: 0 var(--s-5) var(--s-5);
}

.panel-section-body > h3:first-child,
.panel-section-body > h4:first-child {
  margin-top: 0;
}


/* -- scrollbars ----------------------------------------------------------
   The panel and the inventory scroll a lot, and the platform default draws a
   wide light-gray bar that is the brightest thing on a dark surface. Kept
   visible rather than hidden: a scrollable region with no indication that it
   scrolls is the other, worse mistake. */
#panel,
#inventory-body,
.panel-scroll {
  scrollbar-width: thin;
  scrollbar-color: #39414d transparent;
}

#panel::-webkit-scrollbar,
#inventory-body::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

#panel::-webkit-scrollbar-thumb,
#inventory-body::-webkit-scrollbar-thumb {
  background: #39414d;
  border: 3px solid var(--panel-bg);
  border-radius: var(--r-md);
}

#panel::-webkit-scrollbar-thumb:hover,
#inventory-body::-webkit-scrollbar-thumb:hover {
  background: #4a5361;
}

#panel::-webkit-scrollbar-track,
#inventory-body::-webkit-scrollbar-track {
  background: transparent;
}


/* -- recalculate at a speed (#162) ---------------------------------------
   The number and the button are ONE control: the button acts on whatever is
   in the box, and spacing them apart like two independent actions is how an
   operator ends up pressing it without noticing the figure beside it. */
.recalc-at {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.recalc-at .speed-input {
  width: 6.5em;
  background: #21262d;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 6px 8px;
  font-size: var(--t-md);
  font-family: inherit;
  line-height: 1.35;
}

.recalc-at .field-unit {
  color: var(--muted);
  font-size: var(--t-sm);
}


/* -- concise mode (#166) --------------------------------------------------
   The whole feature. `.prose` marks a paragraph that EXPLAINS something --
   what a term means, how a feature is meant to be used -- as opposed to one
   that states a fact about this network. Hiding it is a display choice, so it
   is made here rather than by rendering different text, which would be two
   copies of every sentence and one of them going stale (DESIGN_PRINCIPLES
   #12).

   `.prose` IS NOT `.hint`, and #167 widened this by hand rather than by
   reaching for that class. `.hint` is a STYLE -- small and muted -- and it
   carries explanation, a live status line, an engine caveat and a 404 message
   alike. Hiding every `.hint` would take down `replan`'s error status and the
   trace's "this check is no longer being kept", neither of which a glossary
   would ever have told a reader. So a paragraph is marked one at a time, and
   the test for whether it qualifies is in `tests/frontend/concise.test.js`. */
body.concise .prose {
  display: none;
}

/* -- long lists, a page at a time (ui.js appendPaged) ----------------------
   The "show more" row sits where the next item would have been and says how
   far down the list the reader is, so a partial list never reads as all of
   it. */
.list-more {
  list-style: none;
  padding: var(--s-4) 0;
  font-size: var(--t-sm);
}

.list-more > td {
  padding: var(--s-4) var(--s-3);
  background: var(--panel-bg);
}

.list-more .btn {
  margin-left: var(--s-3);
}

/* -- point-to-point ends (map.js setPointToPoint) --------------------------
   Lettered, because "which end is A" is the first thing the panel's two
   blocks ask, and a pair of identical pins would not say. Grab cursors
   because dragging is how an end is fine-tuned. */
.p2p-marker {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 700 12px/1 -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  color: #0d1117;
  background: var(--accent);
  border: 2px solid #ffffff;
  box-shadow: var(--e-2);
  cursor: grab;
}

.p2p-marker:active {
  cursor: grabbing;
}

/* ===========================================================================
   NARROW SCREENS (ROADMAP item 74, substep 1)

   WHAT THIS FIXES, measured at 390px before it existed: the map was 0 pixels
   wide. `#panel` is a fixed 420px, which is wider than the viewport, so it
   took the whole row, pushed the map to nothing, and the page scrolled
   sideways to 658px. The one breakpoint the file had, at 1180px, hides the
   brand name and nothing else.

   THE BREAKPOINT IS WHERE THE MAP STOPS BEING WORTH HAVING, not a device.
   Below 900px a 420px panel leaves under 480px of map and falls away fast:
   at 768px -- an iPad held upright -- it is 343px. Stacked, that same tablet
   gets a full-width map instead of a cramped column, which is why this is not
   a phone-only rule.
   =========================================================================== */
@media (max-width: 900px) {
  #main {
    flex-direction: column;
  }

  #map {
    /* AN EXPLICIT HEIGHT, because a map has no content to be tall for. Its
       canvas is absolutely positioned, so `flex: 1 1 auto` in a column
       resolves against a zero content height and collapses -- the same
       nothing this rule exists to fix, arriving by a different route.

       `--sheet-map-h` is the sheet position, written by main.js. The default
       here is the middle stop, so the layout is correct before any script
       runs and stays correct if one never does. */
    flex: 0 0 auto;
    height: var(--sheet-map-h, 45vh);
    min-height: 120px;
    transition: height 0.22s ease;
  }

  /* While a finger is on the handle the height must track it exactly. A
     transition here would make the sheet lag the drag, which reads as the
     app being slow rather than as easing. */
  body.resizing #map {
    transition: none;
  }

  /* THE DIVIDER BECOMES THE SHEET'S HANDLE rather than disappearing. It is
     already the element between the map and the panel and it already owns the
     drag, so the alternative -- hiding it and adding a second grab bar -- is
     two controls for one job (DESIGN_PRINCIPLES #4). Turned on its side,
     given a grip, and made big enough to hit with a thumb. */
  #divider {
    flex: 0 0 22px;
    cursor: row-resize;
    background: var(--panel-bg);
    border-top: 1px solid var(--border);
    position: relative;
    /* Or the browser claims the gesture for scrolling and the drag never
       reaches us. */
    touch-action: none;
  }

  #divider::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 36px;
    height: 4px;
    margin: -2px 0 0 -18px;
    border-radius: 2px;
    background: var(--border);
  }

  #divider:hover::after,
  body.resizing #divider::after {
    background: var(--accent);
  }

  /* The horizontal rule's hover color would paint the whole bar blue. */
  #divider:hover,
  body.resizing #divider {
    background: var(--panel-bg);
  }

  body.resizing {
    cursor: row-resize;
  }

  #panel {
    /* Beats `--panel-width` by ordinary cascade, so a panel dragged wide on a
       laptop does not stay 600px wide after the window narrows. */
    flex: 1 1 auto;
    min-height: 0;
    border-left: none;
    border-top: 1px solid var(--border);
  }

  /* ONE ROW EACH, SCROLLED, rather than five wrapped ones. The bar was 228px
     tall at 390px -- 27% of the screen, spent on chrome, before the map got
     any. Wrapping is right when the groups nearly fit and wrong when a group
     is three times too wide, which is what six mode buttons are on a phone.
     The buttons cut off mid-row at the edge, which is the affordance that
     there is more; a 3px scrollbar between two rows of buttons is not, and
     reads as a rendering fault. */
  #topbar .mode-group,
  #topbar .screens-group,
  #topbar .topbar-right {
    flex-wrap: nowrap;
    overflow-x: auto;
    /* SHRINK AND SHARE A ROW, rather than each taking one of its own. Six
       groups at one row each is where the 234px came from; allowed to shrink,
       they pair up and the bar lands near half that. The basis is a floor, not
       a width -- below it a group would be a scroll container too small to
       show a whole button, which is worse than the row it saves. */
    flex: 1 1 150px;
    min-width: 0;
    scrollbar-width: none;
  }

  #topbar .mode-group::-webkit-scrollbar,
  #topbar .screens-group::-webkit-scrollbar,
  #topbar .topbar-right::-webkit-scrollbar {
    display: none;
  }

  /* Or they shrink to fit instead of scrolling, and every label truncates. */
  #topbar .mode-group > *,
  #topbar .screens-group > *,
  #topbar .topbar-right > * {
    flex: 0 0 auto;
  }

  /* THE ONE THAT ACTUALLY OVERFLOWED, and not the one it looked like. With
     the mode buttons scrolling, the page still scrolled to 654px at 390 --
     because `.topbar-right` is a single nowrap row of the status line and
     three groups, 646px of it, held together by `margin-left: auto`. Pushing
     it to the far end is what makes the bar readable on a laptop and what
     makes it overflow here, so the auto margin goes.

     SCROLLED, NOT WRAPPED, and that was measured rather than guessed: letting
     it wrap DID stop the page scrolling and took the bar from 234px to 291px
     -- on an 844px screen that left the panel 173 pixels, which is three
     lines of text. Trading a sideways scroll for a panel nobody can read is
     not a fix. Every group now behaves the same way instead. */
  .topbar-right {
    margin-left: 0;
  }

  /* Nothing to separate once the things either side of it are on different
     rows -- a vertical rule at the end of a wrapped line is just a mark. */
  .topbar-rule {
    display: none;
  }

  #topbar {
    padding: var(--s-3) var(--s-4);
    gap: var(--s-4);
  }

  /* THE MAP'S OWN OVERLAYS, against a map that is now 380px tall rather than
     720. Five basemap names at laptop padding, stacked above an attribution
     that wraps to two lines, covered most of the bottom quarter of it.
     Tightened here and not everywhere, because at full height the roomier
     control is the better one.

     The ATTRIBUTION is left alone deliberately: OSM and Esri require it, so
     it is a licensing element rather than decoration, and MapLibre collapses
     it to a single button on a narrow container without being asked. */
  /* TO THE TOP, because the bottom is spoken for. The attribution wraps to
     two lines in a 390px container and spans almost the full width, so there
     is no longer a bottom-right corner to sit clear of -- tightening the
     picker and leaving it there only moved it BEHIND the attribution, which
     was worse than the crowding it was meant to fix. The top-left holds the
     layer popover, which is transient and opens over everything anyway. */
  .basemap-picker {
    top: var(--s-4);
    bottom: auto;
    right: 8px;
    padding: 2px;
  }

  .basemap-btn {
    padding: 3px 7px;
    font-size: var(--t-xs);
  }

  /* -- the inventory (item 74, substep 3) ---------------------------------
     NOT A TABLE PROBLEM, which is what it looked like. The inventory is two
     panes, and at 390px the 190px sidebar left the twenty-column editable
     grid 200 pixels -- so the grid pushed the PAGE sideways to 501px rather
     than scrolling inside its own box, which `.inv-table-wrap` is already
     built to let it do.

     The sidebar becomes a strip across the top, exactly as the mode buttons
     did: same problem, same answer, and a picker is a picker whichever way
     it is laid out. */
  /* NO `min-width: 0` ANYWHERE HERE, and that was checked rather than
     assumed. One was written for `#inventory-view` on the strength of the
     comment above about needing it "at every level" -- and removing it again
     changed nothing, because stacking the panes is what stops the grid
     competing for width in the first place. A rule that does nothing, under
     a comment explaining why it is essential, is worse than no rule. */
  .inv-shell {
    flex-direction: column;
  }

  .inv-sidebar {
    flex: 0 0 auto;
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    gap: var(--s-4);
    padding: var(--s-2) var(--s-3);
    border-right: 0;
    border-bottom: 1px solid var(--border);
    scrollbar-width: none;
  }

  .inv-sidebar::-webkit-scrollbar {
    display: none;
  }

  /* Each heading and its kinds stay together as one run, so "Catalog" does
     not end up floating above a button belonging to "Network". */
  .inv-group {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    flex: 0 0 auto;
  }

  .inv-group h4 {
    margin: 0;
    white-space: nowrap;
  }

  .inv-kind {
    display: inline-block;
    width: auto;
    flex: 0 0 auto;
    white-space: nowrap;
    /* The left bar marks the active kind in a vertical list; across a row it
       would be a stray tick beside the wrong edge. */
    border-left: 0;
    border-bottom: 2px solid transparent;
    padding: 4px 8px;
  }

  .inv-kind.active {
    border-bottom-color: var(--accent);
  }
}

/* ===========================================================================
   A FINGER, NOT A CURSOR (ROADMAP item 74, substep 5)

   `pointer: coarse` and not a width, deliberately: what decides here is what
   is doing the pointing, not how much room it has. A touch laptop at 1440px
   needs these and a mouse-driven window at 390px does not.

   Measured before this existed: 28 controls between 29px and 32px tall,
   which is the whole top bar. Apple asks for 44px and Android for 48dp;
   40px is the compromise this particular bar can afford -- every extra pixel
   here is a pixel off the map and the panel, on the screen that has fewest
   to give, and four rows of 44px would have taken a quarter of it.
   =========================================================================== */
@media (pointer: coarse) {
  .btn,
  .mode-btn,
  .select,
  .search-input,
  .inv-kind,
  button,
  select,
  input[type="text"],
  input[type="search"],
  input[type="number"] {
    min-height: 40px;
  }

  /* `min-height` alone leaves the text at the top of a taller box, because
     these are inline-blocks with their own padding rather than flex rows. */
  .btn,
  .mode-btn,
  .inv-kind {
    display: inline-flex;
    align-items: center;
  }

  /* The sheet's handle is grabbed rather than tapped, and a thumb finds the
     edge of a bar more easily than its middle -- so it gets the height
     without the 40px the controls take. */
  #divider {
    flex-basis: 28px;
  }

  /* A checkbox is the one control that cannot grow with `min-height`: it has
     an intrinsic size and the box just floats in a taller line. The cell it
     sits in is what a finger actually aims at. */
  .cell-check {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
  }

  /* SIXTEEN PIXELS, AND IT IS NOT A LEGIBILITY CHOICE (item 74, substep 6).
     iOS Safari zooms the whole viewport in when a field smaller than 16px
     takes focus, and it does NOT zoom back out when the field is left: the
     map, the panel and the bar are all left oversized and off-centre, and
     the only way back is a pinch. Every field in this app measured 12-14px,
     so every field did it -- and the ones that matter most, the latitude and
     longitude on the check form, are 12px.

     Under `pointer: coarse` only, because this is a fix for a phone keyboard
     rather than a view about type size: a laptop keeps the density its rows
     were designed for.

     SCOPED TO `#app` TO WIN, not as a tidiness habit. Nearly every field in
     here is reached by a class -- `.search-input`, `.setting-input`,
     `label input` -- and a bare element selector loses to all of them, which
     it did: the first attempt changed two fields out of six and left the
     coordinate inputs at 12px. An id is the smallest thing that outranks a
     class without `!important`, which would take the size away from any rule
     that later has a good reason to set it. */
  #app input,
  #app select,
  #app textarea {
    font-size: var(--t-no-zoom);
  }
}

/* A determinate progress bar (#206). Only ever shown once the server has
   reported a real fraction -- see `progressNote`, which starts as a spinner
   and becomes this. */
.progress-bar {
  display: inline-block;
  vertical-align: middle;
  width: 120px;
  height: 6px;
  margin: 0 0.5em;
  border-radius: var(--r-pill);
  background: var(--bar-track, rgba(127, 127, 127, 0.25));
  overflow: hidden;
}
.progress-bar-fill {
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: var(--accent, #4a9eff);
  /* Follows the server's readings; the transition only smooths the step
     between two real numbers, it never advances on its own. */
  transition: width 200ms linear;
}
.progress-label {
  font-variant-numeric: tabular-nums;
}

/* The coverage key (#207). Top right of the map, opposite the layer panel:
   the two coverage overlays are drawn over each other, and everything that
   said so used to be eleventh and twelfth in a popover over a collapsed
   legend. This is the answer's own key -- it arrives with a footprint and
   leaves with it. */
.coverage-key {
  position: absolute;
  top: var(--s-5);
  right: var(--s-5);
  z-index: 2;
  background: rgba(22, 27, 34, 0.96);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-4) var(--s-5) var(--s-5);
  min-width: 190px;
  max-width: 240px;
  box-shadow: var(--e-3);
}
.coverage-key h4 {
  margin: 0 0 var(--s-3);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
/* The sentence a reader could not work these out without. Not `.prose`: it
   is the whole point of the panel, so brief mode must not take it. */
.coverage-key-note {
  margin: 0 0 var(--s-4);
  font-size: var(--t-xs);
  color: var(--muted);
}
.coverage-key-section + .coverage-key-section {
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}
/* The checkbox heads its own swatches, so the rows below it read as "what
   this layer paints" rather than as a second list. */
.coverage-key-section .layer-toggle {
  margin-bottom: var(--s-2);
}

/* On a phone the map is short and this would cover the network. The layer
   popover already answers the same question there. */
@media (max-width: 760px) {
  .coverage-key {
    display: none;
  }
}

/* What the answer was asked (#208). Directly under the verdict, quiet but not
   muted into invisibility: it is the answer's subject, and the reason it
   exists is that two people could not tell their questions apart. */
.asked-line {
  margin: var(--s-2) 0 var(--s-3);
  font-size: var(--t-sm);
  color: var(--text);
}
.asked-line strong {
  font-variant-numeric: tabular-nums;
}

/* THE "THIS TAB IS OUT OF DATE" BAR (#217). Fixed to the bottom rather than
   the top: the top bar is where the modes live and a strip appearing above
   them shifts every control down, which is worse than the problem. Bottom
   centre is out of the way of the map's own corners -- the coverage key sits
   top right (#207) and the attribution bottom left. */
.stale-bar {
  position: fixed;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100vw - 32px);
  padding: 10px 14px;
  border: 1px solid var(--amber);
  border-radius: var(--r-sm);
  background: var(--panel-bg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

.stale-text {
  font-size: var(--t-sm);
  color: var(--text);
}

/* Wraps rather than crushes its buttons on a phone, the same rule the
   inventory toolbar follows (#214). */
@media (max-width: 560px) {
  .stale-bar {
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  .stale-text {
    flex: 1 1 100%;
  }
}

/* The settings screen's search and its group counts (#218). The screen is 77
   rows under a dozen headings; the groups collapse and this is how you get
   back to one row without opening all of them. */
.settings-filter {
  width: 100%;
  margin: 8px 0 4px;
}

.settings-count {
  margin: 0 0 8px;
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
}
