/* One stylesheet.
 *
 * ── Where these numbers come from ────────────────────────────────────
 *
 * Measured off Jifeng on 2026-08-19, with `getComputedStyle` in a signed-in
 * browser rather than by eye off a screenshot: `docs/jifeng-ui-tokens.md` has
 * the table and how it was taken. Every value below that looks arbitrary is
 * theirs -- 13px body, 40px bar, 32px controls, 8px card, #0050F0.
 *
 * ⚠ **The reason this matters is not taste.** The people who read these screens
 * have read Jifeng's for years. A warehouse screen that is laid out differently
 * is not "our style", it is a screen they have to learn, on a day when they are
 * holding a box. So the shape is theirs and only the contents are ours.
 *
 * ⚠ **The Chinese fonts in the stack are not decoration.** Without
 * "Hiragino Sans GB" / "Microsoft YaHei" the browser falls back to a serif on
 * macOS, and a warehouse console set in Songti reads as broken -- which is
 * exactly the word that came back about this build.
 */

:root {
  /* Jifeng's brand ramp. brand-6 is the one that means "press me". */
  --brand: #0050f0;
  --brand-hover: #3377ff;
  --brand-down: #003ca4;
  --brand-pale: #f7faff;
  --brand-wash: #eaf1ff;

  --bar: #001f4d;
  --bar-hover: #123566;
  --page: #f3f5fc;
  --card: #ffffff;

  --line: #e5eaf8;      /* border-light: between things inside a card */
  --line-mid: #d5dcee;  /* border-normal: inputs, buttons */
  --line-bold: #c4cde4; /* border-bold: rarely, for a real division */
  --rule: #ebeef5;      /* table row rule -- lighter than a border on purpose */

  --ink: #111622;
  --ink-2: #313a52;
  --ink-dim: #677395;
  --ink-faint: #7e89a8;

  --ok: #00b42a;
  --ok-bg: #e6f8ea;
  --warn: #ff7d00;
  --warn-bg: #fff6e6;
  --bad: #f53f3f;
  --bad-bg: #feecec;
  --tag-bg: rgba(17, 22, 34, 0.08);

  /* One radius for controls, one for cards. Two is a system; five is a mess. */
  --r-ctl: 6px;
  --r-card: 8px;
  --h-ctl: 32px;

  /* Kept because 128 screens were written against them. */
  --accent: var(--brand);
}

* { box-sizing: border-box; }

/* ⚠ **The scrollbar's width is reserved whether or not there is a scrollbar.**
   Otherwise a list that grows past one screen takes ~15px off the page as it
   appears, every column shifts left, and the same click reads as the table
   moving again -- measured on the direct-mail board in production on
   2026-09-11: identical column positions on the tabs with rows, 3-13px off on
   the two without, because those fit on one screen and the others did not.
   The columns themselves are pinned (`table.fixedcols`); this pins what they
   are pinned inside. */
html { scrollbar-gutter: stable; }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  /* 13px, and the Chinese faces are load-bearing -- see the head of this file. */
  font: 13px/1.35 Roboto, -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI",
    "Hiragino Sans GB", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* ⚠ **No `-webkit-font-smoothing: antialiased`.** It was here, and it thins
     every stroke on macOS -- worst on the dark bar, where white-on-navy went
     from readable to washed out, and worst again for Chinese, which has far
     more strokes per character than Latin to lose. Jifeng leaves it at `auto`;
     measured, not guessed. The report that came back was "the menu is hard to
     read, the type is thin and small" and this was half of it. */
}

/* ---- top bar ---------------------------------------------------------- */

.topbar {
  display: flex;
  align-items: stretch;
  background: var(--bar);
  color: #fff;
  height: 40px;
  padding: 0 16px 0 0;
  position: sticky;
  top: 0;
  z-index: 40;
}

/* Spanish runs about a third longer than English and German longer still, so
   the bar is built to give way rather than to fit one language exactly: the
   brand and the account truncate, the headings tighten, and the language
   switcher and account stay put. Found by running the thing in Spanish, which
   is the only way this kind of bug is ever found. */

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 18px 0 16px;
  font-weight: 600;
  font-size: 14px;
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 3rem;
}

/* The mark. Drawn rather than fetched when a warehouse has not uploaded one of
   their own: a logo that arrives late leaves the corner of every screen empty
   for a moment, and a logo that 404s leaves it empty for good. web/nav.js falls
   back to the drawn one on either. */
.brand .mark {
  flex: 0 0 auto;
  display: block;
  width: 22px;
  height: 22px;
}

/* An uploaded one, sized into the same 22px square whatever shape it arrived
   as. `contain`, never `cover`: a warehouse's own mark cropped to fit is worse
   than one with space around it, and a wordmark is usually wider than it is
   tall. */
/* Where the settings screen shows the mark a warehouse has uploaded. A fixed
   box with a checked ground behind it, so a transparent PNG reads as
   transparent rather than as white -- which is the difference between a logo
   that will look right on the dark bar and one that will not. */
.logo-slot {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #eef1f6 25%, transparent 25%, transparent 75%, #eef1f6 75%),
    linear-gradient(45deg, #eef1f6 25%, transparent 25%, transparent 75%, #eef1f6 75%);
  background-size: 12px 12px;
  background-position: 0 0, 6px 6px;
}
.logo-slot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.brand .mark img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 4px;
}

.sections {
  display: flex;
  /* ⚠ **Stretch, not center.** The section is the thing the panel hangs off and
     the thing hover is measured against, so it has to be the full height of the
     bar: centred, it was 28px tall in a 40px bar, leaving a 6px dead strip
     between its bottom edge and the top of the panel. The pointer crossed that
     strip on the way down, hover was lost, and the panel shut before anybody
     could reach it. Jifeng's hover target is 40px for the same reason. */
  align-items: stretch;
  flex: 1 1 auto;
  min-width: 0;
  gap: 2px;
}

/* The pill is centred inside the full-height section, rather than being the
   section. */
.section { position: relative; display: flex; align-items: center; min-width: 0; }

/* ⚠ A pill, not a full-height block. Jifeng's current section is a rounded
   blue chip inside the bar -- copied because it is the single strongest "you
   are here" signal on the screen and it is what the reader already scans for. */
