/* Echelon — component layer on top of Tailwind utilities. */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&display=swap');

:root {
  --vault: #0A0B0D;           --vault-rgb: 10 11 13;
  --obsidian: #14161A;        --obsidian-rgb: 20 22 26;
  --gunmetal: #1B1E24;        --gunmetal-rgb: 27 30 36;
  --hairline: #262B33;        --hairline-rgb: 38 43 51;
  --ivory: #E9E5DB;           --ivory-rgb: 233 229 219;
  --slate: #99A0AB;           --slate-rgb: 153 160 171;
  --dim: #6E7581;             --dim-rgb: 110 117 129;
  --champagne: #C9A45C;       --champagne-rgb: 201 164 92;
  --champagne-hi: #C9A45C;    --champagne-hi-rgb: 201 164 92;
  /* THE LOGO'S GOLD, AND IT NEVER MOVES. The note above `body.app-light` says
     why there have to be two of these and which one a rule wants. */
  --champagne-fill: #C9A45C;  --champagne-fill-rgb: 201 164 92;
  --verified: #4DA97D;        --verified-rgb: 77 169 125;
  --cognac: #C98F45;          --cognac-rgb: 201 143 69;
  --signal: #C05A44;          --signal-rgb: 192 90 68;
  --action: #C9A45C;          --action-rgb: 201 164 92;
  --action-hi: #C9A45C;       --action-hi-rgb: 201 164 92;

  /* THE TIER BADGE'S AIR — the fraction of its square that is empty above the
     Formation and below the rule. Direction C is asymmetric and deliberately
     does not fill its own box, so a badge dropped into a centred flex row
     centres its BOX and hangs its INK low against a heading.
     UNITLESS ON PURPOSE, so a call site can multiply by its own size:
       style="margin-top: calc(var(--seal-air-top) * -72px)"   on a 72px badge
     pulls the empty top out and puts the mark's ink where the box edge was.
     Kept in step with ECH.ui.SEAL_AIR (assets/ui.js), which derives the same
     two numbers from the badge's own geometry constants. */
  --seal-air-top: 0.0863;
  --seal-air-bottom: 0.15;
}

/* Light workspace theme — the app portals. Familiar to Central/Super Dispatch
   users: light-gray workspace, white cards, navy rail, blue actions; Echelon's
   champagne gold carries tier and verification identity.
 *
 * ONE GOLD. Owner's ruling, 27 Aug 2026: "I want all of the golds to be the
 * color of the logo."
 *
 * This overturns the two-gold split of 26 Aug, and it is worth keeping the
 * shape of the argument that lost, because the same problem will come back.
 * That ruling read the same complaint — the Formation and the TIER 5 chip
 * beside it were different golds — and answered it by SPLITTING the token in
 * two, so fills could stay #C9A45C while ink darkened to #A8823C for contrast.
 * It fixed the pair he photographed and left five golds in the system. He is
 * now looking at the result and saying the portal still reads as several
 * different golds, which it did.
 *
 * So there is exactly one, on every ground, in every role:
 *
 *   --champagne · --champagne-hi · --champagne-fill · --action · --action-hi
 *                                              all #C9A45C, light and dark
 *
 * The -hi pair survives as a NAME, not as a colour. Fourteen hover rules point
 * at it; collapsing them to the base value rather than deleting the token
 * keeps those call sites honest, and hover now reads through brightness rather
 * than hue (see .btn-primary:hover below). A palette with one gold in it is
 * what was asked for, and a button that does not answer the mouse is not a
 * palette question.
 *
 * WHAT THIS COSTS, MEASURED ON THIS WORKSPACE'S WHITE CARD (--obsidian is
 * #FFFFFF here), so nobody has to rediscover it:
 *
 *   gold TEXT      3.55:1 → 2.35:1.  Already under AA's 4.5:1 before this and
 *                  further under it now. Gold links, the "Advanced search"
 *                  control, "Your verification record", the sort caret.
 *   small MARKERS  3.55:1 → 2.35:1.  The 9px timeline dots and 1.5px bullets
 *                  no longer clear 3:1. They carry no information alone —
 *                  every one of them sits beside a label — but they are now
 *                  decoration rather than signal.
 *   gold FILLS     unchanged at #C9A45C, and BETTER off: see the button below.
 *
 * That is a legibility round, not a brand round, and it is the owner's to
 * call. What this file will not do is answer a brand instruction with a
 * quiet second gold — which is exactly how five of them accumulated.
 *
 * THE ONE THING THAT HAD TO MOVE WITH THE GOLD. `body.app-light .btn-primary`
 * printed WHITE on --action. White on #A8823C was 3.55:1, poor; white on
 * #C9A45C is 2.34:1, unreadable. The label is now #0A0B0D at 8.40:1 — which is
 * not a compromise but the correct pairing, already used by `.btn-primary` on
 * the dark ground and by `.chip-tier-3`, both of which print near-black on
 * this exact gold. The brighter gold is the more readable one the moment you
 * stop trying to read gold ITSELF. */
body.app-light {
  --vault: #F2F4F7;           --vault-rgb: 242 244 247;
  --obsidian: #FFFFFF;        --obsidian-rgb: 255 255 255;
  --gunmetal: #EEF1F4;        --gunmetal-rgb: 238 241 244;
  --hairline: #DFE3E9;        --hairline-rgb: 223 227 233;
  --ivory: #1E2733;           --ivory-rgb: 30 39 51;
  --slate: #5C6875;           --slate-rgb: 92 104 117;
  --dim: #8A94A0;             --dim-rgb: 138 148 160;
  --champagne: #C9A45C;       --champagne-rgb: 201 164 92;
  --champagne-hi: #C9A45C;    --champagne-hi-rgb: 201 164 92;
  /* Re-stated, not re-mapped — the point of the token is that it is the same
     gold here as in :root. Declared rather than inherited so that deleting the
     line is a visible act and not a silent fall-through. Since 27 Aug that is
     true of the four tokens above it as well. */
  --champagne-fill: #C9A45C;  --champagne-fill-rgb: 201 164 92;
  --verified: #248A57;        --verified-rgb: 36 138 87;
  --cognac: #B26F1B;          --cognac-rgb: 178 111 27;
  --signal: #C94F3D;          --signal-rgb: 201 79 61;
  --action: #C9A45C;          --action-rgb: 201 164 92;
  --action-hi: #C9A45C;       --action-hi-rgb: 201 164 92;
  /* SD's control ring, one step darker than the card rule. DECLARED HERE
     rather than only in the pages, because it was never declared here at all:
     nine carrier pages and assets/loadform.css each wrote `--line: #C4CDD5`
     into their own palette block, and every page that did not — board.html
     among them — reached the same grey through the `var(--line, #C4CDD5)`
     fallback that .fb-field and .sel-btn both spell out. Two routes to one
     colour, agreeing today by coincidence: the day the token moves, half the
     portal follows it and half keeps the fallback, silently. This line makes
     the house the single source and changes nothing on any page today —
     verified, every one of the ten local declarations is this same value. */
  --line: #C4CDD5;
}

/* THE PHONE SURFACES — app/driver, driver.html, join.html, pickup.html.
 *
 * There are two palettes in this repo and it is better to say so than to let
 * them drift silently. The :root block above is the PORTAL palette that
 * docs/DESIGN.md names, and thirty carrier, broker and admin pages are drawn
 * in it. The design-lab mocks that the phone app is built to — home-screen,
 * app-login, app-walkthrough-v2 — are drawn in a slightly warmer ivory and a
 * muted sage, and so is the brand line the app was briefed against. Every one
 * of those four files was already half-forked: they carry dozens of
 * rgba(239,235,226,...) literals, which is #EFEBE2 spelled out, sitting
 * beside a var(--ivory) that resolves to #E9E5DB. Solid text and its own
 * hairlines were coming from two different whites.
 *
 * So the fork is declared, in one block, scoped to the four surfaces that
 * want it. If the portals are to win instead, delete this block and the four
 * class attributes; nothing else has to move.
 *
 * The hairline goes translucent for a reason of its own: the mocks draw every
 * rule as ivory at 9% over whatever it sits on, so the same declaration reads
 * correctly on the vault ground AND on a #16181D card. A solid #262B33 cannot
 * do both. */
.ech-phone {
  /* The fork does not touch champagne, but a phone frame drawn INSIDE a
     light workspace page would otherwise inherit that page's ink gold as its
     fill gold. Stated here so the phone's gold is the phone's gold wherever
     the frame is mounted. */
  --champagne-fill: #C9A45C;  --champagne-fill-rgb: 201 164 92;
  --ivory: #EFEBE2;      --ivory-rgb: 239 235 226;
  --slate: #9AA0AA;      --slate-rgb: 154 160 170;
  --dim: #6E747E;        --dim-rgb: 110 116 126;
  --verified: #7FA88B;   --verified-rgb: 127 168 139;
  --signal: #B4553F;     --signal-rgb: 180 85 63;
  --hairline: rgba(239, 235, 226, .09);
}

html { background: var(--vault); }
/* THE CANVAS BEHIND THE WORKSPACE. html cannot see body.app-light's token
   re-map, because those variables are declared ON body and html is its parent —
   so the canvas kept the marketing near-black under every light portal page.
   Invisible until someone overscrolls, and then it is a black band above a
   white workspace on all 37 of them. :has() lets the parent read the child's
   class; the literal matches --vault under app-light exactly. */
