/* 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 -- 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);
  /* 14px on a 22px line (1.57), measured off the forwarder portal the boss
     pointed at on 2026-09-25 ("easy to read"); it was Jifeng's 13px/1.35.
     Secondary words (field captions, SKUs, notes) are 12px, never smaller.
     The Chinese faces are load-bearing -- see the head of this file. */
  font: 14px/1.57 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;
  /* 16px against the menu's 14px regular, as Jifeng draws it (measured
     2026-09-23). At 14px both, the only difference left was the weight, and
     the name ran into the first menu item -- "hard to tell apart". */
  font-size: 16px;
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 3rem;
}

/* The name truncates on its own so the console tag after it never does: with
   the ellipsis on the anchor, a long warehouse name would cut the tag off
   first, and the tag is the one thing that tells WMS and OMS apart. */
.brand-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Plain words, no frame (the boss, 2026-09-23). */
.brand-tag {
  flex: 0 0 auto;
  font-weight: 600;
  color: #fff;
}

/* 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.

   ⚠ **The selector names its parents on purpose.** `.logo-slot` alone is
   (0,1,0) and `.row > div` below is (0,1,1), so a box put in a `.row` -- which
   is where this one was -- lost its `flex: 0 0 auto` to `flex: 1 1 190px` and
   its `width` to that basis. Measured on the settings screen on 2026-09-19:
   551px wide and 48 tall, a checkerboard stripe the width of the card, which
   is what a logo well looks like when it is told to fill a row. A square that
   is only square while nothing around it has an opinion is not a square. */
.row > .logo-slot,
.formgrid > .logo-slot,
.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: 14px; }
  .account .who { display: none; }
}

@media (max-width: 900px) {
  .brand { padding: 0 8px; font-size: 14px; 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: 14px;
  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: 12px;
  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: 14px;
  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: 14px;
  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: 14px;
  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 -------------------------------------- */

/* ⚠ **The console has a measure, and it is not the width of the monitor.**
 *
 * Measured on our own Receivable Invoices at 1920: a six-column table pulled
 * out to 1653px, with the columns floating half a metre apart and the eye
 * walking across nothing to read one row. Every card was full bleed and
 * stacked, so the wider the screen got the emptier it looked and the longer
 * the page ran. Reported on 2026-09-19 as "one wide column all the way down,
 * absurdly long -- there was never any design".
 *
 * The home dashboard has had a measure since 2026-09-14 (`.dash`, 1366,
 * centred, taken off Jifeng's own dashboard). It was the only screen with one.
 * This is that rule for the other 128: the rail and the page together are one
 * block, capped and centred, so a wide monitor gets margins rather than
 * stretched tables.
 *
 * ⚠ 1600 rather than the dashboard's 1366 because the list screens are the
 * wide ones -- the inbound list is fourteen columns, and squeezing those into
 * 1366 to match a screen made of counter cards trades one bad shape for
 * another. The dashboard keeps its own narrower measure. */
.workarea {
  display: flex;
  align-items: flex-start;
  min-height: calc(100vh - 74px);
}

/* ⚠ **The measure is left-aligned, never centred.** The first version centred
   the 1600 at `margin: 0 auto`, which on a 1920 screen left 153px of nothing
   to the LEFT of the left column -- reported the same afternoon: "why is
   there still whitespace to the left of the left column". A navigation column
   floating away from the window edge is not how any console looks, the bar
   above it is full width, and that gap reads as a second, empty column.
   So the block keeps its 1600 and hangs off the left edge; the slack goes on
   the right, where nothing is. */
/* ⚠ **2026-09-23: the cap is gone, site-wide.** The boss asked about the
   empty right-hand side on three screens in one afternoon (shop list,
   warehouse connections, ERP keys) after the 09-20 question above. The
   measure was answering 09-19's "one wide strip, endlessly long"; the
   screens have since grown their columns, and the slack on the right is now
   the complaint. Kept as a note rather than a rule, so the next person sees
   both sides of it before putting a cap back. */

/* ⚠ **A list that can use the width gets the window; nothing else does.**
   Boss, 2026-09-20: "what is the empty area on the right of the whole site
   for? I would rather have that than the left being so crowded." Nothing --
   it is the slack under the 1600 above, 241px of it on his 1841 screen, and
   the note above says why it cannot go on the left instead.

   The cap is there so a six-column table is not smeared across 1900. A screen
   opts out with `fluid` when its table can actually spend the width, and that
   is a claim about one table, measured, never a rule applied to a group:

   - a table whose columns are pinned cannot smear -- it is exactly as wide as
     its colgroup says -- so it leaves ONE column free to take the slack and
     carries a `min-width`, so a narrow window scrolls instead of crushing
     that column to nothing. Sharing the slack among all twelve columns
     instead is what puts 66px behind a pieces count of 1, which is the
     complaint this started as;
   - an automatic table may opt out too, but only when it is already full at
     1600, because then the browser hands the extra to the longest column.
     The ledger does: its note column goes 494 -> 700 and loses a line, while
     both money columns stay put.

   ⚠ **Most screens must NOT have it, and the reason is not obvious.**
   Product stock looks like the same case -- full table, a 457px description
   column -- but its long cells are stacked fields, not wrapped text, so the
   extra width leaves every one of them on exactly the same eleven lines and
   goes to eight figure columns at 15px each. Nothing reads better and the
   numbers get fat. Carrier accounts is the same. So: measure the screen at
   1841 with and without the cap, compare the line counts, and only then. */
.workarea:has(#body table.fluid) { max-width: none; }

/* Two columns on a report screen: the list carries the width, the summary
   sits beside it rather than on top of it. Prefixed, like every layout class
   here -- `.row` is the form row and claiming a bare name collides with it. */
.l-aside {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: 16px;
  align-items: start;
}
@media (max-width: 1180px) {
  .l-aside { grid-template-columns: minmax(0, 1fr); }
}

/* The totals beside a Data screen's series: a label and one big number each,
   down the right-hand column. Same shape as the dashboard's counter cards, at
   the size of a figure somebody reads from across the desk. */
.stat-fig { padding: 4px 0 14px; }
.stat-fig:last-child { padding-bottom: 0; }
.stat-fig label { display: block; color: var(--ink-dim); font-size: 12px; margin-bottom: 4px; }
.stat-fig .v { font-size: 26px; font-weight: 600; line-height: 1.2; }

/* ⚠ **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;
}

/* ⚠ Same as `.qrail-title`: the text starts where the entries' text starts
   (10px in) and the blue bar hangs in the gutter. With the bar in the flow the
   heading sat 8px right of everything under it (boss, 2026-09-26, sort wall). */
.side-title {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-2);
  padding: 2px 10px 10px;
}
.side-title::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 5px;
  width: 3px;
  height: 12px;
  border-radius: 1px;
  background: var(--brand);
}

.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); }


/* ---- 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; }
/* ── ⚠ Second-level ink, not third ────────────────────────────────
 *
 * "Just no grey text -- grey looks dirty" (2026-09-20, after the same
 * complaint on two screens in a row). The values themselves were measured off
 * Jifeng and are right; what was wrong is which level this product spent them
 * at. A field's name and a row's note are the two things nearly every screen
 * sets in grey, and both were at the third level (#677395) -- one step below
 * where Jifeng puts the same words. At 12px on white that reads as faded
 * rather than quiet.
 *
 * So the two commonest carriers of grey move up one step, to the secondary
 * ink. The fourth level is still there for the handful of things that really
 * are furniture (a timestamp beside a code, a fold's summary), and nothing
 * that a person reads to do their job is set in it any more. */
.sub { color: var(--ink-2); 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.

   ⚠ **The type and the air are Q5's, not Jifeng's (boss, 2026-09-25).** Pointed
   at the forwarder's order list (jde.intelink.net.cn/oms/#/orderlist) and said
   its type size, spacing, white space and row height read with less strain.
   Measured there with `getComputedStyle` at 1841 wide:

     body 14px on a 23px line         cell padding 8px 0, inner 0 14px
     header 14px bold #333, 42px      rule #EBEEF5 between rows

   What does the work is the line, not the colour: 22px under 14px type is
   1.57, where this grid had 17.5 under 13 (1.35). So: 14px on 22, 10px above
   and below (a one-line row is 42), 14px a side.

   ⚠ **Two things from that page are deliberately not taken.** Its body text is
   mid-grey #606266 -- "grey looks dirty" (2026-09-20), so ours stays dark. And
   it turns on `-webkit-font-smoothing: antialiased`, which this file removed
   after "the type is thin and small" (2026-08-19) -- see `body`.

   Before this it was 13px on Jifeng's 44px rows (measured 2026-09-06 on
   wms.jfwms.com/web/in). Pinned-width tables (`table.fixedcols`) were sized
   for 13px and each needs its widths checked again. */
th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--rule); font-size: 14px; line-height: 22px; }
th {
  background: var(--page);
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}

/* ⚠ **A cell holding a button keeps the same row height as one holding text.**
   The button is 32px; five and five around it is 42, the height of a row of
   text -- rows of two different heights in one table is the raggedness this
   was once reported as. */
table:not(.jf) td:has(button),
table:not(.jf) td:has(select),
table:not(.jf) td:has(input) { padding-top: 5px; padding-bottom: 5px; }
/* A second line in a cell sits right under the first. `.sub` carries 14px
   under it for its life as a line under a heading; inside a cell that margin
   opened a gap before whatever line came next -- the ZIP floating 14px below
   the city it belongs to, on every row of the dropship list. */
td .sub { margin: 0; }
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: 14px; color: var(--ink-2); 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: 14px; 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: 14px; }

/* ---- 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: 14px;
}
.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;
  line-height: 20px;
  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; }
/* The way from a sign-in page to the community (routes/community.ts). */
.auth-community { max-width: 380px; width: 100%; margin-top: 18px; display: flex; flex-direction: column; gap: 2px; padding: 12px 16px; border-radius: 8px; background: #fff; border: 1px solid var(--line); color: var(--ink); text-decoration: none; }
.auth-community b { font-weight: 600; color: var(--brand); }
.auth-community b::after { content: " \2192"; }
.auth-community span { font-size: 12px; color: var(--ink-faint); line-height: 18px; }
.auth-community:hover { border-color: var(--brand); text-decoration: none; }
.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: 14px; }

/* ⚠ **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;
}

/* ⚠ **A box that cannot be typed into must not wear the colour that says it
   can.** The sorting bench greys its box until somebody has said whose pallet
   is on it; the browser greys the background, but a 3px brand-blue frame
   around it still reads as "scan here", and a control that looks live and
   does nothing gets used twice and then reported as broken. */
.bench-scan input:disabled {
  border-color: var(--line);
  color: var(--ink-faint);
}

.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;
  /* ⚠ **The word is ordinary ink, not grey.** It was #a8b0bd, then the third
     level, and the boss's answer to both was the same: "the text in the
     button does not have to be grey, it can be black" (2026-09-20). The grey
     *background* is what says "not now" -- spending the text colour on it as
     well only makes the word hard to read, which is a separate thing from
     making the button look inactive. */
  color: var(--ink);
  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: 14px; 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; }

/* ---- the two door benches: the work on the left, the day on the right ------

   Boss, 2026-09-24: "is there a better design for sign-in and despatch?".
   Scan box and answer own the left column and are drawn large, for somebody a
   metre away holding a box; the right column is what they glance at between
   boxes (web/lib/bench-side.js). */