.section-head {
  display: flex;
  align-items: center;
  /* Jifeng's, measured: 28px tall, 14px type, 16px either side, 4px corners --
     and the word is **white**, not a dimmed blue-grey. A menu is not decoration
     to be recessive; it is the thing everybody reads first. */
  height: 28px;
  padding: 0 16px;
  border-radius: 4px;
  font-size: 14px;
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The bar carries eight sections, a language switcher, an address and a way
   out. Which of those can go, in order, as it gets tight -- the address first
   because it is the only one that is merely informative. The switcher and the
   way out always stay: they are how somebody who is stuck gets unstuck. */

@media (max-width: 1400px) {
  .brand { padding: 0 12px; }
  .section-head { padding: 0 9px; }
  .account { max-width: 11rem; }
}

@media (max-width: 1180px) {
  .section-head { padding: 0 8px; font-size: 13px; }
  .account .who { display: none; }
}

@media (max-width: 900px) {
  .brand { padding: 0 8px; font-size: 13px; max-width: 9rem; }
  .section-head { padding: 0 6px; }
  .signout { margin-right: 8px; padding: 4px 8px; }
}

/* The corner icons. Same height as the bar so they line up with the section
   heads, and a hit area big enough for a finger -- this is the door to
   settings, not decoration. */
.iconbtn {
  display: flex;
  align-items: center;
  padding: 0 9px;
  color: #b9c6e0;
  text-decoration: none;
  flex: 0 0 auto;
}
.iconbtn:hover { color: #fff; }
.iconbtn.current { color: #fff; }

.section:hover .section-head,
.section-head.current { background: var(--brand); color: #fff; }

/* The panel is the section's own child, so it opens on hover and on keyboard
   focus alike, and there is no state to get stuck. */
/* Measured off Jifeng: flush under the bar, 16px/20px of room, 4px corners, and
   a soft shadow rather than a border -- a panel that is drawn tight reads as a
   context menu, not as the way into a section. */
.panel {
  display: none;
  position: absolute;
  top: 40px;
  left: 0;
  background: var(--card);
  border-radius: 4px;
  box-shadow: 0 4px 16px rgba(29, 33, 41, 0.16);
  padding: 16px 20px;
  gap: 10px;
}

/* Panels near the right edge open leftwards, or they hang off the screen.
   Which sections are "near the right edge" is not a fixed number of items --
   the menu is filtered per tenant -- so it is counted from the end. */
.section:nth-last-child(-n + 3) .panel { left: auto; right: 0; }

.section:hover .panel,
.section:focus-within .panel { display: flex; }

.group { min-width: 148px; padding: 0 4px; }

/* ⚠ The blue tick before a group name is Jifeng's, and it is the thing that
   makes a three-column panel readable at a glance instead of a wall of links. */
.group-head {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 600;
  padding: 0 0 10px 2px;
  line-height: 1;
}
.group-head::before {
  content: "";
  width: 3px;
  height: 12px;
  border-radius: 1px;
  background: var(--brand);
  flex: 0 0 auto;
}

.item {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 8px;
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
  border-radius: 4px;
  white-space: nowrap;
}

.item:hover { background: var(--brand-pale); color: var(--brand); }
.item.current { color: var(--brand); font-weight: 600; }

/* Locked: dimmed and badged, never disabled. It opens the upgrade screen, and
   a menu entry that looks dead is exactly what this system exists to avoid. */
.item.locked { color: var(--ink-faint); }
.item.locked::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 8px;
  margin-left: 7px;
  border-radius: 1px;
  background: currentColor;
  box-shadow: 0 -3px 0 -1px transparent, inset 0 0 0 8px currentColor;
  opacity: 0.55;
  vertical-align: -1px;
}
.item.locked:hover { color: var(--brand); background: var(--brand-pale); }

/* The account, as an avatar and a name. The avatar is initials rather than a
   picture: there is no picture, and a grey circle with nothing in it reads as
   something still loading. */
.account {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 6px 0 12px;
  color: #dbe2f0;
  white-space: nowrap;
  overflow: hidden;
  flex: 0 1 auto;
  max-width: 15rem;
}
.account .pip {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #2f4d80;
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
}
.account .who { overflow: hidden; text-overflow: ellipsis; }

/* Language switcher. Sits in the bar next to the account, because that is
   where somebody who has landed in the wrong language goes looking. */
.lang {
  align-self: center;
  background: transparent;
  color: #dbe2f0;
  border: 1px solid #2b4270;
  border-radius: var(--r-ctl);
  padding: 3px 6px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  flex: 0 0 auto;
}
.lang:hover { background: var(--bar-hover); }
.lang option { color: var(--ink); background: #fff; }

/* The warehouse a seller is looking at, when they work with more than one.
   Same control as the language picker on purpose -- it sits beside it, and a
   second shape in that corner would read as a second kind of thing. Weighted a
   little heavier because it changes whose books are on screen, which the one
   next to it does not. */
.warehouse {
  align-self: center;
  background: transparent;
  color: #eaf0fb;
  border: 1px solid #2b4270;
  border-radius: var(--r-ctl);
  padding: 3px 6px;
  margin-left: 8px;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  flex: 0 0 auto;
  max-width: 180px;
}
.warehouse:hover { background: var(--bar-hover); }
.warehouse:disabled { opacity: 0.6; cursor: default; }
.warehouse option { color: var(--ink); background: #fff; font-weight: 400; }

.signout {
  align-self: center;
  background: transparent;
  color: #c8d2e6;
  border: 1px solid #2b4270;
  border-radius: var(--r-ctl);
  padding: 3px 9px;
  margin-left: 8px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  flex: 0 0 auto;
}
.signout:hover { background: var(--bar-hover); color: #fff; }

.nav-error { background: var(--bad-bg); color: #b42318; padding: 10px 16px; }

/* ---- the open-document strip ------------------------------------------- */

/* ⚠ **Why a warehouse console has browser-style tabs.**
 *
 * Jifeng's has them, and the reason is the job: somebody is part-way through a
 * receipt, a customer rings about a different one, and they need the second
 * screen without losing their place on the first. Without this strip the only
 * way back is to find the entry in the menu again -- which is what the people
 * reading this build were doing, and it is why it read as "unusable" rather
 * than "different".
 *
 * The strip is state the reader owns, so it lives in sessionStorage and not on
 * the server: it is per browser tab, it survives every navigation inside one,
 * and it is gone when they close the window. Nothing here is worth a row in a
 * database.
 */
.tabstrip {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 34px;
  padding: 0 12px;
  background: var(--page);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  position: sticky;
  top: 40px;
  z-index: 30;
}
.tabstrip::-webkit-scrollbar { display: none; }

.doctab {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 8px 0 14px;
  border-radius: var(--r-card) var(--r-card) 0 0;
  color: var(--ink-dim);
  background: transparent;
  white-space: nowrap;
  flex: 0 0 auto;
  max-width: 15rem;
}
.doctab > a {
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
}
.doctab:hover { color: var(--ink); }
.doctab.current { background: var(--card); color: var(--brand); font-weight: 600; }
.doctab.plain { padding-right: 12px; }

/* The × is a button and not a link: it closes a tab, it does not go anywhere.
   It stays 20px square so it can be hit without hitting the tab. */
/* ⚠ **A close button people can see and hit.** It was an 18px circle with a
   14px mark in it, which is smaller than Jifeng's and reads as a speck. The tab
   is 32px tall, so the control can be 20px without crowding anything, and the
   mark inside it is the same weight as the tab's own text. */
.doctab .x {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-dim);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.doctab .x:hover { background: var(--line-mid); color: var(--ink); }
.doctab.current .x { color: var(--ink-2); }

/* ---- work area: side rail + page -------------------------------------- */

.workarea { display: flex; align-items: flex-start; min-height: calc(100vh - 74px); }

/* ⚠ **The second level has to be on the screen, not behind a hover.**
 *
 * Before this rail, once a screen was open its eleven siblings existed only
 * inside a menu panel that closes when the pointer leaves it. So "the receipt
 * screens" was a thing you had to already know the shape of. Jifeng keeps the
 * current section's entries down the left the whole time; this is that.
 *
 * It is drawn by the one renderer from the one menu, so it is a second
 * *placement* of an entry and never a second definition -- the same standing
 * as the gear in the corner. */
.side {
  flex: 0 0 218px;
  width: 218px;
  align-self: stretch;
  background: var(--card);
  border-right: 1px solid var(--line);
  padding: 12px 8px 0;
  position: sticky;
  top: 74px;
  max-height: calc(100vh - 74px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.side-title {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  padding: 2px 8px 10px;
}
.side-title::before {
  content: "";
  width: 3px;
  height: 12px;
  border-radius: 1px;
  background: var(--brand);
  flex: 0 0 auto;
}

.side-group {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--ink-faint);
  padding: 12px 10px 4px;
}

.side-item {
  display: block;
  height: 40px;
  line-height: 40px;
  /* ⚠ **Never shrink.** The rail is a flex column, and a flex child's default
     is to give way when the column is too tall for the screen. With more
     entries than fitted, every row was squeezed from 40px to 18px while its
     line-height stayed 40 -- so each character was clipped top and bottom into
     a smudge, and the whole rail read as corrupted text. It was reported as
     "the words are garbage", and it was a layout bug, not an encoding one.
     The rail scrolls instead. */
  flex: 0 0 auto;
  padding: 0 10px;
  border-radius: var(--r-ctl);
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.side-item:hover { background: var(--page); }
.side-item.current { background: var(--brand-pale); color: var(--brand); font-weight: 600; }
.side-item.locked { color: var(--ink-faint); }

/* Collapsing is for the wide tables, and the state is the reader's, so it is
   remembered in localStorage rather than reset on every page. */
.side-toggle {
  flex: 0 0 auto;
  margin: 8px 0 10px;
  align-self: flex-start;
  background: transparent;
  border: 0;
  color: var(--ink-faint);
  padding: 6px 10px;
  cursor: pointer;
  font: inherit;
}
.side-toggle:hover { color: var(--brand); background: transparent; }

body.rail-shut .side {
  flex-basis: 40px;
  width: 40px;
  padding-left: 0;
  padding-right: 0;
  align-items: center;
}
body.rail-shut .side-title,
body.rail-shut .side-group,
body.rail-shut .side-item { display: none; }

/* ---- page ------------------------------------------------------------- */

main { padding: 16px; flex: 1 1 auto; min-width: 0; }

/* The page's own name sits in the strip above, so the heading in the body is
   quieter than it was -- two copies of the same word, one of them large, is
   how a screen starts to look like a poster. */
h1 { font-size: 16px; margin: 0 0 4px; font-weight: 600; }
.sub { color: var(--ink-dim); margin: 0 0 14px; }

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 16px;
  margin-bottom: 16px;
}

/* A card holding a table scrolls inside itself. A column count that fits in
   English and not in Spanish must not push the whole page sideways. */
.card:has(> table) { padding: 0; overflow-x: auto; }

table { border-collapse: collapse; width: 100%; }

/* Jifeng's grid: a grey header the same colour as the page, a rule between rows
   and nothing between columns.

   ⚠ **Measured, not chosen.** This said "dense on purpose -- forty receipts on
   one screen is the difference between a morning and an afternoon", and the
   rows were 37px against Jifeng's 44 with 12px type against its 13. The
   benchmark is not dense; every screen in this product was about 15% tighter
   than the thing it was copied from -- ragged and starved of white space, in
   the words it was reported in. Taken on wms.jfwms.com/web/in, 2026-09-06:

     header row 40px, 13px, weight 500      body row 44px, 13px
     cell padding 0 7-8px                   rule #E5EAF8 between rows

   The vertical padding here is what makes the row, since these are ordinary
   table cells rather than a fixed-height grid: 13px + 17.55px line + 13px is
   44. Horizontal came down from 15 to 12 to pay for the wider type, so a
   nine-column table is no wider than it was -- an action that needs a
   sideways scroll to reach is one nobody reaches (docs/audit-2026-09-05.md). */
th, td { text-align: left; padding: 13px 12px; border-bottom: 1px solid var(--rule); }
th {
  padding: 11px 12px;
  background: var(--page);
  color: var(--ink-2);
  font-weight: 500;
  font-size: 13px;
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}
td { font-size: 13px; }

/* ⚠ **A cell holding a button keeps the same row height as one holding text.**
   The button is 32px and the cell's own 13px of padding would make that row 58
   while its neighbours are 44 -- rows of two different heights in one table is
   the raggedness this was reported as. Six and six around a 32px control is 44,
   so an actions column changes nothing about the row it is in. */
table:not(.jf) td:has(button),
table:not(.jf) td:has(select),
table:not(.jf) td:has(input) { padding-top: 6px; padding-bottom: 6px; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: #fafbfe; }

/* The identifier in the first column is the way into the record everywhere in
   this trade, so it is blue and it is not underlined until it is aimed at. */
td a { color: var(--brand); text-decoration: none; }
td a:hover { text-decoration: underline; }

label { display: block; font-size: 12px; color: var(--ink-dim); margin-bottom: 4px; }

/* ---- a label beside its control, not above it ----------------------------
   ⚠ **A stacked form's margins do not survive being turned on their side.**
   `label` carries 4px under it for the ordinary case where the words sit above
   the box, and `.sub` carries 14px. Put the two in a centred flex row and that
   margin is still there, shunting the words up off the middle of the control:
   at `.sub` the label rides 7px high, which is what the sorting bench reported
   as the printer label not lining up with its box.

   The row also gets a control's own width. A select sized by its longest
   option is a control that changes width when somebody picks a printer, and a
   panel that moves under the hand is a panel that gets misread. */
.field-inline {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.field-inline > label { margin: 0; flex: 0 0 auto; }
.field-inline > select,
.field-inline > input { flex: 0 1 auto; min-width: 190px; }

/* ⚠ **`input:not([type])` is in this list because a bare `<input>` is a text
   input and matches none of the type selectors.** Thirty-one of them were
   written without a type and five more as `month`, so they fell back to the
   browser's own 13.333px and its own box -- a search field 140px wide beside a
   full-width one is the commonest way a form looks unfinished.
   ⚠ The specificity is deliberately identical to `input[type="text"]` (0,1,1),
   because `input.fc-w` and friends are written to beat exactly that and a
   longer selector here would quietly outrank them. */
input[type="text"], input[type="email"], input[type="month"], input[type="tel"],
input[type="url"], input:not([type]) {
  width: 100%;
  height: var(--h-ctl);
  padding: 0 10px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font: inherit;
  color: var(--ink);
  background: #fff;
}

select {
  height: var(--h-ctl);
  padding: 0 8px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font: inherit;
  color: var(--ink);
  background: #fff;
}

button {
  height: var(--h-ctl);
  background: var(--brand);
  color: #fff;
  border: 1px solid var(--brand);
  border-radius: var(--r-ctl);
  padding: 0 15px;
  font: inherit;
  cursor: pointer;
}
button:hover { background: var(--brand-hover); border-color: var(--brand-hover); }
button:active { background: var(--brand-down); border-color: var(--brand-down); }
button.ghost { background: #fff; color: var(--ink); border: 1px solid var(--line-mid); }
button.ghost:hover { background: #fff; color: var(--brand); border-color: var(--brand); }

.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
.row > div { flex: 1 1 190px; }

/* ⚠ **A row of labelled fields is the same four columns everywhere.**
   `.formgrid` below was built for the product form on 2026-09-03 for exactly
   this reason and was never taken to the other forty-five screens that have a
   form on them. Those went on dividing their card by whatever basis each field
   was written with: 383 hand-typed `flex: 1 1 200px` and the like, sixty-odd
   distinct values, 91 different field widths across the product. Measured at
   1280 on 2026-09-06: of 46 screens carrying a form, 20 put their fields on
   more than four different left edges, so nothing lined up with anything above
   or beside it. That is what "ragged" meant.

   Matched by shape rather than by a class on 190 markup sites: a row is a form
   row when one of its own children carries a label. The others -- a button
   row, a link and its copy button inside a table cell -- keep the flex they
   had, which is the reason this is a `:has()` and not a change to `.row`.
   The inline bases stay in the markup and stop having an effect; they are a
   guide to intent for whoever next gives a field `wide`. */
.row:has(> div > label) {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px 16px;
  align-items: end;
}
.row:has(> div > label) > div { min-width: 0; }
/* A field that really is twice a field says so, the same word `.formgrid` uses. */
.row:has(> div > label) > .wide { grid-column: span 2; }
.row:has(> div > label) > .wider { grid-column: span 3; }
.row:has(> div > label) > .full { grid-column: 1 / -1; }
/* A trailing button belongs at the bottom of its column, not stretched across it. */
.row:has(> div > label) > div > button { justify-self: start; }
@media (max-width: 900px) {
  .row:has(> div > label) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ⚠ **A grid, because the product form was rows and rows do not line up.**
   Built 2026-09-03 out of `.row`, where every line divided the card by its own
   proportions -- a value, a currency, an HS code and a customs attribute at
   four different widths, then a brand, a model, a material and a use at four
   others. Nothing below a field lined up with anything above it, and the boss's
   word for the result was that it is a mess.

   Jifeng's create and edit screens are four equal columns and every field is
   one of them; the last row of a card just runs short. That is the whole
   difference -- not spacing, not colour. Columns, and fields that say when they
   want two. */
.formgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 16px; align-items: end; }
.formgrid > div { min-width: 0; }
.formgrid .wide { grid-column: span 2; }
.formgrid .wider { grid-column: span 3; }
.formgrid .full { grid-column: 1 / -1; }
/* A value and its unit are one field wearing two boxes. */
.formgrid .pair { display: flex; gap: 6px; }
.formgrid .pair > input:first-child { min-width: 0; flex: 1 1 auto; }
.formgrid .pair > input + input { flex: 0 0 62px; text-align: center; }
@media (max-width: 900px) {
  .formgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ⚠ **A heading is one size and one distance from what it introduces.**
   76 of them were written inline across the screens in about twenty different
   combinations of font-size and margin -- 15px over 4px here, 16px over 6px
   there, 14px over 10px on the next screen. Nothing lined up with anything and
   the eye reads that as a different kind of thing each time, which is what
   "ragged" meant. Two rules: a heading inside a card, and one that opens a
   section between cards, which is allowed more air above because it is
   separating rather than introducing. */
main h2,
main h3 { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0 0 14px; }
main h2.sec,
main h3.sec { margin: 24px 0 12px; }

.lines { display: flex; flex-wrap: wrap; gap: 10px 18px; }
.lines label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink); margin: 0; }

.pill {
  display: inline-block;
  background: var(--brand-wash);
  color: var(--brand-down);
  border-radius: var(--r-ctl);
  padding: 1px 8px;
  font-size: 12px;
  margin-right: 5px;
}
.pill.off { background: var(--tag-bg); color: var(--ink-dim); }

.msg { padding: 9px 12px; border-radius: var(--r-ctl); margin-bottom: 14px; }
.msg.ok { background: var(--ok-bg); color: #008c21; }
.msg.bad { background: var(--bad-bg); color: #cc3535; }
/* ⚠ The third answer, and it is not a shade of failure. What we own worked and
   only an outward call did not -- a hand-over recorded but a carrier that
   refused. Reporting that in red sends somebody to undo a thing that is
   correct. Same colour, same reasoning, as the measuring station's amber. */
.msg.warn { background: var(--warn-bg); color: #d56800; }

/* ---- a job that takes longer than a click (web/lib/working.js) ---------- */

/* ⚠ **The same shape and the same margin as `.msg` above**, because it stands
   in the same place and is read for the same reason: it is the screen
   answering. A bar that arrived in its own visual language would read as a
   second thing happening rather than as the reply to what was just pressed. */

.working {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px;
  margin-bottom: 14px;
  background: var(--brand-pale);
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  color: var(--ink-2);
}
.working .what { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
/* ⚠ **10px, not the hairline this started as.** A 6px rule read as a divider
   rather than as something moving -- reported the same afternoon as "too
   thin". It has to be seen from where somebody is standing when they are
   deciding whether the screen heard them. */
.working .track {
  flex: 1 1 auto;
  height: 10px;
  min-width: 60px;
  background: var(--brand-wash);
  border-radius: 5px;
  overflow: hidden;
}
.working .fill { display: block; height: 100%; background: var(--brand); border-radius: 5px; transition: width 0.25s ease; }
/* Tabular figures: 9 / 40 becoming 10 / 40 must not shuffle the line sideways. */
.working .n { color: var(--ink-2); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ⚠ **One box has nothing to count**, so the bar moves instead of filling. It
   says "still going" and claims nothing about how far along it is -- see the
   head of web/lib/working.js. */
.working.unknown .fill { width: 34%; animation: working-slide 1.15s ease-in-out infinite; }
@keyframes working-slide {
  from { transform: translateX(-100%); }
  to { transform: translateX(294%); }
}
/* A moving bar is the one thing on these screens somebody may have asked their
   machine not to do. The line above still says what is happening. */
@media (prefers-reduced-motion: reduce) {
  .working.unknown .fill { width: 100%; opacity: 0.35; animation: none; }
}

/* ---- status tabs ------------------------------------------------------- */

/* The counted status filters a list page draws. Jifeng puts these down the left
   rail; ours stay across the top of the list, which is the same information in
   the same reading order and does not need 128 screens rewritten to move. */

.tabs {
  display: flex;
  gap: 2px;
  margin-bottom: 12px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
}

.tab {
  height: auto;
  background: transparent;
  color: var(--ink-dim);
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 8px 14px;
  font: inherit;
  cursor: pointer;
  margin-bottom: -1px;
}
.tab:hover { background: transparent; color: var(--ink); border-color: transparent; }
.tab.on { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
.tab .n { color: var(--ink-faint); font-weight: 400; margin-left: 3px; }
.tab.on .n { color: var(--brand); }

/* ⚠ **A row action is still a button somebody has to hit.** These were 26px
   tall, 12px type and 46px wide, sitting in pairs -- edit beside delete -- in
   a 37px row. Reported from the floor as awkward to hit, and two of them side
   by side is a misclick that deletes something. Jifeng's smallest button is 32px; so is this one now, and
   the row it sits in has 44px to hold it. */
button.sm { height: 32px; padding: 0 14px; font-size: 13px; }

/* ---- toolbar and footer ------------------------------------------------ */

/* The two rows every list page in this trade has: what you can do to the list,
   and how much of it there is. Named here so a screen reaches for a class
   instead of an inline style. */
.toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  /* ⚠ **One height whatever is in it.** A bulk button is 32px and the reload
     and column icons `lib/tablekit.js` hangs here are 28px, so a screen whose
     toolbar changes with the tab moved its own table up and down by four
     pixels every time somebody clicked. Same rule as the table row that holds
     a control: the strip is the height, not its tallest occupant. Inside a
     list card the band is 64 -- see `.listcard .toolbar` below. */
  min-height: var(--h-ctl);
}
.toolbar .right { margin-left: auto; display: flex; align-items: center; gap: 8px; }

.pager {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 4px 0;
  color: var(--ink-dim);
  font-size: 12px;
}
.pager .right { margin-left: auto; display: flex; align-items: center; gap: 10px; }

/* ---- record status ----------------------------------------------------- */

.status {
  display: inline-block;
  border-radius: var(--r-ctl);
  padding: 1px 8px;
  font-size: 12px;
  background: var(--tag-bg);
  color: var(--ink-dim);
  /* A status that wraps one character per line is unreadable, and Chinese
     wraps anywhere. It keeps its width and the table scrolls instead. */
  white-space: nowrap;
}
.status.pending { background: var(--warn-bg); color: #d56800; }
.status.active { background: var(--ok-bg); color: #008c21; }
.status.rejected { background: var(--bad-bg); color: #cc3535; }
.status.draft { background: var(--brand-wash); color: var(--brand-down); }

code { background: var(--page); border-radius: 3px; padding: 1px 5px; font-size: 12px; }

/* ---- sign in ------------------------------------------------------------ */

/* The two screens with no session behind them, so no bar and nothing to
   navigate to. A centred card and a language switcher, because somebody who
   cannot read this page still has to be able to change it. */

/* Every page in this product is a pale page. The sign-in screens used to sit
   on the dark bar; they do not any more. */
body.plain { background: var(--page); }

.auth {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
}

.auth-card {
  width: 100%;
  max-width: 380px;
  margin: 0;
  padding: 28px 30px 30px;
  box-shadow: 0 6px 24px rgba(17, 22, 34, 0.06);
}

.auth-card h1 { margin-bottom: 4px; font-size: 19px; }
.auth-card .sub { margin-bottom: 20px; }
.auth-card button { width: 100%; height: 36px; }

input[type="password"] {
  width: 100%;
  height: var(--h-ctl);
  padding: 0 10px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font: inherit;
}

.auth-lang { max-width: 380px; width: 100%; text-align: right; }
.lang.light { color: var(--ink); border-color: var(--line-mid); background: #fff; }

/* ---- numeric columns ------------------------------------------------------ */

/* Figures line up on the right and in a tabular font, so a column of them can
   be read down rather than one at a time. */
th.n, td.n { text-align: right; font-variant-numeric: tabular-nums; }

/* A totals row has to stop looking like one more seller.
   Without this the sum sat flush against the last row of data in the same
   weight, and the eye reads it as another customer called "Total". */
tfoot td { border-top: 2px solid var(--ink-faint); }

/* Zero is quieter than a number. */
.zero { color: var(--line-bold); }

/* A count that opens what it counts.
   It stays a figure. A real button in a numeric column reads as an action on
   the row -- which is what the Delete button at the far end of it is -- and a
   column of them turns a table of numbers into a table of controls. The hit
   area is padded out because the figure itself is one or two characters, and a
   two-character target on the right edge of a wide table is a miss. */
/* ⚠ **`button:hover` and `button:active` have to be answered by name.** They
   are (0,1,1) and this class is (0,1,0), so a rule that only clears the base
   background loses the moment a pointer is over it -- and what the reader sees
   is the figure turning into a solid blue block under their cursor. Found on
   2026-08-26 by pressing it. */
.countbtn,
.countbtn:hover,
.countbtn:active {
  background: none; border: 0; height: auto; padding: 2px 0 2px 10px; margin: 0;
  font: inherit; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--brand); cursor: pointer;
}
.countbtn:hover { text-decoration: underline; }
.countbtn.zero, .countbtn.zero:hover, .countbtn.zero:active { color: var(--line-bold); }

/* Owed is loud. A shortfall reported as zero is the bug the ledger exists to
   prevent, so it is never allowed to look like nothing. */
.owed { color: var(--bad); font-weight: 600; }

tr.clickable { cursor: pointer; }
tr.clickable:hover td { background: var(--brand-pale); }

/* ---- the weigh-and-ship bench --------------------------------------------- */

/* Sized for a person a metre from the screen who is holding a box. Nothing
   here is subtle on purpose. */

.bench-scan label { font-size: 13px; }

/* ⚠ **One frame around the scan box, not three.** Asked for as "make the
   border thicker"; what was actually wrong was the layering. The block is a
   card with a border, the input has its own border inside it, and every input
   on the site gets a focus ring -- and this one autofocuses when a bench
   opens, so all three were drawn at once. Thickening the middle one made it
   read as a box inside a box.
   
   So the card's own frame comes off where the card IS the scan block, the
   input carries the whole signal, and the focus ring is off: a field that is
   focused from the moment the screen opens cannot use focus to say anything.
   Chosen off six rendered side by side on 2026-08-23.
   
   ⚠ `.bench-scan.card` and not `.bench-scan`: on the measuring station the
   scan box sits *inside* a card that also holds the weight and the dimensions,
   and that card's frame is structural. */
.bench-scan.card {
  border-color: transparent;
  background: transparent;
  padding: 0;
}

.bench-scan input {
  font-size: 26px;
  height: auto;
  padding: 12px 14px;
  letter-spacing: 0.02em;
  border: 3px solid var(--brand);
  border-radius: 10px;
}

.bench-scan input:focus,
.bench-scan input:focus-visible {
  outline: none;
}

.bench-answer {
  border-radius: var(--r-card);
  padding: 18px 22px;
  margin: 16px 0;
  border: 1px solid var(--line);
}
.bench-answer .big { font-size: 26px; font-weight: 600; line-height: 1.2; }
.bench-answer .small { margin-top: 4px; font-size: 14px; opacity: 0.85; }

/* ---- the facts under a call-out, as rows -----------------------------------

   ⚠ **This was one line and the line was a sentence.** "Customer W1001 -
   X001000007 - 3 in this pile now" puts two barcodes and a count on one row
   separated by punctuation, so telling them apart means reading the line
   through -- at a bench, holding a unit, a metre from the screen. Two of the
   three are codes that differ from a neighbour by one character, and one of
   them decides whose paperwork the unit lands on.

   A label column and a value column: each fact is found by where it sits, and
   the labels say which is which instead of a separator implying it. The
   values are tabular so a code lines up under a code -- a digit out of place
   shows as a jog in the column rather than as nothing at all. */
.answer-fields {
  margin-top: 8px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 3px 12px;
  align-items: baseline;
}
.answer-field {
  font-size: 12px;
  color: var(--ink-faint);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.answer-value {
  font-size: 16px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
/* ⚠ **Narrow, the label column stops earning its width.** On a phone the two
   columns left so little for the value that a barcode broke over four lines --
   "X00 / 100 / 010 / 1" -- which is worse than the one line it replaced. Under
   the label instead, each fact still its own row. The query is on the panel's
   own width (`.bench-answer` is a container), not the window's: what this has
   to fit inside is the panel. */
@container (max-width: 420px) {
  .answer-fields { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .answer-field { margin-top: 7px; }
}

.bench-answer.idle { background: var(--card); color: var(--ink-dim); }
.bench-answer.ok { background: var(--ok-bg); border-color: #bdecc8; color: #008c21; }
.bench-answer.bad { background: var(--bad-bg); border-color: #fccaca; color: #cc3535; }

.bench-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 14px; }
.bench-ref { font-size: 20px; font-weight: 600; }

#weight { font-size: 22px; height: auto; padding: 10px 12px; }

.status.shipped { background: var(--ok-bg); color: #008c21; }
.status.exception { background: var(--bad-bg); color: #cc3535; }
.status.cancelled { background: var(--tag-bg); color: var(--ink-dim); }
.status.toShip, .status.toPack, .status.toPrint { background: var(--warn-bg); color: #d56800; }
.status.awaitingTracking { background: var(--brand-wash); color: var(--brand-down); }

/* A disabled control must look disabled.
   The whole system runs on "what you can see, you can use" -- a button that
   reads as clickable and does nothing is the same lie as a menu entry that
   opens nothing, just smaller. */
button:disabled,
button:disabled:hover {
  background: #eef0f4;
  color: #a8b0bd;
  border-color: var(--line);
  cursor: not-allowed;
}
button.ghost:disabled,
button.ghost:disabled:hover { background: #f7f8fa; color: #a8b0bd; }

input:disabled {
  background: #f7f8fa;
  color: var(--ink-dim);
  cursor: not-allowed;
}

/* ---- the factory measuring station ---------------------------------------- */

/* A page of its own, opened on a fixed machine at the seller's factory. It has
   no top bar because it has no account behind it -- see the head of
   web/station.html. Sized like the weighing bench, for the same reason: the
   person using it is holding a box, not reading. */

.station { max-width: 760px; margin: 0 auto; padding: 22px 20px 60px; }

.station-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.station-head h1 { margin: 0 0 4px; font-size: 19px; }
.station-who { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-dim); }

/* The weight is deliberately not one of four matching boxes: its own row, its
   own colour, away from the three sides. A length typed into the weight is a
   mistake nothing downstream can catch -- both numbers stay plausible and the
   freight is simply booked wrong. */
.station-weight {
  margin-top: 14px;
  background: var(--warn-bg);
  border: 1.5px solid #e0a458;
  border-radius: var(--r-card);
  padding: 10px 12px 12px;
}
.station-weight label { color: #a35f07; font-weight: 600; }
.station-weight input { font-size: 26px; height: auto; padding: 10px 12px; border-color: #e0a458; }

.station-dims { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 12px; }
.station-dims input { font-size: 20px; height: auto; padding: 10px 8px; text-align: center; }

.station-go { width: 100%; margin-top: 16px; font-size: 17px; height: auto; padding: 14px 20px; }

.station-row { padding: 10px 14px; margin-bottom: 8px; display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.station-row .sub { margin: 0; }

/* Recorded, but the forwarder did not answer. Not green and not red: the
   measurement is ours and is safe, and only the push has to be tried again. */
.bench-answer.warn { background: var(--warn-bg); border-color: #e0a458; color: #a35f07; }

/* ---- a sorting bench is two columns ----------------------------------------

   The number to act on down the left, what is already on the wall down the
   right. Stacked they fought each other: a 200px number left the right half
   of its own panel empty, and the wall it is checked against sat below the
   fold.

   ⚠ The left column has a floor wide enough to keep the square and the
   seller's line side by side (`.pile-num` plus its caption). Narrower than
   that and the whole thing goes back to one column -- two cramped columns are
   worse than one honest one, and the answer panel does its own wrapping only
   as the last resort. */
.bench-split {
  display: grid;
  grid-template-columns: minmax(390px, 1fr) minmax(0, 1fr);
  /* ⚠ The wall spans both rows, so the finished rounds sit under the call-out
     rather than under the wall. The call-out is a fixed height and the wall is
     not: a round of 100 codes made the wall 3,564px tall with the finished
     list beneath it, five screens down, beside a column that had been empty
     since the second row of tiles. */
  grid-template-areas:
    "call wall"
    "done wall";
  gap: 20px;
  align-items: start;
  margin-top: 16px;
}
.bench-call { grid-area: call; }
.bench-wall { grid-area: wall; }
.bench-done { grid-area: done; }
.bench-split > div > .bench-answer:first-child { margin-top: 0; }
@media (max-width: 980px) {
  .bench-split {
    grid-template-columns: 1fr;
    /* One column puts the wall back above the finished rounds: what is
       standing on the floor comes before what came off it. */
    grid-template-areas:
      "call"
      "wall"
      "done";
  }
}

/* ⚠ **The wall keeps its own box.** Without this the page grows a row per
   three piles -- 3,964px of page on a 720px bench monitor for one 100-code
   round -- and every panel on the screen moves as the day goes on. The cap is
   the viewport less what stands above it, with a floor so it never collapses
   to a slot on a short window. drawWall() scrolls the pile just called into
   view, so nobody reaches for this bar mid-scan. */
.wall-scroll {
  max-height: calc(100vh - 320px);
  min-height: 220px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ---- the wall, drawn as the wall ------------------------------------------

   ⚠ **This was a three-column table and that was the wrong shape twice.**
   It read as a spreadsheet when the thing it describes is physical, and it
   squashed: given half the page, the code column and the count column pulled
   apart with dead air between them while the figures shrank. Reported from
   the floor as crowded and ugly, which it was.

   A pile is a pigeonhole. So it is drawn as one: a tile carrying the number
   somebody reads off the wall, what is in it, and how many. The grid reflows
   at any width instead of compressing, and it is the same object the person
   is looking at across the room -- which a table row never was. */

.wall-round { margin-bottom: 18px; }
.wall-round:last-child { margin-bottom: 0; }

.wall-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.wall-who { display: flex; align-items: baseline; gap: 6px; min-width: 0; flex-wrap: wrap; }
/* ⚠ A bare `1001` next to a bare `1001-20260821-01` is two numbers and no
   way to tell which is which. Reported from the floor exactly that way. The
   label is not prose -- it is the field's name, which is the one kind of
   words this system's screens are supposed to carry. */
.wall-field {
  font-size: 11px;
  color: var(--ink-faint);
  letter-spacing: 0.02em;
}
.wall-field + * { margin-right: 8px; }
.wall-seller { font-size: 17px; font-weight: 650; letter-spacing: 0.01em; }
/* The round's code is a reference, not a heading -- it is read when somebody
   is looking for it and skipped every other time. */
.wall-round-code {
  font-size: 12px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ⚠ **The one action in the wall's header, and it was the quietest thing on
   the screen.** It was a ghost button -- white on white, at list-toolbar size
   -- on a panel whose loudest object is a number nobody presses. Reported from
   the floor on 2026-08-23 as hard to find and hard to read.

   Solid, and sized like the bench control it is rather than a toolbar one:
   this is the press that closes the round and books what it counted into
   stock, and since 2026-08-23 it is the only press that does -- there is no
   second button behind it to catch a round somebody could not find the button
   for. */
.wall-finish {
  height: auto;
  padding: 10px 22px;
  font-size: 16px;
  font-weight: 600;
  white-space: nowrap;
}

.wall-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  gap: 10px;
}

.pigeon {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 10px 12px 11px;
  min-width: 0;
}
/* Big enough to match against a pigeonhole from a step away, small enough
   that it never competes with the call-out the bench just made. */
.pigeon-n {
  font-size: 30px;
  font-weight: 750;
  line-height: 1.05;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
}
/* ⚠ Ellipsis, not wrap. A barcode broken across two lines is a barcode
   somebody misreads; cut short it is obviously cut short, and the full code
   is on the tile's title. */
.pigeon-sku {
  margin-top: 4px;
  font-size: 12px;
  color: var(--ink-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pigeon-qty { margin-top: 3px; font-size: 12px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* ⚠ **The tile the bench just called, in the colour it called it.**
   Reported from the floor: the call-out draws a green 4 and the wall beside it
   draws 1 2 3 4 in one flat colour, so the person who was just told "4" has to
   go and find the 4 by reading. Colour is the one thing that can carry an
   answer between two panels without being read, and it was being spent on
   nothing.

   Exactly the three values `.bench-answer.ok` uses, because it is the same
   answer drawn twice. There is no amber variant: a scan that lands is green
   whether or not the pile is new, because opening a pile is not an event --
   it is what the first unit of anything does. */
.pigeon-ok {
  background: var(--ok-bg);
  border-color: #bdecc8;
}
.pigeon-ok .pigeon-n { color: #008c21; }
.pigeon-ok .pigeon-sku, .pigeon-ok .pigeon-qty { color: #12833a; }


/* ---- rounds already finished ----------------------------------------------

   Closed, not closed-with. A seller is invoiced from these and this wall
   counts blind, so the figures have to stay readable after the piles come
   down -- see db/sorting.ts doneBatches(). One line each, because what
   somebody checks is the total; the piles behind it open on demand. */
.done-round {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--card);
  margin-bottom: 8px;
}
.done-round > summary {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 9px 12px;
  cursor: pointer;
  list-style: none;
}
.done-round > summary::-webkit-details-marker { display: none; }
.done-round > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid var(--ink-faint);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.12s;
}
.done-round[open] > summary::before { transform: rotate(90deg); }
.done-round > summary .wall-seller { font-size: 14px; }
.done-sum { font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.done-when { margin-left: auto; font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.done-round .wall-grid { padding: 0 12px 12px; }

/* ---- the pile number, as a square ------------------------------------------

   The one thing a person at the sorting wall reads, and they read it with a
   box in both hands from a metre away while looking mostly at a wall of
   pigeonholes. A number in a box is found by the eye without being read --
   which is why Lingxing draws it this way and why this does too.

   ⚠ **It is meant to be the largest thing on the screen by a long way**, not
   merely large. A first pass at 62px looked big next to body text and was
   still reported from the floor as small, which is the right verdict: the
   comparison that matters is the pigeonhole across the room, not the
   paragraph beside it. The box is sized in `em` off the digit so the square
   stays a square at every size, and clamped so a bench laptop and a wall
   display both get the biggest number their width allows.

   ⚠ **In `cqw`, not `vw`.** The panel is half the page now; a number measured
   against the window keeps growing after the space it actually lives in has
   stopped.

   Tabular figures so 11 and 17 have the same silhouette: at this size a
   proportional 1 makes two-digit numbers different widths, and the shape is
   what somebody matches against the pigeonhole. */
/* ⚠ Wraps. A 200px square beside a sentence does not fit a narrow bench
   tablet side by side, and the half that gets pushed off is the sentence --
   the seller's code, which is the one thing the wall settling ownership on
   its own must never hide. Below the number is fine; off the screen is not. */
.bench-answer:has(.pile-num) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 24px;
  padding: 22px;
  /* ⚠ So the number can measure **this panel** and not the window. Sized in
     `vw` it went on growing after the bench became two columns, until a 244px
     square in a 500px column pushed the seller's line onto its own row. What
     it competes with for space is its own panel; that is what it must read. */
  container-type: inline-size;
}
.bench-answer .answer-text { flex: 1 1 170px; min-width: 0; overflow-wrap: anywhere; }
/* ⚠ CJK breaks between any two characters by default, so a Chinese line that
   has to wrap splits a word down the middle -- and it is a two-character word,
   so half of it reads as something else. `keep-all` moves the break onto the
   spaces and punctuation already in the line. `overflow-wrap` above still
   rescues a barcode too long for its column; this only stops the gratuitous
   case. */
.bench-answer .answer-text .small { word-break: keep-all; }
.pile-num {
  flex: 0 0 auto;
  /* ⚠ **The box is a fixed square; the digits are what shrink.** Sized off
     `--pile-box` rather than off the text, because `min-width` sized it off
     the text and a third digit turned the square into a rectangle -- and past
     a hundred piles on the wall, three digits is an ordinary afternoon. The
     box is a landmark the eye returns to at a known size and a known place,
     so a wall that grew busier must not be a box that changed shape. Two
     digits fit at full size (2 x 0.556em advance + padding = 1.35em, inside
     1.45em); three do not, and drop to 72% on the `.pile-num-long` below. */
  --pile-box: clamp(110px, 37.7cqw, 244px);
  width: var(--pile-box);
  height: var(--pile-box);
  font-size: calc(var(--pile-box) / 1.45);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 18px;
  border: 4px solid currentColor;
  background: var(--card);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* ⚠ **The digits sit on their own baseline, not in the middle of the box.**
   Centring a line box is not centring what is drawn in it: a digit has no
   descender, so the ink hangs above the baseline while the line box reserves
   room under it for one. Measured at this size: 47.4px of air above the
   glyphs and 41.5px below, which is the whole number looking dropped inside
   its square. The span is lifted by half the difference. Kept in `em` so it
   scales with the clamp, and applied to the span rather than the box so the
   border does not move with it. */
.pile-num > span { display: block; transform: translateY(-0.0247em); }
/* Three digits or more, which starts at pile 100 and is an ordinary afternoon
   on a wall this size. The divisor is measured, not derived: at 1.91 the three
   glyphs advanced 179.6px inside a 173px square -- the weight is drawn bolder
   than the metrics a nominal 0.556em digit predicts. 2.2 leaves air on both
   sides at every width the clamp reaches. */
.pile-num-long { font-size: calc(var(--pile-box) / 2.2); }
/* Beside a number that size, the sentence is a caption. Same reasoning as the
   old station bench, which shrank its own line the moment a slot appeared. */
.bench-answer:has(.pile-num) .big { font-size: 30px; }
.bench-answer:has(.pile-num) .small { font-size: 16px; margin-top: 6px; }

.status.measured { background: var(--brand-wash); color: var(--brand-down); }
.status.handedOver { background: var(--warn-bg); color: #d56800; }
.status.arrived { background: var(--ok-bg); color: #008c21; }
.status.closed { background: var(--tag-bg); color: var(--ink-dim); }

/* The switcher on the station page sits on a pale background, not on the dark
   bar it was drawn for. */
.station .lang { color: var(--ink); border-color: var(--line-mid); }
.station .lang:hover { background: var(--brand-pale); }

/* One focus ring, and it is the accent colour -- not the browser's, which on
   some builds is amber and quietly competes with the weight box for the one
   piece of amber on the measuring station. */
input:focus-visible,
select:focus-visible,
button:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.station-weight input:focus-visible { outline-color: #a35f07; }
.station-who button { white-space: nowrap; }
.status.suspended { background: var(--bad-bg); color: #cc3535; }

/* --- what the seller's screens and the despatch bench needed --------------
 *
 * Added rather than worked around in each page: a screen that reaches for an
 * inline style because the stylesheet lacks a rule is a screen that looks
 * almost like the ones beside it, which is worse than looking different. */

/* Every text-ish input, not only the two the console happened to use first.
 * A search box that is 140px wide because nobody wrote the selector is the
 * commonest way a form looks unfinished. */
input[type="search"],
input[type="date"],
input[type="number"] {
  width: 100%;
  height: var(--h-ctl);
  padding: 0 10px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font: inherit;
}
textarea {
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font: inherit;
  resize: vertical;
}

/* A link that does a button's job. It has to look like one, because "new
 * product" sitting in a row of buttons as blue underlined text reads as a
 * footnote. It is an anchor and not a button because it navigates -- so it
 * opens in a new tab on a middle click, like every other link. */
a.button {
  display: inline-flex;
  align-items: center;
  height: var(--h-ctl);
  background: var(--brand);
  color: #fff;
  border: 1px solid var(--brand);
  border-radius: var(--r-ctl);
  padding: 0 15px;
  text-decoration: none;
}
a.button:hover { background: var(--brand-hover); border-color: var(--brand-hover); }

/* A link that has to look like a secondary button. `a.button` above is the
   primary one; this is the same shape in the ghost colours, for a download
   sitting beside a real action that matters more than it does. */
a.button.ghost { background: #fff; color: var(--ink); border-color: var(--line-mid); }
a.button.ghost:hover { background: #fff; color: var(--brand); border-color: var(--brand); }

/* ⚠ The card is the drop target, so the whole area accepts a dragged file and
   not just a strip inside it. The border is what says so at the moment it
   matters -- a drop zone that looks identical while a file is over it leaves
   somebody guessing whether letting go will do anything. */
.card.on { border-color: var(--brand); background: #f5f8ff; }


/* Sent out, and it is a different ending from closed -- see
 * server/src/db/consignments.ts. Amber rather than green: the box has left the
 * building, which is finished but is not the same as finished-and-on-a-shelf. */
.status.despatched { background: var(--warn-bg); color: #d56800; }

/* A row carrying a debt. The figure inside it is already red; this is what
 * makes the row findable when the table is forty long. */
tr.warn td { background: #fffaf3; }

/* The row whose detail is open below the table.
 *
 * ⚠ Written because three screens were already setting `class="on"` on a
 * table row and nothing here answered it -- so the batch you had chosen and
 * the eleven you had not looked identical, and the panel underneath read as
 * belonging to whichever row your eye landed on. */
tr.on td { background: var(--brand-wash); }

/* A relabel batch, and it is a document like any other: open, finished, or
 * called off. Amber while it is work, because that is what an open one is.
 * `cancelled` is already grey above and needs nothing of its own. */
.status.open { background: var(--warn-bg); color: #d56800; }
.status.done { background: var(--ok-bg); color: #008c21; }

/* ⚠ The warehouse is working inside a seller's screens.
 *
 * Amber and impossible to miss, at the top of every page, for as long as it
 * lasts. Everything done here is filed under the operator's name and the seller
 * can read that list -- somebody cannot be expected to act as if they are on
 * the record unless the screen says so. */
.viewing {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  background: var(--warn-bg);
  color: #d56800;
  border-bottom: 1px solid #f0c36d;
  font-size: 13px;
}
.viewing span { color: #a35f07; }
.viewing button { margin-left: auto; }

/* ---- the home workbench -------------------------------------------------- */

/* ⚠ **What used to be here was the sentence "Pick a screen from the menu
 * above."** That was the first thing anybody saw after signing in, every
 * morning, and it is a large part of why this build came back described as
 * unreadable: the screen that is supposed to say what needs doing today said
 * nothing at all.
 *
 * Jifeng's home is a row of cards, one per part of the day's work, each a grid
 * of "label over a big number", with the numbers that mean trouble in red. The
 * figures are all counts of things waiting, because a warehouse's morning
 * question is not "how did last month go", it is "what is queued at my door".
 */

.home-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.home-head h2 { font-size: 16px; font-weight: 600; margin: 0; }
.home-head .sub { margin: 0; }

.home-cards {
  /* ⚠ **Wrapping flex, not a grid, so the last row has no hole in it.**
     As a grid of equal columns the cards divided badly: four cards where the
     inbound one is twice as wide is five columns' worth in a four-column grid,
     so the last card sat alone with three card-shaped gaps beside it -- which
     reads as three things that failed to load rather than as the end of the
     list. Flex items on the final row grow into the space instead, and how
     many cards there are depends on which business lines a warehouse runs, so
     no fixed column count is right for everybody. */
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 16px;
  /* Each card is as tall as what is in it. Stretching them all to the tallest
     leaves three cards with a hand's width of empty white under the figures,
     which reads as something that failed to load. */
  align-items: flex-start;
}

.home-cards > .home-card { flex: 1 1 240px; min-width: 0; }

.home-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 14px 16px 16px;
}
.home-card h3 {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  font-weight: 600;
  margin: 0 0 12px;
  color: var(--ink);
}
.home-card h3::before {
  content: "";
  width: 3px;
  height: 13px;
  border-radius: 1px;
  background: var(--brand);
  flex: 0 0 auto;
}

/* ⚠ **`auto-fill`, and a ceiling on how wide a column gets.** With `auto-fit`
   and a plain `1fr` the figures inside a card that grew to fill the last row
   were dealt out across the whole width -- three numbers with a hand's width
   between them, which is harder to read as a group than the same three close
   together. The columns keep their size and the spare width stays as margin. */
.home-figs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 12px 10px;
  /* ⚠ **A ceiling, not a narrower column.** A card that grew to fill the last
     row was dealing its figures out across the whole width -- three numbers a
     hand's width apart read as three unrelated things rather than one card.
     The cap only bites on a card that got extra width; every card narrower
     than this is laid out exactly as before.
     ⚠ Capping the column instead (`minmax(84px, 150px)`) looks equivalent and
     is not: `auto-fit` counts how many *maximums* fit, so a 266px card went
     from three columns to one. */
  max-width: 640px;
}

/* The whole figure is the link, not the number inside it: the reader is aiming
   at "the eight receipts waiting", and a hit area the size of one digit is a
   control that technically works. */
.home-fig {
  display: block;
  text-decoration: none;
  color: inherit;
  border-radius: var(--r-ctl);
  padding: 4px 6px;
  margin: -4px -6px;
}
.home-fig:hover { background: var(--brand-pale); }
.home-fig .k { display: block; font-size: 12px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.home-fig .v { display: block; font-size: 22px; font-weight: 600; line-height: 1.3; font-variant-numeric: tabular-nums; }
.home-fig .v.zero { color: var(--ink-faint); font-weight: 500; }
/* ⚠ Red is for a queue that should not exist, not for a big number. An
   exception count and a cut-off count are somebody's afternoon; a hundred
   receipts waiting is a Tuesday. */
.home-fig .v.bad { color: var(--bad); }

/* A card that covers more than one business line carries a row per line, each
   named. One row is unnamed: its name would be the card's name said twice. */
.home-row + .home-row { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.home-rowname { font-size: 12px; color: var(--ink-dim); margin-bottom: 8px; }

/* ⚠ **Two columns, because Jifeng's home page is two columns.** Measured on
   wms.jfwms.com 2026-09-08: the counts take the left and a 287px rail takes the
   right -- the warehouse's own announcements, a banner, the help links. Ours was
   one column, so a warehouse had nowhere at all to put a word in front of the
   people who open this screen every morning.

   The rail folds under the cards below 1100px rather than squeezing: at
   1280 the two columns are 940 and 300, and taking another 200 off the left
   would start wrapping the figure grids. */
.home-grid { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 16px; align-items: start; }
.home-grid.solo { grid-template-columns: minmax(0, 1fr); }
.home-grid > .home-cards { margin-bottom: 0; }
.home-rail { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
@media (max-width: 1100px) { .home-grid { grid-template-columns: minmax(0, 1fr); } }

/* The advertising slot. A picture the warehouse put up, at the top of the rail,
   where Jifeng puts its own. No border of its own -- whatever is in the picture
   is the card. */
.home-banner {
  display: block;
  position: relative;
  border-radius: var(--r-card);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--card);
  text-decoration: none;
  color: inherit;
}
.home-banner img { display: block; width: 100%; height: auto; }
.home-banner .cap {
  display: block;
  padding: 8px 12px;
  font-size: 13px;
  color: var(--ink);
}
a.home-banner:hover { border-color: var(--brand); }

/* A rail card is a home card with rows instead of figures. */
.rail-card { padding-bottom: 6px; }
.rail-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 7px 0;
  border: 0;
  border-top: 1px solid var(--line);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  text-decoration: none;
}
.rail-card > h3 + .rail-row { border-top: 0; }
.rail-row:hover .t { color: var(--brand); }
.rail-row .t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-row .d { flex: 0 0 auto; font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* ---- the console's home: Jifeng's dashboard -------------------------------- */

/* ⚠ **Measured, not designed.** wms.jfwms.com/web/dashboard in the boss's own
 * signed-in Chrome on 2026-09-14, `getComputedStyle` on each block, at an
 * 1841px window:
 *
 *   column        1366 wide, centred, no side rail on this page
 *   count cards   240 tall, padding 16, radius 8, border #E5EAF8, gap 16;
 *                 widths 157 / 502 / 330 / 157 / 157
 *   card head     16px icon + 8, title 14px/700/22px
 *   figure        label 12px/20px #313A52 over value 20px/600/28px, 12 apart;
 *                 red #F53F3F; yesterday 12px/20px #677395, 2 above
 *   grid dividers 1px dashed, 16px either side
 *   panels        title 16px/700, controls 32px, padding 16 20 20
 *   series tiles  216 x 80, padding 16 20, 3px colour edge
 *   money tiles   value 24px/600, bar 6px, legend 14px, dots 8px square
 *                 #0064FA #11A8A8 #9FDB1D #F0C014
 *   rail          330 wide; title 16px/700; tag 12px/18px on #EEF4FF r3;
 *                 date 13px #677395; rows divided 1px dashed #DCDFE6
 *
 * ⚠ **Zero is not greyed here, and that is Jifeng's choice copied.** The
 * seller's board below fades zeros; this page prints every figure in ink, and
 * red stays red at zero on the two cards that use it. */

.dash { max-width: 1366px; margin: 0 auto; }
.dash-head { display: flex; align-items: center; min-height: 22px; margin: 4px 0 20px; }
.dash-head h2 { font-size: 16px; font-weight: 700; line-height: 22px; margin: 0; color: var(--ink); }

.dash-top { display: flex; gap: 16px; align-items: stretch; margin-bottom: 16px; }
.dash-mod,
.dash-stack {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  min-width: 0;
}
.dash-mod { padding: 16px; }
/* The widths are Jifeng's, as proportions: a row of these at 1366 is exactly
   its row, and a narrower window takes the same share off each. */
.dash-top > .dash-mod,
.dash-top > .dash-stack { flex: 157 1 0; }
.dash-top > .dash-mod.l-grid { flex: 502 1 0; }
.dash-top > .dash-mod.l-pair { flex: 330 1 0; }
/* ⚠ The second row is the modules Jifeng's tenant does not run. They sit in a
   row of their own rather than eight to a 1366px row, which is 110px a card. */
.dash-top.more > .dash-mod { flex: 1 1 0; }

.dash-stack { display: flex; flex-direction: column; }
.dash-stack > .dash-mod { border: 0; border-radius: 0; background: none; flex: 1 1 auto; position: relative; }
.dash-stack > .dash-mod + .dash-mod::before {
  content: "";
  position: absolute;
  left: 16px;
  right: 16px;
  top: 0;
  border-top: 1px dashed var(--line-mid);
}

.dash-mod h3 {
  display: flex;
  align-items: center;
  font-size: 14px;
  font-weight: 700;
  line-height: 22px;
  margin: 0;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
}
.dash-mod h3 > span:last-child { overflow: hidden; text-overflow: ellipsis; }
.dash-ico { display: inline-flex; flex: 0 0 auto; margin-right: 8px; }
.dash-ico.inbound, .dash-ico.truck { color: #00b42a; }
.dash-ico.outbound, .dash-ico.product { color: #0064fa; }
.dash-ico.returns { color: #e80073; }
.dash-ico.workorder, .dash-ico.finance { color: #a728b3; }
.dash-ico.count { color: #11a8a8; }

.dash-figs { padding-top: 16px; }
.dash-figs.l-list { display: flex; flex-direction: column; gap: 12px; }
.dash-figs.l-grid,
.dash-figs.l-pair,
.dash-figs.l-row { display: grid; row-gap: 16px; }
.dash-figs.l-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dash-figs.l-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* A second-row card puts its figures side by side, divided as the grid is. */
.dash-figs.l-row { grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); justify-content: start; }
.dash-figs.l-grid > .dash-fig,
.dash-figs.l-pair > .dash-fig,
.dash-figs.l-row > .dash-fig { padding: 0 16px; border-left: 1px dashed var(--line-mid); }
.dash-figs.l-grid > .dash-fig:nth-child(4n + 1),
.dash-figs.l-pair > .dash-fig:nth-child(2n + 1),
.dash-figs.l-row > .dash-fig:first-child { padding-left: 0; border-left: 0; }

.dash-fig { min-width: 0; }
.dash-fig .hit { display: block; color: inherit; text-decoration: none; }
.dash-fig .k {
  display: block;
  font-size: 12px;
  line-height: 20px;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dash-fig .v { display: block; font-size: 20px; font-weight: 600; line-height: 28px; color: var(--ink); font-variant-numeric: tabular-nums; }
.dash-fig .v.bad { color: var(--bad); }
.dash-fig .hit:hover .k,
.dash-fig .hit:hover .v:not(.bad) { color: var(--brand); }
.dash-fig .y { display: block; margin-top: 2px; font-size: 12px; line-height: 20px; color: var(--ink-dim); text-decoration: none; }
.dash-fig .y:hover { color: var(--brand); }

.dash-grid { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 16px; align-items: start; }
.dash-grid.solo { grid-template-columns: minmax(0, 1fr); }
@media (max-width: 1100px) { .dash-grid { grid-template-columns: minmax(0, 1fr); } }
.dash-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
/* Jifeng's two lower panels are 617 and 388 wide. */
.dash-pair { display: flex; gap: 16px; align-items: stretch; flex-wrap: wrap; }
.dash-pair > .money { flex: 617 1 420px; }
.dash-pair > .capacity { flex: 388 1 300px; }

.dash-panel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 16px 20px 20px;
  min-width: 0;
}
.dash-ph { display: flex; align-items: center; gap: 12px; min-height: 32px; flex-wrap: wrap; }
.dash-ph h4 { font-size: 16px; font-weight: 700; line-height: 22px; margin: 0; color: var(--ink); white-space: nowrap; }
.dash-ph h4 .unit { font-size: 14px; font-weight: 400; color: var(--ink-dim); margin-left: 2px; }
.dash-ph .tools { margin-left: auto; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.dash-seg { display: inline-flex; }
.dash-seg button {
  height: var(--h-ctl);
  min-width: 66px;
  padding: 0 12px;
  margin: 0 0 0 -1px;
  border: 1px solid var(--line-mid);
  border-radius: 0;
  background: var(--card);
  color: var(--ink);
  font-size: 13px;
  font-weight: 400;
  position: relative;
}
.dash-seg button:first-child { margin-left: 0; border-radius: var(--r-ctl) 0 0 var(--r-ctl); }
.dash-seg button:hover { color: var(--brand); background: var(--card); }
.dash-seg button.on { border-color: var(--brand); color: var(--brand); z-index: 1; }
.dash-range {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: var(--h-ctl);
  padding: 0 10px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  background: var(--card);
}
/* Joined to the preset buttons, as Jifeng's range sits flush against "This year". */
.dash-seg + .dash-range { margin-left: -13px; border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }
.dash-range input[type="date"] {
  height: 30px;
  width: 118px;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  font-size: 13px;
}
.dash-range .sep { color: var(--ink-dim); }
.dash-select { height: var(--h-ctl); min-width: 104px; }

.dash-tiles { display: flex; gap: 20px; flex-wrap: wrap; margin: 24px 0 12px; }
.dash-tile {
  position: relative;
  width: 216px;
  padding: 16px 20px;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  overflow: hidden;
}
.dash-tile::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--brand); }
.dash-tile.s1::before { background: var(--ok); }
.dash-tile .k { display: block; font-size: 13px; line-height: 17px; color: var(--ink); }
.dash-tile .v { display: block; margin-top: 8px; font-size: 20px; font-weight: 600; line-height: 22px; font-variant-numeric: tabular-nums; }

.dash-chart { height: 242px; min-width: 0; overflow: hidden; }
.dash-chart svg { display: block; }
.dash-chart .gl { stroke: var(--line); stroke-dasharray: 4 3; }
.dash-chart .ax { font-size: 12px; fill: var(--ink-dim); }
.dash-chart .line { fill: none; stroke-width: 2; stroke: var(--brand); }
.dash-chart .dot { fill: var(--card); stroke-width: 2; stroke: var(--brand); }
.dash-chart .s1.line,
.dash-chart .s1.dot { stroke: var(--ok); }

.dash-money { display: grid; grid-template-columns: minmax(0, 204fr) minmax(0, 258fr); gap: 20px; margin-top: 16px; }
.dash-panel.capacity > .dash-tile2 { margin-top: 16px; }
.dash-tile2 { border-radius: var(--r-card); padding: 16px 16px 20px; min-width: 0; }
.dash-tile2.blue { background: linear-gradient(180deg, #f5f9ff 0%, rgba(255, 255, 255, 0) 100%); }
.dash-tile2.sky { background: linear-gradient(180deg, #eaf3ff 0%, rgba(255, 255, 255, 0) 100%); }
.dash-tile2.green { background: linear-gradient(180deg, #effbee 0%, rgba(255, 255, 255, 0) 100%); }
.dash-tile2 .k { font-size: 13px; font-weight: 600; line-height: 17px; color: var(--ink); }
.dash-tile2 .v { margin-top: 8px; font-size: 24px; font-weight: 600; line-height: 28px; color: var(--ink); font-variant-numeric: tabular-nums; }
.dash-tile2 .rule { margin-top: 10px; border-bottom: 1px dashed var(--line-mid); }
.dash-tile2 .k2 { margin-top: 10px; font-size: 14px; line-height: 19px; color: var(--ink); }
.dash-tile2 .ring {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-top: 20px;
  border: 1px solid var(--brand);
  border-radius: 50%;
  background: #e8f3ff;
  color: var(--brand);
  font-size: 18px;
  font-weight: 600;
}
.dash-bar { display: flex; height: 6px; margin: 10px 0 16px; background: var(--line); overflow: hidden; }
.dash-bar > span { flex: 0 1 0; }
.dash-legend { list-style: none; margin: 0; padding: 0; }
.dash-legend li { display: flex; align-items: center; font-size: 14px; line-height: 19px; color: var(--ink-2); }
.dash-legend li + li { margin-top: 7px; }
.dash-legend .name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-2); text-decoration: none; }
.dash-legend a.name:hover { color: var(--brand); }
.dash-legend .val { flex: 0 0 auto; margin-left: 12px; font-variant-numeric: tabular-nums; }
.dash-legend .dot { flex: 0 0 auto; width: 8px; height: 8px; margin-right: 8px; }
.dash-bar > .c0, .dash-legend .c0 { background: #0064fa; }
.dash-bar > .c1, .dash-legend .c1 { background: #11a8a8; }
.dash-bar > .c2, .dash-legend .c2 { background: #9fdb1d; }
.dash-bar > .c3, .dash-legend .c3 { background: #f0c014; }

.dash-rail { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.dash-rcard {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 16px 20px 12px;
  min-width: 0;
}
.dash-rh { display: flex; align-items: center; justify-content: space-between; }
.dash-rh h4 { font-size: 16px; font-weight: 700; line-height: 22px; margin: 0; color: var(--ink); }
.dash-rh .more { display: inline-flex; align-items: center; gap: 2px; font-size: 13px; color: var(--ink-2); text-decoration: none; }
.dash-rh .more:hover { color: var(--brand); }
.dash-notice {
  display: block;
  width: 100%;
  margin: 0;
  padding: 12px 0 8px;
  border: 0;
  border-bottom: 1px dashed #dcdfe6;
  border-radius: 0;
  background: none;
  height: auto;
  text-align: left;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}
.dash-notice:hover { background: none; }
.dash-notice:last-child { border-bottom: 0; }
.dash-notice .line { display: flex; align-items: center; gap: 8px; min-width: 0; }
.dash-notice .tag { flex: 0 0 auto; padding: 0 6px; border-radius: 3px; background: #eef4ff; color: var(--brand); font-size: 12px; line-height: 18px; }
.dash-notice .title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.dash-notice:hover .title { color: var(--brand); }
.dash-notice .date { display: block; margin-top: 4px; font-size: 13px; line-height: 17px; color: var(--ink-dim); }
.dash-link {
  display: block;
  padding: 8px 0;
  border-bottom: 1px dashed #dcdfe6;
  color: var(--ink);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dash-rh + .dash-link { margin-top: 8px; }
.dash-link:last-child { border-bottom: 0; }
.dash-link:hover { color: var(--brand); }

/* ---- the seller's page: Jifeng's OMS dashboard ----
 *
 * ⚠ **Measured on meirun.jfwms.net/web/dashboard, 2026-09-14, 1841px window.**
 * The ad banner is 1366 x 48 across the top. The count cards are the console's
 * (157 wide, 330 for handover, 228 tall) but they sit in one row that scrolls
 * sideways inside the 1021px left column, because the rail starts level with
 * them. The account card: padding 16 20; title 16px/600; the available figure
 * in a box padded 12 with a #E6F8EA edge, label 14px #313A52, value 24px/600,
 * the currency 12px/500 white on green; the rows under it 12px label, 16px
 * value; the top-up button full width, 32px, brand outline. */
.dash-banners { display: flex; flex-direction: column; gap: 12px; margin: 4px 0 12px; }
.dash-banners .home-banner { border: 0; border-radius: var(--r-ctl); }
.dash-top.scroll { flex-wrap: nowrap; overflow-x: auto; margin-bottom: 0; padding-bottom: 8px; }
.dash-top.scroll > .dash-mod { flex: 0 0 157px; }
.dash-top.scroll > .dash-mod.l-pair { flex: 0 0 330px; }

.dash-assets .avail {
  margin-top: 16px;
  padding: 12px;
  border: 1px solid #e6f8ea;
  border-radius: var(--r-card);
  background: linear-gradient(180deg, #effbee 0%, rgba(255, 255, 255, 0) 100%);
}
.dash-assets .avail .top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.dash-assets .avail .k { font-size: 14px; line-height: 18px; color: var(--ink-2); }
.dash-assets .avail .cur { font-size: 12px; font-weight: 500; line-height: 16px; padding: 0 3px; border-radius: 4px; background: var(--ok); color: #fff; }
.dash-assets .avail .v { margin-top: 6px; font-size: 24px; font-weight: 600; line-height: 28px; color: var(--ink); font-variant-numeric: tabular-nums; }
.dash-assets .avail .v.bad { color: var(--bad); }
.dash-assets .kv { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 9px 0; border-bottom: 1px solid var(--rule); }
.dash-assets .kv .k { font-size: 12px; color: var(--ink); }
.dash-assets .kv .val { display: flex; flex-direction: column; align-items: flex-end; }
.dash-assets .kv .v { font-size: 16px; line-height: 24px; color: var(--ink); font-variant-numeric: tabular-nums; }
.dash-assets .kv .sub { font-size: 12px; line-height: 18px; color: var(--ink-dim); }
.dash-assets .kv .v.bad { color: var(--bad); }
.dash-topup {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--h-ctl);
  margin: 12px 0 4px;
  border: 1px solid var(--brand);
  border-radius: var(--r-ctl);
  background: var(--card);
  color: var(--brand);
  font-size: 13px;
  text-decoration: none;
}
.dash-topup:hover { background: var(--brand-pale); }

/* The outbound list opened from "handed over today" says which day it is. */
.daychip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: var(--h-ctl);
  padding: 0 4px 0 10px;
  border-radius: var(--r-ctl);
  background: var(--brand-wash);
  color: var(--brand);
  font-size: 13px;
  white-space: nowrap;
}
.daychip button { height: 24px; min-width: 24px; padding: 0; border: 0; background: none; color: var(--brand); font-size: 16px; line-height: 1; }
.daychip button:hover { background: none; color: var(--brand-down); }

/* ---- a list screen, in Jifeng's three parts ------------------------------- */

/* ⚠ **Filters, then the list, then how much of it there is.** Jifeng's list
 * screens are three stacked blocks in that order on every one of the hundred-odd
 * of them, which is why somebody can open a screen they have never seen and know
 * where to look. Ours were a bare search box above a bare table.
 *
 * The filter row is its own card because it is a different act from reading the
 * list: you come back to it to change your mind. */
.filterbar {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  flex-wrap: wrap;
  /* 16px all round, as Jifeng's filter band is: its card is 64px tall around a
     32px control, and ours was 58 around the same control. */
  padding: 16px;
}
.filterbar .grow { flex: 1 1 260px; min-width: 180px; }
.filterbar .right { margin-left: auto; display: flex; align-items: center; gap: 8px; }

/* A filter bar and the list it filters are one card, not two.

   ⚠ **Done here rather than in the screens, and that is the point.** Nineteen
   places write `card filterbar` immediately above their list, across five
   shared libraries and thirteen screens. Rewriting all of them to move search
   into the toolbar would have been nineteen chances to get one wrong, and any
   screen missed would be the odd one out -- which is the complaint this is
   answering. Two rules do the whole product at once and no screen's markup
   changes.

   The seam keeps exactly one hairline: the filter bar drops its bottom border
   and bottom corners, the card below keeps its top border, so what was a gap
   between two cards becomes a divider inside one. */
.card.filterbar:has(+ .card) {
  margin-bottom: 0;
  border-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.card.filterbar + .card {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* The list card holds three rows of its own, so it cannot use the plain card's
   padding -- each row pads itself and the table scrolls between them. */
.listcard { padding: 0; }
/* ⚠ **64px, whatever is in it.** That is the measured band -- 16px of padding
   around a 32px control -- and it has to hold even on a tab whose only
   occupants are the 28px reload and column icons `lib/tablekit.js` hangs here.
   Without it a screen whose bulk buttons come and go with the tab moved its
   own table four pixels every time somebody clicked one. */
.listcard .toolbar { padding: 16px; margin: 0; min-height: 64px; }
.listcard .toolbar + .tablewrap table th { border-top: 1px solid var(--line); }
.tablewrap { overflow-x: auto; }

/* ── A list whose columns hold still ─────────────────────────────

   A table sizes its columns to whatever is in them, so a screen with tabs
   re-lays itself on every click -- the reader is not looking at the header,
   they are looking at the column they were reading a second ago, and it has
   moved. Measured on the direct-mail board at 1280 on 2026-09-11: "arrived at"
   sat at x=942 on one tab and x=1149 on the next. Reported, twice, as the
   screen jumping.

   ⚠ **The widths belong to the screen, not to this rule.** A `<colgroup>`
   next to the `<thead>` is where a reader of that screen will look for them;
   all this does is stop the browser having an opinion of its own.

   ⚠ **Fixed layout means cells stop stretching, so they have to be told what
   to do when they overflow.** Without the clipping below, a seller's name
   simply draws over the column beside it -- which is a worse table than the
   one that moved. The first column wraps, because it carries a code and its
   figures on two lines; the rest are single values and end in an ellipsis,
   with the whole string still on the element's title. */
table.fixedcols { table-layout: fixed; }
table.fixedcols th,
table.fixedcols td,
table.fixedcols td .sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ⚠ **Opt in to wrapping, cell by cell.** The default has to be the ellipsis:
   a name that overflows a fixed column does not push it wider any more, it
   draws straight over the column beside it, and two columns of text on top of
   each other is a worse table than the one that moved. So the cells that hold
   something which may legitimately take two lines say so. */
table.fixedcols td.wrap,
table.fixedcols td.wrap .sub { white-space: normal; overflow-wrap: anywhere; }
/* ⚠ **The identifier does not break, the line under it does.** Wrapping the
   whole cell put `AOOS-0609-1520-3221` down four lines a character or two at a
   time, which is not a reference any more; clipping the whole cell loses the
   figures, which is what the row is read for. So the code keeps one line and
   ends in an ellipsis with the whole of it on the title, and the measurements
   below it wrap at their spaces like the sentence they are. */
/* ⚠ **The code wraps at its own hyphens rather than being cut off.** Sizing
   this column to the longest code is a race nobody wins -- 203px covered
   `AOOS-0609-1520-3208-2` and then the cancelled tab produced
   `AOOS-STOCK-20260824-ONAIR12B-3`, and tomorrow there is a longer one. A
   reference on two lines is still a reference; half of one is not, and this
   column is read to match a number against the one printed on a box.
   `word-break: normal` is what keeps it breaking after the hyphens instead of
   mid-token the way `anywhere` did. */
table.fixedcols td.boxcell,
table.fixedcols td.boxcell strong { white-space: normal; overflow-wrap: normal; word-break: normal; }
table.fixedcols td.boxcell .sub { white-space: normal; overflow-wrap: normal; }
/* ⚠ The row's buttons wrap onto a second line rather than being clipped by
   the column. A button cut off at the edge of its cell cannot be pressed, and
   an unreachable button is a worse fault than the table that moved. */
table.fixedcols td.rowacts { white-space: nowrap; overflow: visible; }
table.fixedcols td.rowacts button { margin-left: 4px; }
/* ⚠ Three dots do not need a word's worth of padding either side. At the
   button's usual 15px it pushed the row 4px past the table's right edge --
   enough to give a table that otherwise fits a scrollbar, which reads as the
   layout being 4px wrong rather than as 4px of anything. */
table.fixedcols td.rowacts button.dots { padding-left: 8px; padding-right: 8px; letter-spacing: 1px; }

/* ── A column of tracking numbers ──────────────────────────────

   ⚠ **Proportional digits do not line up, and these are read as a column.**
   `383620641823` over `877102701070` over `1Z16HE400337469451`: in the body
   face a 1 is half the width of a 0, so eighty-three numbers down the screen
   have no shared edge to run an eye along -- which is the whole of what this
   column is for, since nobody reads a tracking number, they compare it to one
   on a box. `tabular-nums` gives every digit the same advance; the letters in
   a UPS number keep their own.

   ⚠ **Not a monospace face.** Switching family would make this one column a
   different typeface from every other on the row, which is louder than the
   problem. This is the same font, told to stop kerning its figures. */
.trackno,
a.trackno { font-variant-numeric: tabular-nums; letter-spacing: .01em; }

/* The rest of a row's actions, stacked in the popover the dots open.
   ⚠ Full width and left-aligned: a column of labels reads as a list of
   things you may do, where a row of them reads as a second toolbar. */
.rowmenu { display: flex; flex-direction: column; gap: 4px; }
.rowmenu button { width: 100%; justify-content: flex-start; text-align: left; }
.listcard .pager {
  /* 48px around a 32px control, as Jifeng's grid footer is. */
  padding: 8px 16px;
  border-top: 1px solid var(--line);
  margin: 0;
  min-height: 48px;
}

/* An icon that repeats an action already on the screen -- refresh, print. Square
   and quiet, on the right of the toolbar where Jifeng keeps them. */
.toolbtn {
  width: var(--h-ctl);
  height: var(--h-ctl);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  color: var(--ink-dim);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  cursor: pointer;
}
.toolbtn:hover { background: #fff; color: var(--brand); border-color: var(--brand); }

/* The bench's own submit button. The scan box is set at 26px for somebody a
   metre away holding a box, so a 32px control beside it reads as a different
   screen's button -- and it is the one thing a phone keyboard can press when
   Enter never arrives (see web/lib/scanbox.js). */
.bench-scan button,
.bench-scan a.button {
  height: auto;
  align-self: stretch;
  padding: 0 20px;
  font-size: 16px;
}

/* The badge above the sign-in card. There is no warehouse to name yet, so it
   stands alone -- see the comment in web/login.html. */
.auth-mark { display: flex; justify-content: center; margin-bottom: 2px; }

/* A field, not a flat grey. The sign-in page is the first thing anybody sees of
   this system and the one screen with nothing on it to look at. */
.auth {
  background: radial-gradient(120% 90% at 50% 0%, #e8eeff 0%, var(--page) 55%, var(--page) 100%);
}

/* The super-admin sign-in sits inside that console's own view element, so its
   centring column must not also inherit the full-height viewport of a page that
   already has a bar above it. */
main .auth { min-height: calc(100vh - 160px); }

/* A card whose rows all ask the same question: the words once across the top,
   the numbers in a grid under them. See the note in web/lib/home.js. */
/* A matrix needs room for a column of names plus its columns of figures. On a
   board of five cards it was squeezed until every heading was one character and
   an ellipsis, and a heading nobody can read is a heading that is not there. So
   a card carrying one takes two columns of the board. */
/* The matrix card carries a row of column headings and needs about twice the
   width to keep them off two lines. Basis, not a hard span: it still shrinks
   to one card's width when the window cannot hold both. */
.home-cards > .home-card:has(> .home-matrix) { flex: 2 1 496px; }

.home-matrix {
  display: grid;
  grid-template-columns: minmax(96px, max-content) repeat(var(--cols, 4), minmax(52px, 1fr));
  align-items: center;
  gap: 2px 4px;
}
.home-matrix .mh {
  font-size: 12px;
  color: var(--ink-dim);
  text-align: right;
  padding: 0 6px 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.home-matrix .mr {
  font-size: 12px;
  color: var(--ink-2);
  text-decoration: none;
  padding: 6px 4px 6px 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
a.mr:hover { color: var(--brand); }
.home-matrix .mc {
  display: block;
  text-align: right;
  text-decoration: none;
  padding: 4px 6px;
  border-radius: var(--r-ctl);
}
.home-matrix .mc:hover { background: var(--brand-pale); }
.home-matrix .mc .v {
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.home-matrix .mc .v.zero { color: var(--ink-faint); font-weight: 500; }
.home-matrix .mc .v.bad { color: var(--bad); }

a.home-rowname:hover { color: var(--brand); }
a.home-rowname { text-decoration: none; }

/* ---- a credential form with more than three boxes in it ------------------- */

/* ⚠ **Twelve inputs in one grid is a wall, not a form.** FedEx wants three keys
 * and an eight-line postal address; flat, they read as one undifferentiated
 * list of things to type, with the two secret ones somewhere in the middle. The
 * module says which fields belong together (`CredentialSpec.group`) and this is
 * what that looks like: a quiet heading, a rule, and the boxes under it. */
.fieldset-head {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-2);
  padding-bottom: 8px;
  margin-top: 18px;
  border-bottom: 1px solid var(--line);
}
.fieldset-head:first-of-type { margin-top: 4px; }
.fieldset { margin-top: 12px; }

/* ── Tracking ────────────────────────────────────────────────────────
   The two halves of a journey on one row: three segments we can fill from
   our own timestamps, a gap, then four the carrier fills. The gap is drawn
   rather than implied -- it is where a parcel goes missing, and an unbroken
   bar hides exactly that. See web/lib/wms-tracking.js. */

.track-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.track-chip {
  font: inherit; text-align: left; cursor: pointer;
  background: var(--card); border: 1px solid var(--line-mid); border-radius: var(--r-card);
  padding: 7px 12px 8px; min-width: 96px;
  display: flex; flex-direction: column; gap: 1px;
  /* ⚠ Buttons inherit white here. Without this the counts are white on white
     and every chip but the red one reads as empty. */
  color: var(--ink);
}
.track-chip:hover { border-color: var(--line-bold); }
.track-chip[aria-pressed="true"] { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.track-chip .n { font-size: 18px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; }
.track-chip .l { font-size: 12px; color: var(--ink-dim); white-space: nowrap; }
.track-chip.bad .n { color: var(--bad); }

.rail { display: inline-flex; align-items: center; gap: 3px; }
.rail i { width: 14px; height: 5px; border-radius: 3px; background: var(--line-mid); display: block; }
.rail i.on { background: var(--brand); }
.rail i.warn { background: var(--warn); }
.rail i.bad { background: var(--bad); }
/* The handover. Not a segment -- nobody scans it, which is the point. */
.rail .railgap { width: 1px; height: 11px; background: var(--line-bold); margin: 0 4px; display: block; }

.track-row { cursor: pointer; }
.track-row:hover { background: var(--brand-pale); }
.track-row.open { background: var(--brand-pale); }
.track-row.open > td { border-bottom-color: transparent; }

/* ── "which one you are looking at", above a filtered list ──────────────────
 *
 * A screen reached by clicking a row on another screen has to say which row,
 * and carry the way back. Drawn tinted rather than as a plain card because it
 * is not part of the list -- it is the reason the list is this short, and a
 * reader who cannot see that reads the filter as a missing-rows bug.
 *
 * ⚠ **Both ways out live here.** "Back to the list you came from" and "drop
 * the filter entirely" are different intentions, and a screen offering only
 * the first makes the second a trip through the menu. */
.ctxbar {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 12px 16px;
  background: var(--brand-pale);
  border: 1px solid #d8e4ff;
  border-radius: var(--r-card);
  margin-bottom: 12px;
}
.ctxbar .who { font-weight: 600; font-size: 14px; }
.ctxbar .bits { color: var(--ink-dim); }
.ctxbar .bits b { color: var(--ink); font-weight: 600; }
.ctxbar .right { margin-left: auto; display: flex; gap: 8px; }

/* The filter is off while the reader searches everything, and the line has to
   say so without changing height -- what it describes is no longer what is in
   the table below it. Same strip, drained of the tint. */
.ctxbar.crossed { background: var(--card); border-color: var(--line); }
.ctxbar.crossed .who,
.ctxbar.crossed .bits b { color: var(--ink-faint); font-weight: 400; }
.track-ref { font-weight: 500; }

.stall { font-size: 12px; color: var(--warn); font-weight: 500; white-space: nowrap; }
.stall.bad { color: var(--bad); }

.track-open > td { background: var(--brand-pale); padding: 0; }
.track-detail {
  display: grid; grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
  gap: 26px; padding: 4px 14px 18px;
}
@media (max-width: 820px) { .track-detail { grid-template-columns: 1fr; gap: 16px; } }
.pane-h {
  font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-faint); margin-bottom: 7px;
}
.track-num { font-size: 14px; font-weight: 500; word-break: break-all; }
.track-eta { font-size: 14px; font-variant-numeric: tabular-nums; }

/* A number in a table that opens something, drawn as the number and not as a
   button. Three screens had written this rule inside their own block; a
   waybill that opens its first-leg scans is the fourth, and a fourth copy is
   where they start to differ. */
.linky {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--brand);
  cursor: pointer;
  text-decoration: underline;
}

.track-tl { list-style: none; margin: 0; padding: 0 0 0 18px; position: relative; }
.track-tl::before {
  content: ""; position: absolute; left: 4px; top: 5px; bottom: 5px;
  width: 1px; background: var(--line-mid);
}
.track-tl li { position: relative; padding-bottom: 11px; }
.track-tl li:last-child { padding-bottom: 0; }
.track-tl li::before {
  content: ""; position: absolute; left: -18px; top: 4px;
  width: 9px; height: 9px; border-radius: 50%; box-sizing: border-box;
}
/* Filled is ours, hollow is theirs. Which side recorded a step is the whole
   question this screen answers, so it is the strongest mark on the line. */
.track-tl li.ours::before { background: var(--brand); }
.track-tl li.theirs::before { background: var(--card); border: 1.5px solid var(--brand); }
.track-tl li.bad::before { background: var(--bad); border-color: var(--bad); }
.track-tl .ev-t { font-size: 13px; }
.track-tl .ev-m { font-size: 11.5px; color: var(--ink-faint); margin-top: 1px; }

/* The seam. Sits between our last step and the carrier's first. */
.track-tl li.gap {
  margin-left: -18px; padding: 6px 10px 6px 18px;
  border-left: 2px dashed var(--warn); background: var(--warn-bg);
  border-radius: 0 var(--r-ctl) var(--r-ctl) 0;
}
.track-tl li.gap::before { display: none; }
.track-tl li.gap .ev-t { color: #d56800; font-weight: 600; }
.track-tl li.gap.bad { border-left-color: var(--bad); background: var(--bad-bg); }
.track-tl li.gap.bad .ev-t { color: #cc3535; }

.status.pickedUp, .status.inTransit { background: var(--brand-wash); color: var(--brand-down); }
.status.outForDelivery { background: var(--warn-bg); color: #d56800; }
.status.delivered { background: var(--ok-bg); color: #008c21; }

/* ---- the pre-advice links, as a list ----------------------------------- */

/* ⚠ **A list of links reads as a list, and this was cards for one afternoon.**
   The table had grown to ten columns and scrolled sideways with half of it off
   the edge, so it was rebuilt as a grid of cards -- and the boss put it back
   the same day. The right fix for a table with too much in it is fewer columns:
   the two date columns nobody acted on are gone, the first leg is one cell
   holding two small boxes, and the PIN shares a cell with its own button.

   Kept as a note because the next person to meet a wide table will reach for
   cards too, and this is the answer that was actually wanted. */

/* The workshop's name, editable where it is read. A plain box in a table cell
   reads as a form; a box with no border until it is aimed at reads as the name,
   which is what it is the rest of the time. */
.line-name {
  width: 100%;
  min-width: 7em;
  height: 28px;
  padding: 0 6px;
  border: 1px solid transparent;
  border-radius: var(--r-ctl);
  background: transparent;
  font: inherit;
  font-weight: 600;
  color: var(--ink);
}
.line-name:hover { border-color: var(--line-mid); background: #fff; }
.line-name:focus { border-color: var(--brand); background: #fff; outline: none; }

/* Every control inside a row is the short one. A 32px control in a 12px table
   row is what made this table tall enough to need scrolling twice. */
td select { height: 32px; font-size: 13px; max-width: 100%; }

/* ⚠ **The forwarder and its service line share one line and clip.**
   Stacked, they made every row in the table twice as tall for a column most
   sellers set once and never look at again -- and this seller's channel names
   are thirty-character sentences, so no width short of half the screen would
   have shown them whole anyway. They are narrow, they clip, and the full text
   is on the tooltip and in the open dropdown, which is where somebody choosing
   between them actually reads it. */
.firstleg { display: flex; gap: 4px; flex-wrap: nowrap; }
.firstleg > select { flex: 1 1 0; min-width: 0; width: 92px; }

/* ---- a batch of SKUs, pasted or dropped in ----------------------------- */

/* ⚠ **Opened by a button and drawn above the fold**, not hidden behind a
   hover and not left at the bottom of a long card. Both were reported: a
   panel whose action sits below the fold reads as a button that does nothing. */
.sku-pad { display: grid; gap: 10px; }
.sku-pad textarea {
  width: 100%;
  min-height: 120px;
  padding: 8px 10px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink);
  resize: vertical;
}
.sku-pad textarea:focus { border-color: var(--brand); outline: none; }

/* What the paste turned into, before anything is written. A count somebody can
   read back is the difference between "apply" and "hope". */
.sku-tally { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; font-size: 12px; }
.sku-tally b { font-variant-numeric: tabular-nums; }
.sku-tally .miss { color: #cc3535; }

/* The checkbox column on a list that can be acted on in bulk. */
th.pick, td.pick { width: 34px; padding-right: 0; }
td.pick input, th.pick input { width: 14px; height: 14px; margin: 0; cursor: pointer; accent-color: var(--brand); }

/* ⚠ The bar that appears when rows are selected. It replaces the toolbar's
   contents rather than pushing them down: a bar that grows when you tick a box
   moves the row you were about to tick next. */
.picked {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 12px;
  margin-bottom: 12px;
  border: 1px solid var(--brand);
  background: var(--brand-pale);
  border-radius: var(--r-ctl);
  font-size: 12px;
}
.picked .n { font-weight: 600; color: var(--brand-down); }
/* The bar's controls wrap; a group inside it does not.
   ⚠ The right-hand side held one label, one select and two buttons and had no
   wrap on it. A second group -- the line, added 2026-08-28 -- runs it past the
   edge at 1280 and the last control leaves the screen without a scrollbar to
   say so. Wrapping the groups fixes that; keeping each group unwrapped is the
   other half, because a select that has wrapped away from its own label is a
   control nobody can name. */
.picked .right { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
.picked .right .grp { display: flex; gap: 8px; align-items: center; }

/* ── First-leg cost: two prices on one screen ─────────────────────────
   ⚠ The whole visual job here is keeping THEIR number and OURS apart. A
   seller looking at a quote has to know which half to take to the forwarder
   and which half is this system's arithmetic off their own settings. So the
   base and the additions are separate columns, the landed total is the only
   one given weight, and every added line shows the sum it came from. */

.fc-form { align-items: flex-end; gap: 10px; }
.fc-form > div { flex: 0 0 auto; }
.fc-form > div.grow { flex: 1 1 200px; min-width: 160px; }
.fc-form > div.go { margin-left: auto; }
/* ⚠ Typed as `input.fc-w`, not `.fc-w`. The base sheet sets
   input[type=number] to width:100%, which is (0,1,1) -- a bare class is
   (0,1,0) and loses, so the weight and box-count fields silently ignored their
   width while the text fields obeyed it. */
input.fc-w { width: 92px; }
.fc-dims { display: flex; gap: 4px; }
.fc-dims input { width: 58px; text-align: center; }
.fc-check { display: flex; align-items: center; gap: 6px; height: var(--h-ctl); white-space: nowrap; font-size: 13px; }
.fc-check input { width: auto; height: auto; margin: 0; }

/* The money columns. Base is plain, ours is dimmed until it matters, landed
   carries the weight -- reading down the landed column is the whole task. */
/* ⚠ Money never wraps. "2616.00" over one line and "CNY" over the next is
   two numbers as far as anybody scanning the column is concerned. The table
   scrolls inside its card instead -- .card:has(> table) already does that. */
td.fc-base, td.fc-add, td.fc-landed { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
td.fc-add { color: var(--ink-dim); }
td.fc-landed { font-weight: 600; font-size: 13px; }
table.fc-quotes { min-width: 900px; }
table.fc-quotes td:first-child { min-width: 210px; }
.fc-lines { margin-top: 3px; font-size: 11px; color: var(--ink-faint); line-height: 1.5; white-space: nowrap; }
.fc-lines .f { opacity: .72; }

/* How a weight was arrived at, under the weight itself. Shown always, not on
   hover: a chargeable weight nobody can check is a number nobody believes. */
.fc-weighing { margin-top: 3px; font-size: 11px; color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A service picker, and the half of it that CSS can do.
   ⚠ **The money in these dropdowns is padded into a column by hand**, in
   `lib/rate-pick.js`, because an <option> holds text and nothing else. Padding
   only lines up in a font whose characters are all one width, so this class is
   the other half of that fix and has to go on the select -- Chrome and Safari
   draw the popup in the select's own font, and Firefox needs the rule on the
   option too. */
select.rate-pick,
select.rate-pick option {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}

.fc-lane { font-weight: 500; }
.fc-lane .meta { margin-top: 3px; font-size: 11px; color: var(--ink-faint); font-weight: 400; }
.fc-cheapest td { background: var(--brand-pale); }
.fc-cheapest td.fc-landed { color: var(--brand-down); }

/* Settings: 194 service lines, most of them on a fallback nobody chose. The
   set ones are marked so the unset majority is visible as a fact. */
.fr-head { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 12px; }
.fr-head .right { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.fr-count { font-size: 12px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
tr.fr-set td { background: var(--brand-wash); }
tr.fr-set td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
.fr-fallback { color: var(--ink-faint); font-size: 11px; }
td.fr-ctl select, td.fr-ctl input { height: 28px; font-size: 12px; }
td.fr-ctl input { width: 72px; text-align: right; }
.fr-bulk { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; }
.fr-bulk > div { flex: 0 0 auto; }

/* Five small money fields, not five columns. Left to the generic .row they
   each claim 190px and wrap onto a second line, which reads as five unrelated
   settings rather than one short list. */
.fr-money > div { flex: 0 0 auto; }
.fr-money input { width: 108px; }
.fr-money label { white-space: nowrap; }

/* Where a lane's rule came from. Read off its own name, off its family in the
   rate sheet, set by hand -- or nothing said it, which is the only state that
   still needs a person and is the only one coloured. */
.fr-fallback[data-src="default"] { color: var(--warn); }
.fr-fallback[data-src="set"] { color: var(--brand-down); }

/* ── Quick Links ─────────────────────────────────────────────────────
 *
 * ⚠ **A sitemap, and nothing cleverer than that.** This screen has been drawn
 * three ways in one day. The first put four controls on every row, which made
 * a list nobody could scan. The second made every link a tile with a letter in
 * a square, and grew a rail of categories down the left -- so the page had two
 * trees on it, the menu's and its own, side by side. The boss's word for both
 * was ugly, and the instruction was: like a sitemap, every one of them a
 * hyperlink.
 *
 * So: a heading, and under it the links as text, wrapped. No rail, no tiles,
 * no icons, no counts. It is the densest possible way to show four hundred
 * addresses, and the only shape where the thing on the screen is the link
 * itself rather than a container holding one. */
.q-bar { display: flex; gap: 10px; align-items: center; margin-bottom: 14px; }
.q-bar input { max-width: 340px; }

.q-map { padding: 4px 16px 16px; }
.q-map section { padding-top: 14px; }
.q-map section + section { border-top: 1px solid var(--line); }

/* The same heading as the workbench's cards, down to the 3px rule -- this is
   the one part of the old draft that was already the product's own. */
.q-map h3 {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  font-weight: 600;
  margin: 0 0 10px;
  color: var(--ink);
}
.q-map h3::before {
  content: "";
  width: 3px;
  height: 13px;
  border-radius: 1px;
  background: var(--brand);
  flex: 0 0 auto;
}

/* ⚠ Row gap smaller than the column gap on purpose: what separates two links
   is the space beside them, and a wrapped list with equal gaps reads as a
   grid of unrelated cells rather than as a paragraph of links. */
.q-links { display: flex; flex-wrap: wrap; gap: 7px 22px; }
.q-item { display: inline-flex; align-items: baseline; gap: 5px; font-size: 13px; max-width: 100%; }
.q-item a { color: var(--brand); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-item a:hover { color: var(--brand-down); text-decoration: underline; }
.q-item em { font-style: normal; font-size: 11px; color: var(--ink-faint); white-space: nowrap; }

/* ---- shelf count (web/pages/count-scan.js) ------------------------------ */

/* A phone held in one hand in an aisle. Everything here is sized for a thumb
   rather than a mouse, and the page it sits on is the console's -- so the one
   thing this block must not do is restyle anything outside `.cs`.

   ⚠ **16px is the floor for every input on this screen, and it is not a
   preference.** Mobile Safari zooms the whole page in when a focused field is
   smaller than that, and it does not zoom back out on blur. One tap into a
   quantity box and the shelf list is half off the right edge for the rest of
   the aisle. 13px is the console's body size everywhere else and it stops at
   this block's edge. */

.cs { max-width: 720px; }

.cs-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.cs-ref {
  font-size: 20px;
  font-weight: 600;
  color: var(--brand);
  margin-right: auto;
  /* The shelf code is read off a rack label a metre away and compared with
     what is on the screen. Tabular figures so 8 and 3 cannot be confused at
     arm's length, and no ellipsis: a truncated shelf code is a wrong one. */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.5px;
}

/* Which stocktake this is going into. Quiet on purpose: it is chosen once and
   then it is context, while the shelf below it changes every few minutes. Two
   equally loud headings would have somebody reading the document reference as
   the shelf they are standing at. */
.cs-doc {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ink-dim);
  margin-bottom: 8px;
}
.cs-doc span { font-weight: 600; color: var(--ink-2); }

/* The list's heading, with the two print buttons on it. ⚠ They sit here
   rather than up in the shelf bar because what they print is this list -- a
   print button at the top of a screen prints "the screen", and this screen has
   a form on it that is not on the paper. */
.cs-shelfhead {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}
.cs-shelfhead > div:first-child { flex: 1 1 200px; min-width: 0; }
.cs-shelfhead .cs-label { margin-bottom: 2px; }
.cs-prints { display: flex; gap: 6px; flex: 0 0 auto; }
.cs-prints button { height: 36px; padding: 0 12px; font-size: 13px; }

.cs-block {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 14px;
  margin-bottom: 12px;
}

.cs-label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-2);
  margin-bottom: 6px;
}
.cs-block .sub { font-size: 13px; color: var(--ink-dim); margin: 0 0 10px; }

.cs-scan,
.cs-select {
  width: 100%;
  height: 44px;              /* a finger, not a pointer */
  font-size: 16px;           /* see the note above -- not negotiable on iOS */
  padding: 0 12px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  background: var(--card);
  color: var(--ink);
  margin-bottom: 10px;
}
.cs-scan:focus,
.cs-select:focus { outline: 2px solid var(--brand-wash); border-color: var(--brand); }

/* Choosing a stocktake, or a shelf out of the filtered list. A whole row is
   the target because the thing being pressed is a decision, not a word. */
.cs-take {
  display: block;
  width: 100%;
  text-align: left;
  min-height: 48px;
  padding: 10px 12px;
  margin-bottom: 8px;
  background: var(--card);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}
.cs-take:hover { border-color: var(--brand); background: var(--brand-pale); }
.cs-take strong { display: block; font-size: 16px; }
.cs-take .sub { margin: 0; font-size: 13px; }

/* One row per SKU on the shelf. The eye travels down the rack, so the form
   does too -- see the head of count-scan.js for why this is not one card at a
   time the way the measuring sheet is. */
.cs-rows { list-style: none; margin: 0; padding: 0; }

.cs-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 0;
  border-bottom: 1px solid var(--rule);
}
.cs-row:last-child { border-bottom: 0; }
.cs-row-what { flex: 1 1 180px; min-width: 0; }
.cs-row-what strong { display: block; font-size: 15px; }
.cs-row-what .sub { margin: 0; font-size: 13px; }
.cs-row-in { display: flex; gap: 8px; flex: 0 0 auto; }
.cs-row-in .cs-qty {
  width: 92px;
  height: 44px;
  font-size: 16px;
  text-align: center;
  padding: 0 8px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  margin: 0;
}
.cs-row-in button { height: 44px; padding: 0 16px; font-size: 15px; }

/* ⚠ Green means a row was written, and it is the *only* thing on this screen
   that reports a figure back. It is this person's own count, never the
   ledger's -- showing what the system expects would turn counting into
   confirming, which is the one thing a stocktake must not become. */
.cs-row.done { background: var(--ok-bg); }
.cs-saved {
  display: inline-block;
  margin-top: 2px;
  font-size: 13px;
  color: #008c21;
  font-weight: 600;
}
/* Where a scan just landed. A tint and a rule down the edge, because on a
   handheld the row has to be findable in the half second after the beep. */
.cs-row.hit { background: var(--brand-wash); box-shadow: inset 3px 0 0 var(--brand); }

/* ---- the shelf sheet, which exists to be printed ------------------------ */

/* ⚠ **No quantities on it, ever.** Taped to a rack it is read for months:
   the shelf code, the SKU and the barcode are still true in March, and a
   quantity is true until the next pick. One line of a posted document going
   quietly false is worse than no document, because a reader cannot tell which
   line it was. `stock.shelvesWith` refuses to return them at all. */
.shelf-sheet { display: none; }

/* ⚠ **The sheet is moved to be a child of <body> before printing**, by
   printShelf() in count-scan.js, and this hides its siblings while it is
   there. The first version left it in place and lifted it out with
   `position: absolute` under `body * { visibility: hidden }` -- which makes
   the printed page depend on the containing block of whatever wraps it, and a
   printer is the one output nobody sees going wrong until the paper is in
   their hand. Now its layout depends on the page box and nothing else. */
body.printing > *:not(.shelf-sheet) { display: none !important; }
body.printing .shelf-sheet { display: block; }

@media print {
  /* Belt and braces: if a print is started from the browser's own menu rather
     than the button, there is no `printing` class and nothing has been moved.
     Hiding everything but the sheets still puts the right thing on paper --
     it is the layout that is less certain, not the content. */
  body:not(.printing) > *:not(.shelf-sheet) { display: none !important; }
  body:not(.printing) .shelf-sheet { display: block; }

  .shelf-sheet { color: #000; padding: 0; }
  .shelf-sheet-code {
    font-size: 46pt;
    font-weight: 700;
    letter-spacing: 2px;
    font-variant-numeric: tabular-nums;
    border-bottom: 3px solid #000;
    padding-bottom: 6pt;
    margin-bottom: 10pt;
  }
  .shelf-sheet-rows { width: 100%; border-collapse: collapse; font-size: 11pt; }
  .shelf-sheet-rows th {
    text-align: left;
    border-bottom: 1px solid #000;
    padding: 4pt 6pt;
    font-size: 9pt;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }
  .shelf-sheet-rows td { padding: 6pt; border-bottom: 1px solid #bbb; vertical-align: top; }
  .shelf-sheet-rows .sub { font-size: 8.5pt; color: #444; }
  /* A shelf list runs onto a second page on a full rack, and a row split down
     the middle is a row somebody reads as two. */
  .shelf-sheet-rows tr { page-break-inside: avoid; }
  .shelf-sheet-foot { margin-top: 10pt; font-size: 9pt; color: #444; }

  /* ---- 4 x 6, on the thermal printer ----------------------------------

     ⚠ **Not the letter sheet scaled down.** A quarter of the paper, so what
     earns its space is different: the shelf code has to be readable from the
     aisle it is stuck in, and the description and seller columns are what go.
     A SKU and a barcode identify the row; a description only reassures
     somebody who has already found it.

     ⚠ **The page box is set in count-scan.js, not here.** `@page` cannot be
     qualified by a body class, so one stylesheet cannot carry both sizes. */
  body.printing-label .shelf-sheet-code {
    font-size: 30pt;
    letter-spacing: 1px;
    border-bottom-width: 2px;
    padding-bottom: 4pt;
    margin-bottom: 6pt;
  }
  body.printing-label .shelf-sheet-rows { font-size: 9pt; }
  body.printing-label .shelf-sheet-rows th { font-size: 7pt; padding: 2pt 3pt; }
  body.printing-label .shelf-sheet-rows td { padding: 3pt; }
  body.printing-label .shelf-sheet-rows .sub { font-size: 7.5pt; }
  /* Dropped rather than squeezed: a column three characters wide misleads. */
  body.printing-label .shelf-sheet-rows th:nth-child(2),
  body.printing-label .shelf-sheet-rows td:nth-child(2),
  body.printing-label .shelf-sheet-rows th:nth-child(3),
  body.printing-label .shelf-sheet-rows td:nth-child(3) { display: none; }
  /* ⚠ The footer stays: it is the line saying there are no quantities here,
     which is the one thing a reader could otherwise take for an oversight. */
  body.printing-label .shelf-sheet-foot { margin-top: 6pt; font-size: 7pt; }
}

/* ---- the round: many shelves, walked top to bottom --------------------- */

/* One aisle per screen. See the head of count-scan.js for why the unit is the
   round and not the shelf. */
.cs-aisles { display: flex; flex-wrap: wrap; gap: 8px; }
.cs-aisle {
  min-height: 44px;
  padding: 0 16px;
  font-size: 16px;
  background: var(--card);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  color: var(--ink);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.cs-aisle:hover { border-color: var(--brand); background: var(--brand-pale); }

.cs-shelf {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: 12px 14px;
  margin-bottom: 12px;
}
.cs-shelf h2 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: var(--brand);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.5px;
}

/* ⚠ Where the walker is up to, and it counts rows *on this round* -- it
   answers "where was I", not "is the stocktake done". The second is the desk
   screen's question and needs the whole document. */
.cs-progress {
  margin-left: auto;
  font-size: 13px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
.cs-progress.all { color: #008c21; font-weight: 600; }

/* The way in to adding a row that the ledger does not have. Quiet until it is
   wanted: on a round it would otherwise repeat under every shelf. */
.cs-morelink {
  background: none;
  border: 0;
  padding: 6px 0 0;
  font: inherit;
  font-size: 13px;
  color: var(--brand);
  cursor: pointer;
  text-decoration: underline;
}
.cs-extra { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--line-mid); }
.cs-extra .sub { margin-bottom: 8px; }
.cs-findbox { position: sticky; top: 46px; z-index: 5; }

/* ---- the console on a phone -------------------------------------------- */

/* ⚠ **Reachable, not redesigned.** The console is a desk product and this is
   not an attempt to make 128 table screens work on a 390px screen. It is the
   narrow case for the shell itself, because a screen built for an aisle that
   cannot be navigated to from an aisle is not built.

   The rail is what goes: 218px of second-level menu out of 390px leaves a
   column too narrow to hold a shelf row, and every entry it draws is also in
   the bar above it -- so nothing becomes unreachable by dropping it. */
@media (max-width: 560px) {
  .side { display: none; }
  main { padding: 10px; }

  /* ⚠ **The bar stays one 40px row and scrolls sideways.** Wrapping onto a
     second line is the obvious fix and it breaks the menu: the panels hang off
     `top: 40px`, measured against a bar that is 40px tall, so a bar that is
     sometimes 80px opens every submenu across the page content instead of
     under its own heading.

     ⚠ **Every entry needs `flex: 0 0 auto`, and this is the second time
     that has been true.** A flex child gives way by default, so at 375px the
     six section headings were squeezed into 32px between them -- a strip of
     clipped characters where the menu should be, unreadable and unpressable,
     with nothing overflowing for the scroll to catch. The rail down the left
     had the identical bug and was reported as "the words are garbage"; it is a
     layout failure both times, not an encoding one. The bar scrolls; it does
     not compress. */
  .sections {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .sections::-webkit-scrollbar { display: none; }
  .section { flex: 0 0 auto; }
  .section-head { padding: 0 10px; }

  /* What gives way instead, in the order the desktop breakpoints already
     established: the decorative first, the way out never.

     The warehouse's own name goes. At this width it had 48px and was drawing
     four clipped letters of "Meirun Overseas Warehouse" -- a brand rendered as
     debris says less than no brand at all, and the same name is on the sign-in
     page and in the settings screen. */
  .brand { display: none; }
  .account { display: none; }
  /* The corner icons go too, and for the same reason the rail does: they are a
     second *placement* of entries that are already on the bar, so dropping them
     makes nothing unreachable -- while the bar they are crowding is the only
     way to anything at all. At 375px they were 105px of shortcut sitting next
     to 116px of menu. ⚠ On a desktop they stay: there the bar has room, and
     the settings door being one press away is why they exist. */
  .iconbtn { display: none; }
  /* ⚠ The switcher and the sign-out button stay, at every width. They are how
     somebody who is stuck gets unstuck -- somebody who cannot read the screen
     they are on, and somebody on a shared handheld who has to hand it over.
     The switcher only gets narrower. */
  .lang { max-width: 4.5rem; }
}

/* ---- loose goods intake (web/pages/in-loose.js) ------------------------- */

/* One seller, one pallet, a few hundred items. The seller is named once at the
   top and then it is context -- see the head of that file for why that is a
   default and not a guess. */
.lo-who {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  padding: 8px 0 2px;
}
.lo-who strong { font-size: 18px; color: var(--brand); }
.lo-who .sub { margin: 0; }

/* ⚠ The loose-or-return targets stood here. The choice is made by which menu
   entry was opened (see web/pages/in-loose-return.js), so there is no control
   left on the screen to style. */

.lo-go-row { display: flex; gap: 8px; align-items: stretch; }
.lo-qty { width: 110px; flex: 0 0 auto; margin: 0; }
.lo-go { flex: 1 1 auto; height: 44px; font-size: 16px; }
.cs-pending .lo-go { width: 100%; margin-bottom: 8px; }

/* ⚠ The number is the loudest thing on the screen, because the next thing
   that happens is somebody writing it on the goods -- and it names what it
   belongs to, since a bare number has to be held in the head while a pen is
   found. Same reasoning as the receiving bench. */
.msg.lo-number { display: flex; flex-direction: column; gap: 2px; padding: 12px; }
.msg.lo-number strong { font-size: 30px; font-weight: 700; letter-spacing: 1px; font-variant-numeric: tabular-nums; }
.msg.lo-number span { font-size: 14px; }

.lo-round { width: 100%; border-collapse: collapse; font-size: 14px; }
.lo-round td { padding: 8px 6px; border-bottom: 1px solid var(--rule); vertical-align: middle; }
.lo-round td:first-child { color: var(--ink-dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
.lo-round td:nth-child(3) { text-align: right; font-variant-numeric: tabular-nums; }
.lo-round td:last-child { text-align: right; white-space: nowrap; }
/* Said once at the end of a sitting rather than per item: worth knowing, not
   worth interrupting a hundred times. */
.lo-filed { margin: 10px 0 0; font-size: 13px; }
/* The shelf being filled, chosen once beside the seller. */
.lo-shelflabel { margin-top: 12px; }

/* ---- catalogue and stock lists ----------------------------------------- */

/* ── ⚠ Measured off Jifeng's own pixels, 2026-09-02 ───────────────────
 *
 * The first attempt at these screens had the right columns in the right order
 * and the boss still threw it back as rubbish. It was not the columns. Their
 * list is nearly
 * two and a half times as tall per row as ours was, and the difference is
 * entirely in three things: a 64px tile, 22px line pitch in the product block,
 * and cells that breathe. Read out of `docs/jifeng-shots/inventory` with PIL
 * rather than guessed:
 *
 *   row rules            #EBEEF5, at y = 382 / 502 / 622 / 743  ->  row 120px
 *   product tile         66 x 66 including its border, 19px below the rule
 *   product block lines  4 of them, pitch 22px
 *   text                 #1D2129; a zero is the SAME colour as any other digit
 *   numeric header       #F7F8FA ground, sort arrows a stacked PAIR
 *   catalogue rows       80-94px, tile 48px
 *
 * `table.jf` is opt-in rather than a change to the global grid. It was written
 * when the grid above was 12px and 37px rows; that grid has since been measured
 * against the same source and is 13px on 44px rows. This one stays separate
 * because a catalogue row holds a picture and four lines and is 120px on
 * Jifeng, which is a different measurement rather than a tighter one.
 */

table.jf th,
table.jf td { padding: 14px 16px; font-size: 13px; vertical-align: top; }
table.jf th {
  background: #f7f8fa;
  color: var(--ink-2);
  font-weight: 500;
  padding-top: 11px;
  padding-bottom: 11px;
  vertical-align: middle;
}
/* Values sit on the first line of a tall cell, level with the tile beside them. */
table.jf td { line-height: 22px; }
/* ⚠ **A zero is not quieter than any other figure here.** Jifeng prints every
   digit in one colour and the eye reads down the column; greying the zeros
   turned a column of figures into a column of holes. `.zero` still means what
   it means everywhere else in the product. */
table.jf .zero { color: inherit; }

/* One line, and an ellipsis rather than a wrap. A table where one long product
   name makes its row three times as tall is the ragged edge that got this
   screen thrown back -- Jifeng lets the table scroll sideways instead, and so
   do we. */
table.jf td.one { white-space: nowrap; max-width: 260px; overflow: hidden; text-overflow: ellipsis; }

/* ── The product block ─────────────────────────────────────────────── */

/* ⚠ **Not `.lines`.** That class already exists in this stylesheet as a wrapping
   flex row for checkbox groups, and reusing the name laid the four lines of the
   product block out side by side -- which is exactly the run-together mess this
   block was rewritten to fix. Caught in the browser on 2026-09-02. */
.prodcell { display: flex; gap: 12px; align-items: flex-start; }
.prodcell .prodlines { display: block; min-width: 0; }
.prodcell .prodlines > div { display: flex; gap: 6px; line-height: 22px; }
/* The four labels share a column so the values line up under each other. A
   ragged left edge on four short labels is most of why a block like this reads
   as noise. */
.prodcell .prodlines .k { flex: 0 0 40px; color: var(--ink-dim); }
.prodcell .prodlines .v { min-width: 0; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* No product photographs anywhere in this system, so the tile is a tile. It is
   drawn rather than left out because the row's height and the reader's
   starting point both come from it. */
.thumb {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border: 1px solid var(--line);
  border-radius: var(--r-ctl);
  background: #f4f4f6;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #c9ccd6;
}
.thumb.sm { width: 48px; height: 48px; }

/* ── A head that sorts ─────────────────────────────────────────────── */

/* Two arrows, not one: the pair says the column can go either way and which
   way it is going, which a single caret cannot. Drawn with borders because a
   glyph would be a character in somebody's language. */
th.sortable { position: relative; cursor: pointer; user-select: none; padding-right: 28px; }
th.sortable::before,
th.sortable::after {
  content: "";
  position: absolute;
  right: 12px;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
}
th.sortable::before { top: calc(50% - 8px); border-bottom: 5px solid var(--line-bold); }
th.sortable::after { top: calc(50% + 3px); border-top: 5px solid var(--line-bold); }
th.sortable.asc::before { border-bottom-color: var(--brand); }
th.sortable.desc::after { border-top-color: var(--brand); }

/* ── The search group ──────────────────────────────────────────────── */

/* ⚠ **One control, not four.** Jifeng's is a segmented strip -- which field,
   what to look for, how to match, go -- and reading it takes one glance
   because it is one object. Four boxes with gaps between them is four
   decisions, and that is what ours looked like. */
.searchgroup { display: flex; align-items: stretch; flex: 0 1 auto; }
.searchgroup > select,
.searchgroup > input,
.searchgroup > button {
  height: var(--h-ctl);
  border: 1px solid var(--line-mid);
  border-radius: 0;
  margin-left: -1px;
}
.searchgroup > :first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); margin-left: 0; }
.searchgroup > :last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }
.searchgroup > select { flex: 0 0 auto; width: auto; background: #fff; color: var(--ink); }
.searchgroup > input { flex: 1 1 240px; min-width: 150px; }
.searchgroup > *:focus { position: relative; z-index: 2; }
.searchgroup > .go {
  flex: 0 0 auto;
  width: 38px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  color: var(--ink-dim);
}
.searchgroup > .go:hover { background: #fff; color: var(--brand); border-color: var(--brand); }

/* ── Toolbar furniture ─────────────────────────────────────────────── */

/* The quiet icons at the right of a list toolbar. A button, so it is reachable
   from the keyboard, drawn as an icon because that is what it is. */
.iconact {
  /* Toolbar band, so 32 like everything else standing in it. */
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-ctl);
  color: var(--ink-dim);
}
.iconact:hover { background: var(--brand-pale); border-color: transparent; color: var(--brand); }
.iconact:active { background: var(--brand-wash); border-color: transparent; }

/* A tick that sits in a row of controls rather than over a field. */
.tick { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-size: 13px; color: var(--ink-2); white-space: nowrap; }
.tick input { margin: 0; }

/* Two toolbar rows, as Jifeng stacks them: what is being shown, then what can
   be done to it. The first carries no buttons on most screens, so it collapses
   to its own height rather than leaving a gap. */
.listcard .toolbar.thin { padding-bottom: 0; min-height: 0; }

/* ⚠ **The totals line is part of the table, not a sentence under it.** It is
   pinned, so it survives scrolling a long list -- the first thing anybody
   checking a stock figure reads. */
tfoot.sum td {
  position: sticky;
  bottom: 0;
  background: #f7f8fa;
  border-top: 1px solid var(--line);
  font-weight: 600;
  color: var(--ink);
  vertical-align: middle;
}

/* The page buttons in a list footer: square, quiet, disabled at the ends.
   28px, which is what Jifeng's own footer controls measure -- its filter band
   is 32 and its footer is 28, and the two sizes are the two jobs. */
.pagebtn {
  height: 28px;
  min-width: 28px;
  padding: 0 8px;
  background: #fff;
  color: var(--ink-2);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-ctl);
  font-size: 12px;
}
.pagebtn:hover:not(:disabled) { background: #fff; color: var(--brand); border-color: var(--brand); }
.pagebtn:disabled { color: var(--ink-faint); background: var(--page); border-color: var(--line); cursor: default; }
.pager select { height: 32px; font-size: 13px; }

/* The unit switch Jifeng puts at the right of a catalogue toolbar. It changes
   what is printed and never what is stored -- see server/src/units.ts. */
.unitswitch { display: inline-flex; }
.unitswitch button {
  height: 28px;
  padding: 0 11px;
  font-size: 12px;
  background: #fff;
  color: var(--ink-2);
  border: 1px solid var(--line-mid);
  border-radius: 0;
  margin-left: -1px;
}
.unitswitch button:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); margin-left: 0; }
.unitswitch button:last-child { border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }
.unitswitch button:hover { background: #fff; color: var(--brand); border-color: var(--brand); }
.unitswitch button.on { background: var(--brand-wash); color: var(--brand); border-color: var(--brand); position: relative; z-index: 1; }

/* ⚠ **An opened catalogue row.** Amazon puts the quantity on the listing row
   and stops there, because a seller SKU has one number; ours has the same SKU
   in several buildings and in two ledgers, so the row carries the sum and this
   carries the split. See web/oms/oms-product-list.js. */
.opener {
  width: 20px; height: 20px; padding: 0; margin-right: 6px;
  font-size: 13px; line-height: 1; vertical-align: middle;
}
tr.opened > td { background: var(--wash, #f7f9fc); }
table.jf.inner { margin: 0; box-shadow: none; }
table.jf.inner th, table.jf.inner td { padding: 6px 10px; font-size: 12px; }
table.jf.inner thead th { background: transparent; }

/* ⚠ **The last column stays put while the table scrolls sideways.**
   Jifeng pins their action column and we did not, so on a 15-column catalogue
   the Edit button sat off the right edge -- present, reachable, and invisible
   until somebody scrolled looking for something they did not know was there.
   Opt-in, because a table whose last column is data rather than buttons would
   be worse pinned than not. */
table.jf.pinlast th:last-child,
table.jf.pinlast td:last-child {
  position: sticky;
  right: 0;
  background: #fff;
  box-shadow: -8px 0 8px -8px rgb(15 23 42 / 18%);
}
table.jf.pinlast thead th:last-child { background: #f7f8fa; z-index: 2; }
table.jf.pinlast tbody td:last-child { z-index: 1; }
/* ⚠ An opened row is two cells wide, so its last one is the whole panel --
   pinning that would stick the panel to the right edge and slide the figures
   out from under it. */
table.jf.pinlast tr.opened > td:last-child { position: static; box-shadow: none; background: transparent; }

/* A figure that has crossed its own line. Amber, not red: being low is a thing
   to act on, not a thing that has gone wrong. */
table.jf .warn { color: #a35f07; }


/* A modal, and the first one in this system.
 *
 * ⚠ Added for "add a carrier account", which is a two-step choice -- pick the
 * carrier, then type its keys -- and every other form on these screens opens a
 * panel *under* the table instead. That panel is what `reveal()` in
 * carriers-online.js exists to scroll to, because on a warehouse with fifteen
 * channels it draws a screenful below the fold and the button reads as dead.
 * A dialog cannot land below the fold.
 *
 * ⚠ **Every number here was measured on the thing being copied**, on
 * 2026-09-06, the same way docs/jifeng-ui-tokens.md was taken -- so 720 and 12
 * and 20 are their values and not a guess that looks about right. Two of them
 * are worth naming because the obvious choice is the wrong one: the dialog has
 * **no rule under its title and none above its buttons**, and a carrier card
 * is outlined with `outline` rather than `border`, which is what lets the
 * selected state go to 2px without every card in the grid moving.
 *
 * ⚠ The backdrop closes it and so does Escape; both are wired in the page,
 * because a dialog dismissable only by a button somebody has to find is a trap
 * on a narrow screen. */
/* ⚠ **One dialog in the product, and this is it.** The carrier screens grew
 * their own on 2026-09-06 -- the first in the system -- and `web/lib/modal.js`
 * arrived on 2026-09-08 for everything else. Two sets of rules under the same
 * class names is how two dialogs start looking different, so the two markups
 * are aliased onto one set here: `.modal-back` / `.modal-mask` are the same
 * overlay, `.modal-x` / `.modal-close` the same way out.
 *
 * The numbers are Jifeng's, re-measured on 2026-09-08 on its home page's
 * customise dialog: 48px head, 16px/700 title, body padding 16px 20px 0,
 * footer 16px 20px with 12px between two 32px buttons, radius 12, overlay
 * rgba(0,0,0,.5). No rule under the title and none above the buttons. */
.modal-back,
.modal-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  /* High enough that the title lands where the eye already is, low enough that
     a tall dialog still has room under it: Jifeng's 719px dialog opened at
     y=87 in a 934px window. */
  padding: 80px 16px 24px;
  overflow: auto;
  z-index: 2012;
}
.modal {
  background: var(--card);
  border-radius: 12px;
  /* ⚠ 720 is the default because the carrier dialogs were built at it. A
     caller that wants another width sets `--modal-w`; nothing sets a width in
     its own markup. */
  width: min(92vw, var(--modal-w, 720px));
  max-height: calc(100vh - 120px);
  display: flex;
  flex-direction: column;
  box-shadow: 0 12px 32px 4px rgba(0, 0, 0, 0.04), 0 8px 20px 0 rgba(0, 0, 0, 0.08);
  outline: 0;
}
.modal-head {
  min-height: 48px;
  flex: 0 0 auto;
  padding: 0 20px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.modal-head h3,
.modal-title { margin: 0; font-size: 16px; font-weight: 700; color: var(--ink); }
/* 48 square, in the corner, and the only control in the product with no border
   and no fill -- Jifeng's is the same, and a bordered box in a corner reads as
   a second action rather than a way out. */
.modal-x,
.modal-close {
  margin-left: auto;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 0;
  background: none;
  border-radius: var(--r-ctl);
  color: var(--ink-dim);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.modal-x:hover,
.modal-close:hover { background: none; color: var(--brand); }
/* ⚠ Scrolls on its own, so a long credential form -- FedEx asks for fourteen
 * fields -- never pushes the buttons off the bottom of the screen. */
.modal-body { padding: 16px 20px 0; overflow: auto; flex: 1 1 auto; }
/* A dialog's last card has nothing under it, so its 16px bottom margin is a
   gap above the footer's own 16px -- 32px of nothing, which reads as an
   unfinished panel. */
.modal-body > :last-child { margin-bottom: 0; }
.modal-body .card { box-shadow: none; }
.modal-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  padding: 16px 20px;
}
/* An action that is not one of the two answers -- "reset to the default
   layout" -- sits at the far left of the footer, as Jifeng's does. */
.modal-foot .away { margin-right: auto; }
.modal-foot button { min-width: 80px; }
/* A sentence, not a form: the confirm dialog's one line of body. */
.modal-ask { margin: 4px 0 8px; color: var(--ink-2); }
/* The one destructive answer. Nothing else in the product is this colour,
   which is the point of it. */
button.danger { background: var(--bad); border-color: var(--bad); }
button.danger:hover { background: #f76560; border-color: #f76560; }
button.danger:active { background: #cb2634; border-color: #cb2634; }
/* The page behind an open dialog must not scroll: a wheel over the overlay that
   moves the list is how somebody loses the row they opened. */
body.modal-open { overflow: hidden; }

/* The two-step rail across the top of the dialog. */
.steps { display: flex; align-items: center; justify-content: center; gap: 12px; padding-bottom: 16px; }
.steps .step { display: flex; align-items: center; gap: 8px; color: var(--ink-dim); font-size: 13px; }
.steps .dot {
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--tag-bg); color: var(--ink-dim);
  font-size: 12px; display: flex; align-items: center; justify-content: center;
}
.steps .step.on { color: var(--ink); }
.steps .step.on .dot { background: var(--brand); color: #fff; }
.steps .bar { width: 76px; height: 1px; background: var(--line-mid); }

/* The grid of carriers to choose from. Five across, as measured. */
.pick-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 20px;
  padding: 2px 2px 24px;
}
.pick {
  border: 0;
  outline: 1px solid #e5e6eb;
  border-radius: 8px;
  padding: 12px;
  min-height: 104px;
  text-align: center;
  cursor: pointer;
  background: var(--card);
  font-size: 13px;
  line-height: 20px;
  color: var(--ink);
}
.pick:hover { outline-color: var(--brand); }
/* ⚠ `outline`, not `border`: 2px of border on the selected card would move
 * every other card in the grid by a pixel. */
.pick.on { outline: 2px solid #0064fa; }
.pick .mark {
  display: block;
  width: 32px; height: 32px;
  margin: 0 auto 8px;
  border-radius: 8px;
  background: var(--brand-wash);
  color: var(--brand-down);
  font-weight: 600;
  line-height: 32px;
  font-size: 13px;
}
.pick-none { color: var(--ink-dim); padding: 26px 0 40px; text-align: center; }

/* The second step: two columns of fields, as measured. */
.modal-form { display: grid; grid-template-columns: 1fr 1fr; column-gap: 40px; }
.modal-form .fld { margin-bottom: 20px; }
.modal-form .fld.wide { grid-column: 1 / -1; }
.modal-form label { display: block; font-size: 13px; color: var(--ink); margin-bottom: 4px; font-weight: 400; }
/* A field the carrier answers and nobody types into. */
.modal-form .readonly {
  min-height: var(--h-ctl);
  border-radius: var(--r-ctl);
  background: var(--page);
  padding: 5px 11px;
}

/* ── One order's carrier, in one dialog ──────────────────────────────
 *
 * Measured off Jifeng's own carrier dialog on 2026-09-06: the services are a
 * scrolling column of radio rows, name on the left and price hard right, with
 * the carrier's reason under the name when it could not price one. A row that
 * cannot be priced stays selectable -- a refusal at the quote is not always a
 * refusal at the purchase -- so it is dimmed rather than disabled. */
.linkish { color: var(--brand); cursor: pointer; font-size: 12px; }
.chnlist { max-height: 46vh; overflow-y: auto; border: 1px solid var(--line); border-radius: 6px; }
.chn {
  display: grid;
  grid-template-columns: 16px 1fr auto;
  gap: 8px;
  align-items: center;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.chn:last-child { border-bottom: 0; }
.chn:hover { background: var(--row-hover, #f6f8fe); }
.chn.on { background: #eef3ff; }
.chn .nm { min-width: 0; overflow-wrap: anywhere; }
.chn .pr { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.chn .wy { grid-column: 2 / -1; font-size: 11px; color: var(--ink-dim); }

/* ── A pipeline reads down the side, not across the top ──────────────
 *
 * Measured off Jifeng's platform-order screen, 2026-09-06: the states are a
 * left rail with a count against each, and the one that needs a person is
 * selected on arrival. Across the top the same eight read as filters on one
 * list; down the side they read as the stages they are. */
.queue { display: grid; grid-template-columns: 170px 1fr; gap: 14px; align-items: start; }
.qrail { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--card); }
.qrail-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 0 12px;
  height: 36px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: none;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.qrail-row:last-child { border-bottom: 0; }
.qrail-row:hover { background: var(--row-hover, #f6f8fe); }
.qrail-row.on { background: #eef3ff; color: var(--brand); font-weight: 500; }
.qrail-row .n { color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.qrail-row.on .n { color: var(--brand); }
@media (max-width: 860px) { .queue { grid-template-columns: 1fr; } }

/* ---- the popover beside a toolbar icon --------------------------------- */

/* The popover: 362px, a 4px radius rather than the dialog's 12, and a plain
   grey hairline. It is a panel of the page, not a thing that took the page
   over, and Jifeng draws that difference exactly this way. */
/* Above the dialog's own layer, so a chooser opened from inside one is not
   drawn behind it. */
.pop-layer { position: fixed; inset: 0; z-index: 2020; }
.popover {
  position: fixed;
  background: var(--card);
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.12);
  overflow: auto;
  padding: 0;
}
.popover h4 {
  margin: 0;
  padding: 10px 16px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  border-bottom: 1px solid var(--line);
}
/* 36px rows, as measured. Tall enough to press with a thumb, short enough that
   a fifteen-column table's chooser does not need scrolling. */
.pop-row {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 36px;
  padding: 0 16px;
  cursor: pointer;
  color: var(--ink);
}
.pop-row:hover { background: var(--brand-pale); }
.pop-row input { margin: 0; }
.pop-row .grow { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pop-foot {
  border-top: 1px solid var(--line);
  padding: 8px 16px;
  position: sticky;
  bottom: 0;
  background: var(--card);
}
.pop-foot button { width: 100%; }

/* ---- the icons at the right of a list toolbar --------------------------- */

/* ⚠ Borderless and 28px, which is not what `.toolbtn` above is. Measured the
 * same day: Jifeng's list toolbars end in a row of 28x28 link buttons holding a
 * 16px glyph, and a bordered 32px box beside them reads as a fourth action
 * rather than a tool. `.toolbtn` stays as it is -- it is used on benches, where
 * the icon *is* the action. */
.listtools { display: inline-flex; align-items: center; gap: 2px; margin-left: auto; }
.listtool {
  width: 28px;
  height: 28px;
  padding: 6px;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--ink-2);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.listtool:hover { background: var(--brand-wash); color: var(--brand); }
.listtool svg { width: 16px; height: 16px; display: block; }
/* A column the reader has switched off is hidden by a generated rule per
   index -- see web/lib/tablekit.js. Nothing is removed from the table, so a
   screen that redraws itself comes back with the same columns hidden without
   knowing this feature exists. */
/* A bar made only to carry the icons, in a card that had no toolbar of its own.
   It keeps the card's own inner margin so the icons line up with the ones on a
   card that did have a toolbar. */
.toolbar.tools-only { margin: 0; padding: 8px 12px; min-height: 44px; }
.card:has(> table) > .toolbar.tools-only { border-bottom: 1px solid var(--line); }