html:has(> body.app-light) { background: #F2F4F7; }
body { font-family: 'Archivo', system-ui, sans-serif; font-size: 15px; line-height: 1.6; }
::selection { background: rgba(201, 164, 92, 0.28); }

/* ---------- Type roles ---------- */
.eyebrow {
  font-family: 'Archivo', sans-serif;
  font-stretch: 115%;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
}
.eyebrow-accent { color: var(--champagne); }

.display-xl, .display-lg, .display-md {
  font-family: 'Fraunces', Georgia, serif;
  font-variation-settings: 'opsz' 90;
  font-weight: 380;
  color: var(--ivory);
  letter-spacing: -0.015em;
}
.display-xl { font-size: clamp(1.95rem, 7.2vw, 4.2rem); line-height: 1.06; }
.display-lg { font-size: clamp(1.6rem, 5.2vw, 2.9rem); line-height: 1.12; }
.display-md { font-size: clamp(1.32rem, 3.6vw, 1.9rem); line-height: 1.2; }
.display-xl em, .display-lg em { font-style: italic; font-weight: 420; color: var(--champagne); }

/* ---------- Buttons ---------- */
.btn-primary, .btn-ghost, .btn-danger, .btn-success {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: 'Archivo', sans-serif; font-weight: 600; font-size: 14px;
  letter-spacing: 0.02em; padding: 6px 16px; border-radius: 4px;
  /* 6 + 20 + 6 = 32 EXACTLY, and it only works with no border in the box.
     A 1px border adds 2px and the control comes out 34 — which is the defect
     dashboard.html's own skin note named months ago: "a 32px control drawn
     with one comes out 34px and drags everything on its row two pixels wide of
     the reference." So the ring is an INSET BOX-SHADOW on every variant below,
     painted inside the box and costing the layout nothing. `line-height` is
     stated rather than left to the face: Archivo's normal line box is ~21px
     and would put the button at 33 before the border was even counted. */
  line-height: 20px; border: 0;
  /* THE REFERENCE'S OWN NUMBERS, off the reference's own DevTools.
     The owner measured Super Dispatch live on 28 Aug 2026 and sent the panels:
     the Add Contact primary is 191x32, the Search primary 79.55x32, BOTH at
     padding 6px 16px, and the search field's grid cell is 40 tall carrying 4px
     of padding -- a 32px input. The whole control ladder is ONE height.

     32, NOT 36. Our own written note (docs/reference/superdispatch/
     02-design-system.md) said "Primary button 36px; secondary 32px", and that
     note is wrong -- corrected there in the same change. A measurement off the
     running product beats a line somebody typed about it.

     We sat at 44px tall, 10x20 pad, 13.5px type, 3px radius -- twelve pixels
     taller and four wider per side than the screen we are a clone of, on every
     button on every page. That is the whole of why our controls read chunky
     beside theirs; nothing about the colour or the type was ever the problem.

     THE 44px TOUCH FLOOR DOES NOT DIE, IT MOVES. 36px is right under a mouse
     and too small under a thumb, so the floor is re-asserted at the bottom of
     this block for coarse pointers only. The desk gets the sleek control; the
     phone still gets the target. Deleting that media query silently makes
     every portal button a 32px tap. */
  min-height: 32px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  cursor: pointer; white-space: nowrap;
}
.btn-primary { background: var(--champagne); color: #0A0B0D; box-shadow: inset 0 0 0 1px var(--champagne); }
/* --champagne-hi and --action-hi are the same gold as their base since the
   one-gold ruling, so hover cannot be carried by the token any more. It is
   carried by brightness instead: same hue, visibly pressed. The filter reaches
   the label too, which is why the label is near-black — 7% off near-black is
   still near-black. */
.btn-primary:hover { background: var(--champagne-hi); box-shadow: inset 0 0 0 1px var(--champagne-hi); filter: brightness(0.93); }
.btn-ghost { background: transparent; color: var(--ivory); box-shadow: inset 0 0 0 1px var(--hairline); }
.btn-ghost:hover { box-shadow: inset 0 0 0 1px var(--champagne); color: var(--champagne-hi); }
.btn-danger { background: transparent; color: var(--signal); box-shadow: inset 0 0 0 1px rgb(var(--signal-rgb) / 0.5); }
.btn-danger:hover { background: rgb(var(--signal-rgb) / 0.12); }
.btn-success { background: #27A567; color: #fff; box-shadow: inset 0 0 0 1px #27A567; }
.btn-success:hover { background: #1F8A55; box-shadow: inset 0 0 0 1px #1F8A55; }
/* THE DENSE VARIANT IS NOT A SHORTER BUTTON ANY MORE. Every control is 32px
   now, so .btn-sm only buys back horizontal room in a crowded row -- same
   height, same type, tighter sides. `min-height: 0` used to mean "opt out of
   the 44px floor"; the floor is pointer-gated below, so this states its own
   height rather than cancelling someone else's. */
.btn-sm { padding: 6px 12px; font-size: 14px; min-height: 32px; }
/* THE ONE CONTROL THAT IS ALLOWED TO BE TALLER, and it is not a "big button":
   it is the single primary action in a page's TOP BAR — the reference's own
   "Add Load" seat, taped at 152.34x36. Everything else on every page is 32.
   Same padding, same type, same radius; four pixels of height and nothing
   else, because the page header's row is taller than a table row and a 32px
   control floats in it.

   USE IT ONCE PER PAGE AT MOST, and only in the header. A second .btn-lg on a
   page means the page has two primary actions, which is the real bug. Dense
   row actions keep .btn-sm; everything between them is the bare class. */
.btn-lg { min-height: 36px; }
/* .sel-pop IS IN THIS LIST BECAUSE IT TAKES FOCUS AND IS NOT A TAG.
   ui.js opens the drawn menu by calling pop.focus() on a div[role="listbox"]
   (ui.js:1138) so the arrow keys have somewhere to land. The list below named
   a/button/input/select/textarea, none of which a div is, so nothing of ours
   applied and Chrome's own ring painted instead.

   IT IS REACHABLE ONLY FROM THE KEYBOARD, which is the whole point. Measured
   on board.html, 29 Aug: opening the Sort by menu with the MOUSE leaves
   :focus-visible unmatched and paints no ring at all; tabbing to the control
   and pressing Enter matches :focus-visible and paints `auto 1px` in
   rgb(0, 95, 204). That colour is `-webkit-focus-ring-color`, a SYSTEM colour
   that follows the OS accent — it reads #005FCC on a machine set to Blue and
   #E59700 on one set to Orange, which is why two people measuring the same
   defect on the same page can print two different numbers and both be
   reporting honestly. What is stable is that the colour is the browser's and
   never ours, in a workspace whose only accent is the gold. */
.btn-primary:focus-visible, .btn-ghost:focus-visible, .btn-danger:focus-visible,
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible,
.sel-pop:focus-visible {
  outline: 1.5px solid var(--champagne); outline-offset: 2px;
}
.btn-primary[disabled], .btn-ghost[disabled], .btn-success[disabled], .btn-danger[disabled] { opacity: 0.45; cursor: not-allowed; pointer-events: none; }

/* THE TOUCH FLOOR, re-asserted where it is actually needed. A fine pointer
   lands on 36px without thinking; a thumb does not. Keyed on the pointer and
   not on width, because a 1024px tablet is a touch device and a 900px window
   on a laptop is not. */
@media (pointer: coarse) {
  .btn-primary, .btn-ghost, .btn-danger, .btn-success, .btn-sm, .btn-lg { min-height: 44px; }
  .input, .select { min-height: 44px; }
}
/* A control the signed-in role may not use. It reads as unavailable but still
   takes the click, because a button that swallows a tap and says nothing is
   indistinguishable from one that is broken — and a title tooltip never
   appears on a touch screen at all. */
[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
[aria-disabled="true"]:hover { opacity: 0.6; }

/* ---------- The action menu ----------
   OWNER'S TAPE, 29 Aug 2026, off the reference's own Options dropdown:
   item 193.53x36 at padding 8px 16px; panel 193.53x415 at padding 8px 0.

   THE ITEM IS 36, NOT 32. A menu row is not a button and does not want the
   button's height: 8 + 20 + 8 = 36 EXACTLY, on the same 20px line box the
   button block states for the same reason (Archivo's normal line box is ~21
   and would put the row at 37 before anything else was counted).

   THE PANEL'S PADDING IS VERTICAL ONLY — 8px 0. Items run edge to edge and
   paint their hover the full width of the menu; a panel with side padding
   gives every row an inset highlight the reference does not draw.

   THE DIVIDER CARRIES NO MARGIN, and the same 29 Aug 2026 tape proves it
   rather than asserting it. The panel it measured is 415 tall over eleven
   items and three rules, and the sum closes on exactly one arrangement:
   415 = 8 + 8 of panel padding, plus 11 x 36 of items, plus 3 x 1 of rule.
   Restore the 8px margin this line used to carry and the panel is 48 taller
   than the owner's tape says it is.

   THAT 415 IS THE CONTENT BOX, AND OURS MEASURES 417 OUTSIDE. The tape was
   taken on the reference's UL, whose border lives on the MUI Paper wrapping
   it; .opt-pop is both the paper and the list, so its own 1px hairline top and
   bottom rides outside the sum above. Two pixels, inside the house tolerance,
   and named here so the next reader measuring 417 against 415 knows which two
   they are looking at rather than going hunting for a margin.

   SO A SEPARATOR IS A HAIRLINE BETWEEN ROWS, NOT A GAP. The grouping is
   carried by the line itself and by the order of the items either side of
   it — never by air. The rows already pad themselves 8px top and bottom, so
   the rule lands in the middle of a 17px clearing it never had to buy. One
   thing follows for whoever writes the next menu: do not open or close a
   panel with a separator. With no margin of its own it would sit 8px off the
   panel edge against nothing, and read as a stray line rather than a break.

   WIDTH IS THE PAGE'S CALL, not this file's. Every consumer states a FIXED
   width for its own reason (a menu sized to its content walks its edge around
   as the items change), and those reasons differ per page — loads.html sizes
   to its longest verb, contacts.html's carries the word "Delete". So this
   block dresses the menu and the page sizes it.

   THIS IS NOT .sel-pop. That is a picker — you open it to choose a value and
   the control keeps it. This is a verb list: you open it to do something and
   it closes. They are allowed to differ and they do. */
.opt-btn {
  display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 6px 16px;
  border: 0; box-shadow: inset 0 0 0 1px var(--line); border-radius: 4px; background: #FFFFFF;
  font-family: inherit; font-size: 14px; font-weight: 600; line-height: 20px;
  color: var(--ivory); cursor: pointer;
}
.opt-btn:hover { background: #F3F5F8; }
.opt-btn[aria-expanded="true"] { box-shadow: inset 0 0 0 1px var(--action); color: var(--action); }
.opt-pop {
  position: fixed; z-index: 85; padding: 8px 0;
  background: #FFFFFF; border: 1px solid var(--hairline); border-radius: 4px;
  box-shadow: 0 8px 24px rgba(16, 24, 40, 0.14);
}
.opt-pop[hidden] { display: none; }
.opt-item {
  display: block; width: 100%; text-align: left; min-height: 36px; padding: 8px 16px;
  font-family: inherit; font-size: 14px; line-height: 20px; color: var(--ivory);
}
.opt-item:hover { background: #F3F5F8; }
.opt-item.danger { color: var(--signal); }
/* The divider between action GROUPS — not between every item. */
.opt-sep { height: 1px; background: var(--hairline); }

/* ---------- The page action bar ----------
   Owner, 29 Aug 2026. The form screens' old header row "kinda just
   exists in its own no man's land". It did: it was a 64px band floating in
   the grey ground inside the form column, with the page's own background
   running above, below and both sides of it. The reference does not do that.
   It runs a REAL BAR — a full-width strip on card-white, hard against the
   identity bar above it, with a hairline underneath, and the grey ground
   with its cards starting below that line.

   SO THE BAR LEFT .sd-form AND LEFT .sd-main. It is a sibling of <main> now,
   directly under #app-shell, which is what lets it span edge to edge: the
   body carries the rail's 88px inset (body.app-light.with-rail), so a
   block-level child of the body already runs from the rail to the viewport's
   right edge with nothing to negate.

   AND THE 32 OF THE TAPE IS LITERAL AGAIN. While the bar lived inside the
   form column it could not state its horizontal padding — the column was
   already 32px off the edge and stating 32 again would have doubled it, so
   the bar took `16px 0` and inherited the rest. Out here there is nothing
   between the bar and the window, so the tape's 16px 32px is simply the
   bar's own padding, exactly as measured. That deviation is now retired.

   GROUND BY TOKEN, NOT BY LITERAL: --obsidian is the card ground and
   resolves to #FFFFFF on the light work surface, so the bar cannot drift
   away from the cards it sits above. Static, not sticky — the reference
   capture scrolls it away with the page. */
.sd-actionbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 16px; min-height: 64px; padding: 16px 32px;
  background: var(--obsidian); border-bottom: 1px solid var(--hairline);
}

/* THE TWO-LINE VARIANT — the load DOCUMENT page, owner's ruling 29 Aug 2026.
   A form screen's bar says one thing ("Create Load") and stays 64. A record's
   bar has to say WHICH record, so it carries the id as a heading with the
   load's tags beside it, and the one secondary fact under them. Nothing here
   sets a height: the second line makes the bar taller by existing, which is
   why .sd-actionbar states a min-height and never a height. 16 + 29 + 10 + 20
   + 16 = 91, the tape's number, reached by adding up rather than by asserting.

   THE BACK CONTROL STAYS TOP-ALIGNED, not centred on a two-line stack: a 32px
   square floating halfway down a 91px bar reads as unmoored, and every other
   bar in the family has it level with the title. */
.sd-bar-id { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
.sd-bar-stack { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
/* The id line: the heading and whatever chips the record earns, on one row. */
.sd-bar-line { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; min-width: 0; }
.sd-bar-line .sd-h1 { line-height: 29px; }
/* The secondary fact — quiet, and never a door. */
.sd-bar-meta { font-size: 14px; line-height: 20px; color: var(--slate); }

/* ---------- Elegant figures (marketing surfaces) ----------
   Fraunces lining/tabular numerals — engraved, not techy. Use for tier bands,
   statistics, and offers on the dark marketing pages. The app portals keep
   mono figures: there, numbers are working data, not typography. */
.fig {
  font-family: 'Fraunces', Georgia, serif;
  font-variant-numeric: lining-nums tabular-nums;
  font-weight: 500; letter-spacing: 0.01em; font-optical-sizing: auto;
}

/* Severity labels (enforcement ledger) — small caps over a short rule.
   Replaces pills on marketing surfaces; quieter, engraved. */
.sev {
  font-family: 'Archivo', sans-serif; font-size: 11px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--slate);
}
.sev::after { content: ''; display: block; width: 26px; height: 1px; margin-top: 9px; background: currentColor; opacity: 0.55; }
.sev-major { color: var(--cognac); }
.sev-disqualifying { color: var(--signal); }

/* ---------- Chips, pills, badges ----------
 *
 * A TRANSLUCENT COLOUR MUST FOLLOW THE THEME, AND AN rgba() LITERAL CANNOT.
 *
 * Every palette colour above is declared twice — as a hex for solid use and as
 * an `--x-rgb` triplet for translucent use — and `body.app-light` re-maps both.
 * The triplet is the only one of the two that a fractional alpha can reach:
 * `rgb(var(--verified-rgb) / .4)` re-maps with the theme, and
 * `rgba(77, 169, 125, .4)` is the DARK mint, forever, on a light page.
 *
 * That is how every status pill in the portal came to be outlined in a
 * different hue from its own label: the text was `var(--verified)`, which under
 * app-light is #248A57, while the border stayed the dark #4DA97D — a forest
 * green word inside a mint ring, 292 times across 33 pages.
 *
 * So: no rgba() literal here may stand for a palette colour. Structural
 * neutrals (black shadows, white insets) are not palette colours and stay as
 * they are; so does anything with a real `body.app-light` override below. */
/* THE TIER CHIP — direction C. The Formation, then the tier, on one plane.
 *
 * TWO MEASUREMENTS DO ALL THE WORK, and both are stated relative to the type
 * rather than in pixels, because this chip renders in THREE typefaces on three
 * grounds: Fraunces on the dark marketing pages, Roboto in the light workspace,
 * Archivo on the phone (both overrides below). Their cap heights are not the
 * same fraction of an em, and a pixel here would be right on one of them.
 *
 *   HEIGHT — `height: 1cap` is exactly the cap height of whatever font the
 *   chip resolved to. The `.71em` above it is the fallback for an engine
 *   without the unit: an unsupported unit invalidates its own declaration and
 *   the previous one stands, which is the whole pattern. 0.71 is Roboto's
 *   ratio (1456/2048) and within a hair of Fraunces' and Archivo's.
 *
 *   PLANE — `align-items: baseline`. A replaced element's baseline is its
 *   bottom edge, so the mark's feet land ON the text baseline; being exactly
 *   one cap tall, its top lands on the cap line. Centring instead would float
 *   it against the LINE BOX — ascender and descender air included — and it
 *   would sit visibly low beside capitals. The owner asked for one plane.
 *
 * THE MARK IS CROPPED TO ITS INK by the viewBox the renderer passes
 * (`2 4 20 16`), not scaled down inside a 24-unit square: the square carries
 * two units of air at the left and four at the top, which at 8px tall is a
 * drifting gap where the composition wants a measured one. Width follows the
 * 20:16 ratio on its own — never state one.
 *
 * NO MIN-WIDTH ANY MORE. The 72px track existed because roman numerals are
 * five different widths, and a fixed CENTRED track is now the one thing that
 * would break this composition: it pushes the mark off the column's left edge,
 * which is the edge the whole direction is built on.
 * Arabic pays for its removal on two of the three grounds — measured, all five
 * chips are 69.89px in the light workspace (Roboto) and 70.47px on the phone
 * (Archivo), because both faces set every figure to one advance. FRAUNCES DOES
 * NOT: on the dark marketing ground the five come out 75.55–77.30px, a 1.75px
 * spread that `tabular-nums` cannot fix — the widths are unchanged with the
 * property on, with `font-feature-settings:'tnum' 1`, and with both, so the
 * Google build of the face simply has no `tnum` to switch on (measured). No
 * shipping dark surface stacks tier chips in a column today — apply-carrier
 * shows one at a time — so the spread is recorded here rather than papered
 * over with a track. If a dark column ever appears, that is the moment to
 * decide, and the decision is a typeface one. */
.chip-tier {
  display: inline-flex; align-items: baseline; gap: 6px; flex: none;
  font-family: 'Fraunces', Georgia, serif; font-variant-numeric: lining-nums tabular-nums;
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--champagne); border: 1px solid rgb(var(--champagne-rgb) / 0.55);
  /* SLEEK — owner ruling 26 Aug 2026 (evening): "sleek and not too thick...
     not too much room above and below the actual text." line-height 1 makes
     the text box exactly the type; 2px of breathing room, no more. The status
     chip below states the same two numbers so every chip lands the same
     height on the same ground. */
  line-height: 1; padding: 2px 8px; border-radius: 4px; white-space: nowrap;
}
.chip-tier > .chip-fm {
  flex: none; display: block; width: auto;
  height: .71em;           /* fallback: no `cap` unit */
  height: 1cap;            /* the real rule — exactly the text's cap height */
}
/* The name is historical: it marks the TOP OF THE LADDER, not the third rung.
   `currentColor` on the mark means the inversion carries the Formation with
   it — one declaration, and the badge and the chip invert the same way.
     THE FILL IS `--champagne-fill`, THE OUTLINE IS `--champagne`, and that is
   the whole split (see the note above `body.app-light`). The four outline
   rungs are gold WORDS with a gold hairline round them and a Formation drawn
   in the same currentColor — ink, all three, and ink has to stay readable, so
   they follow the per-ground token. This one is a gold PLATE with near-black
   printed on it, which is the logo's job, so it is the logo's gold on every
   ground. Filling it with the ink gold is what made the owner's chip disagree
   with the Formation standing four pixels above it in the same bar. */
.chip-tier-3 { background: var(--champagne-fill); color: #0A0B0D; border-color: var(--champagne-fill); }

/* THE STATUS CHIP — the same rectangle, in a status colour.
 *
 * `.pill` rides along on every selector here and keeps only its name. The
 * shape is gone ecosystem-wide by the owner's ruling of 25 Aug 2026, and there
 * are 106 hand-written `class="pill…"` spans across 26 portal and website
 * pages, plus 14 ECH.ui.statusPill call sites: making the class MEAN the
 * rectangle retires the shape in one edit instead of 120, on pages this agent
 * has no fence over. Each name is corrected as its own page is next opened.
 *
 * A LIGHT BACKDROP WITH TONED LETTERING, not a filled slab — the owner chose
 * the quiet treatment over solid green. The wash and the border are both
 * fractions of the tone's own `--x-rgb` triplet, so they re-map with the theme
 * (see the note above this section: an rgba() literal cannot).
 *
 * THE DOT STAYS ROUND. It is round because it is a dot, not because it is a
 * capsule, and the ruling that killed pills protects dots by name. */
.chip-status, .pill {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  /* Same two numbers as .chip-tier — the sleek ruling wants every chip the
     same height on the same ground. */
  line-height: 1; padding: 2px 9px; border-radius: 4px;
  border: 1px solid rgb(var(--slate-rgb) / 0.28); background: rgb(var(--slate-rgb) / 0.10);
  color: var(--slate); white-space: nowrap;
}
.chip-status::before, .pill::before { content: ''; width: 5px; height: 5px; border-radius: 999px; background: currentColor; }
/* THE GREEN IS OUT. Owner's ruling, 28 Aug 2026: "take out all of these light
   green active chips and tags and the verified chips… we can add those and come
   up with a rule set for those at another point."

   It is neutralised HERE rather than edited out of seventy-nine call sites in
   twenty-nine files, so there is one place to reverse it when that rule set
   arrives — and so no page can quietly keep a green chip the others lost.

   WHAT SURVIVES IN GREEN: nothing on the portal. The driver CARD's attestation
   keeps its sage (`--ech-verified`, a separate token on a black artefact), and
   the seal on the users panel was always gold. Neither is one of these chips. */
.chip-status-verified, .pill-verified, .pill-active, .pill-delivered {
  color: var(--slate); border-color: rgb(var(--slate-rgb) / 0.28); background: rgb(var(--slate-rgb) / 0.10);
}
.chip-status-caution, .pill-watch, .pill-pending {
  color: var(--cognac); border-color: rgb(var(--cognac-rgb) / 0.28); background: rgb(var(--cognac-rgb) / 0.10);
}
.chip-status-alert, .pill-restricted, .pill-suspended, .pill-expelled {
  color: var(--signal); border-color: rgb(var(--signal-rgb) / 0.28); background: rgb(var(--signal-rgb) / 0.10);
}
.chip-status-accent, .pill-open {
  color: var(--champagne); border-color: rgb(var(--champagne-rgb) / 0.28); background: rgb(var(--champagne-rgb) / 0.10);
}
.pill-assigned, .pill-transit, .pill-requested { color: var(--slate); }

/* THE PHONE GROUND. The palette forks itself at `.ech-phone` up at the top of
   this file, so the colours here are already right — champagne is not forked,
   sage green is. What the fork does not carry is the type register: the phone
   surfaces are set in Archivo throughout and a Fraunces tier chip would be the
   only serif on the screen. Stated once, both chips. */
.ech-phone :is(.chip-tier, .chip-status, .pill) { font-family: 'Archivo', sans-serif; letter-spacing: 0.07em; }
.ech-phone .chip-tier { font-weight: 700; font-size: 10.5px; }

.badge-verified {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--slate); white-space: nowrap;
}
/* The badge's green dot goes with the green chips — same ruling, and it was
   the same colour doing the same job in a rounder shape. */
.badge-verified::before { content: ''; width: 6px; height: 6px; border-radius: 999px; background: var(--slate); box-shadow: 0 0 0 3px rgb(var(--slate-rgb) / 0.15); }

/* ---------- The ledger line — carrier chrome row two ---------- */
/* THE LEDGER LINE IS GONE — owner's ruling, 28 Aug 2026, off header-mock.html.
   .hdr-ledger and the .hl-* cells lived here (a white 40px four-cell rule under
   the carrier strip: Standing · Coverage · Insurer · Next re-verification).
   The tier chip moved up into the identity row, and the remaining facts read
   off the account on the carrier profile's Insurance card instead. The classes
   died with the bar — nothing else ever wore them. design-lab/style-guide-web.html
   still documents them and is not this file's to edit; see the session report. */

/* ---------- Redaction (signature element) ---------- */
/* Access matrix — who can see each field, and when.
   Replaces the redaction bars. Those read as a loading skeleton rather than a
   seal, and because each bar was as long as the value it hid, they published
   the length of every sealed field while claiming to hide it. This shows the
   grant instead: what the board carries, what the assigned carrier gets and at
   which stage, and what only the broker ever sees. */
.mx { width: 100%; border-collapse: collapse; font-size: 12px; }
.mx th {
  font-family: 'Archivo', sans-serif; font-size: 9px; font-weight: 700;
  letter-spacing: 0.11em; text-transform: uppercase; color: var(--dim);
  padding: 0 0 9px; text-align: center;
}
.mx th:first-child { text-align: left; }
.mx td { padding: 7px 0; border-top: 1px solid rgba(245,242,235,0.07); text-align: center; color: var(--slate); }
.mx td:first-child { text-align: left; padding-right: 10px; }
.mx .yes { color: var(--verified); font-size: 15px; line-height: 1; }
/* The literal here was #9AA0AA — the PHONE fork's slate, not this palette's
   #99A0AB — on a table no phone surface renders. One unit of red and blue
   apart in the dark, and a whole shade wrong under app-light. */
.mx .no { color: rgb(var(--slate-rgb) / 0.3); font-size: 13px; line-height: 1; }
/* A stage name rather than a tick: the carrier does get this, later. */
.mx .later { font-variant-numeric: tabular-nums; font-size: 10px; letter-spacing: .04em;
  color: var(--champagne); }
/* The line where board-visible ends and sealed begins. */
.mx tr.split td { border-top: 1px solid rgba(201,164,92,0.28); padding-top: 9px; }
body.app-light .mx td { border-top-color: rgba(10,11,13,0.08); }
body.app-light .mx tr.split td { border-top-color: rgba(201,164,92,0.34); }

/* A field sealed even from the desk reading the page. Stated, not drawn. */
.mx-sealed {
  font-family: 'Archivo', sans-serif; font-size: 9.5px; font-weight: 700;
  letter-spacing: 0.13em; text-transform: uppercase; color: var(--dim);
  border: 1px solid var(--hairline); border-radius: 3px; padding: 2.5px 8px;
}

.redact {
  /* Even character width so the hatch reads as a redaction bar rather than
     ragged text — but that is a WIDTH requirement, not a typeface one, and
     tabular figures deliver it in our own face. */
  font-variant-numeric: tabular-nums;
  color: transparent; position: relative; border-radius: 2px; user-select: none;
  background: repeating-linear-gradient(90deg, #23262c 0 9px, #1b1e23 9px 11px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
  letter-spacing: 0.05em; font-size: 12px;
}
.redact-note {
  font-size: 11px; color: var(--dim); font-style: italic;
  display: inline-flex; align-items: center; gap: 5px;
}
.redact-note::before { content: '⬒'; font-style: normal; font-size: 10px; color: var(--champagne); }

/* ---------- Cards, rules, notices ---------- */
.card { background: var(--obsidian); border: 1px solid var(--hairline); border-radius: 4px; }
.card-pad { padding: 24px; }
.rule { position: relative; border: 0; border-top: 1px solid var(--hairline); margin: 0; }
.rule::before { content: ''; position: absolute; top: -1px; left: 0; width: 48px; border-top: 1px solid var(--champagne); }
.notice {
  border-left: 2px solid var(--champagne); background: rgb(var(--champagne-rgb) / 0.05);
  padding: 16px 20px; font-size: 14px; color: var(--slate); border-radius: 0 3px 3px 0;
}
.notice strong { color: var(--ivory); }
.notice-red { border-left-color: var(--signal); background: rgb(var(--signal-rgb) / 0.06); }

/* ---------- Tables ---------- */
.table-board { width: 100%; border-collapse: collapse; font-size: 13px; }
.table-board thead th {
  font-family: 'Archivo', sans-serif; font-stretch: 112%;
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--dim); text-align: left; padding: 10px 14px;
  background: var(--obsidian); border-bottom: 1px solid var(--hairline);
  white-space: nowrap;
}
/* Headers hold one line while there is room for it. Below that they wrap
   rather than set a floor under the table's width — a two-line "Recorded
   revenue" costs nothing, and a table that scrolls sideways costs the reader
   the columns they cannot see. */
@media (max-width: 1450px) {
  .table-board thead th { white-space: normal; }
}

/* Sortable column headers. Three states, cycled by clicking: ascending,
   descending, then back to the table's own order — because a broker who
   sorted by mistake needs the original list back without reloading. */
.table-board thead th.sortable { cursor: pointer; user-select: none; padding-right: 22px; position: relative; }
.table-board thead th.sortable:hover { color: var(--champagne); }
.table-board thead th.sortable::after {
  content: '\2195'; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  font-size: 11px; opacity: 0.32; font-weight: 400;
}
.table-board thead th.sortable[data-dir="asc"]::after  { content: '\2191'; opacity: 1; color: var(--champagne); }
.table-board thead th.sortable[data-dir="desc"]::after { content: '\2193'; opacity: 1; color: var(--champagne); }
.table-board thead th.sortable[data-dir]:not([data-dir=""]) { color: var(--champagne); }
/* Long free text wraps rather than forcing the table wider than its box. */
.table-board td.wrap { white-space: normal; min-width: 0; }
.table-board tbody td {
  padding: 11px 14px; border-bottom: 1px solid rgba(38, 43, 51, 0.6);
  color: var(--ivory); vertical-align: middle;
}
.table-board tbody tr { transition: background 0.12s; }
.table-board tbody tr:hover { background: rgba(27, 30, 36, 0.7); }
.table-board tbody tr.row-selected { background: rgba(201, 164, 92, 0.06); }
/* NUMBERS ALIGN WITHOUT A MONOSPACE FACE. The only reason a table column was
   ever set in IBM Plex Mono is that digits had to line up down the column —
   and OpenType tabular figures do exactly that in our own typeface. The
   monospace read as dated ("tacky and dated" was the owner's verdict) and it
   was never carrying anything but column alignment. Archivo with tabular-nums
   keeps the alignment and loses the terminal. */
.table-board .num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1;
  font-size: 13px; text-align: right; white-space: nowrap; }
.table-board .mono { font-variant-numeric: tabular-nums; letter-spacing: .02em;
  font-size: 13px; white-space: nowrap; }
.table-board .sub { color: var(--slate); font-size: 12px; }

/* ---------- Forms ---------- */
.label { display: block; font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim); margin-bottom: 6px; }
.input, .select, textarea.input {
  width: 100%; background: var(--gunmetal); border: 1px solid var(--hairline);
  /* 32px, the same height as every button -- the owner's DevTools capture puts
     the reference's search field in a 40px cell with 4px of padding on it.
     line-height and min-height are both stated because a bare padding sum
     lands on 32 or 33 depending on which face actually loads. */
  color: var(--ivory); font-size: 14px; line-height: 20px; padding: 5px 12px;
  min-height: 32px; border-radius: 4px;
  transition: border-color 0.15s;
}
/* A textarea's height comes from its rows, not from a control ladder, so it
   keeps the roomier padding the 32px rule just took off the single-line
   fields. */
textarea.input { padding: 8px 12px; }
.input::placeholder { color: var(--dim); }
.input:focus, .select:focus { border-color: var(--champagne); outline: none; }
.input-mono { font-variant-numeric: tabular-nums; letter-spacing: 0.06em; }

/* ---------- The labelled select — one box holding a prefix and a value ----
   OWNER'S TAPE, 29 Aug 2026, off the reference's Filter By and again off its
   Sort By: a 32-tall box, the LABEL LIVING INSIDE IT as a 14px quiet prefix
   rather than floating above it, the value at 14px in the body ink, and the
   right-hand seat given over to the chevron.

   IT GRADUATED OUT OF app/carrier/loads.html. That page's skin note set the
   rule for its own contents — page-local while two pages wear a shape, house
   -owned the moment a third does. app/carrier/board.html's Sort by is the
   third wearer, so the shape lives here and each page states only its own
   width (Filter By 145, Sort by 195). loads.html still carries its own copy of
   these rules in its <style>, which now wins on its page by source order alone
   and resolves identically; it is redundant and should be deleted in the sweep
   that graduates the rest of that block.

   ONLY THE LIVE HALF MOVED. loads.html also styles `.fb-field select` — a
   chevron, a 6/36/6/4 padding, and an `.on` class that flips the value's ink.
   None of it renders on any page: assets/ui.js skins every plain <select>,
   clips the real one to 1px, and draws .sel-btn over it. Those rules are dead
   where they stand and are not carried here to be dead somewhere better.

   --line IS READ WITH ITS FALLBACK. Nine carrier pages and assets/loadform.css
   declare it in their own palette block; board.html declares none at all, so a
   bare `var(--line)` there is invalid at computed-value time — which collapses
   the whole border shorthand and ships the control with no ring. Same reason
   .sel-btn below reads it this way. */
.fb-field {
  display: inline-flex; align-items: center; position: relative;
  height: 32px; padding: 0 0 0 8px;
  background: var(--obsidian); border: 1px solid var(--line, #C4CDD5); border-radius: 4px;
}
/* HOVER MOVED HOUSE WITH THE RING. Suppressing the value button's own ring
   further down this file (see the note at .fb-field .sel-skin > .sel-btn) took
   `body.app-light .sel-btn:hover`'s `inset 0 0 0 1px #98A2B3` with it, and
   nothing replaced it: measured on both wearers, hovering the control changed
   nothing at all but the cursor. This is that same signal, in the same grey,
   drawn by the box that now owns the ring. Declared BEFORE :focus-within, at
   equal specificity, so a focused field that is also hovered stays gold. */
.fb-field:hover { border-color: #98A2B3; }
.fb-field:focus-within { border-color: var(--action); }
.fb-lbl { font-size: 14px; line-height: 20px; color: var(--slate); white-space: nowrap; flex: none; }
/* The value cell takes the rest of the box rather than sizing to its own text,
   so the chevron sits at the trailing edge on a short value as well as a long
   one, and a value too long for the width ellipses instead of pushing out. */
.fb-field > .sel-skin { flex: 1; min-width: 0; }

/* ---------- The dropdown, drawn by us instead of by the OS ----------
   A native <select> renders its menu with the operating system: grey Aqua
   chrome and a blue macOS highlight, inside a workspace that is white and
   gold. assets/ui.js draws a listbox over the top and leaves the <select>
   underneath as the value. These are its clothes.

   Metrics off the reference's own menu: white panel, 4px radius, a soft
   overlay shadow rather than a hard border, and rows appreciably taller than
   the 32px control that opens them — a menu is read, not scanned past. */
.sel-skin { position: relative; display: block; }
/* The real control is still there and still the truth; it is simply not the
   thing you look at. Not `display:none` — a hidden-from-layout select stops
   being focusable by label-click and stops reporting validity. */
.sel-skin > select {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  border: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden;
  white-space: nowrap;
}
.sel-btn {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; min-height: 32px; padding: 5px 10px;
  background: var(--gunmetal); color: var(--ivory);
  font-family: inherit; font-size: 14px; line-height: 20px; text-align: left;
  border: 0; border-radius: 4px; cursor: pointer;
  box-shadow: inset 0 0 0 1px var(--hairline);
}
.sel-btn:hover { box-shadow: inset 0 0 0 1px var(--slate); }
/* Open and focus both wear the accent ring the reference paints in blue. */
.sel-skin[data-open="true"] > .sel-btn,
.sel-btn:focus-visible { box-shadow: inset 0 0 0 2px var(--champagne); outline: none; }
.sel-btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.sel-caret { flex: none; width: 16px; height: 16px; color: var(--dim); transition: transform 0.15s; }
.sel-skin[data-open="true"] .sel-caret { transform: rotate(180deg); }
.sel-val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sel-pop {
  /* FIXED, NOT ABSOLUTE — owner's bug, 28 Aug 2026: the Type menu opened
     clipped inside the vehicle table, whose wrapper is a scroll container
     (overflow-x: auto clips both axes; nothing absolute escapes one). The
     panel is measured and placed against the viewport by skinSelects (ui.js
     place()), which also owns the up/down flip — the old data-drop top/bottom
     rules are gone because inline coordinates now carry the position.
     overscroll-behavior stops a wheel that reaches the list's end from
     spilling into the page: the menu scrolls, the page does not. */
  position: fixed; z-index: 90; width: max-content;
  max-width: min(360px, 90vw); max-height: 320px; overflow-y: auto;
  overscroll-behavior: contain;
  padding: 6px 0; border-radius: 4px;
  background: var(--obsidian); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
    inset 0 0 0 1px var(--hairline);
  display: none;
}
.sel-skin[data-open="true"] .sel-pop { display: block; }
.sel-opt {
  padding: 0 16px; min-height: 44px; display: flex; align-items: center;
  font-size: 14px; line-height: 20px; color: var(--ivory); cursor: pointer;
}
.sel-opt[data-active="true"] { background: rgb(var(--ivory-rgb) / 0.06); }
/* The chosen row carries the accent wash the reference gives it in pale blue.
   No tick beside it: the wash IS the mark, and the reference draws it that
   way — a tick would put two statements of one fact on the row. */
.sel-opt[aria-selected="true"] { background: rgb(var(--champagne-rgb) / 0.14); color: var(--champagne); font-weight: 600; }
.sel-opt[aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; }

.field-lock { position: relative; opacity: 0.75; }
.field-lock .input { padding-right: 90px; }
.field-lock::after {
  content: 'VERIFIED'; position: absolute; right: 10px; top: 50%; transform: translateY(calc(-50% + 9px));
  font-variant-numeric: tabular-nums; font-weight: 600; font-size: 10px;
  letter-spacing: 0.12em; color: var(--verified);
}

/* ---------- KPI ---------- */
.kpi .kpi-value { font-variant-numeric: tabular-nums; font-size: 28px; font-weight: 600;
  letter-spacing: -.01em; color: var(--ivory); line-height: 1.1; }
.kpi .kpi-value .unit { font-size: 14px; color: var(--slate); }
.kpi .kpi-label { margin-top: 6px; }

/* ---------- App rail (Super Dispatch-style icon rail, Echelon register) ---------- */
body.with-rail { padding-left: 78px; }
.app-rail {
  position: fixed; inset: 0 auto 0 0; width: 78px; z-index: 60;
  display: flex; flex-direction: column; align-items: center;
  background: #0D0E11; border-right: 1px solid var(--hairline);
  padding: 14px 0 12px;
}
.rail-logo { display: flex; align-items: center; justify-content: center; height: 40px; margin-bottom: 10px; }
.rail-nav { display: flex; flex-direction: column; gap: 2px; width: 100%; flex: 1; overflow-y: auto; scrollbar-width: none; }
.rail-item {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 9px 4px 8px; width: 100%; color: var(--dim);
  border-left: 2px solid transparent; transition: color 0.15s, background 0.15s;
}
.rail-item svg { width: 19px; height: 19px; }
.rail-item span { font-size: 9px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; text-align: center; line-height: 1.15; }
.rail-item:hover { color: var(--slate); background: rgba(27, 30, 36, 0.5); }
.rail-item.active { color: var(--champagne); border-left-color: var(--champagne); background: rgba(201, 164, 92, 0.06); }
.rail-item.locked { opacity: 0.38; cursor: not-allowed; }
.rail-foot { margin-top: 8px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.rail-acct {
  width: 34px; height: 34px; border-radius: 999px; display: flex; align-items: center; justify-content: center;
  background: var(--gunmetal); border: 1px solid var(--hairline);
  font-family: 'Archivo', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: 0.05em; color: var(--slate);
  cursor: pointer;
}
.rail-pop {
  position: fixed; left: 86px; bottom: 14px; z-index: 70; width: 240px;
  background: var(--obsidian); border: 1px solid var(--hairline); border-radius: 6px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
.pop-item {
  display: block; padding: 8px 16px; font-size: 13px; color: var(--slate);
  transition: background 0.12s, color 0.12s;
}
.pop-item:hover { background: var(--gunmetal); color: var(--ivory); }

/* ---------- Board cards (load rows) ---------- */
.load-card { display: block; width: 100%; text-align: left; background: var(--obsidian); border: 1px solid var(--hairline); border-radius: 4px; transition: border-color 0.15s, background 0.15s; cursor: pointer; }
.load-card:hover { border-color: rgba(201, 164, 92, 0.45); background: #16181d; }
.route-line { position: relative; padding-left: 18px; }
.route-line::before { content: ''; position: absolute; left: 4px; top: 7px; width: 7px; height: 7px; border-radius: 999px; background: var(--champagne); }
.route-line::after { content: ''; position: absolute; left: 7px; top: 18px; bottom: 16px; border-left: 1px dotted var(--dim); }
.route-line .route-dest::before { content: ''; position: absolute; left: 4px; width: 7px; height: 7px; background: var(--verified); margin-top: 7px; }
.bookmark-btn { color: var(--dim); transition: color 0.15s; }
.bookmark-btn:hover { color: var(--champagne); }
.bookmark-btn.saved { color: var(--champagne); }

/* ---------- Detail modal / drawer ---------- */
.ech-modal {
  background: var(--vault); border: 1px solid var(--hairline); border-radius: 6px;
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.7); width: 100%; max-width: 1060px;
  max-height: calc(100vh - 48px); overflow-y: auto;
}
.ech-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 80; width: 360px; max-width: 92vw;
  background: var(--obsidian); border-left: 1px solid var(--hairline);
  overflow-y: auto; visibility: hidden;
  transform: translateX(100%); transition: transform 0.22s ease-out, visibility 0.22s;
}
.ech-drawer.open { transform: none; visibility: visible; box-shadow: -24px 0 60px rgba(0, 0, 0, 0.35); }

/* ---------- Stage tabs (Loads) ---------- */
.stage-tabs { display: flex; gap: 26px; border-bottom: 1px solid var(--hairline); overflow-x: auto; }
.stage-tab {
  display: inline-flex; align-items: center; gap: 7px; padding: 0 2px 11px;
  font-size: 13.5px; font-weight: 500; color: var(--slate);
  border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; transition: color 0.15s;
}
.stage-tab:hover { color: var(--ivory); }
.stage-tab.active { color: var(--ivory); border-bottom-color: var(--champagne); }
.stage-tab .n { font-variant-numeric: tabular-nums; font-size: 11.5px; color: var(--dim); }
.stage-tab.active .n { color: var(--champagne); }

/* ---------- Chrome bits ---------- */
.nav-link { font-size: 15.5px; color: var(--slate); transition: color 0.15s; white-space: nowrap; }
.nav-link:hover { color: var(--ivory); }
.nav-link.active { color: var(--champagne); }
.app-nav-link {
  display: inline-flex; align-items: center; padding: 0 2px 10px;
  font-size: 13px; font-weight: 500; color: var(--slate);
  border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.15s;
  white-space: nowrap;
}
.app-nav-link:hover { color: var(--ivory); }
.app-nav-link.active { color: var(--ivory); border-bottom-color: var(--champagne); }

/* ---------- Rows that must line up ----------
   A right-aligned flex group lets each row's own content decide where its
   columns land: a narrow TIER I sits left of a wide TIER II, "Aug 6–8" starts
   somewhere different from "Aug 10–14", and the eye has to re-find every
   column on every line. Fixed tracks put them on one plane, so a straight edge
   runs down the tier chips, the dates and the prices. */
.row-plane { display: grid; align-items: center; gap: 18px;
  grid-template-columns: minmax(0, 1fr) 76px 88px; }
@media (min-width: 640px) {
  .row-plane { grid-template-columns: minmax(0, 1fr) 76px 96px 88px; }
}
.row-plane .rp-window { display: none; }
@media (min-width: 640px) { .row-plane .rp-window { display: block; } }
.row-plane .rp-price { text-align: right; }

/* ---------- Toast & modal ---------- */
.ech-toast {
  position: fixed; bottom: 24px; right: 24px; z-index: 90;
  background: var(--obsidian); border: 1px solid var(--hairline); border-left: 2px solid var(--champagne);
  color: var(--ivory); font-size: 13.5px; padding: 13px 18px; border-radius: 3px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55); max-width: 380px;
  animation: ech-toast-in 0.22s ease-out;
  pointer-events: none;   /* a notification, not a control — never eat a click */
}
@keyframes ech-toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.ech-modal-backdrop {
  position: fixed; inset: 0; z-index: 80; background: rgba(5, 6, 8, 0.78);
  backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; padding: 24px;
}

/* ---------- Light-theme component overrides (app portals) ---------- */
/* Typography register: standard auto-transport software (Roboto, tabular
   figures), not terminal-tech. Monospace survives nowhere in the app. */
body.app-light { font-family: 'Roboto', 'Archivo', system-ui, sans-serif; min-height: 100vh; }
/* A DEAD BUTTON MUST NOT WEAR THE LIVE COLOUR. The fade greys the ground, the
   border and the ink — and then `body.app-light .btn-primary` further down
   re-asserts `inset 0 0 0 1px var(--action)`, so a gold ring survived around a
   control that cannot be pressed. Measured on load-create's disabled Save and
   Decode before this line existed. The box-shadow has to be stated HERE, in the
   disabled rule, because that is the only place later than the one that draws
   the ring. Found on board-notifications.html, where two of three buttons spend
   most of their life disabled; it was never that page's bug. */
body.app-light .btn-success[disabled], body.app-light .btn-primary[disabled] {
  opacity: 1; background: #E5E9EE; border-color: #E5E9EE; color: #8B95A3;
  box-shadow: inset 0 0 0 1px var(--hairline);
}
body.app-light :is(.font-mono, .input-mono, .kpi-value, .num, .mono, .stage-tab .n, .board-tab .n, .redact-note) {
  font-family: 'Roboto', sans-serif; font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
}
/* The chip changes typeface with the ground and the mark does not have to be
   told: `1cap` is re-measured against Roboto the moment this rule applies. */
body.app-light .chip-tier { font-family: 'Roboto', sans-serif; font-weight: 700; letter-spacing: 0.06em; font-size: 10.5px; }
body.app-light .eyebrow { font-family: 'Roboto', sans-serif; font-stretch: normal; letter-spacing: 0.08em; font-weight: 600; }
body.app-light .label { font-family: 'Roboto', sans-serif; letter-spacing: 0.06em; }
body.app-light .table-board thead th { font-family: 'Roboto', sans-serif; font-stretch: normal; letter-spacing: 0.08em; }
body.app-light .field-lock::after { font-family: 'Roboto', sans-serif; }
body.app-light :is(.chip-status, .pill) { font-family: 'Roboto', sans-serif; letter-spacing: 0.05em; }
body.app-light .btn-primary, body.app-light .btn-ghost, body.app-light .btn-success, body.app-light .btn-danger { font-family: 'Roboto', sans-serif; }
/* SUPER DISPATCH'S BLUE, ON EVERY DRAG-SELECT IN THE WORKSPACE. rgba(37, 99,
   217, 0.18) stood here and painted on all thirty portal pages every time a
   carrier selected a word — the one accent in this file that fired on an
   ordinary gesture rather than on a state, and the reason it outlived the
   27 Aug gold unification is that nobody selects text while auditing colour.
   Same wash, read off the triplet, the idiom already used a dozen lines down. */
body.app-light ::selection { background: rgb(var(--champagne-rgb) / 0.18); }
/* #0A0B0D, not #fff — the note at the head of body.app-light says why, with
   the numbers. Near-black on the logo's gold is 8.40:1; white on it is 2.34:1
   and could not be read. */
body.app-light .btn-primary { background: var(--action); box-shadow: inset 0 0 0 1px var(--action); color: #0A0B0D; border-radius: 4px; }
body.app-light .btn-primary:hover { background: var(--action-hi); box-shadow: inset 0 0 0 1px var(--action-hi); filter: brightness(0.93); }
/* The ring follows the base into an inset shadow — a `border-color` here
   would paint nothing now that the box carries no border, and the ghost
   would wear the DARK theme's hairline on a white ground. */
body.app-light .btn-ghost { background: var(--obsidian); box-shadow: inset 0 0 0 1px #C9D2DE; color: var(--ivory); border-radius: 4px; }
body.app-light .btn-ghost:hover { box-shadow: inset 0 0 0 1px var(--action); color: var(--action); }
body.app-light .btn-danger, body.app-light .btn-success { border-radius: 4px; }
body.app-light .input:focus, body.app-light .select:focus { border-color: var(--action); }

/* The dropdown in the light workspace — the reference's own white menu, with
   our gold standing where its accent blue does. */
body.app-light .sel-btn {
  background: #FFFFFF; color: var(--ivory);
  box-shadow: inset 0 0 0 1px var(--line, #C4CDD5);
}
body.app-light .sel-btn:hover { box-shadow: inset 0 0 0 1px #98A2B3; }
body.app-light .sel-skin[data-open="true"] > .sel-btn,
body.app-light .sel-btn:focus-visible { box-shadow: inset 0 0 0 2px var(--action); }
/* INSIDE A LABELLED FIELD THE BOX IS ALREADY DRAWN, so the value button stops
   drawing its own. .sel-btn is built to stand alone — its ring and its white
   ground are what make it a control on a page — and composing it into .fb-field
   put a second ring inside the first. What that painted was a full-height 1px
   rule between the prefix and the value plus a doubled edge along the value's
   half of the box: a control that reads as two boxes, when the tape says the
   label lives INSIDE the field. The field keeps the ring, the state and the
   focus (.fb-field:focus-within takes the accent), and the caret keeps the
   rotation, so nothing is lost but the seam.

   FOUR RULES HAVE TO BE OUTRANKED, base through open, and the open one is
   (0,0,4,1) — hence the selector's length and its place after them rather than
   beside .fb-field. Both palettes are written because the ring is declared
   twice, once above and once under body.app-light. */
.fb-field .sel-skin > .sel-btn,
body.app-light .fb-field .sel-skin > .sel-btn { background: transparent; box-shadow: none; }
body.app-light .sel-pop {
  background: #FFFFFF;
  box-shadow: 0 8px 24px rgba(16, 24, 40, 0.14), 0 1px 3px rgba(16, 24, 40, 0.08),
    inset 0 0 0 1px #DFE3E9;
}
body.app-light .sel-opt { color: var(--ivory); }
body.app-light .sel-opt[data-active="true"] { background: #F3F5F8; }
/* The chosen option's ink is THE gold, not a darkened one. #8A6D2F sat here
   as a contrast darkening and survived the 27 Aug one-gold unification by
   being a literal; the owner's 28 Aug restatement ("the logo gold is the only
   gold I wanna see ... nothing darker") retires it. */
body.app-light .sel-opt[aria-selected="true"] { background: rgb(var(--champagne-rgb) / 0.16); color: var(--champagne); }

/* The literal here was rgba(168, 130, 60, 0.08) — numerically right under this
   theme and still the one shape this file forbids, because it is a palette
   colour spelled out. Same wash, read off the triplet. */
body.app-light .chip-tier { background: rgb(var(--champagne-rgb) / 0.08); }
/* Without this the wash above (0,2,1) beats .chip-tier-3 (0,1,0), and the filled top-of-ladder chip never renders filled on the only ground it ships on.
   It re-states the FILL token, so this line changes the specificity and nothing
   else: the gold here and the gold on the dark ground are now the same gold. */
body.app-light .chip-tier-3 { background: var(--champagne-fill); }
body.app-light .redact {
  background: repeating-linear-gradient(90deg, #D7DCE2 0 9px, #C9CFD7 9px 11px);
  box-shadow: inset 0 0 0 1px rgba(30, 39, 51, 0.05);
}
body.app-light .table-board tbody td { border-bottom-color: rgba(223, 227, 233, 0.85); }
body.app-light .table-board tbody tr:hover { background: #F6F8FA; }
/* The third and last SD-blue literal in the light workspace, same retirement
   as ::selection above: a selected row's wash is the house gold at 5%. */
body.app-light .table-board tbody tr.row-selected { background: rgb(var(--champagne-rgb) / 0.05); }
body.app-light .ech-toast { box-shadow: 0 12px 32px rgba(16, 24, 40, 0.16); }
body.app-light .ech-modal-backdrop { background: rgba(23, 30, 40, 0.55); }
body.app-light .card { box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04); }
body.app-light .load-card:hover { background: #FFFFFF; border-color: #C3CCD8; box-shadow: 0 3px 10px rgba(16, 24, 40, 0.09); }
body.app-light .stage-tab.active { border-bottom-color: var(--action); }
body.app-light .stage-tab.active .n { color: var(--action); }
body.app-light .app-nav-link.active { border-bottom-color: var(--action); }

/* Rail stays dark navy in the light theme — the Super Dispatch signature.
   (The old #1D2A3C ground that used to sit here was dead on every page —
   the ONE RAIL block below always out-specified it — so it is gone, and the
   acct puck wears the values measured off the SD reference.) */
body.app-light .app-rail .rail-acct { background: rgba(255, 255, 255, 0.09); border-color: rgba(255, 255, 255, 0.16); color: #C6CDD4; }

/* ONE RAIL EVERYWHERE — owner ruling 26 Aug 2026 (late evening), from his own
   screen recording: "the whole left side kinda changes sizes... make sure
   every Sidebar tab is displayed the same when you click on all of them."
   Three clone pages carried a local SD rail (22px icons, 13px sentence-case,
   grey active block) while the other nine ran the old 19px/9px-caps/gold-bar
   rail — the frame re-skinned itself per page. The SD rail wins portal-wide
   (the clone pages are the owner's reference standard).
   THE EXTRA .rail-nav IN THE SELECTOR IS THE MECHANISM, not decoration: the
   clone pages' local <style> blocks used to restate the rail at
   (0,3,1)/(0,4,1) AFTER this sheet loads, so equal specificity would lose on
   order. Those local chrome blocks are DELETED now (task #82) — every
   declaration in them was either a duplicate of this block or is superseded
   by the chrome-authority rules below — and this sheet is the ONLY place the
   rail and the two strips are styled. The deep selectors stay anyway: they
   are the fence that keeps any future page-local <style> from re-skinning
   the frame by accident. */
/* The container too — measured across all 12 carrier pages, the clone trio
   ran the rail at 88px (pad 12/0/10, ground #1B2333) while everyone else ran
   78px (pad 14/0/12): the column visibly EXPANDED on Dashboard/Loads, and
   at 77px inner width two-word labels wrapped to a second line (items 76px
   tall against the clones' 59). The aside element in the selector buys the
   deciding point of specificity over any page-local block.
   The hairline is a LITERAL: the clone pages spelled it var(--hairline),
   which their own palette re-map resolved to #E1E5EA while every other page
   resolved #DFE3E9 translucent-white — one variable, two rails. The frame
   does not read its colour off whatever palette the page happens to carry;
   #E1E5EA is the measured SD value and it is pinned here, the .hdr-ledger
   precedent. */
body.app-light aside.app-rail {
  width: 88px; padding: 12px 0 10px; background: #1B2333;
  border-right: 1px solid #E1E5EA;
}
/* THE RAIL ITEM, to the owner's tape of 29 Aug 2026: a 87×60 at padding 8,
   4px between items, the glyph 24×24 with a 4px gap above a 13px label.
   Three of those were already right — the 88px track (theirs 87, and ours
   cannot be 87 without moving body.app-light.with-rail's inset by a pixel for
   nothing), the 8px padding, and the 13px label. What moved: the glyph 22 → 24,
   the gap under it 5 → 4, the space between items 2 → 4, and the active wash.

   THE HEIGHT IS NOT STATED, IT ADDS UP: 8 + 24 + 4 + 16 + 8 = 60 exactly, on
   a 13px label's 1.25 line box. A stated min-height on top of that would be a
   second opinion about the same number, and the first one to drift wins.

   THE ACTIVE WASH WAS TWICE AS STRONG AS THE REFERENCE'S. Theirs is #5B637133
   — rgb(91,99,113) at alpha 0x33/255 ≈ 0.2 — and ours carried the same three
   channels at 0.5, which reads as a selected block rather than the faint
   quiet-tint it is meant to be. The label at full strength is what marks the
   active item; the wash only seats it. Ink stays house: rail quiet #C6CDD4
   inactive, white at full strength active. */
body.app-light .app-rail .rail-nav { gap: 4px; }
body.app-light .app-rail .rail-nav .rail-item {
  color: #C6CDD4; border-left: 0; padding: 8px; gap: 4px; border-radius: 3px;
}
body.app-light .app-rail .rail-nav .rail-item svg { width: 24px; height: 24px; }
body.app-light .app-rail .rail-nav .rail-item span {
  font-family: inherit; font-size: 13px; font-weight: 400;
  letter-spacing: 0; text-transform: none; line-height: 1.25;
}
body.app-light .app-rail .rail-nav .rail-item:hover { color: #FFFFFF; background: rgba(255, 255, 255, 0.06); }
body.app-light .app-rail .rail-nav .rail-item.active { color: #FFFFFF; background: rgba(91, 99, 113, 0.2); border-left-color: transparent; }

/* ONE CHROME EVERYWHERE — the rest of the frame, same ruling, same mechanism.
   The rail above got one authority; the two strips ui.js draws over it (the
   identity strip and the carrier Ledger Line) were still splitting into two
   families — four pages ran them through a local SD skin (Inter 14/20, white
   strip, full-bleed 32px gutters) and the other twelve ran them off this
   sheet's older register (Roboto 15, translucent strip, 88rem cap, 24px
   gutters). Same DOM, two planes: the header visibly JUMPED 18px and changed
   typeface when the owner clicked from Contacts to Loads.
   The SD-measured chrome is the design truth (owner ruling), so those values
   move HERE and the page-local copies are deleted. Everything is scoped to
   #app-shell — the chrome's own container — so a page's content below the
   shell, including the clone pages' SD palette re-maps, is untouched. Colours
   are LITERALS for the .hdr-ledger reason: the frame must not change when a
   page re-maps its palette vars. */
/* The body inset matches the rail's occupied width. tokens' dark-ground story
   (:550) says 78px; the light rail is 88px, and every page that took the
   78px inset slid 10px of its content under the rail. (0,2,1) beats (0,1,1). */
body.app-light.with-rail { padding-left: 88px; }
/* The chrome's inheritance root. Inter is what the SD reference wears and the
   14/20 fixed line box is how it wears it; the rail labels (`inherit` above),
   the account name, the SESSION chip and every .hl-* label now resolve the
   SAME family and line box on all sixteen pages, whether or not the page
   body is Roboto. Roboto is the fallback, not system-ui, so a page that
   somehow misses the Inter link degrades to the portal's other real face. */
body.app-light #app-shell { font-family: 'Inter', 'Roboto', sans-serif; font-size: 14px; line-height: 20px; }
/* The Roboto component pins above (:674-:686) reach into the chrome — the
   SESSION chip is .font-mono, the role label is .eyebrow, Sign out is
   .btn-ghost, the ledger chip is .chip-tier. Inside the shell they all defer
   to the root: (1,2,1) outranks every (0,2,1) pin, chrome-scoped only. */
body.app-light #app-shell :is(.font-mono, .eyebrow, .chip-tier, .label,
  .btn-primary, .btn-ghost, .btn-success, .btn-danger) { font-family: inherit; }
/* The account popover is rail furniture and converges with it. */
body.app-light #app-shell .rail-pop { border-color: #E1E5EA; border-radius: 4px; box-shadow: 0 12px 32px rgba(16, 24, 40, 0.16); }
body.app-light #app-shell .pop-item { color: #5D6370; }
body.app-light #app-shell .pop-item:hover { background: #F3F5F8; color: #192334; }
/* The View-record link in the popover. This literal has now been three
   colours in two days — SD blue, then portal gold, now the logo's gold — which
   is the argument for it not being a literal at all. It stays one because the
   chrome must not re-resolve per page (the note at the head of the ONE CHROME
   block says why), so it is pinned to the one gold by hand. */
body.app-light #app-shell .text-action { color: #C9A45C; }
/* SD's workspace is full-bleed with a 32px gutter; an 88rem cap would centre
   the strips against a full-width page and park the ledger's cells at a
   different x on every page. Full-bleed is the truth for the CHROME on all
   pages — content containers below the shell keep their own widths. */
body.app-light #app-shell .max-w-8xl { max-width: none; padding-left: 32px; padding-right: 32px; }
body.app-light #app-shell .bg-vault\/95 { background: #FFFFFF; }
body.app-light #app-shell .bg-obsidian\/60 { background: #F3F5F8; }
/* The strip's own inks and rules, pinned against palette drift: the page vars
   put these 1-2 units apart per page (#DFE3E9 vs #E1E5EA and friends). */
body.app-light #app-shell .border-hairline { border-color: #E1E5EA; }
body.app-light #app-shell .bg-hairline { background-color: #E1E5EA; }
body.app-light #app-shell .text-ivory { color: #192334; }
body.app-light #app-shell .text-slate { color: #5D6370; }
body.app-light #app-shell .text-dim { color: #90949D; }
/* The role label, at the SD register (the old one ran 11/600/.08em). */
body.app-light #app-shell .eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; font-stretch: normal; color: #5D6370; }
/* Sign out is SD's 32px secondary — inset ring, not a border, so the control
   is 32px and not 34 — identical to the one the clone pages measured. */
body.app-light #app-shell .btn-ghost {
  font-size: 14px; font-weight: 600; letter-spacing: 0; line-height: 20px;
  min-height: 32px; padding: 6px 16px; border-radius: 4px;
  background: #FFFFFF; border: 0; box-shadow: inset 0 0 0 1px #C4CDD5; color: #192334;
}
body.app-light #app-shell .btn-ghost:hover { background: #F3F5F8; color: #192334; box-shadow: inset 0 0 0 1px #C4CDD5; }

/* ---------- Marketing de-tech: warm glow instead of blueprint grid, and the
   big stat figures set in Archivo rather than terminal monospace ---------- */
.hero-grid {
  background-image: radial-gradient(ellipse 72% 58% at 62% 22%, rgba(201, 164, 92, 0.07), transparent 70%) !important;
  background-size: auto !important;
  mask-image: none !important;
  -webkit-mask-image: none !important;
}
body:not(.app-light) .font-mono.text-3xl {
  font-family: 'Archivo', sans-serif; font-weight: 600; letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
}

/* ---------- Motion discipline ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* ---------- Print sanity ---------- */
@media print { body { background: #fff; color: #000; } }

/* ---------- Full-screen mobile menu ----------
   The small dropdown panel is a utility; at this tier the menu should be a
   room you step into. Full viewport, right-aligned engraved capitals, one
   staggered reveal, and a single way out. */
.navsheet {
  position: fixed; inset: 0; z-index: 70;
  display: flex; flex-direction: column;
  background:
    radial-gradient(ellipse 90% 60% at 85% 8%, rgba(201,164,92,0.10), transparent 60%),
    linear-gradient(165deg, #1B1E24 0%, #121418 45%, #0A0B0D 100%);
  opacity: 0; visibility: hidden;
  transition: opacity 0.34s cubic-bezier(0.22,1,0.36,1), visibility 0.34s;
}
.navsheet.open { opacity: 1; visibility: visible; }
.navsheet-top { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; }
.navsheet-close {
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--hairline); border-radius: 3px; color: var(--ivory);
  background: transparent; cursor: pointer; transition: border-color 0.25s, color 0.25s;
}
.navsheet-close:hover { border-color: var(--champagne); color: var(--champagne); }
.navsheet-body { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 0 26px 8px; overflow-y: auto; }
.navsheet-link {
  display: block; text-align: right;
  font-family: 'Archivo', sans-serif; font-weight: 500;
  font-size: clamp(17px, 5vw, 23px); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ivory); padding: 13px 0;
  opacity: 0; transform: translateY(12px);
  transition: color 0.25s, opacity 0.45s cubic-bezier(0.22,1,0.36,1), transform 0.45s cubic-bezier(0.22,1,0.36,1);
}
.navsheet-link:active, .navsheet-link:hover { color: var(--champagne); }
.navsheet-link.current { color: var(--champagne); }
.navsheet.open .navsheet-link { opacity: 1; transform: none; }
.navsheet-rule { height: 1px; background: var(--hairline); margin: 20px 0 18px; opacity: 0; transition: opacity 0.4s 0.2s; }
.navsheet.open .navsheet-rule { opacity: 1; }
.navsheet-sub {
  display: block; text-align: right;
  font-family: 'Archivo', sans-serif; font-size: 12.5px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--slate);
  padding: 9px 0; opacity: 0; transform: translateY(10px);
  transition: color 0.25s, opacity 0.45s cubic-bezier(0.22,1,0.36,1), transform 0.45s cubic-bezier(0.22,1,0.36,1);
}
.navsheet.open .navsheet-sub { opacity: 1; transform: none; }
.navsheet-sub.accent { color: var(--champagne); }
.navsheet-foot { padding: 18px 26px 26px; text-align: right; font-size: 11.5px; color: var(--dim); opacity: 0; transition: opacity 0.4s 0.28s; }
.navsheet.open .navsheet-foot { opacity: 1; }
body.nav-open { overflow: hidden; }
/* The support launcher is fixed and would sit over the menu's contact line. */
body.nav-open #desk-fab, body.nav-open #desk-panel { display: none !important; }
@media (prefers-reduced-motion: reduce) {
  .navsheet, .navsheet-link, .navsheet-sub, .navsheet-rule, .navsheet-foot { transition: none; }
  .navsheet-link, .navsheet-sub { opacity: 1; transform: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE INVITE DRAWER — assets/invite-drawer.js
   ═════════════════════════════════════════════════════════════════════════

   It lived in app/carrier/users.html's page styles, which is why the carrier
   profile could not open it without leaving for the roster. Owner's ruling,
   28 Aug 2026: the drawer opens where the registry is. Component and sheet
   moved together; the metrics below are unchanged from the page copy.

   users.md FOURTH PASS, measured live off the reference while the drawer
   stood open. The invite FORM is behind Super Dispatch's seat paywall on that
   account — Echelon has no paywall and never will — but the drawer SHELL is
   the same component the form ships in, so its chrome is the chrome.

     anchor right, full height · width 494px on this content, component
     bounds min 432 / max 600 · transform 0.225s cubic-bezier(0,0,0.2,1) ·
     elevation-16 three-layer shadow · white ground, 0 radius · backdrop
     rgba(0,0,0,0.5) · title row 14/20.

   It composes on .ech-drawer above (fixed right, z-80, translateX(100%)
   closed, `.open` supplying `transform:none; visibility:visible`). Equal
   specificity, later source order — the width, the timing and the shadow all
   land without an !important. */

/* The module injects its fixture once and then keeps it, so the closed state
   has to be real: without this the scrim would sit over the page at zero
   opacity and swallow every click on it. */
#invite-drawer[hidden] { display: none; }

.invite-drawer {
  width: 494px; min-width: 432px; max-width: 600px;
  display: flex; flex-direction: column;
  background: #FFFFFF; border-radius: 0;
  /* VISIBILITY IS ASYMMETRIC, AND IT HAS TO BE. .ech-drawer parks the closed
     drawer at `visibility: hidden`, so something must turn it back on. Give
     visibility a DURATION and the browser holds the old value at t=0 — the
     name field is still computed hidden the instant the class lands, and
     focus() is refused outright. Measured: it was, twice, including from
     inside a requestAnimationFrame.
     So the two directions get different rules. Closing (this line) delays the
     flip to hidden by the length of the slide, which is what lets the drawer
     be SEEN leaving. Opening (.open below) flips it at once, so the field is
     focusable in the same tick the drawer is asked for. The transform is the
     measured 0.225s decelerate in both. */
  transition: transform 0.225s cubic-bezier(0, 0, 0.2, 1), visibility 0s linear 0.225s;
}
.invite-drawer.open {
  transition: transform 0.225s cubic-bezier(0, 0, 0.2, 1), visibility 0s linear 0s;
  box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
              0 16px 24px 2px rgba(0, 0, 0, 0.14),
              0 6px 30px 5px rgba(0, 0, 0, 0.12);
}
/* The reference's own rule: the drawer is full-width below the sm breakpoint
   and takes its bounds above it. */
@media (max-width: 599px) { .invite-drawer { width: 100vw; min-width: 0; max-width: 100vw; } }
.invite-drawer .drawer-body { flex: 1; overflow-y: auto; padding: 16px 24px; }
@media (min-width: 960px) { .invite-drawer .drawer-body { padding: 16px 32px; } }

/* The drawer chrome, shared with every other drawer and sheet in the portal —
   app/carrier/profile.html's two edit sheets were already writing .drawer-title
   and .drawer-x with nothing to resolve them. */
/* FIFTH PASS — re-measured 28 Aug 2026 off the reference drawer's own DOM,
   handed over whole: 64px title and action toolbars with 24px gutters (32
   at >=960), the title an h3 at 20/28/500, content padded 16/24 (16/32 at
   >=960), the form capped at 480. */
.drawer-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  min-height: 64px; padding: 0 24px; border-bottom: 1px solid var(--hairline);
}
.drawer-title { font-size: 20px; font-weight: 500; line-height: 28px; color: var(--ivory); }
.drawer-x { font-size: 20px; line-height: 1; color: var(--dim); }
.drawer-x:hover { color: var(--ivory); }
.drawer-foot {
  display: flex; align-items: center; min-height: 64px;
  padding: 12px 24px; border-top: 1px solid var(--hairline);
}
@media (min-width: 960px) {
  .drawer-head, .drawer-foot { padding-left: 32px; padding-right: 32px; }
}
/* The action bar's button sits flush left and is never full-width. It used to
   state 40px at 10px 16px, and that 40 was real reference tape once — but the
   owner's 29 Aug 2026 ruling measures the whole platform at 32 and supersedes
   it. A drawer's send is not a special height; it is a button. The only
   control on the platform allowed past 32 is the page-header CTA (.btn-lg,
   36px), and a drawer footer is not a page header. Geometry now comes from
   .btn-primary; this rule keeps only the seat. */
.inv-send { flex: none; }
.drawer-scrim {
  position: fixed; inset: 0; z-index: 79; background: rgba(0, 0, 0, 0.5);
  opacity: 0; transition: opacity 0.225s cubic-bezier(0, 0, 0.2, 1);
}
.drawer-scrim.open { opacity: 1; }

/* Field labels are 14/20 REGULAR — MuiFormLabel-root in the reference's own
   DOM is weight 400, not the 600 its body1 text style carries. Not the
   uppercase micro-caps .label gives elsewhere either. */
.invite-drawer .label {
  font-size: 14px; font-weight: 400; line-height: 20px; letter-spacing: 0;
  text-transform: none; color: var(--ivory); margin-bottom: 4px;
}
.invite-drawer .inv-lbl-2 { margin-top: 16px; }

/* The form is capped at 480 inside a 494 drawer — the reference's own number,
   measured, and the reason the fields do not run edge to edge. */
.inv-form { max-width: 480px; }
/* Section heads, h4 at 16/24/500. They name a group of controls; they do not
   explain one. */
.inv-sec-h { margin: 0 0 12px; font-size: 16px; font-weight: 500; line-height: 24px; color: var(--ivory); }
.inv-sec-h-2 { margin-top: 32px; }

/* ---- The access cards --------------------------------------------------
   THE WHOLE CARD IS THE BUTTON. The hit target is the card, not the 24px
   square — the detail anyone rebuilding this from a screenshot gets wrong.
   The square is drawn like a checkbox and behaves as a radio, because an
   Echelon invitation grants ONE access. */
.inv-cards { display: flex; flex-direction: column; gap: 16px; }
/* 16px padding, an 8px seam after the 24px square — the reference's card is
   416x86 on this content and these are the numbers that make it so. */
.inv-card {
  display: flex; align-items: flex-start; gap: 8px;
  width: 100%; text-align: left; padding: 16px;
  border: 1px solid var(--hairline); border-radius: 4px;
  background: #FFFFFF; color: var(--dim); cursor: pointer; font-family: inherit;
  transition: border-color 0.15s, box-shadow 0.15s, color 0.15s;
}
.inv-card:hover { border-color: #C4CDD5; }
.inv-card:focus-visible { outline: 2px solid var(--action); outline-offset: 2px; }
/* The selected card takes the accent border the reference gives it — doubled
   into an inset ring so the box does not shift by a pixel on selection. */
.inv-card[aria-checked="true"] {
  border-color: var(--action); box-shadow: inset 0 0 0 1px var(--action); color: var(--action);
}
.inv-box, .inv-card .inv-ic { flex: none; width: 24px; height: 24px; }
.inv-card .inv-box-on { display: none; }
.inv-card[aria-checked="true"] .inv-box-off { display: none; }
.inv-card[aria-checked="true"] .inv-box-on { display: inline; }
.inv-card-txt { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.inv-card-t {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 16px; font-weight: 500; line-height: 24px; color: var(--ivory);
}
.inv-card-t .inv-ic { flex: none; width: 24px; height: 24px; color: var(--slate); }
.inv-card-d { font-size: 14px; font-weight: 400; line-height: 20px; color: var(--slate); }

.inv-err { margin-top: 12px; font-size: 13px; line-height: 20px; color: var(--signal); }

/* GATED, AND IT LOOKS GATED. body.app-light turns a disabled primary into a
   grey slab, which on the one control this drawer exists to reach reads as a
   different button rather than as the same one waiting. It keeps its gold and
   fades — the reference's own treatment, and the state says "not yet", not
   "not here". */
/* No dress of its own: a gated Send Invite wears the workspace's one disabled
   primary — the grey slab at body.app-light .btn-primary[disabled] — exactly
   as the load forms' gated Save does. A 45%-opacity gold here was a SECOND
   disabled look, and (28 Aug, one-gold round three) a second gold. */

/* ══════════════════════════════════════════════════════════════════════════
   THE LEG MARKS — pickup rises, delivery lands
   ══════════════════════════════════════════════════════════════════════════
   Owner, 5 Sep 2026: "From this point forward, pickup is going to be a
   circle, and delivery is going to be a square. Pickup is always going to be
   a gold circle. Delivery is always going to be a green square." Said with
   the board's own search lanes open: "let's change that green circle with the
   arrow to a gold circle with an up arrow, and this red square will change to
   green because green square means delivery."

   So the pair is GOLD CIRCLE UP / GREEN SQUARE DOWN, and the two search lanes
   now AGREE with the load cards underneath them. Until today the board said
   green-circle pickup in its toolbar and gold-circle pickup in every card in
   the list — the same page teaching two colour codes for one leg.

   HISTORY, kept because the shapes came from it and the shapes did not move.
   Owner, 29 Aug 2026, with the reference's own inspector on the symbol beside
   its section heads: "for pickup ... a green up arrow with a white arrow
   inside of a green circle. And then for the delivery information, I want it
   to be a red square with a white down arrow." Circle-up and square-down are
   that ruling's, unchanged; only the two fills and the pickup ink moved.

   TWO THINGS CARRY THE MEANING, so the mark reads without colour: the SHAPE
   (a circle collects, a square delivers) and the ARROW (up off the ground,
   down onto it). Colour is the third telling, not the only one — printed in
   grey or read by somebody who cannot separate gold from green, the pair is
   still two different marks pointing two different ways.

   20 × 20 is the reference's tape, and the arrow is drawn INTO the ground
   rather than laid over it: one element, no stacking, nothing to fall out of
   alignment. Delivery's ground is the house --verified rather than a raw
   green, so it re-maps with the theme the way every other status colour does.
   Pickup's ground is --champagne-fill, the FILL gold — one of the five names
   that are all #C9A45C since 27 Aug, and the one whose ROLE this is: it is
   what .chip-tier-3 fills with, and it is the one .ech-phone restates, so a
   leg mark drawn inside a phone frame keeps the phone's gold instead of
   inheriting a light page's ink gold.

   THE PICKUP ARROW IS NEAR-BLACK, NOT WHITE, and that is the rule the head of
   this file already records for body.app-light .btn-primary: white on #C9A45C
   is 2.35:1 and cannot be read; #0A0B0D on it is 8.39:1. Gold is the one
   ground in this palette you stop trying to print white on.

   THE DELIVERY ARROW STAYS WHITE: 4.33:1 on the light theme's --verified
   (#248A57), and the portal is always light — assets/ui.js puts app-light on
   every workspace body. On the :root palette's #4DA97D the same white would
   be 2.88:1, under 3:1 for a 2.6 stroke. Written down rather than fixed: no
   portal page wears that palette today, and moving a house status colour is
   not this pass's to call.

   Used by the load forms' Pickup/Delivery section heads and by the board's
   search lanes and advanced-search drawer — same two marks, one definition. */
.leg-mark {
  display: inline-flex; flex: none; width: 20px; height: 20px;
  background-repeat: no-repeat; background-position: center;
  background-size: 12px 12px;
}
.leg-mark-pickup {
  border-radius: 50%;
  background-color: var(--champagne-fill);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230A0B0D' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 19V5M5.5 11.5 12 5l6.5 6.5'/%3E%3C/svg%3E");
}
.leg-mark-delivery {
  border-radius: 4px;
  background-color: var(--verified);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FFFFFF' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14M5.5 12.5 12 19l6.5-6.5'/%3E%3C/svg%3E");
}