.bench-duo {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 420px;
  gap: 20px;
  align-items: start;
}
.bench-duo .bench-main { min-width: 0; }
.bench-duo .bench-side { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.bench-top { display: flex; align-items: flex-end; gap: 14px; }
.bench-top .bench-scan { flex: 1 1 auto; min-width: 0; }
.bench-duo .bench-answer { min-height: 118px; }
.bench-duo .bench-answer .big { font-size: 32px; }
.bench-side-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.bench-side-title .n { color: var(--ink-dim); font-weight: 400; }
.bench-figs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.bench-fig { display: flex; flex-direction: column; gap: 4px; }
.bench-fig > i { font-style: normal; font-size: 12px; color: var(--ink-dim); }
.bench-fig > b { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.bench-fig.lead > b { font-size: 30px; color: var(--brand); }
.bench-log { list-style: none; margin: 0; padding: 0; max-height: 520px; overflow-y: auto; }
.bench-log li {
  display: grid;
  /* ⚠ The code is never cut (boss, 2026-09-24: "the order number is too
     short, the tail is hidden"). The tail is the part people read, so the
     code column is as wide as the code and the sentence beside it gives way. */
  grid-template-columns: max-content 14px max-content minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}
.bench-log li:last-child { border-bottom: 0; }
.bench-log .t { color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.bench-log .m { font-weight: 700; text-align: center; }
.bench-log .c { font-weight: 600; white-space: nowrap; }
.bench-log .x { color: var(--ink-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bench-log li.ok .m { color: #008c21; }
.bench-log li.warn .m { color: #a35f07; }
.bench-log li.bad .m { color: #cc3535; }
.bench-log li.bad .c { color: #cc3535; }
.printer-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 52px;
  padding: 0 16px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  text-decoration: none;
  max-width: 280px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.printer-chip .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ink-faint); flex: 0 0 auto; }
.printer-chip.ok .dot { background: #19a44a; }
.printer-chip.bad { border-color: #fccaca; background: var(--bad-bg); color: #cc3535; }
.printer-chip.bad .dot { background: #cc3535; }
.printer-chip.wait { color: var(--ink-dim); }
.batch-progress { height: 10px; border-radius: 5px; background: var(--line); overflow: hidden; margin-top: 10px; }
.batch-progress > span { display: block; height: 100%; background: var(--brand); }
.batch-progress.done > span { background: #19a44a; }
@media (max-width: 1100px) {
  .bench-duo { grid-template-columns: 1fr; }
}

/* ---- 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);
  gap: 20px;
  align-items: start;
  margin-top: 16px;
}
/* ⚠ The scan box, the answer, and then everything that is not either of them
   -- the printing settings and the finished rounds, both folded -- down the
   left; the wall down the right with the whole column to grow in. The
   call-out is a fixed height and the wall is not: a round of 100 codes made
   the wall 3,564px tall, beside a column that had been empty since the second
   row of tiles. */
.bench-call { min-width: 0; }
.bench-wall { min-width: 0; }
/* The answer's own margin would push it a line below the top of the wall. */
.bench-call > .bench-answer:first-child { margin-top: 0; }
@media (max-width: 980px) {
  .bench-split { grid-template-columns: 1fr; }
}

/* ---- the one question above a sorting bench -------------------------------

   ⚠ **Whose pallet this is, asked once.** It used to be asked per code, in a
   dropdown that stopped the line -- and an FBA return pallet is one seller's,
   so it was the same answer over and over. Reported 2026-09-19 as hard to use.
   The loose goods bench learnt this on 2026-08-29 and this is its shape: the
   question, the answer beside it, and it stays on the screen so that a default
   is something the reader can see rather than something the screen assumed. */
.wall-top {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 12px;
  padding: 12px 16px;
  margin-bottom: 14px;
}
.wall-top-q { font-size: 14px; color: var(--ink-2); }
.wall-top-who { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; min-width: 0; }
.wall-top-seller { font-size: 20px; font-weight: 700; color: var(--brand); letter-spacing: 0.01em; }
.wall-top-pick { max-width: 320px; }

/* ---- what a bench is set to, out of the way of what it does ----------------

   The printer this bench points at and how new labels come out are set when
   the bench is set up; the scan box and the answer are used hundreds of times
   a day. They were three rows between the two. Folded, with what they are set
   to in the summary -- which is the fact anybody checks -- they cost one line.
   The finished rounds fold the same way and for the same reason. */
.wall-fold {
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--card);
  margin-top: 14px;
}
.wall-fold > summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 14px;
  cursor: pointer;
  list-style: none;
  font-size: 14px;
}
.wall-fold > summary::-webkit-details-marker { display: none; }
.wall-fold-name { font-weight: 600; }
.wall-fold-name::before { content: "\25B8"; color: var(--ink-faint); margin-right: 6px; }
.wall-fold[open] > summary .wall-fold-name::before { content: "\25BE"; }
.wall-fold-sum {
  font-size: 12px;
  color: var(--ink-dim);
  text-align: right;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wall-fold-body { padding: 4px 14px 14px; }

/* ⚠ **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 {
  /* Raised from 320 when the pallet's seller took a line above the bench. */
  max-height: calc(100vh - 380px);
  min-height: 220px;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The tiles measure their offset against this box, so drawWall() can scroll
     the pile just called into view without touching the page -- see there. */
  position: relative;
}

/* ---- 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: 12px;
  color: var(--ink-dim);
  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. */
/* The round's own presses, together at the right of its header: print the
   whole wall's labels, and finish the round. */
.wall-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.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; }
/* What the pile becomes: the new code (or "keeps its code"), under the old
   one, in the brand colour so it reads as the answer rather than the input.
   Red when two orders disagree -- nothing goes on those units until somebody
   settles which. */
.pigeon-to {
  margin-top: 2px;
  font-size: 12px;
  color: var(--brand);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pigeon-to.bad { color: var(--bad); font-family: inherit; }
/* ⚠ Nothing on file yet, which is an ordinary state and not a fault: returns
   land before the seller raises the order as often as after. Quiet, and not
   in the brand colour the new code is set in -- that colour means "this is
   what goes on the unit". */
.pigeon-to.none { color: var(--ink-dim); font-family: inherit; }
.pigeon .ghost.sm { margin-top: 6px; }


/* ---- 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. */
  /* ⚠ **Raised from 37.7cqw on 2026-09-19**: "the text next to the 23 does not
     mean much, make the 23 bigger". Three of the four lines beside it went
     (see web/pages/sort-wall.js), and the square took the room -- what is left
     beside it is one barcode and what to stick on the unit. */
  --pile-box: clamp(130px, 52cqw, 300px);
  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; }
/* ⚠ The sorting bench's caption is not a sentence -- it is the barcode just
   read, which is the only thing on the screen that can say the scanner read
   what the person is holding. Set like every other code on this site so a
   character out of place shows as a jog, and smaller than a sentence would be
   because the number beside it is what the answer is. */
.bench-call .bench-answer:has(.pile-num) .big {
  font-size: 22px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: -0.01em;
}

/* One colour per step of a forwarded box (boss, 2026-09-24: handed over and
   despatched were the same amber). Grey at the factory, blue on the way (the
   same blue as "in transit" on a tracking line), amber in the building and
   waiting for us, green gone, grey closed. */
.status.measured { background: var(--tag-bg); color: var(--ink-dim); }
.status.handedOver { background: var(--brand-wash); color: var(--brand-down); }
.status.arrived { background: var(--warn-bg); color: #d56800; }
.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: green, the one finished state a forwarded box is meant to reach.
 * (It was amber, to tell it from closed; closed is grey now, and amber is for
 * a box waiting in the building.) */
.status.despatched { background: var(--ok-bg); color: #008c21; }

/* 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: 14px;
}
.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: 14px;
  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); }

/* The two to-do buttons above the first row (boss, 2026-09-24): one line's
   work each, the whole block one press into where that work is done.
   Prefixed h-: `.row` and friends are already taken by the form rows. */
/* ⚠ Three to a row, never more (boss, 2026-09-27: "at most three; don't
   make it four, it's a mess"). A grid, not wrapping flex: a fourth button
   starts the next row at the same width instead of squeezing all four into
   one row or stretching alone across the page. */
.h-tasks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
@media (max-width: 1000px) { .h-tasks { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) { .h-tasks { grid-template-columns: minmax(0, 1fr); } }
/* ⚠ Filled, not white cards (boss, 2026-09-24: "the two buttons have no
   background colour"). A white block on the white page read as one more card
   in the row below, not as the two things to press first. Each line has its
   own colour; text is white on both. */
/* Half the row each, title and figures on one line, and the figures spread
   evenly over the rest of the block (boss: "not smaller buttons -- the
   numbers are too crowded"). */
.h-task {
  position: relative;
  display: flex;
  align-items: center;
  gap: 28px;
  min-width: 0;
  background: #0040c7;
  border: 0;
  border-radius: var(--r-card);
  min-height: 104px;
  padding: 20px 24px;
  cursor: pointer;
  color: #fff;
}
/* Blue and a blue-teal (boss chose C, 2026-09-24: blue beside green read
   as two colours fighting); hover lifts each a step. */
.h-task.mail { background: #0e7c8f; }
.h-task:hover { background: #0044d0; }
.h-task.mail:hover { background: #1592a8; }
.h-task .dash-ico { color: #fff; }
.h-task-title { flex: 0 0 170px; display: flex; align-items: center; gap: 4px; font-size: 18px; font-weight: 700; line-height: 22px; color: #fff; text-decoration: none; }
.h-task-title:hover { color: #fff; text-decoration: none; }
/* ⚠ One figure each, centred on the whole button, the title on the left
   (boss, 2026-09-24: "centred"). */
.h-task-figs { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: flex; gap: 32px; }
.h-task-figs.beside { position: static; transform: none; flex: 1 1 auto; justify-content: center; }
/* Too narrow to centre past the title: back beside it. */
@media (max-width: 700px) {
  .h-task-figs { position: static; transform: none; margin-left: auto; }
}
/* The seller's one button: supplier replies (boss, 2026-09-25). Not half the
   row -- one button stretched across the column reads as a banner. Quiet
   slate when there is nothing new, orange while there is. */
.h-tasks.seller { grid-template-columns: repeat(3, minmax(0, 420px)); }
@media (max-width: 1000px) { .h-tasks.seller { grid-template-columns: minmax(0, 420px); } }
.h-tasks.seller .h-task { min-height: 76px; padding: 14px 22px; background: #52607a; }
.h-tasks.seller .h-task:hover { background: #5f6e8a; }
.h-tasks.seller .h-task.hot { background: #e8590c; }
.h-tasks.seller .h-task.hot:hover { background: #f06a1f; }
/* The console's third button, messages from sellers: slate, orange while
   something is new -- the same two states as the seller's buttons. */
/* A seller's new outbound orders (2026-09-27): the warehouse's own blue, like
   the dropship button -- it is work, not news. */
.h-task.sellerout { background: #3b5bdb; }
.h-task.sellerout:hover { background: #4c6ef5; }
.h-task.inbox { background: #52607a; }
.h-task.inbox:hover { background: #5f6e8a; }
.h-task.inbox.hot { background: #e8590c; }
.h-task.inbox.hot:hover { background: #f06a1f; }

/* Messages (web/lib/messages-view.js): who on the left, the conversation on
   the right. */
/* ⚠ **The Messages screen fills the window** (boss, 2026-09-26: "looks bad,
   not designed"). It was a short card with the page empty under it; a chat is
   two panes the height of the screen, the list scrolling on the left and the
   conversation scrolling above a box pinned to the bottom on the right.
   40 bar + 36 tabs + 2 x 16 padding = 108. */
/* ⚠ **1366 wide and centred, the home page's column** (boss, 2026-09-26:
   "the messages screen is too full-width"). At 1841 the conversation pane was
   1472px and the two sides of one exchange sat 1300px apart. */
.msgs { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 0; height: calc(100vh - 110px); min-height: 480px;
  max-width: 1366px; margin: 0 auto;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; }
.msgs > .card { border: 0; border-radius: 0; margin: 0; box-shadow: none; }
.msgs-side { padding: 0; overflow-y: auto; border-right: 1px solid var(--line) !important; display: flex; flex-direction: column; }
.msgs-head { padding: 14px 16px 6px; font-size: 12px; color: var(--ink-faint); font-weight: 500; }
.msgs-search { padding: 12px 12px 4px; }
.msgs-search input { width: 100%; }
.msgs-row { display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; column-gap: 10px; row-gap: 2px; align-items: center; width: 100%; padding: 10px 16px; min-height: 64px; flex: 0 0 auto; border: 0; border-radius: 0; background: transparent; text-align: left; color: var(--ink); text-decoration: none; cursor: pointer; height: auto; font: inherit; }
.msgs-row:hover { background: #f5f7fc; color: var(--ink); text-decoration: none; }
.msgs-row.on { background: #eaf1ff; }
.msgs-av { grid-row: 1 / span 2; width: 40px; height: 40px; border-radius: 50%; background: #e8efff; color: var(--brand); font-size: 14px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
.msgs-row.platform .msgs-av { background: var(--brand); color: #fff; }
.msgs-row.doc .msgs-av { background: #fff4e5; color: #b25e09; border-radius: 8px; }
.msgs-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.msgs-name .sub { font-weight: 400; color: var(--ink-faint); }
.msgs-n { justify-self: end; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--bad); color: #fff; font-size: 12px; line-height: 18px; text-align: center; }
.msgs-when { justify-self: end; font-size: 12px; color: var(--ink-faint); }
.msgs-last { grid-column: 2 / -1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--ink-faint); }
.msgs-none { padding: 24px 16px; color: var(--ink-faint); text-align: center; }
.msgs-main { padding: 0; display: flex; flex-direction: column; min-height: 0; }
.msgs-title { flex: 0 0 56px; display: flex; align-items: center; gap: 10px; padding: 0 20px; border-bottom: 1px solid var(--line); font-size: 15px; }
.msgs-title .msgs-av { width: 32px; height: 32px; }
.msgs-title .grow { flex: 1 1 auto; }
.msgs-main .talk { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.msgs-main .talk-list { flex: 1 1 auto; max-height: none; min-height: 0; background: #f7f8fb; padding: 12px 20px 16px; }
.msgs-main .talk-box { border: 0; border-top: 1px solid var(--line); border-radius: 0; padding: 12px 20px 10px; }
.msgs-main .talk-box textarea { min-height: 64px; resize: none; }
@media (max-width: 860px) { .msgs { grid-template-columns: 1fr; } }
/* ⚠ On a phone one pane at a time: the list, or the conversation with a back
   arrow (web/lib/messages-view.js). Stacked, the conversation started below a
   screenful of sellers and its box below that. dvh, not vh: on a phone vh is
   the height with the browser's bars hidden, and the send box sat under them. */
.msgs-title .msgs-back { display: none; }
@media (max-width: 860px) {
  .msgs { height: calc(100dvh - 110px); min-height: 360px; }
  .msgs.m-list .msgs-main, .msgs.m-conv .msgs-side { display: none; }
  .msgs-side { border-right: 0 !important; }
  .msgs-title .msgs-back { display: inline-flex; margin-left: -8px; }
  .msgs-title { padding: 0 12px; gap: 8px; }
  .msgs-main .talk-list { padding: 10px 12px 12px; }
  .msgs-main .talk-box { padding: 10px 12px 8px; }
  .msgs-main .talk-box textarea { min-height: 44px; }
}
/* This device's notifications, at the top of the list. */
.msgs-push { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--line); font-size: 12px; color: var(--ink-faint); flex: 0 0 auto; }
.msgs-push .grow { flex: 1 1 auto; }
.msgs-push.on { color: var(--good, #1a7f37); }
.msgs-push button.ghost { display: inline-flex; align-items: center; gap: 6px; }
.msgs-bell { display: inline-flex; }
.msgs-push .linkbtn { font-size: 12px; }
/* Volans Chat (web/chat.html): the conversations filling the window, under
   one thin bar of its own -- no console around them. */
body.chat { margin: 0; background: #fff; }
.chatbar { height: 48px; display: flex; align-items: center; gap: 10px; padding: 0 16px; background: #001F4D; color: #fff; }
.chat-name { font-size: 16px; font-weight: 600; }
.chat-who { flex: 1 1 auto; min-width: 0; font-size: 13px; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chatbar .signout { flex: 0 0 auto; }
.chat-main { padding: 0; }
body.chat .msgs { height: calc(100dvh - 48px); min-height: 0; }
body.chat #msg { margin: 0; border-radius: 0; }
/* A machine translation's original, or what the other side was shown, folded
   under the words (web/lib/thread.js). Quiet: it is there to be checked, not read. */
.talk-orig { margin-top: 4px; font-size: 12px; }
.talk-orig summary { cursor: pointer; opacity: 0.7; list-style: none; }
.talk-orig summary::-webkit-details-marker { display: none; }
.talk-orig summary::before { content: "\21C4  "; }
.talk-orig > div { margin-top: 2px; opacity: 0.85; white-space: pre-wrap; }
/* Volans: what is waiting on the reader, at the top of the list. */
.todo-row { display: flex; align-items: center; gap: 10px; padding: 10px 16px; min-height: 44px; flex: 0 0 auto; color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--line); }
.todo-row:hover { background: #f5f7fc; text-decoration: none; color: var(--ink); }
.todo-label { flex: 1 1 auto; min-width: 0; }
.todo-n { flex: 0 0 auto; min-width: 22px; height: 22px; padding: 0 7px; border-radius: 11px; background: #e8efff; color: var(--brand); font-weight: 600; font-size: 12px; line-height: 22px; text-align: center; }
.todo-row.bad .todo-n { background: var(--bad); color: #fff; }
/* Topics (web/lib/messages-view.js, 2026-09-26): the head carries the open /
   solved switch and the button that starts one. */
.msgs-topics { display: flex; align-items: center; gap: 8px; padding-right: 12px; }
.msgs-topics > span:first-child { flex: 1 1 auto; }
.msgs-tabs { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.msgs-tabs button { height: 24px; padding: 0 10px; border: 0; border-radius: 0; background: #fff; color: var(--ink-soft, #4b5563); font-size: 12px; font-weight: 400; }
.msgs-tabs button + button { border-left: 1px solid var(--line); }
.msgs-tabs button.on { background: #eaf1ff; color: var(--brand); }
.msgs-topics .linkbtn { font-size: 12px; }
.msgs-row.topic.solved .msgs-name { color: var(--ink-faint); }
.msgs-none.small { padding: 10px 16px 14px; font-size: 12px; }
.msgs-title .msgs-doc, .msgs-title > .sub { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.msgs-title > b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.msgs-title button { flex: 0 0 auto; white-space: nowrap; }
.msgs-solved { padding: 6px 20px; font-size: 12px; color: var(--ink-faint); background: #f3f5fc; border-bottom: 1px solid var(--line); }
.msgs-found { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; }
.msgs-pick { display: block; width: 100%; height: auto; padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px; background: #fff; color: var(--ink); text-align: left; font: inherit; font-weight: 400; }
button.msgs-pick:hover { border-color: var(--brand); background: #f5f8ff; }
.msgs-pick.on { border-color: var(--brand); background: #eaf1ff; }
.h-task-fig { display: flex; flex-direction: column; align-items: center; color: inherit; text-decoration: none; min-width: 0; }
.h-task-fig .v { font-size: 30px; font-weight: 600; line-height: 38px; color: #fff; font-variant-numeric: tabular-nums; }
.h-task-fig .k { font-size: 14px; line-height: 20px; color: rgba(255, 255, 255, 0.85); white-space: nowrap; }
/* Red on blue or green does not read; a failure is a white-on-red badge. */
.h-task-fig.bad { background: var(--bad); border-radius: 6px; padding: 0 10px; }
.h-task-fig.bad .v, .h-task-fig.bad .k { color: #fff; }

/* The seller's to-do notices, above the ad slot. */
.h-alerts { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.h-alert {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 16px;
  border-radius: var(--r-card);
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
  text-decoration: none;
}
.h-alert.bad { background: var(--bad-bg); color: var(--bad); border: 1px solid var(--bad); }
.h-alert.warn { background: var(--warn-bg); color: #b25700; border: 1px solid var(--warn); }
.h-alert:hover { text-decoration: underline; }

.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: 14px;
  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: 14px;
}
.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: 14px; line-height: 20px; 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: 14px; font-weight: 600; line-height: 20px; 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; }
/* The community card on the seller's front page (web/lib/home.js). */
.dash-cm-row { display: flex; justify-content: space-between; gap: 10px; padding: 7px 0; color: var(--ink); text-decoration: none; border-top: 1px solid var(--line); }
.dash-cm-row:first-of-type { border-top: 0; }
.dash-cm-row .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-cm-row .date { flex: 0 0 auto; color: var(--ink-faint); font-size: 12px; }
.dash-cm-row:hover .title { color: var(--brand); }
.dash-cm-shelves { display: flex; gap: 8px; margin: 2px 0 8px; }
.dash-cm-shelf { flex: 1 1 0; text-align: center; padding: 6px 8px; border-radius: 6px; background: #f3f5fc; color: var(--brand); text-decoration: none; font-size: 12px; }
.dash-cm-shelf:hover { background: #e8eeff; }
.dash-cm-empty { color: var(--ink-faint); margin: 6px 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: 14px; 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: 14px; }
.dash-notice:hover .title { color: var(--brand); }
.dash-notice .date { display: block; margin-top: 4px; font-size: 14px; line-height: 20px; 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: 14px;
  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: 14px;
  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; }
/* ⚠ **A cell that holds only a tick box never gets an ellipsis** (boss,
   2026-09-24: "why is there a dot between the tick and the number?"). The box
   is 13px plus the browser's own 3+4px margins -- 20px -- in a 40px column
   with 12px padding each side, so it overflowed by 4px and the ellipsis above
   was drawn after it, clipped down to a dot. */
table th:has(> input[type="checkbox"]:only-child),
table td:has(> input[type="checkbox"]:only-child) { text-overflow: clip; overflow: visible; }
/* ⚠ **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; }
/* ⚠ **A tracking number is a link and has to stay selectable** (boss,
   2026-09-25: "the last-mile number cannot be copied"). A press-and-drag on a
   link starts dragging the link, so the number could be opened but never
   selected. Chrome and Safari select the text once the link is not draggable. */
a.trackno { -webkit-user-drag: none; user-select: text; }

/* 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 batch's header as a card of facts: a small grey name over each value,
   divided by hairlines (boss, 2026-09-24: the one-line strip was crowded). */
.ctxbar.ctxcard { background: var(--card); border-color: var(--line); padding: 14px 20px; gap: 0; }
.ctxf { display: flex; flex-direction: column; gap: 4px; padding: 0 32px; border-left: 1px solid var(--line); }
.ctxf:first-child { padding-left: 0; border-left: 0; }
.ctxf > i { font-style: normal; font-size: 12px; color: var(--ink-dim); }
.ctxf b { font-size: 15px; font-weight: 600; color: var(--ink); }
.ctxf b.who { font-size: 16px; }

/* 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: 12px; 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: 14px; }
.track-tl .ev-m { font-size: 12px; 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: 14px; 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: 14px;
  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; }
/* ⚠ **No width or height on the tick.** It was 14px square, and the box is
   square -- but Chrome draws the native control at its own 13px and scales the
   drawing to fit, without scaling the border or the corner radius with it. At
   14 that comes out visibly wider than it is tall, with fat corners: reported
   2026-09-20 as "the tick boxes are all deformed", and they were. Measured side
   by side at 10x: forced 14px is an oblong, the native size is a square. One
   pixel of size is not worth a control that is drawn wrong on every list in the
   building. */
td.pick input, th.pick input { 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: 14px; }
.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: 14px; }
table.fc-quotes { min-width: 900px; }
table.fc-quotes td:first-child { min-width: 210px; }
.fc-lines { margin-top: 3px; font-size: 12px; 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: 12px; 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: 12px; 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: 12px; }
td.fr-ctl select, td.fr-ctl input { height: 28px; font-size: 14px; }
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: 14px; 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: 12px; 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: 14px;
  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: 14px; }

.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: 14px; 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: 14px; }

/* 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: 14px; }
.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: 14px;
  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: 14px;
  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: 14px;
  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; }
/* `input.` so it outranks `input[type="number"] { width: 100% }` -- as a bare
   class it lost, the box took the whole row and squeezed the button to one
   character a line. */
input.lo-qty { width: 110px; flex: 0 0 auto; margin: 0; }
.lo-go { flex: 1 1 auto; height: 44px; font-size: 16px; white-space: nowrap; }
.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: 14px; }
/* 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: 14px; 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 product's picture in a list: see picture() in web/lib/list-kit.js. */
.pimg {
  /* Block, not the span's inline: inside the seller's clickable label it is
     not a flex item, and an inline box ignores its width. */
  display: block;
  flex: 0 0 40px;
  width: 40px;
  height: 40px;
  border-radius: 4px;
  overflow: hidden;
  align-self: center;
}
.pimg.lg { flex-basis: 48px; width: 48px; height: 48px; }
/* The seller's tile opens a file picker, and has a small x to take the
   picture off -- only on hover, so the list does not wear forty of them. */
.pic-edit { position: relative; flex: 0 0 auto; }
.pic-edit > label { display: block; cursor: pointer; }
.pic-edit > label:hover .thumb, .pic-edit > label:hover .pimg img { border-color: var(--brand); }
.pic-x {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  min-width: 0;
  padding: 0;
  border-radius: 50%;
  font-size: 12px;
  line-height: 16px;
  display: none;
}
.pic-edit:hover .pic-x { display: block; }
.pimg img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #fff;
}

/* ── 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: 14px; 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: 14px;
}
.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: 14px; }

/* 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: 14px;
  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: 14px; 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: 14px; }
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: 14px; }
.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;
}
/* ⚠ Scoped to the grid: `td.pick` is the tick column of two product lists,
   and an unscoped `.pick` drew this card's outline and 8px corners round
   every checkbox cell there (2026-09-26, "the lists are a mess"). */
.pick-grid > .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: 14px;
  line-height: 20px;
  color: var(--ink);
}
.pick-grid > .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-grid > .pick.on { outline: 2px solid #0064fa; }
.pick-grid > .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: 14px;
}
.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: 14px; 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: 12px; 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. */
/* ⚠ **One column down the left, never two.** The shell draws the group's
   screens in `.side`; a screen with states of its own draws them here, and
   two stacked lists side by side read as two menus -- reported in those
   words. Jifeng has exactly one left column, headed by the screen's name,
   with the states under it and the sibling screens in the top bar. So the
   page's own rail takes the column over, and it is sized and coloured like
   the one it replaces. */
.workarea:has(#body .queue) > .side { display: none; }
/* The rail takes the column's place, so it takes its edges too: the page's
   own padding moves inside, onto what the rail is beside. */
.workarea:has(#body .queue) > main { padding: 0; }
.workarea:has(#body .queue) #body > :not(.queue) { margin-left: 16px; margin-right: 16px; }
.workarea:has(#body .queue) #body > :first-child:not(.queue) { margin-top: 16px; }
.queue { display: grid; grid-template-columns: 218px 1fr; gap: 0; align-items: start; }
.qrail {
  background: var(--card);
  border-right: 1px solid var(--line);
  align-self: stretch;
  min-height: calc(100vh - 74px);
  position: sticky;
  top: 74px;
  max-height: calc(100vh - 74px);
  overflow-y: auto;
  padding: 12px 8px 0;
}
/* ⚠ The text starts where the rows' text starts (10px in), and the blue bar
   hangs in the gutter to its left (boss, 2026-09-24: the heading and the
   entries under it were not aligned -- the bar pushed the heading 8px in). */
.qrail-title {
  position: relative;
  display: flex;
  align-items: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-2);
  padding: 2px 10px 10px;
}
.qrail-title::before { content: ""; position: absolute; left: 2px; top: 5px; width: 3px; height: 12px; border-radius: 1px; background: var(--brand); }
/* ⚠ **Never shrink**, the same rule `.side-item` carries: a flex/grid child
   squeezed from 40px to 18px with its line-height still 40 is a row of
   clipped characters, and that was reported as "all corrupted". */
.qrail-row {
  display: flex;
  flex: 0 0 auto;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 0 10px;
  height: 40px;
  border-radius: 4px;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  /* 14px, measured on Jifeng's rail (2026-09-24: "the text on your left is all
     smaller than Jifeng's"); it inherited the 13px body. */
  font-size: 14px;
  cursor: pointer;
  text-align: left;
}
.qrail-row:hover { background: var(--row-hover, #f6f8fe); }
.qrail-row.on { background: #eef3ff; color: var(--brand); font-weight: 600; }
.qrail-row .n { color: #7e89a8; font-size: 14px; font-variant-numeric: tabular-nums; }
.qrail-row.on .n { color: var(--brand); }
/* A stage with nothing in it today, drawn faint so the ones with work stand out. */
.qrail-row.zero:not(.on), .qrail-row.zero:not(.on) .n { color: #b3bbd0; }
/* A rail row's note ("cheapest wins", "same as all") is a caption: 12px, grey,
   regular, and it gives way before the row's name does. */
.qrail-row .meta { font-size: 12px; font-weight: 400; color: var(--ink-dim); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The group's own entries, with this screen's states under its name
   (list-kit.js `railed`). The entry being looked at is a heading, not a link. */
.qrail-row.qrail-here { font-weight: 600; color: var(--ink); cursor: default; }
.qrail-row.qrail-here:hover { background: transparent; }
.qrail-row.qrail-sub { padding-left: 28px; }
.qrail-row.qrail-sub2 { padding-left: 44px; }
a.qrail-row { text-decoration: none; color: inherit; }
/* Narrow: the states go above the list, as a plain strip -- a sticky
   full-height column with nothing beside it is a column of empty screen. */
@media (max-width: 860px) {
  .queue { grid-template-columns: 1fr; }
  .qrail { position: static; min-height: 0; max-height: none; border-right: 0; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 4px; }
  .qrail-title { width: 100%; }
  .qrail-row { width: auto; }
  .workarea:has(#body .queue) > main { padding: 16px; }
  .workarea:has(#body .queue) #body > :not(.queue) { margin-left: 0; margin-right: 0; }
  .queue-main { padding: 16px 0 0; }
}

/* ---- 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: 14px;
  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. */
/* ⚠ `tabletop` is the same strip with something of the screen's own on it --
   a bulk action over the column it acts on. `tools-only` is the one
   lib/tablekit.js makes when a screen has not drawn one; both sit flush on
   the table and both take the icons at their right. */
.toolbar.tools-only,
.toolbar.tabletop { margin: 0; padding: 8px 12px; min-height: 44px; }
.card:has(> table) > .toolbar.tools-only,
.card:has(> table) > .toolbar.tabletop { border-bottom: 1px solid var(--line); }

/* ── The relabel sheet: a table edited in place ─────────────────────

   web/lib/relabel-sheet.js. The inputs fill their cells, and ⚠ **their
   borders are always drawn** -- the first version hid them until pointed at,
   so the table read cleaner, and the boss's answer was that the boxes must be
   there to see (2026-09-19): a cell that does not look typeable is a cell
   nobody types into. The fixed columns keep the table from moving while a
   long name is typed. A row the server refused is tinted, and what is wrong
   with it is the thin line under it -- in the row, not in a box at the top of
   the page the reader has to match back to row seven. */
.listcard .toolbar .ttl { font-size: 15px; font-weight: 600; color: var(--ink); }
th .req { color: var(--bad); font-weight: 400; }
table.sheet-edit td { padding-top: 4px; padding-bottom: 4px; }
table.sheet-edit td input { width: 100%; height: 30px; background: #fff; }
table.sheet-edit tr.bad td { background: var(--bad-bg); }
table.sheet-edit tr.sheet-why td { padding-top: 0; padding-bottom: 8px; border-top: 0; font-size: 12px; white-space: normal; }
tr.bad + tr.sheet-why td { background: var(--bad-bg); }
tr.sheet-why span { display: inline-block; margin-right: 14px; }
tr.sheet-why span.bad { color: #cc3535; }
tr.sheet-why span.warn { color: #d56800; }
.sheetcard.on { border-color: var(--brand); background: var(--brand-pale); }
table.sheet-edit tr:has(+ tr.sheet-why) td { border-bottom: 0; }

/* ── A document being written: Jifeng's create screens ───────────────
 *
 * Read off Jifeng's "new FBA return outbound" screen on 2026-09-19 (help-centre
 * screenshots, taken at 125% -- every figure below is the pixel count divided
 * by 1.25). The boss's words that day were "you have nothing, look at Jifeng's
 * design", about a form that asked for one number.
 *
 *   header      white, 56px, a 26px round back button, the title 16px/600,
 *               then Cancel / Save (outlined) / Save and submit on the right
 *   cards       white, 8px radius, 24px padding, 16px apart, no shadow
 *   card title  16px/600, 20px above the first field
 *   fields      three columns of 272px, 90px apart; label 13px ink-2, 8px
 *               above a 32px control; rows 24px apart
 *   long text   the width of all three columns, with a "0 / 200" count
 *               inside the bottom-right corner
 *   tables      40px heading on the page grey, rows 56px, inputs inside
 *
 * ⚠ The header sits under the bar and the tab strip (74px), which is where
 * the window's own scroll starts. */
.dochead {
  position: sticky;
  top: 74px;
  z-index: 20;
  margin: -16px -16px 16px;
  background: #fff;
  border-bottom: 1px solid var(--line);
}
/* ⚠ **The header bleeds out of `main`'s padding, so it has to know what
   that padding is** -- the -16px above is written against the 16px the desktop
   layout uses. At the phone breakpoint that padding is 10, and the header hung
   6px off each edge: a sideways scrollbar on every new-document screen.
   ⚠ It has to sit *here* and not up in the 560px block, which is earlier in
   this file and therefore loses to the rule above at equal specificity. Found
   by measuring at 390px on the order-rule editor, 2026-09-20. */
@media (max-width: 560px) { .dochead { margin-left: -10px; margin-right: -10px; } }
.dochead .in { max-width: 1136px; margin: 0 auto; height: 56px; padding: 0 16px; display: flex; align-items: center; gap: 12px; }
.dochead h1 { font-size: 16px; font-weight: 600; margin: 0; color: var(--ink); white-space: nowrap; }
.dochead .grow { flex: 1 1 auto; }
.dochead .back {
  width: 26px; height: 26px; padding: 0; border-radius: 50%;
  background: #fff; color: var(--ink-2); border: 1px solid var(--line-mid);
  display: inline-flex; align-items: center; justify-content: center;
}
.dochead .back:hover { color: var(--brand); border-color: var(--brand); background: #fff; }
.dochead button:not(.back) { min-width: 80px; }
button.outline { background: #fff; color: var(--brand); border: 1px solid var(--brand); }
button.outline:hover { background: var(--brand-pale); color: var(--brand-hover); border-color: var(--brand-hover); }
button.outline:disabled { background: #fff; color: #a8b0bd; border-color: var(--line-mid); }
button.linkbtn { height: auto; min-width: 0; padding: 0; background: none; border: 0; color: var(--brand); }
button.linkbtn:hover { background: none; color: var(--brand-hover); }
button.linkbtn:disabled { color: #a8b0bd; background: none; }

.docpage { max-width: 1136px; margin: 0 auto; }
.docpage > .msg { margin-bottom: 16px; }
.doccard { background: var(--card); border-radius: var(--r-card); padding: 24px; margin-bottom: 16px; }
.doccard .dc-title { display: flex; align-items: center; gap: 12px; margin: 0 0 20px; }
.doccard .dc-title h3 { font-size: 16px; font-weight: 600; margin: 0; color: var(--ink); }
.doccard .dc-title .grow { flex: 1 1 auto; }
.doccard .dc-sub { font-size: 16px; font-weight: 600; margin: 28px 0 16px; color: var(--ink); }

.docgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 272px)); column-gap: 90px; row-gap: 24px; }
.docgrid > .f { min-width: 0; }
.docgrid > .f.all { grid-column: 1 / -1; }
.docgrid label { display: block; font-size: 14px; color: var(--ink-2); margin: 0 0 8px; }
.docgrid label .req { color: var(--bad); margin-left: 4px; }
.docgrid select, .docgrid input { width: 100%; }
.docgrid .f.bad input, .docgrid .f.bad select, .docgrid .f.bad textarea { border-color: var(--bad); }
.docgrid .f .why { color: var(--bad); font-size: 12px; margin-top: 4px; }
@media (max-width: 1100px) { .docgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; } }
@media (max-width: 560px) { .docgrid { grid-template-columns: 1fr; } }

/* A long text box with its count in the corner, as Jifeng draws it. */
.counted { position: relative; }
.counted textarea {
  width: 100%; min-height: 64px; resize: vertical; display: block;
  padding: 6px 10px 22px; border: 1px solid var(--line-mid); border-radius: var(--r-ctl);
  font: inherit; color: var(--ink); background: #fff;
}
.counted textarea.tall { min-height: 100px; }
.counted .count { position: absolute; right: 12px; bottom: 6px; font-size: 12px; color: var(--ink-faint); pointer-events: none; }

/* Tables inside a document: the heading on the page grey, inputs in cells. */
table.doctable { width: 100%; border-collapse: collapse; table-layout: fixed; }
table.doctable th {
  position: static; height: 40px; padding: 0 10px; text-align: left;
  background: var(--page); color: var(--ink); font-weight: 600; font-size: 14px; white-space: nowrap;
}
table.doctable th .req { color: var(--bad); margin-left: 4px; }
table.doctable td { padding: 12px 10px; border-bottom: 1px solid var(--rule); vertical-align: middle; overflow-wrap: anywhere; }
table.doctable td.top { vertical-align: top; }
table.doctable td input[type="text"] { height: 32px; }
table.doctable tr.bad td { background: var(--bad-bg); }
table.doctable .none { text-align: center; color: var(--ink-faint); height: 72px; }
table.doctable .dims { display: flex; }
table.doctable .dims input { border-radius: 0; margin-left: -1px; min-width: 0; text-align: left; }
table.doctable .dims input:first-child { border-radius: var(--r-ctl) 0 0 var(--r-ctl); margin-left: 0; }
table.doctable .dims .unit {
  flex: 0 0 auto; display: inline-flex; align-items: center; padding: 0 10px; margin-left: -1px;
  border: 1px solid var(--line-mid); border-radius: 0 var(--r-ctl) var(--r-ctl) 0; background: #f5f7fa; color: var(--ink-dim);
}
table.doctable .packline { display: grid; grid-template-columns: 1fr 1fr 110px 40px; gap: 10px; align-items: center; min-height: 40px; }
table.doctable .packline + .packline { margin-top: 4px; }

/* Jifeng's on/off switch, for "relabel?" on a line. */
.switch { position: relative; display: inline-block; width: 36px; height: 20px; vertical-align: middle; }
.switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.switch span { position: absolute; inset: 0; border-radius: 10px; background: #c9cdd4; cursor: pointer; transition: background .15s; }
.switch span::after { content: ""; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: left .15s; }
.switch input:checked + span { background: var(--brand); }
.switch input:checked + span::after { left: 18px; }

/* A group heading inside the status rail: Jifeng's "| Drafts" and "| Orders". */
.qrail-head {
  position: relative;
  display: flex; align-items: center; height: 34px; padding: 0 10px;
  font-size: 12px; color: var(--ink-dim); background: #fff; border-bottom: 1px solid var(--line);
}
/* Same as the rail title: aligned text, the bar in the gutter. */
.qrail-head::before { content: ""; position: absolute; left: 3px; top: 11px; width: 2px; height: 12px; background: var(--brand); }
/* Under the screen it belongs to when the group's other screens share the rail
   (list-kit.js railGroup): indented with the states it heads. */
.qrail-head.qrail-sub { padding-left: 28px; }
.qrail-head.qrail-sub::before { left: 21px; }

/* A document's figures down its row: "Created 2026-..., Submitted 2026-...". */
.stamps { display: grid; grid-template-columns: auto 1fr; column-gap: 8px; row-gap: 2px; font-size: 14px; }
.stamps dt { color: var(--ink-faint); }
.stamps dd { margin: 0; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.subline { color: var(--ink-faint); font-size: 12px; }
.subline b { color: var(--ink-2); font-weight: 400; }

/* Four to a row: Jifeng's inbound form puts warehouse, arrival, transport and
   the seller's own number on one line. A field that wants two says span2. */
.docgrid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 36px; }
.docgrid > .f.span2 { grid-column: span 2; }
@media (max-width: 1100px) { .docgrid.four { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── The packing grid: a row per SKU, a column per box ────────────────
 * Jifeng's inbound "product packing" (help 7109110603): numbered box chips
 * across the top, a count in each cell, then a totals row and a weight and a
 * size under every box. Wide orders scroll sideways; the first column stays. */
.gridwrap { overflow-x: auto; }
table.doctable.packgrid { table-layout: auto; width: max-content; min-width: 100%; }
table.doctable.packgrid th.who, table.doctable.packgrid td:first-child { min-width: 220px; position: sticky; left: 0; background: #fff; z-index: 1; }
table.doctable.packgrid thead th.who { background: var(--page); }
table.doctable.packgrid .boxcol { width: 96px; min-width: 96px; text-align: center; }
table.doctable.packgrid .boxcol input { width: 76px; text-align: center; }
table.doctable.packgrid .boxno {
  display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 0 6px;
  border-radius: 4px; background: #dbe7ff; color: var(--brand); font-weight: 600; font-size: 12px;
}
table.doctable.packgrid th .linkbtn { margin-left: 4px; color: var(--ink-faint); }
table.doctable.packgrid tr.gridfoot td { background: #f7f8fa; }
table.doctable.packgrid tr.gridfoot td:first-child { background: #f7f8fa; }
table.doctable.packgrid .dims3 input { width: 76px; display: block; margin: 0 auto 4px; height: 26px; }
table.doctable.packgrid th.ops { width: 70px; text-align: center; }
.addbox {
  width: 30px; height: 26px; padding: 0; border-radius: 4px; font-size: 18px; line-height: 1;
  background: #fff; color: var(--brand); border: 1px solid var(--brand);
}
.addbox:hover { background: var(--brand-pale); color: var(--brand-hover); }
button.linkbtn.danger { color: var(--bad); }
.queue-main { min-width: 0; padding: 16px; }
.queue-main > .card:first-child, .queue-main > .msg:first-child { margin-top: 0; }

/* A SKU or a code as a small tag, on a work order. */
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 0 8px; height: 22px; border-radius: 4px; background: var(--brand-wash); color: var(--brand); font-size: 12px; margin: 0 4px 4px 0; }
/* A money box with its currency in a grey cell on the right, Jifeng's
   "0 | CNY": the currency belongs to the seller, so it is read, not typed. */
.suffixed { display: flex; }
.suffixed input { flex: 1 1 auto; min-width: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.suffixed .suffix { flex: 0 0 auto; display: inline-flex; align-items: center; padding: 0 12px; height: 32px; border: 1px solid var(--line-mid); border-left: 0; border-radius: 0 6px 6px 0; background: var(--page); color: var(--ink-dim); font-size: 12px; }

/* A document's conversation (web/lib/thread.js), Jifeng's communication
   record: the messages, then one box with the paperclip, the 0 / 600 counter
   and a round send button in its bottom edge. The reader's own side sits on
   the right in the brand wash, the other side on the left in the page grey,
   so a thread reads as two voices. */
.talk-list { min-height: 180px; max-height: 420px; overflow-y: auto; padding: 8px 4px 12px; display: flex; flex-direction: column; gap: 4px; }
.talk-none { margin: auto; color: var(--ink-faint); }
/* "No time now" (2026-09-27): mark unread / follow up, one quiet line over
   the messages, right-aligned so it never reads as part of the talk. */
.talk-marks { display: flex; justify-content: flex-end; gap: 16px; padding: 4px 4px 6px; font-size: 12px; }
.talk-marks .linkbtn { font-size: 12px; }
.talk-flag.on { color: #e8590c; font-weight: 600; }
.msgs-main .talk-marks { padding: 6px 20px; border-bottom: 1px solid var(--line); }
/* Marked unread: the messages fold behind one line until somebody reads it. */
.talk.parked .talk-list, .talk.parked .talk-box, .talk.parked .talk-marks { display: none; }
.talk-parked { padding: 16px 4px; color: var(--ink-dim); }
.msgs-flag { display: inline-flex; vertical-align: -1px; margin-right: 4px; color: #e8590c; }
/* A date between days, centred and quiet. */
.talk-day { align-self: center; margin: 10px 0 6px; font-size: 12px; color: var(--ink-faint); }
.talk-day span { display: inline-block; padding: 0 10px; border-radius: 10px; background: rgba(17, 22, 34, 0.05); line-height: 20px; }
/* One message: their side on the left under a round initial, this side on
   the right in the brand colour; the sender and time only when it changes. */
.talk-msg { display: flex; align-items: flex-start; gap: 10px; max-width: 78%; align-self: flex-start; }
.talk-msg.first { margin-top: 8px; }
.talk-msg.mine { align-self: flex-end; flex-direction: row-reverse; }
.talk-av { flex: 0 0 32px; width: 32px; height: 32px; border-radius: 50%; background: #e8efff; color: var(--brand); font-size: 14px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
.talk-av.blank { visibility: hidden; }
.talk-col { min-width: 0; display: flex; flex-direction: column; }
.talk-msg.mine .talk-col { align-items: flex-end; }
.talk-who { font-size: 12px; color: var(--ink-faint); margin-bottom: 4px; line-height: 18px; }
.talk-who b { color: var(--ink-2); font-weight: 500; }
.talk-who .talk-at { margin-left: 4px; }
.talk-bubble { padding: 8px 12px; border-radius: 4px 12px 12px 12px; background: #fff; border: 1px solid var(--line); color: var(--ink); max-width: 100%; }
.talk-msg.mine .talk-bubble { border-radius: 12px 4px 12px 12px; background: var(--brand); border-color: var(--brand); color: #fff; }
.talk-msg.mine .talk-file, .talk-msg.mine .talk-file .subline { color: #fff; }
.talk-body { white-space: pre-wrap; word-break: break-word; }
.talk-pic { display: inline-block; margin-top: 6px; }
.talk-pic img { display: block; max-width: 220px; max-height: 160px; border-radius: 6px; border: 1px solid var(--line); }
.talk-file { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; }
.talk-body + .talk-pic, .talk-body + .talk-file { margin-top: 6px; }
.talk-bubble > .talk-pic:first-child, .talk-bubble > .talk-file:first-child { margin-top: 0; }
.talk-box { border: 1px solid var(--line-mid); border-radius: 8px; padding: 10px 14px 8px; }
.talk-box:focus-within { border-color: var(--brand); }
.talk-box textarea { border: 0; padding: 0; min-height: 40px; width: 100%; resize: vertical; box-shadow: none; outline: none; }
.talk-foot { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.talk-foot .grow { flex: 1 1 auto; }
.talk-foot .linkbtn { display: inline-flex; align-items: center; gap: 4px; }
.talk-foot .count { position: static; font-size: 12px; color: var(--ink-faint); }
.talk-picked { font-size: 12px; color: var(--ink-2); }
.talk-send { width: 24px; height: 24px; min-width: 0; padding: 0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; }

/* ── The settings screens: modules, not a list one function to a line ────
 *
 * Reported on 2026-09-19, about /settings: "one function to a line is really
 * tiring, and it doesn't look good -- module design isn't like this". Both
 * halves of that were true and they were different faults.
 *
 * The switches sat in a table, the name hard left and the control hard right,
 * with about a thousand pixels of nothing between them at 1366. Four lines of
 * business took four full-width rows to say four words each, and reading one
 * meant tracking across the card and back. Jifeng's permission editor (help
 * 7106210521) is the shape this borrows: the options are laid out in columns,
 * each control against its own name, so a block of them is read as a block.
 *
 * ⚠ **The block, not the switch, carries the answer.** A row of grey switches
 * is read one at a time; a lit tile is read at a glance, which is what makes
 * "which lines are on" a question the eye answers rather than the finger. */
.set-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
.set-opt {
  display: flex; align-items: center; gap: 10px; margin: 0;
  min-height: 44px; padding: 7px 14px;
  border: 1px solid var(--line); border-radius: var(--r-ctl);
  background: var(--card); cursor: pointer;
}
.set-opt:hover { border-color: var(--line-mid); }
/* On is the brand wash the rest of the site uses for a chosen thing (.card.on). */
.set-opt:has(input:checked) { border-color: var(--brand); background: var(--brand-pale); }
.set-opt .set-label { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 6px; }
/* ⚠ **Wraps rather than truncates.** The name of the line is the whole point of
   the tile, and Spanish's "Correo directo transfronterizo" does not fit in
   220px -- it came back as "Correo directo tra...", which is a tile you have to
   already know the answer to. A grid row is as tall as its tallest tile, so a
   name on two lines costs a few pixels and nothing else. */
.set-opt .set-label b { font-weight: 400; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.set-opt:not(:has(input:checked)) .set-label b { color: var(--ink-dim); }
.set-opt .pill { margin: 0; flex: 0 0 auto; }

/* The name and the mark over the door are one row of fields, not two cards.
   The mark is a 48px square and the name is a text field, so they sit beside
   each other and the row ends where the fields end -- a settings card that
   runs the width of the page to hold two controls is the same complaint.

   ⚠ **Top-aligned, because the fields are not the same height.** The logo well
   is 48 and an input is 32, so aligning their bottoms puts the two labels on
   different lines -- which reads as two rows that failed to line up rather
   than as one row. Their tops are what the eye follows down a form. */
.set-brand { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 12px 16px; }
.set-brand > div { flex: 0 0 auto; }
.set-brand .set-namefield { flex: 1 1 260px; max-width: 420px; min-width: 0; }
/* A button in this row has no label over it, so it needs the space one takes:
   12px of label over a 4px margin. Written out rather than guessed -- see the
   `label` rule near the top. */
.set-brand .set-act { padding-top: 20px; }
.set-mark { display: flex; align-items: center; gap: 8px; }

/* ── The order-rule editor ───────────────────────────────────────────
   Jifeng's shape (help centre 7109050603): the rule down the left, what it
   does down the right. The reading order is the point -- left is "which
   orders", right is "then what" -- and it replaced a form that put nine
   condition boxes in three rows with nothing saying which of them were in use.

   ⚠ **The left column is a fixed 340 and does not share the width.** It holds
   a name, two radio groups and a tick list, none of which get better at 600px;
   given a share it stretches and the pair stops reading as two columns of one
   form. */
.rulegrid { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 16px; align-items: start; }
@media (max-width: 1000px) { .rulegrid { grid-template-columns: minmax(0, 1fr); } }

/* One and two column runs of `.docgrid`, which is three by default. Named
   rather than written inline on each block: the same two shapes were being
   spelled out as `grid-template-columns:repeat(2,minmax(0,1fr))` in every
   screen that wanted them. */
.docgrid.one { grid-template-columns: minmax(0, 1fr); row-gap: 20px; }
.docgrid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; row-gap: 18px; }
.docgrid textarea {
  width: 100%; min-height: 62px; resize: vertical; display: block;
  padding: 6px 10px; border: 1px solid var(--line-mid); border-radius: var(--r-ctl);
  font: inherit; color: var(--ink); background: #fff;
}
.docgrid textarea:focus { outline: none; border-color: var(--brand); }

/* A radio on its own line, the way Jifeng stacks "Enabled / Disabled" and the
   two scopes. ⚠ No width or height on the input: see the tick-box note
   further up -- Chrome draws a native control at its own size and scales it,
   and the border and the corners do not come with it. */
/* ⚠ Written under `.docgrid` on purpose. That block sets `label { display:
   block }` and `input { width: 100% }` for the text fields, and a radio caught
   by both is a full-width block whose dot lands at the far end of the row,
   miles from the word it belongs to -- which is exactly how this drew the
   first time. */
.docgrid .rule-radio,
.docgrid .rule-pick { display: flex; align-items: center; gap: 8px; min-height: 26px; margin: 0; font-size: 14px; cursor: pointer; }
.docgrid .rule-radio { color: var(--ink); }
.docgrid .rule-pick { color: var(--ink-2); }
.docgrid .rule-radio input,
.docgrid .rule-pick input { width: auto; margin: 0; flex: 0 0 auto; accent-color: var(--brand); }
/* Enabled / Disabled is a pair of short words and reads as one answer on one
   line; the two scopes are sentences and stack. */
.docgrid .f.inline-radios { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 20px; }
.docgrid .f.inline-radios > label:first-child { flex: 1 0 100%; }

/* The tick list under "orders matching the conditions below". Indented under
   the radio it belongs to, so it reads as that answer's detail rather than as
   a third question. */
.rule-picks { margin: 10px 0 0 22px; }
.rule-pickgroup + .rule-pickgroup { margin-top: 14px; }
.docgrid .rule-pickhead { display: block; font-size: 14px; font-weight: 600; color: var(--ink); margin: 0 0 4px; }

/* The priority cell: the number the rule is read by, then the three ways to
   move it. The number is tabular so column one does not shuffle at 10. */
.rule-seq { display: inline-block; min-width: 20px; font-variant-numeric: tabular-nums; color: var(--ink); }

/* ── An order is a block, not a row ──────────────────────────────────

   ⚠ **Measured off Jifeng, 2026-09-20**, from `7106150521_0.png` (dropship
   outbound), `O_oms_out.png` (the same list with the goods column open) and
   the platform-order page of help article 7109060603. Every order list they
   draw is two rows per order: a grey band across the full width holding the
   numbers the order is known by, then one row of columns whose first cell is
   the goods.

   Read out of the screenshots rather than guessed:

     band ground        #F2F5FA, 13px, ~36px tall, values #1D2129 on #86909C
     band facts         separated by ~44px, label and value 6px apart
     goods line         52px tile, code on line 1, name under it in grey,
                        quantity hard right in bold
     goods separator    a hairline between SKU lines, none after the last
     block separator    the page ground showing through, ~8px

   ⚠ **The band is a `td` spanning every column**, so it cannot be knocked out
   of step with the header the way a parallel div would.

   ⚠ **Every class here is prefixed `g`, and the detail row is `gbody` rather
   than `grow`.** `.grow` already means "the flexible box in a bar" in six
   places in this stylesheet -- the same trap `.l-row` was named around after
   `.row` laid a whole page of cards out as one line, and the same one the
   product block hit when it was called `.lines`. */

table.doclist tr.gband > td {
  background: #f2f5fa;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 8px 16px;
  font-size: 14px;
  line-height: 22px;
  white-space: normal;
}
/* ⚠ **The gap between two orders is the page showing through**, drawn as a
   fat top border on the band rather than as an empty row: an empty row is a
   row a screen reader announces and a `colspan` somebody has to keep in step
   with the header. Every band but the first in the body gets it, which is
   exactly "between two orders". */
table.doclist tbody > tr.gband:not(:first-child) > td { border-top: 8px solid var(--page); }
/* The facts run left to right and wrap as a paragraph does. ⚠ Not a flex row
   with `nowrap`: a band of six identifiers on a 1200px window has to be able
   to go onto a second line, and a flex row would instead squeeze every one of
   them until the references were unreadable -- which is the failure this whole
   change is answering, one level down. */
tr.gband > td .gb-f { display: inline-block; margin-right: 34px; }
tr.gband > td .gb-f i { font-style: normal; color: var(--ink-dim); margin-right: 6px; }
tr.gband > td .gb-f b { font-weight: 400; color: var(--ink); }
tr.gband > td .gb-tick { display: inline-block; margin-right: 14px; vertical-align: middle; }
tr.gband > td .gb-tick input { margin: 0; vertical-align: middle; }
/* Where the order came from, hard right, as Jifeng prints "OMS created". */
tr.gband > td .gb-src { float: right; color: var(--ink-dim); }

table.doclist tbody > tr.gbody > td {
  vertical-align: top;
  border-bottom: 1px solid var(--line);
  padding: 10px 16px;
}
/* ⚠ Vertical hairlines between the columns of the detail row, which Jifeng has
   and our tables do not. Without them the goods block on the left reads as
   though it owns the whole row. */
table.doclist tbody > tr.gbody > td + td { border-left: 1px solid var(--line); }

/* ── The goods cell ──────────────────────────────────────────────── */

.gd { display: flex; align-items: flex-start; gap: 10px; padding: 6px 0; }
.gd + .gd { border-top: 1px solid var(--line); }
.gd:first-child { padding-top: 0; }
.gd:last-child { padding-bottom: 0; }
.gd .gd-t { min-width: 0; flex: 1 1 auto; line-height: 20px; }
.gd .gd-sku { color: var(--ink); word-break: break-word; }
.gd .gd-q { flex: 0 0 auto; color: var(--ink); font-weight: 400; padding-left: 8px; }
.gd-tile {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line);
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--line-bold);
  background: #fbfcfe;
}

.gw { color: var(--ink); }

/* ── The operations cell ─────────────────────────────────────────── */

.gops { display: inline-flex; gap: 4px; }
button.gop {
  width: 28px;
  height: 28px;
  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;
}
button.gop:hover { color: var(--brand); border-color: var(--brand); }

/* The shop key form, in its dialog: one field to a line, label above, the
   save button at the foot on the right -- the shape of Jifeng's own
   authorise-shop dialog. The form's markup is a row of flex boxes from when it
   opened under a table row; here they stack. */
.shopform .row { display: grid; grid-template-columns: 1fr; gap: 12px; }
.shopform .row > div { flex: none; }
.shopform .row > div:last-child:has(button[type="submit"]) { justify-self: end; margin-top: 4px; }
.shopform input[type="text"], .shopform input[type="password"], .shopform select { width: 100%; }

/* A marketplace's mark beside its name on the shop list: 18px, the size of
   Jifeng's own rail icons, and never squeezed by a long name next to it. */
.pname { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.pmark { flex: 0 0 auto; width: 18px; height: 18px; border-radius: 4px; object-fit: contain; }
.pmark-letter {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--brand-wash, #eef3ff); color: var(--brand, #0050f0);
  font-size: 12px; font-weight: 600;
}

/* The shop list's two status columns: a dot and a word, as Jifeng draws them
   (green on, grey off) -- not the pill every other list uses, measured off
   /web/shop/auth/<platform> on 2026-09-23. */
.sdot { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.sdot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--ink-faint, #a0a8bd); }
.sdot.on::before { background: #00b42a; }
/* The logistics pairing: platform name, the two-way mark, our carrier, remove. */
.pairgrid { display: grid; grid-template-columns: minmax(0, 1fr) 20px minmax(0, 1fr) 32px; gap: 8px; align-items: center; }
.pairgrid > span.sub { text-align: center; }
.pairgrid > span.sub:first-child, .pairgrid > span.sub:nth-child(3) { text-align: left; }
/* A list of tick boxes in a dialog: one per line, the id in grey after the name. */
.checklist { display: flex; flex-direction: column; gap: 6px; max-height: 240px; overflow-y: auto; }
.checklist label.check { display: flex; align-items: center; gap: 8px; font-weight: normal; }
.checklist label.check input { width: auto; }

/* The shop list's help: numbered steps, then one short section per dialog.
   The section a "how do I...?" link opened at is marked, so the eye lands on
   it after the scroll. */
.helpdoc section { padding: 10px 0; border-top: 1px solid var(--line); }
.helpdoc section:first-child { border-top: 0; padding-top: 0; }
.helpdoc section.here { background: var(--brand-wash, #eef3ff); margin: 0 -12px; padding: 10px 12px; border-radius: 6px; }
.helpdoc h3 { font-size: 14px; margin: 0 0 6px; }
.helpdoc p { margin: 6px 0 0; line-height: 1.6; }
.helpsteps { margin: 0; padding-left: 20px; line-height: 1.8; }
.helplink { font-size: 12px; margin-right: auto; }

/* The warehouse mapping page's line above the table: the help link on the
   left, Sync warehouses on the right, where Jifeng keeps it. */
.maptools { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }

/* Settings: the directory switch, a tick box and its sentence on one line. */
.set-listed { display: flex; align-items: center; gap: 8px; font-weight: normal; }
.set-listed input { width: auto; }
/* The seller's "my warehouses": a card's own heading in its toolbar, and the
   directory as a grid of cards -- a warehouse is chosen by looking, not by
   reading down a column. */
.cardhead { margin: 0; font-size: 14px; font-weight: 600; }
.whgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.whcard { border: 1px solid var(--line); border-radius: 8px; padding: 14px; display: flex; flex-direction: column; gap: 8px; }
.whlines { display: flex; flex-wrap: wrap; gap: 6px; min-height: 22px; }
.whlines .tag { font-size: 12px; padding: 1px 8px; border-radius: 4px; background: var(--brand-wash, #eef3ff); color: var(--brand, #0050f0); }
/* Warehouse reviews (web/oms/oms-warehouses.js, the customers screen's
   reviews rail). One gold for a star everywhere; an unlit one is the line
   colour, so a row of five always reads as five. */
.stars { display: inline-flex; gap: 1px; color: #d9dde6; font-size: 14px; line-height: 1; vertical-align: -1px; }
.stars .on { color: #faad14; }
.stars .half { background: linear-gradient(90deg, #faad14 50%, #d9dde6 50%); -webkit-background-clip: text; background-clip: text; color: transparent; }
.stars.pick button { height: auto; min-width: 0; padding: 0 2px; background: none; border: 0; font-size: 24px; color: #d9dde6; cursor: pointer; }
.stars.pick button.on { color: #faad14; }
.whrate { font-size: 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.revhead { padding: 0 0 12px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.revaspects { display: flex; gap: 16px; margin-top: 6px; font-size: 12px; color: var(--ink-soft, #5b6475); }
.revitem { padding: 12px 0; border-bottom: 1px solid var(--line); }
.revitem:last-child { border-bottom: 0; }
.revbody { margin-top: 6px; white-space: pre-wrap; }
.revreply { margin-top: 8px; padding: 8px 10px; background: #f5f7fb; border-radius: 6px; font-size: 12px; white-space: pre-wrap; }
.revrate { display: flex; align-items: center; justify-content: space-between; padding: 4px 0; }
.whact { margin-top: auto; }

/* A typed filter's suggestions, drawn by us rather than by a <datalist>: the
   browser's own list came up black and outside the box on a Mac (2026-09-23,
   the shop list). Under the box, the site's colours, the box's width. */
.suggestbox { position: relative; }
.suggest {
  position: absolute; top: calc(100% + 4px); left: 0; min-width: 100%; z-index: 30;
  margin: 0; padding: 4px 0; list-style: none; background: #fff;
  border: 1px solid var(--line); border-radius: 6px; box-shadow: 0 6px 16px rgba(0, 31, 77, 0.08);
  max-height: 240px; overflow: auto; display: none;
}
.suggestbox:focus-within .suggest { display: block; }
.suggest li { padding: 6px 12px; cursor: pointer; white-space: nowrap; font-size: 14px; }
.suggest li:hover { background: var(--brand-pale, #eef3ff); color: var(--brand, #0050f0); }

/* US West / Central / East on the warehouse directory (2026-09-23). */
.regiontabs { display: inline-flex; gap: 6px; margin-left: 12px; }
.regiontabs button.on { border-color: var(--brand, #0050f0); color: var(--brand, #0050f0); background: var(--brand-pale, #eef3ff); }
/* Service providers (web/oms/oms-providers.js): the categories are more than
   the three regions, so the row wraps rather than squeezing each chip. */
.provtabs { flex-wrap: wrap; margin-left: 0; }
.provtabs button, .provlist td button { white-space: nowrap; }
.provlist td:first-child { min-width: 140px; }
.tag[class*="region-"] { display: inline-block; font-size: 12px; padding: 1px 8px; border-radius: 4px; }
.tag.region-west { background: #e8f3ff; color: #0050f0; }
.tag.region-central { background: #fff4e5; color: #b25e09; }
.tag.region-east { background: #e9f7ef; color: #1a7f45; }

/* ── Platform orders, laid out as Jifeng's (2026-09-23) ─────────────────
   web/oms/oms-platform-orders.js. Two rows of dropdown filters, the name of
   what was bought on top of its code, and the three-line time cell. */

/* The goods cell: the name on top, the code small under it -- and the codes
   this system made up for a listing with no SKU smaller and fainter still. */
/* ⚠ Normal weight, grey (the boss, 2026-09-23: "the title is too big,
   it feels crowded"); 14px since 2026-09-25 with the rest of the site, so the
   title no longer reads a size smaller than the band above it. A listing title is a sentence -- "Personalized Wedding
   Neon Sign, Light Up Name LED Sign for Wall Decor, Bedroom..." -- and at 13px
   medium it outweighed every other cell on the row. Jifeng's goods lines are
   13px codes over 12px grey words.
   ⚠ **One line, not two** (boss, 2026-09-25: "the title doesn't need to be
   that long, it takes too much room"). The whole sentence is in the tooltip. */
.gd .gd-n { color: var(--ink-2, #3a4256); font-size: 14px; line-height: 22px; font-weight: 400;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gd .gd-code { margin: 0; overflow-wrap: anywhere; font-size: 12px; line-height: 18px; }
.gd .gd-code.made { font-size: 12px; color: var(--ink-faint, #9aa3b5); }

/* The filter band: every dropdown is Jifeng's 32px pill, sized to its words. */
.pofilters { align-items: center; gap: 8px; }
.pofilters .fgroup { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.pofilters .fgroup input[type="date"] { width: 140px; height: 32px; }
.pofilters .fnums { flex: 1 1 360px; min-width: 300px; }
.pofilters .fnums input[type="search"] { flex: 1 1 auto; min-width: 120px; height: 32px; }

/* The rail's get-tracking heading and the three piles under it. */
.po-rail-head { padding: 0 10px; height: 40px; display: flex; align-items: center; justify-content: space-between;
  flex: 0 0 auto; color: var(--ink); font-size: 14px; }
.po-rail-head .n { color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.qrail-row.po-sub { padding-left: 24px; }

/* Label over value, as Jifeng stacks ordered / paid / left to ship. */
.po-t { display: flex; flex-direction: column; line-height: 20px; }
.po-t i { font-style: normal; color: var(--ink-dim); font-size: 12px; }
.po-t + .po-t { margin-top: 2px; }
.po-no { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
/* The order number is the thing a person is holding when they open this
   screen (the boss, 2026-09-23: "the order number matters, make it blue"),
   and like Jifeng's blue numbers it opens the order. */
button.po-link { border: 0; background: none; padding: 0; height: auto; min-height: 0; font: inherit;
  color: var(--brand, #0050F0); cursor: pointer; }
button.po-link:hover { text-decoration: underline; }
/* Label then value on one line; the value wraps under itself, not under the label. */
.po-l { display: flex; gap: 6px; line-height: 22px; align-items: baseline; }
.po-l > i { font-style: normal; color: var(--ink-dim); font-size: 12px; flex: 0 0 auto; }
.po-l > span { min-width: 0; overflow-wrap: anywhere; }
.po-l .status { margin: 0; }
/* 10px a side, not the list's 16: nine columns of dates and numbers do not
   fit in what 16 leaves them, and a date broken over two lines is unreadable. */
table.po-table > thead > tr > th,
table.po-table > tbody > tr.gbody > td { padding-left: 10px; padding-right: 10px; }
.po-name { font-weight: 400; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Jifeng's filter pill, measured off their platform-order page: as wide as
   its words, 32px, 4px 12px, radius 6, a 1px #D5DCEE ring, grey words; the
   real select lies over it invisible. See pill() in oms-platform-orders.js. */
.pofilters { column-gap: 12px; row-gap: 12px; }
.fpill {
  position: relative; display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  height: 32px; padding: 0 12px; border-radius: 6px; background: #fff;
  box-shadow: inset 0 0 0 1px #D5DCEE; color: #7E89A8; font-size: 14px; white-space: nowrap;
  cursor: pointer; margin: 0; max-width: 220px;
}
.fpill:hover { box-shadow: inset 0 0 0 1px var(--brand); }
.fpill.on { color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand); }
.fpill .fpill-t { overflow: hidden; text-overflow: ellipsis; }
.fpill .fpill-c { flex: 0 0 auto; }
.fpill select { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; margin: 0; padding: 0; border: 0; }
.fpill.bare { box-shadow: none; color: var(--ink); border-radius: 0; border-right: 1px solid #D5DCEE; height: 30px; }
/* The date range and the number box are one ringed control each. */
.pofilters .fgroup.fdate,
.pofilters .fgroup.fnums { box-shadow: inset 0 0 0 1px #D5DCEE; border-radius: 6px; height: 32px; gap: 0; background: #fff; padding-right: 2px; }
.pofilters .fgroup.fdate input[type="date"],
.pofilters .fgroup.fnums input[type="search"] { border: 0; height: 30px; box-shadow: none; background: transparent; }
.pofilters .fgroup.fdate input.fdate-in { width: 118px; border: 0; height: 30px; box-shadow: none; background: transparent; padding: 0 6px; }
.pofilters .fgroup.fdate .fcal { color: #7E89A8; margin-left: 10px; flex: 0 0 auto; }
/* Jifeng's second row starts with the dates; the number box is a fixed
   width there, not the rest of the row. */
.pofilters .fbreak { flex: 0 0 100%; height: 0; margin-top: -12px; }
.pofilters .fgroup.fnums { flex: 0 0 auto; width: 460px; min-width: 0; }
.pofilters .fgroup.fnums .toolbtn { border: 0; }
.pofilters .fgroup.fnums input + .fpill.bare { border-right: 0; border-left: 1px solid #D5DCEE; }

/* ── The after-sale dialog (web/lib/aftersale.js, 2026-09-23) ─────────── */
.as-head { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px; padding: 10px 12px; background: #F3F5FC; border-radius: 6px; margin-bottom: 12px; font-size: 14px; }
.as-head i { font-style: normal; color: var(--ink-dim); margin-right: 6px; }
.as-modes { display: flex; gap: 20px; margin: 10px 0; }
.as-modes label { display: inline-flex; align-items: center; gap: 6px; margin: 0; cursor: pointer; }
.as-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
.as-row { display: flex; align-items: center; gap: 8px; margin: 0; }
.as-row select, .as-row .as-sku { flex: 1 1 auto; min-width: 0; }
.as-row .as-sku { display: flex; flex-direction: column; font-size: 14px; }
.as-row .as-sku .sub { margin: 0; font-size: 12px; }
.as-row input[type="number"] { width: 80px; flex: 0 0 auto; }
.as-dims { display: flex; align-items: center; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.as-dims .unitswitch { flex: 0 0 auto; white-space: nowrap; }
.as-dims .sub { width: 100%; margin: 0; }
.as-dims input { width: 100px; }
.as-reason { display: flex; gap: 8px; margin-top: 14px; }
.as-reason select { width: 200px; flex: 0 0 auto; }
.as-reason input { flex: 1 1 auto; }

/* The platform-order row's actions, in words, one under another. */
.po-ops { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
/* "More" on a platform-order row: a small menu under the word, closed until
   asked. A details element, so it needs no script to open and closes with
   the row's redraw. */
.po-more { position: relative; }
.po-more > summary { list-style: none; cursor: pointer; }
.po-more > summary::-webkit-details-marker { display: none; }
.po-more > summary::after { content: " \25BE"; font-size: 12px; }
.po-more .po-menu {
  position: absolute; right: 0; top: 22px; z-index: 20; min-width: 96px;
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  background: #fff; border: 1px solid var(--line, #E5EAF8); border-radius: 6px;
  box-shadow: 0 6px 16px rgba(0, 31, 77, 0.12); padding: 4px 0;
}
.po-more .po-menu .po-link { text-align: left; padding: 6px 14px; color: var(--ink); white-space: nowrap; }
.po-more .po-menu .po-link:hover { background: #F3F5FC; text-decoration: none; color: var(--brand); }

/* The resend screen's reason counts: the statistics the reason is kept for. */
.rs-reasons { display: inline-flex; gap: 6px; margin-left: 16px; flex-wrap: wrap; }

/* A row whose first cell holds a tick box is itself the thing to click
   (web/lib/tablekit.js, onRowClick). */
tbody tr:has(> td:first-child input[type="checkbox"]:not(:disabled)) { cursor: pointer; }
tbody tr:has(> td:first-child input[type="checkbox"]:not(:disabled)):hover > td { background: var(--row-hover, #f6f8fe); }

/* A copy button beside a number (web/lib/copy.js): out of sight until the
   pointer is over its cell, so a list of numbers does not become a list of
   buttons. */
button.cp-btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: 20px; min-width: 0; padding: 0 2px; margin-left: 4px;
  border: 0; border-radius: 4px; background: none; color: #313a52;
  vertical-align: -4px; cursor: pointer; opacity: 0;
}
/* Measured on Jifeng's parcel list (2026-09-24): a 14px icon in #313a52, 4px
   after the number, shown on the row's hover. Ours is 16px (boss: "a bit
   bigger"), and shown when the pointer is anywhere on the row. */
button.cp-btn svg { width: 16px; height: 16px; }
tr:hover button.cp-btn, td:hover button.cp-btn, button.cp-btn:focus-visible, button.cp-btn.done { opacity: 1; }
button.cp-btn:hover { background: none; color: var(--brand); border: 0; }
button.cp-btn.done { color: var(--ok); }

/* The factory to-do conversation in the OMS (web/oms/oms-factory-tasks.js):
   the office's own words on the right, the workshop's on the left. */
.ft-msg { max-width: 80%; padding: 8px 12px; border-radius: 8px; background: #f3f5fc; font-size: 14px; }
.ft-msg.mine { align-self: flex-end; background: #e8f0ff; }
.clip2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The OMS factory to-do list: fewer, quieter columns (boss, 2026-09-25: less
   to read). The order number is plain, the task is the line to read, the
   workshop's reply is one line with its time; the whole row opens. */
/* Taller rows (boss, 2026-09-25: a little more height reads better). */
.ft-table td, .ft-table td.ft-what { vertical-align: middle; padding-top: 15px; padding-bottom: 15px; }
.ft-row { cursor: pointer; }
.ft-row:hover td { background: #f6f8fe; }
.ft-order { white-space: nowrap; color: var(--ink-2, #3a4256); font-size: 14px; }
/* The task wraps rather than running on (boss, 2026-09-25: narrower, several lines if need be). */
.ft-table td.ft-what { font-size: 14px; color: var(--ink, #111621); white-space: normal; overflow-wrap: anywhere; line-height: 1.5; }
.ft-link, .ft-due { white-space: nowrap; color: var(--ink-faint, #8a93a6); font-size: 12px; }
.ft-due.late { color: #c0392b; font-weight: 600; }
.ft-reply { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
.ft-reply span:first-child { flex: 1 1 auto; min-width: 0; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ft-reply { align-items: center; }
/* A workshop reply the office has not opened: an orange dot and bold words
   (the home page's orange button counts these). */
.ft-unread .ft-reply span:first-child { font-weight: 600; color: var(--ink); }
.ft-unread .ft-reply::before { content: ""; flex: 0 0 8px; height: 8px; border-radius: 50%; background: #e8590c; align-self: center; }
.ft-reply .sub { flex: 0 0 auto; margin: 0; font-size: 12px; color: var(--ink-faint, #8a93a6); }
/* The three buttons under the reply box, as on the supplier's page
   (web/production.html): close in black on the left, photo + send right. */
.ft-acts { display: flex; gap: 10px; align-items: center; }
.ft-acts button { width: 128px; height: 36px; padding: 0; }
.ft-acts .ft-push { margin-left: auto; }
.ft-acts button.ft-black { background: #fff; color: var(--ink, #111621); border: 1px solid var(--ink, #111621); font-weight: 600; }
/* The OMS "new factory to-do" dialog: one field to a row, label on the left. */
.ft-form { display: grid; grid-template-columns: 72px 1fr; gap: 14px 12px; align-items: center; }
.ft-form label { margin: 0; color: var(--ink-2, #3a4256); font-size: 14px; text-align: right; }
.ft-form label[for="nWhat"] { align-self: start; padding-top: 7px; }
.ft-form select, .ft-form input, .ft-form textarea { width: 100%; box-sizing: border-box; }
.ft-form input[type="date"] { width: 180px; }
.ft-form textarea { min-height: 110px; resize: vertical; }
.ft-form .msg { margin: 0; }
/* The OMS supplier to-do detail: facts, the task, the conversation, the reply
   (boss, 2026-09-25: the first layout was a jumble). */
.ft-facts { display: flex; flex-wrap: wrap; gap: 8px 28px; padding: 0 0 14px; border-bottom: 1px solid #e5eaf8; margin-bottom: 14px; }
.ft-facts > div { display: flex; flex-direction: column; gap: 4px; font-size: 14px; }
.ft-facts > div > span:first-child { color: var(--ink-faint, #8a93a6); font-size: 12px; }
.ft-label { color: var(--ink-faint, #8a93a6); font-size: 12px; margin: 0 0 6px; }
.ft-task { background: #f7f9fe; border: 1px solid #e5eaf8; border-radius: 8px; padding: 10px 14px; margin-bottom: 16px; font-size: 14px; line-height: 1.6; }
.ft-thread { display: flex; flex-direction: column; gap: 10px; max-height: 42vh; overflow-y: auto; overflow-x: hidden; padding: 2px; }
.ft-msg { overflow-wrap: anywhere; word-break: break-word; line-height: 1.5; }
.ft-who { color: var(--ink-faint, #8a93a6); font-size: 12px; margin: 0 0 4px; }
.ft-text { white-space: pre-wrap; }
.ft-img { display: block; max-width: 220px; max-height: 220px; border-radius: 6px; margin-top: 4px; }
.ft-replybox { display: grid; gap: 10px; margin-top: 16px; padding: 14px 0 10px; border-top: 1px solid #e5eaf8; }
.ft-picked { border: 1px solid #0050f0; border-radius: 6px; padding: 6px 10px; font-size: 14px; }
.ft-replybox textarea { width: 100%; box-sizing: border-box; resize: vertical; }
.ft-tick { width: 44px; text-align: center; cursor: default; }

/* ⚠ **No bold inside a list** (boss, 2026-09-26: "a lot of lists don't need
   bold, bold looks ugly"). Screens wrap SKUs, channel names and order numbers
   in <strong>/<b> one by one; at 14px on a whole column that is a slab of
   black. One rule here instead of editing forty templates: a row's words are
   all the same weight, and the header (500) is the only thing set heavier.
   Totals in <tfoot> are untouched. */
tbody td strong, tbody td b { font-weight: 400; }

/* The grade at the returns bench (web/pages/in-loose.js): a bench control, so
   bench-sized, and the destruction grade reads red when it is the one on --
   booking a run of good units as scrap is the mistake worth seeing. */
.grade-pick button { height: 40px; padding: 0 22px; font-size: 16px; }
.grade-pick button[data-grade="scrap"].on { background: var(--bad-bg); color: #cc3535; border-color: var(--bad); }
.grade-pick button[data-grade="damaged"].on { background: var(--warn-bg); color: #d56800; border-color: var(--warn); }

/* ---- the super-admin console (web/admin.html) ---------------------------

   ⚠ **The same house as the warehouse console** (boss, 2026-09-26: "the
   super-admin is a mess, no design"). It had a form standing open over a bare
   table on three tabs, a page title and a line of prose on every one, and
   three tables stacked down the directory. Now: a left rail of states where
   the tab has states, then filter bar, list, pager -- with forms in dialogs.
   The console has no tab strip under its bar, so the rail hangs from 40px,
   not the warehouse console's 74. */
body.admin main { padding: 0; }
body.admin .admin-pad { padding: 16px; }
body.admin .qrail { top: 40px; min-height: calc(100vh - 40px); max-height: calc(100vh - 40px); }
body.admin .queue { min-height: calc(100vh - 40px); }
body.admin #vmsg:empty { display: none; }
body.admin #vmsg .msg { margin: 0 0 12px; }
body.admin .msgs { height: calc(100vh - 72px); }
/* The callback URL every marketplace console is given: one line in the
   toolbar, selectable, with its copy button beside it. */
.admin-callback { display: inline-flex; align-items: center; gap: 8px; min-width: 0; font-size: 12px; color: var(--ink-dim); }
.admin-callback code { font-size: 12px; color: var(--ink); background: var(--page); border: 1px solid var(--line); border-radius: 4px; padding: 3px 8px; user-select: all; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 460px; }
.admin-addons { display: flex; flex-wrap: wrap; gap: 4px; }
.admin-addons .pill { margin: 0; }

/* ⚠ **A sortable head in a `jf` table keeps its room for the caret.**
   `table.jf th` (padding 16px all round) outranks `th.sortable` (padding-right
   28px), so on every jf list the caret sat on top of the heading's last
   character -- "In transit" read with a triangle through it (2026-09-26). */
table.jf th.sortable { padding-right: 28px; }

/* Inbound review (2026-09-26): waiting for the warehouse, the same amber as
   every other state that is somebody's turn. */
.status.review { background: var(--warn-bg); color: #d56800; }

/* ---- the two door benches, redrawn (boss, 2026-09-26: "is there a better
   layout for sign-in and despatch?") -------------------------------------

   ⚠ **The answer is the screen.** It was a 118px strip with the rest of the
   left column empty below it -- the one thing read from a metre away was the
   smallest block on the page. Now it is a tall panel, centred, the sentence
   large and the facts under it one to a row (boss: "box 2 of 3 · W1001 ·
   FedEx ... on one line is hard to read"). The side column narrows to what it
   holds -- three figures and a list -- and this sitting's log moves under the
   answer, full width, where a refused box is found by its red row.
   Scoped to `.bench-duo`: the other benches share `.bench-answer` and keep
   theirs. */
.bench-duo { grid-template-columns: minmax(0, 1fr) 300px; }
.bench-duo .bench-answer {
  min-height: 260px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 28px 32px;
}
.bench-duo .bench-answer .answer-text { flex: 0 1 auto; }
.bench-duo .bench-answer .big { font-size: 36px; }
.bench-duo .bench-answer .small { font-size: 18px; margin-top: 10px; }
.bench-duo .bench-answer .answer-fields {
  display: inline-grid;
  margin-top: 18px;
  gap: 8px 18px;
  text-align: left;
}
.bench-duo .bench-answer .answer-field { font-size: 15px; color: inherit; opacity: 0.75; }
.bench-duo .bench-answer .answer-value { font-size: 22px; }
.bench-duo .bench-answer.idle { border: 2px dashed var(--line-mid); background: transparent; font-size: 20px; }
/* The printer's state is a small tag on the label line above the scan box, at
   its right end -- so the box the scanner types into is as wide on this door
   as on the other one, and the tag lines up with nothing it could be taken
   for a button beside. */
.bench-duo .bench-top { position: relative; }
.bench-duo .bench-top > #printer { position: absolute; top: -2px; right: 0; }
.bench-duo .printer-chip { height: 24px; font-size: 12px; padding: 0 10px; border-radius: 12px; max-width: 260px; }
.bench-duo .printer-chip .dot { width: 8px; height: 8px; }
/* Today, stacked: the figure this door is about on top and large. */
.bench-duo .bench-figs { grid-template-columns: 1fr; gap: 0; }
.bench-duo .bench-fig { flex-direction: row; justify-content: space-between; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--line); }
.bench-duo .bench-fig:last-child { border-bottom: 0; }
.bench-duo .bench-fig > i { font-size: 14px; }
.bench-duo .bench-fig.lead > b { font-size: 36px; }
.bench-duo .bench-main > #round .bench-round { margin-top: 16px; }
.bench-duo .bench-main .bench-log { max-height: 420px; }
.bench-duo .bench-main .bench-log li { font-size: 15px; padding: 9px 0; }

/* The sellers' community framed under the OMS menu (web/lib/community-pass.js
   embedCommunity sets the height to the window's bottom edge). */
.cm-frame { display: block; width: 100%; border: 1px solid var(--line, #e5eaf8); border-radius: 8px; background: #fff; }

/* ── The despatch bench's wall: one tile per van (2026-09-27) ─────────────
   A tile is a van, not a carrier -- FedEx Express and FedEx Ground are two
   drivers. See server/src/db/trucks.ts. The figure is gone over due, big enough
   to read from the cage; the list under an open tile is what to go and find
   before that driver leaves. The lit tile is where the last box went. */
.vans { display: flex; flex-direction: column; gap: 8px; }
.van { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-card); }
.van-top {
  display: block; width: 100%; height: auto; padding: 10px 12px; text-align: left;
  background: transparent; border: 0; color: inherit; font: inherit; cursor: pointer;
}
.van-top:hover { background: var(--brand-pale); }
.van-head { display: flex; justify-content: space-between; gap: 8px; font-size: 14px; color: var(--ink-2); }
.van-head > span:last-child { color: var(--ink-dim); font-size: 12px; }
.van-n { display: block; font-size: 28px; line-height: 36px; font-variant-numeric: tabular-nums; color: var(--ink); }
.van-n small { font-size: 18px; color: var(--ink-dim); }
.van-bar { display: block; height: 6px; border-radius: 3px; background: var(--page); overflow: hidden; margin-top: 4px; }
.van-bar > i { display: block; height: 100%; background: var(--ok); }
.van.short .van-head > span:last-child { color: var(--warn); }
.van.lit { border: 2px solid var(--brand); background: var(--brand-wash); }
.van.lit .van-n { color: var(--brand-down); }
.van.held { border-color: var(--bad); }
.van.held .van-head, .van.held .van-n { color: var(--bad); }
.van.lagging { border-color: var(--warn); }
.van.lagging .van-head, .van.lagging .van-n { color: var(--warn); }
.van.nolabel .van-n { color: var(--ink-dim); }
.van-list { list-style: none; margin: 0; padding: 0 12px 8px; max-height: 320px; overflow-y: auto; }
.van-list li {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px;
  padding: 5px 0; border-top: 1px solid var(--rule); font-size: 13px; line-height: 20px;
}
.van-list .c { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.van-list .x, .van-list .t { color: var(--ink-dim); white-space: nowrap; }
/* A purchase order's line with a problem: the box itself goes red, the same
   as a field in the grid above it (web/oms/oms-purchase-new.js). */
table.doctable td.bad input { border-color: var(--bad); }

/* Two-step sign-in (web/lib/mfa-steps.js, web/pages/profile.js): the steps
   of setting it up, the QR code, the key to type by hand, and the ten
   recovery codes -- monospaced, two columns, easy to copy by eye. */
.mfa-steps { margin: 0; padding-left: 20px; display: grid; gap: 12px; }
.mfa-steps li { padding-left: 2px; }
.mfa-steps input { display: block; margin-top: 6px; max-width: 200px; letter-spacing: 2px; font-family: ui-monospace, Menlo, monospace; }
.mfa-qr { margin: 8px 0 6px; width: 176px; height: 176px; padding: 8px; background: #fff; border: 1px solid var(--line); border-radius: 6px; }
.mfa-qr svg { display: block; width: 100%; height: 100%; }
.mfa-key { font-family: ui-monospace, Menlo, monospace; font-size: 13px; letter-spacing: 1px; word-break: break-all; }
.mfa-codes {
  margin: 12px 0 0; padding: 12px 16px; background: #f3f5fc; border: 1px solid var(--line); border-radius: 6px;
  font-family: ui-monospace, Menlo, monospace; font-size: 15px; line-height: 26px; column-count: 2; column-gap: 32px; white-space: pre;
}
.mfa-head { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.mfa-policy { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
#mfa button.ghost.danger { background: #fff; color: var(--bad); border-color: var(--line-mid); }
