/* ════════════════════════════════════════════════════════════════════════════
   n-office.css — the back-office surface (office spec §1, §3, §5)

   Every value in this file is a token or arithmetic over tokens. The spec's
   fixed sizes are written as the token sums that equal them, so the whole
   surface answers to the token file rather than to a number typed here:

     command line  56 = --n-space-7 + --n-space-2
     rail          56 = --n-space-7 + --n-space-2
     rail, named  208 = --n-space-8 × 3 + --n-space-4
     inspector    320 = --n-space-8 × 5
     table column 128 = --n-space-8 × 2      (the drag stores a RATIO of this,
                                              floored at 0.5 = the spec's 64 px)
     ink rule       2 = the spec's rule, drawn as two hairlines' worth of ink
                        via --n-rule; a hairline is `thin`, never a literal

   Three things have no token and are written as keywords or unitless numbers
   rather than pixels, and are listed here for the lead exactly as
   css/n-floor.css lists its two:
     · a hairline is `thin` (the UA's 1 px);
     · the donut's geometry (viewBox, radius, stroke width) is in SVG user
       units, set as attributes in js/n-office-views.js — chart geometry, not
       a design value;
     · a column's width is a unitless MULTIPLE of --n-tbl-col.

   Nothing here restyles an app.css class. The only existing selectors touched
   are the shell's ids and #helpFab, and only under [data-surface="office"] —
   an attribute that exists solely while this surface is mounted.
   ════════════════════════════════════════════════════════════════════════ */

/* ── 0. one genuinely shared atom ─────────────────────────────────────── */
/* Visually hidden, still announced. Sized from a token rather than the usual
   1 px so this file holds no literal; it is clipped to nothing either way.
   The rail's group labels borrow it while the rail is icons-only: a label
   that is `display:none` leaves its button with no accessible name at all,
   which is what the eight rail buttons had. */
.n-vh,
.n-office[data-rail="icons"] .n-rg-label {
  position: absolute;
  width: var(--n-space-1);
  height: var(--n-space-1);
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The paper's own page header lives in the DOM at all times; only
   css/n-print.css shows it. */
.n-printhead { display: none; }

/* ── 1. the shell gives way to the office ─────────────────────────────── */
[data-surface="office"] #shell {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  grid-template-areas: "main";
}
[data-surface="office"] #side,
[data-surface="office"] #top,
[data-surface="office"] #tabbar { display: none; }
/* The shell sits in the app's own "main" cell, beside #main rather than
   inside it (see js/n-office.js's header), and #main is moved into the
   content column: an office screen this batch has not adapted yet renders
   into it exactly as it always has, under the same page header. */
[data-surface="office"] #nOffice { grid-area: main; min-width: 0; min-height: 0; }
[data-surface="office"] #main {
  grid-area: auto;
  padding: 0 var(--n-space-6) var(--n-space-8);
  overflow: visible;
}
.n-office[data-mode="screen"] .n-olegacy { display: none; }
.n-office[data-mode="legacy"] .n-obody { display: none; }
[data-surface="office"] [hidden] { display: none !important; }
/* The helper moves into the command line as an icon button (spec §1), and
   the floor's push-to-talk mic is the floor's — voice rings up a sale, and
   there is nothing to ring up in the back office. */
[data-surface="office"] #helpFab,
[data-surface="office"] #vcBtn { display: none; }
/* app.css puts every <label> in small uppercase with tracking — office chrome
   from the old design that the new labels do not want. */
[data-surface="office"] label { text-transform: none; letter-spacing: normal; }
[data-surface="office"] .n-lbl { font-size: var(--n-step-0); }

/* ── 2. sizes derived once, on the surface, so sheets (children of <body>)
      inherit them too ─────────────────────────────────────────────────── */
[data-surface="office"] {
  --n-cmd-h: calc(var(--n-space-7) + var(--n-space-2));
  --n-rail-w: calc(var(--n-space-7) + var(--n-space-2));
  --n-rail-wide-w: calc(var(--n-space-8) * 3 + var(--n-space-4));
  --n-ins-w: calc(var(--n-space-8) * 5);
  --n-tbl-col: calc(var(--n-space-8) * 2);
  /* The measure a paragraph stops at, said in the unit a measure is made
     of. It used to be 11 × the largest spacing token (704 px), which on a
     1440 desk let a Setup note run to 96 characters — half again the 45–75
     a line of prose is read at, and the reason the office's sentences were
     the one thing on this surface nobody finished. `ch` is the width of the
     face's own "0", so the line holds the same NUMBER of characters
     whatever the tenant's type size turns out to be (fix round, O15).

     64ch was still not the measure it claimed to be, and `ch` was the reason
     it could not be checked. `ch` is the width of the face's "0", and this
     face sets a "0" a good deal wider than its average letter, so 64 of them
     carried 88 characters of a Setup › Printers note at 1440 (measured on
     the first line; the second ran to 84) — over the 70 a line of prose is
     read at, and back in the same trouble as 704 px. It was also a raw
     literal that scripts/design_lint.py cannot see, and teaching the lint to
     read `ch` would not have helped: its size pattern looks at width and
     height declarations, not at a custom property.

     So the measure is derived from a token again — but from the one it is
     actually made of. 30 × the office's body step is the old "two and a half
     alphabets", said in the size the prose is set in: change the type size
     and the line still holds the same number of characters, which is the
     whole of what `ch` was bought for. Measured at 1440 on the two Setup ›
     Printers notes, first line: 66 and 67 characters, in a box 420 px wide
     (fixer round, blocker 1). */
  --n-read: calc(var(--n-step-0) * 30);
  /* How tall a page of an archived invoice is shown, inside a sheet whose own
     ceiling is the viewport less --n-space-7. Derived the same way as every
     other size in this block — a multiple of the largest spacing token — and
     the box scrolls, so a long till slip is not cut off, only framed. */
  --n-docview-h: calc(var(--n-space-8) * 8);
  /* Messages (§12e). The conversation list is as wide as a guest's name and
     a snippet of their last line need; the thread scrolls to a page's worth
     and a bubble stops well short of the column so the two sides of the
     conversation are told apart by where they are, not only by their wash.
     Multiples of the largest spacing token, like every other size here. */
  --n-msg-list-w: calc(var(--n-space-8) * 5);
  --n-msg-h: calc(var(--n-space-8) * 9);
  --n-msg-bb-w: calc(var(--n-space-8) * 6);
}

/* ── 3. the grid ──────────────────────────────────────────────────────── */
.n-office {
  height: 100%;
  display: grid;
  grid-template-columns: var(--n-rail-w) minmax(0, 1fr) 0;
  grid-template-rows: var(--n-cmd-h) minmax(0, 1fr);
  grid-template-areas: "cmd cmd cmd" "rail content inspector";
  background: var(--n-bg);
  color: var(--n-text);
  font-family: var(--n-font-ui);
  font-size: var(--n-step-0);
  line-height: var(--n-lh-ui);
}
.n-office[data-rail="wide"] { grid-template-columns: var(--n-rail-wide-w) minmax(0, 1fr) 0; }
.n-office[data-inspector="on"] { grid-template-columns: var(--n-rail-w) minmax(0, 1fr) var(--n-ins-w); }
.n-office[data-rail="wide"][data-inspector="on"] { grid-template-columns: var(--n-rail-wide-w) minmax(0, 1fr) var(--n-ins-w); }

/* ── 4. shared controls ───────────────────────────────────────────────── */
.n-office :is(button, [href], input, select, textarea, tr):focus-visible,
.n-sheet :is(button, input, select, textarea):focus-visible {
  outline: none;
  box-shadow: var(--n-focus-ring);
}
/* …and neither can a TABLE ROW, for a different reason and with the same
   consequence. --n-focus-ring is a box-shadow, and Chrome does not paint a
   box-shadow on a `display: table-row` box at all: measured on Stock ›
   Invoices, the row took the keyboard (document.activeElement was the <tr>,
   `matches(":focus-visible")` was true, getComputedStyle reported both shadow
   layers) and NOTHING was drawn on the screen — a screenshot of the focused
   row was pixel-identical to one of the same row unfocused. So j and k have
   been walking an invisible cursor down every table in this office since the
   shell shipped, which is the whole keyboard path the spec asks for (§1,
   "focus is always visible").

   The row therefore takes an OUTLINE, exactly as the tick below does and for
   the same practical reason — it is the one focus mechanism that paints here.
   It is drawn INSIDE the row (a negative offset of its own width) so the
   table's scroll container cannot clip it, and in --n-accent-line, the token
   the file tone-maps to 3:1 against both bg and surface in both schemes. */
.n-office .n-tbl tbody tr:focus-visible {
  outline: var(--n-ring-w) solid var(--n-accent-line);
  outline-offset: calc(-1 * var(--n-ring-w));
}

/* A checkbox and a radio cannot wear the halo above: css/app.css:52 ends the
   native tick's rule with `box-shadow: none !important`, which no
   specificity beats, so every tick in the office — the item editor's
   Staff-only, every line of the Columns checklist, the stocktake's rows —
   focused INVISIBLY. The ring is therefore drawn as an OUTLINE, which that
   `!important` does not reach, in the same two colours and the same two
   widths the halo token uses: a --n-space-1 gap, then the accent LINE, which
   the token file tone-maps to 3:1 on both bg and surface in both schemes.

   The 2 px thickness is --n-ring-w, the token the lead added for exactly
   this: the halo used to write its widths only INSIDE --n-focus-ring, with
   no width token of its own, so this outline was arithmetic over the
   nearest token (`calc(var(--n-space-1) / 2)`) instead of a real one. */
[data-surface="office"] :is(input[type="checkbox"], input[type="radio"]):focus-visible {
  outline: var(--n-ring-w) solid var(--n-accent-line);
  outline-offset: var(--n-space-1);
}
.n-obtn, .n-oseg, .n-tbl-btn {
  min-height: var(--n-hit-button);
  padding: 0 var(--n-space-3);
  font-family: var(--n-font-ui);
  font-size: var(--n-step-0);
  font-weight: var(--n-w-semibold);
  color: var(--n-text);
  background: var(--n-surface);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
  cursor: pointer;
  transition: background var(--n-dur-fast) var(--n-ease), border-color var(--n-dur-fast) var(--n-ease);
}
.n-obtn:hover, .n-oseg:hover, .n-tbl-btn:hover { background: var(--n-surface-sunken); }
/* A button that is not available yet reads as unavailable in TONE, not in
   transparency. css/app.css:35 dims every disabled button to `opacity: .5`,
   which on Stock › Invoices put the words "Connect Drive" — a real control,
   with a real label somebody has to read to know what saving the credentials
   is FOR — at half strength, and text on this surface is never at less than
   full opacity (§8's contrast rule is measured on text as drawn). It keeps
   its muted colour, its sunken well and its not-allowed cursor, all of which
   still say "not yet", and the label stays legible. */
.n-office :is(.n-obtn, .n-oseg, .n-tbl-btn, .n-act):disabled {
  opacity: 1;
  color: var(--n-text-muted);
  background: var(--n-surface-sunken);
  border-color: var(--n-border-soft);
}
/* …and the same look for a primary that is SAVING rather than idle.
   js/n-office-views-people.js disables it from the click until the request
   settles, so a double press cannot post a second employee or a second
   stock-deducting staff-menu item. It is restated without the .n-office
   ancestor because the inspector becomes an nSheet below the inspector's
   breakpoint, and a sheet is a child of <body>; without it css/app.css:35
   would take the words on a saving button down to half strength. */
.n-act[aria-busy="true"][disabled] {
  opacity: 1;
  color: var(--n-text-muted);
  background: var(--n-surface-sunken);
  border-color: var(--n-border-soft);
}
/* …and the same again for an action a sheet disables AT REST rather than
   while it saves. Batch B3b's typed confirmation (Books → "Mark as filed",
   which stays disabled until the owner has typed FILE) is the first of
   those, and it showed what the two rules above had between them missed: a
   sheet is a child of <body>, so the `.n-office` ancestor above cannot reach
   it, and css/app.css took the words on the button to half strength —
   measured at 1.26:1 on a crimson tenant in the dark register, against the
   4.5:1 §8 asks for. Same treatment as every other disabled control on this
   surface: full opacity, the muted ink, the sunken well. */
.n-sheet :is(.n-obtn, .n-oseg, .n-tbl-btn, .n-act):disabled {
  opacity: 1;
  color: var(--n-text-muted);
  background: var(--n-surface-sunken);
  border-color: var(--n-border-soft);
}
.n-obtn-go { background: var(--n-accent); color: var(--n-accent-ink); border-color: var(--n-accent-line); }
.n-obtn-go:hover { background: var(--n-accent-hover); }
.n-osegs { display: inline-flex; gap: 0; }
.n-osegs-wide { display: flex; flex-wrap: wrap; gap: var(--n-space-2); margin-bottom: var(--n-space-4); }
.n-osegs .n-oseg { border-radius: 0; margin-left: calc(var(--n-space-1) * -0.25); }
.n-osegs .n-oseg:first-child { border-top-left-radius: var(--n-radius-sm); border-bottom-left-radius: var(--n-radius-sm); margin-left: 0; }
.n-osegs .n-oseg:last-child { border-top-right-radius: var(--n-radius-sm); border-bottom-right-radius: var(--n-radius-sm); }
.n-osegs-wide .n-oseg { border-radius: var(--n-radius-sm); margin-left: 0; }
/* THE PRESSED OUTLINE SEGMENT (fix round, I11 — office-wide: Tonight's range
   presets, Reports' presets, Payroll's quick pick, the Books question chips).
   It used to be the accent TEXT tone on the accent SOFT wash, and on a
   crimson tenant in the light register that pair measured 4.30:1 — under the
   4.5 §8 asks for. The oracle (scripts/contrast_check.py, step 4) derives the
   text tone against a wash it BLENDS over --n-surface; the shipped
   --n-accent-soft is an rgba, so on screen it composites over whatever is
   actually behind the segment, and inside a block that is not --n-surface.
   The wash was the only reason the pair was close, and a wash is not what
   makes a pressed segment read as pressed. So it goes: the segment keeps the
   page's own surface, takes the accent's border and text tone — that pair is
   the one the oracle proves for every tenant hue on every surface — and says
   PRESSED with a bar of the accent itself along its bottom edge, drawn as an
   inset shadow so nothing moves by a pixel when it lights up. */
.n-oseg[aria-pressed="true"] {
  background: var(--n-surface);
  border-color: var(--n-accent-line);
  color: var(--n-accent-text);
  box-shadow: inset 0 calc(var(--n-space-1) * -1) 0 0 var(--n-accent);
}
/* …and hover keeps the sunken well every other segment gets: --n-accent-text
   is derived against the sunken surface too, so the pair holds there. */
/* Three controls sat side by side on Reports in the same outline pill —
   the four report TABS (which of four screens am I on), the three range
   PRESETS (which days am I looking at) and Print (do a thing) — and nothing
   said they were three different kinds of decision. What separates them is
   GROUPING, not colour: the tabs are one shared border box with no gaps
   between them and a margin under the strip, the presets keep their own
   outline pill, and Print is pushed clear of both. Wrapping is allowed:
   four long labels do not fit a phone in one line, and the first/last radii
   from .n-osegs still round the ends. */
.n-osegs-tabs { flex-wrap: wrap; max-width: 100%; margin-bottom: var(--n-space-4); }
/* THE PRESSED TAB HAS NO RULE OF ITS OWN (taste round 2, R1), and that is the
   point. It used to take the accent as a FILL, then the wash: first a solid
   --n-accent, which the owner's DO NOT list reserves for the primary ACTION
   (on Setup › Printers the pressed tab and the "Add printer" button were the
   same block of saffron side by side), then --n-accent-soft with the ink and
   an --n-accent-line bar. The wash fails DESIGN.md's own Composite Rule:
   --n-accent-soft is an rgba, so it composites over whatever is actually
   behind the tab strip, and the tabs sit on --n-bg, not --n-surface. Measured
   there the ink is 4.60:1 (the oracle proves 4.95:1, but it blends the wash
   over --n-surface) and a tenant hue can push that under 4.5; the inset
   --n-accent-line bar measures 2.76:1 against the wash, under the 3:1 a UI
   boundary needs. The Composite Rule says what to do: a pressed segment keeps
   the page's own surface. So the tabs take the SAME recipe as every other
   pressed segment — .n-oseg[aria-pressed="true"] above, the page's surface,
   --n-accent-text, --n-accent-line and an inset bar of --n-accent — and the
   two rules are one. The solid accent stays on .n-obtn-go alone. */
/* Print is not a range: it gets the gap that says so. */
[data-n="rep-print"] { margin-left: var(--n-space-4); }
/* The From/To pair, revealed by the Custom… segment (spec §5). */
.n-odates { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--n-space-2); }
.n-odate { display: inline-flex; margin: 0; }
.n-odate input {
  min-height: var(--n-hit-button);
  height: var(--n-hit-button);
  font-size: var(--n-step-0);
  font-family: var(--n-font-num);
  font-variant-numeric: var(--n-font-num-variant);
  border-color: var(--n-border);
  border-radius: var(--n-radius-sm);
  background: var(--n-surface);
  color: var(--n-text);
}
.n-office :is(input, select, textarea) {
  font-family: var(--n-font-ui);
  font-size: var(--n-step-0);
  background: var(--n-surface);
  color: var(--n-text);
  border-color: var(--n-border);
  border-radius: var(--n-radius-sm);
}

/* ── 5. the command line (spec §1) ────────────────────────────────────── */
.n-cmdrow {
  grid-area: cmd;
  display: flex;
  align-items: center;
  gap: var(--n-space-3);
  padding: 0 var(--n-space-4);
  background: var(--n-surface);
  border-bottom: thin solid var(--n-rule);
  position: relative;
  z-index: 30;
}
.n-cmd-brand { display: inline-flex; align-items: center; gap: var(--n-space-2); flex: 0 0 auto; }
.n-flame { color: var(--n-flame); }        /* chrome, and static — no lick */
/* THE WORDMARK IS CHROME, NOT A HEADING (fix round, O8). The plan's register
   gives the office's display face to the page title and the one headline
   number per screen — two elements, and the proof of the rule is that you
   can count them. "noosh" in the masthead made three, at 19 px, in the one
   place on the surface that is never about the page you are on. It takes the
   UI face at the same step and weight; the flame beside it is what says the
   brand, and it is drawn, not set. */
.n-cmd-word {
  font-family: var(--n-font-ui);
  font-size: var(--n-step-2);
  font-weight: var(--n-w-semibold);
  letter-spacing: normal;
  color: var(--n-text);
}
.n-cmd-field { position: relative; flex: 1 1 auto; min-width: 0; }
.n-cmd-field input {
  width: 100%;
  height: var(--n-hit-button);
  min-height: var(--n-hit-button);
  padding: 0 var(--n-space-4);
  font-size: var(--n-step-1);
  background: var(--n-surface-sunken);
  border: thin solid var(--n-border);
  /* A PILL IS A CHIP'S SHAPE, NOT A FIELD'S (fix round, O14). The masthead
     carries three pills — the shift chip, a flag, the intel bar — and every
     one of them is a small READING you cannot type into. The one box on the
     row that takes a caret was wearing the same shape, which is why at 390
     it read as a fourth chip rather than as the search box. The field takes
     the surface's own field radius; --n-radius-pill stays on .n-cmd-chip,
     .n-flag and .n-intel-bar. */
  border-radius: var(--n-radius-md);
  color: var(--n-text);
}
/* This is `type="search"` (the Escape-to-clear and the mobile keyboard's own
   Search key both come free with it), and Chrome reserves layout width for
   its native cancel-button decoration even while it is EMPTY and even under
   `appearance: none` (css/app.css's blanket rule on every input) — the
   generic rule strips its paint, not this reserved space. Un-reserving it
   with `-webkit-appearance: textfield` and hiding the decoration
   pseudo-elements outright is what actually gives the placeholder its width
   back; the space this frees is exactly the BLOCKER at 390 px (item 1). */
.n-cmd-field input { -webkit-appearance: textfield; appearance: textfield; }
.n-cmd-field input::-webkit-search-decoration,
.n-cmd-field input::-webkit-search-cancel-button,
.n-cmd-field input::-webkit-search-results-button,
.n-cmd-field input::-webkit-search-results-decoration { display: none; }
.n-cmd-field input::placeholder { color: var(--n-text-muted); }
.n-cmd-list {
  position: absolute;
  top: calc(100% + var(--n-space-1));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--n-space-1);
  list-style: none;
  background: var(--n-surface-raised);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
  box-shadow: var(--n-shadow-2);
  max-height: calc(var(--n-space-8) * 6);
  overflow: auto;
  z-index: 40;
}
.n-cmd-opt {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--n-space-2);
  min-height: var(--n-hit-office);
  padding: 0 var(--n-space-3);
  border-radius: var(--n-radius-sm);
  cursor: pointer;
}
.n-cmd-opt[aria-selected="true"] { background: var(--n-accent-soft); }
.n-cmd-opticon { color: var(--n-text-muted); display: inline-flex; }
.n-cmd-optlabel { font-weight: var(--n-w-medium); }
.n-cmd-optlock { color: var(--n-warn); display: inline-flex; }
.n-cmd-opthint { color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-cmd-note {
  padding: var(--n-space-2) var(--n-space-3);
  color: var(--n-text-muted);
  font-size: var(--n-step--1);
}
.n-cmd-chip {
  flex: 0 0 auto;
  padding: 0 var(--n-space-3);
  line-height: var(--n-hit-office);
  font-size: var(--n-step--1);
  font-weight: var(--n-w-semibold);
  color: var(--n-text-2);
  background: var(--n-surface-sunken);
  border-radius: var(--n-radius-pill);
  white-space: nowrap;
}
.n-cmd-chip.n-chip-warn { color: var(--n-warn); background: var(--n-warn-soft); }
.n-cmd-icon, .n-cmd-user, .n-cmd-rail {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--n-space-2);
  min-height: var(--n-hit-button);
  padding: 0 var(--n-space-2);
  color: var(--n-text-2);
  background: transparent;
  border: 0;
  border-radius: var(--n-radius-sm);
  cursor: pointer;
  font-size: var(--n-step-0);
}
.n-cmd-icon:hover, .n-cmd-user:hover, .n-cmd-rail:hover { background: var(--n-surface-sunken); color: var(--n-text); }
/* The phone's way into the rail (spec §1). Hidden at every width where the
   rail itself is on screen — the phone block at the foot of this file turns
   it back on. It sits AFTER the shared block above and not before it: the
   two rules have the same specificity, so a `display: none` written first
   was simply overwritten by that block's `display: inline-flex` and the ⋯
   appeared on every desktop beside a rail that was already on screen. */
.n-cmd-rail { display: none; }
.n-cmd-initial {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--n-space-6);
  height: var(--n-space-6);
  border-radius: var(--n-radius-pill);
  background: var(--n-accent-soft);
  color: var(--n-accent-text);
  font-weight: var(--n-w-bold);
  font-size: var(--n-step--1);
}
.n-cmd-name { white-space: nowrap; }

/* ── 6. the rail (spec §1) ────────────────────────────────────────────── */
.n-orail {
  grid-area: rail;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  background: var(--n-surface);
  border-right: thin solid var(--n-border);
  overflow: visible;
}
.n-rail-list { list-style: none; margin: 0; padding: var(--n-space-2) 0; }
.n-rg { position: relative; }
.n-rg-btn {
  display: flex;
  align-items: center;
  gap: var(--n-space-3);
  width: 100%;
  min-height: var(--n-hit-button);
  padding: 0 var(--n-space-3);
  background: transparent;
  border: 0;
  border-left: var(--n-space-1) solid transparent;
  color: var(--n-text-2);
  font-size: var(--n-step-0);
  font-weight: var(--n-w-semibold);
  cursor: pointer;
  text-align: left;
}
.n-rg-btn:hover { background: var(--n-surface-sunken); color: var(--n-text); }
.n-rg-btn[aria-current="page"] {
  border-left-color: var(--n-accent-line);
  background: var(--n-accent-soft);
  color: var(--n-accent-text);
}
.n-rg-ico { display: inline-flex; flex: 0 0 auto; }
/* Not display:none — the §0 clip hides the label from the eye while the rail
   is icons-only and leaves it to the accessibility tree, which is where the
   button's name has to come from. Expanded (⌘.), it simply reads. */
.n-rg-label { display: inline; }
/* The office rail's foot. `.n-orail-foot`, not the bare `.n-rail-foot` it was:
   css/n-floor.css declares that name for the FLOOR rail's foot, this file
   loads after it, and so the floor's foot on every till was wearing the
   office's padding and gap — a collision the floor lane worked around at the
   phone strip (css/n-floor.css, `.n-floor .n-railstrip > .n-rail-foot`) and
   asked the office lane to name. tests/n-css-classes.test.mjs is the gate
   that stops the next one being shipped. */
.n-orail-foot {
  display: flex;
  flex-direction: column;
  gap: var(--n-space-1);
  padding: var(--n-space-2);
  border-top: thin solid var(--n-border-soft);
}
.n-rail-mini {
  min-height: var(--n-hit-button);
  background: transparent;
  border: 0;
  color: var(--n-text-muted);
  cursor: pointer;
  border-radius: var(--n-radius-sm);
}
.n-rail-mini:hover { background: var(--n-surface-sunken); color: var(--n-text); }
.n-rail-mini[aria-pressed="true"] { color: var(--n-accent-text); background: var(--n-accent-soft); }

/* The second level: a flyout at 56 px, inline when the rail is named. */
.n-rg-flyout {
  position: absolute;
  top: 0;
  left: 100%;
  min-width: var(--n-rail-wide-w);
  margin: 0;
  padding: var(--n-space-2);
  list-style: none;
  background: var(--n-surface-raised);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
  box-shadow: var(--n-shadow-2);
  z-index: 25;
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--n-dur-fast) var(--n-ease), visibility var(--n-dur-fast) var(--n-ease);
}
.n-rg:hover > .n-rg-flyout,
.n-rg:focus-within > .n-rg-flyout { visibility: visible; opacity: 1; }
.n-rg-head {
  padding: 0 var(--n-space-2) var(--n-space-1);
  color: var(--n-text-muted);
  font-size: var(--n-step--1);
  font-weight: var(--n-w-semibold);
}
.n-rg-item {
  display: block;
  width: 100%;
  min-height: var(--n-hit-office);
  padding: 0 var(--n-space-2);
  background: transparent;
  border: 0;
  border-radius: var(--n-radius-sm);
  color: var(--n-text);
  font-size: var(--n-step-0);
  text-align: left;
  cursor: pointer;
}
.n-rg-item:hover { background: var(--n-accent-soft); }
.n-rg-item[aria-current="page"] { color: var(--n-accent-text); font-weight: var(--n-w-semibold); }

/* ── 7. the content column ────────────────────────────────────────────── */
.n-ocontent { grid-area: content; overflow: auto; scrollbar-gutter: stable; }
.n-ohead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--n-space-3);
  padding: var(--n-space-5) var(--n-space-6) var(--n-space-3);
}
.n-crumb {
  grid-column: 1;
  margin: 0;
  color: var(--n-text-muted);
  font-size: var(--n-step--1);
}
.n-otitle {
  grid-column: 1;
  margin: 0;
  font-family: var(--n-font-display);
  font-size: var(--n-step-4);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-display);
  letter-spacing: normal;
  color: var(--n-text);
}
.n-oacts {
  grid-column: 2;
  grid-row: 1 / span 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--n-space-2);
}
.n-obody { padding: 0 var(--n-space-6) var(--n-space-8); }

/* Blocks, separated by ink rules — not cards with shadows (spec §1). */
.n-block { padding: var(--n-space-5) 0; border-top: thin solid var(--n-rule); }
.n-block:first-child { border-top: 0; padding-top: var(--n-space-2); }
.n-block-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--n-space-3);
  margin-bottom: var(--n-space-3);
}
/* TWO FRAUNCES THINGS PER SCREEN, NOT NINE (fix round, O8; plan §4 and the
   token file's own note at --n-font-display: "office page titles and the ONE
   headline number per screen"). Tonight was drawing the brand word, the page
   title, the headline number, five section heads and the legacy checklist's
   own h3 in the display face — eight editorial voices on one screen, which
   is none. A section head is a LABEL for the block under it: it takes the UI
   face at the same step and weight it already had, so nothing moves and the
   only two serif things left in the page are the title and the number.
   `:is(h2, .n-block-h)` rather than the class alone because the office also
   carries h2s that are not blocks — the inspector's title is one — and they
   are the same kind of thing. h3 is the legacy card's heading (css/app.css
   :149 sets it to a raw 16.5 px in the display face, the one off-scale size
   measured on Tonight): it comes back onto the scale at step 1. */
.n-block-h {
  margin: 0;
  font-size: var(--n-step-2);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-display);
  color: var(--n-text);
}
[data-surface="office"] :is(h2, .n-block-h) {
  font-family: var(--n-font-ui);
  font-size: var(--n-step-2);
  font-weight: var(--n-w-semibold);
}
[data-surface="office"] h3 {
  font-family: var(--n-font-ui);
  font-size: var(--n-step-1);
  font-weight: var(--n-w-semibold);
}
/* .n-note and .n-summary are the floor's atoms too (css/n-floor.css declares
   both, and this file loads after it), so a bare rule here was not a second
   opinion — it was the LAST one, on every till: a floor sheet's note was
   being sized and coloured for a laptop at desk distance. Both are scoped to
   the surface that means them. --n-read and the desk's step are the office's
   answer to a sentence; the floor keeps its own. (fix round, H1) */
[data-surface="office"] .n-note { max-width: var(--n-read); color: var(--n-text-muted); font-size: var(--n-step-0); }
[data-surface="office"] .n-summary { max-width: var(--n-read); font-size: var(--n-step-1); }
.n-good { color: var(--n-ok); }
.n-bad { color: var(--n-danger); }
/* Cards survive for a grouped form, and only for that (spec §1). */
.n-card {
  padding: var(--n-space-4);
  background: var(--n-surface);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
}
.n-formrow { display: flex; flex-wrap: wrap; align-items: end; gap: var(--n-space-3); margin: var(--n-space-3) 0; }
.n-formrow input { width: auto; }
.n-formgrid {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--n-space-2) var(--n-space-3);
  max-width: var(--n-read);
}
.n-formgrid .n-lbl { margin: 0; }
.n-formgrid .n-obtn { grid-column: 2; justify-self: start; }

/* ── 8. the one number, the KPIs (spec §5) ────────────────────────────── */
/* TWO FACTS, TWO LINES (fix round, O7). The headline's caption used to read
   "Taken · Today": what the number is and which days it covers, parted by a
   middle dot — the `A · B` meta string the owner's DO NOT list rules out.
   Each fact is a line of its own; js/n-office-views.js writes them as two
   spans so the caption is still one element with one margin. */
.n-onumber-l { margin: 0; color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-onumber-l > span { display: block; }
.n-onumber {
  margin: 0;
  font-family: var(--n-font-display);
  font-variant-numeric: var(--n-font-num-variant);
  font-feature-settings: var(--n-font-num-features);
  font-size: var(--n-step-6);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-display);
  color: var(--n-text);
}
.n-onumber-c {
  margin: var(--n-space-1) 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-3);
  color: var(--n-text-2);
  font-size: var(--n-step-0);
}
.n-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(calc(var(--n-space-8) * 2 + var(--n-space-5)), 1fr));
  gap: var(--n-space-5);
}
.n-kpi-l { margin: 0; color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-kpi-v {
  margin: 0;
  font-size: var(--n-step-3);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-display);
  color: var(--n-text);
}
.n-kpi[data-tone="ok"] .n-kpi-v { color: var(--n-ok); }
.n-kpi[data-tone="warn"] .n-kpi-v { color: var(--n-warn); }
.n-kpi[data-tone="danger"] .n-kpi-v { color: var(--n-danger); }
.n-kpi-s { margin: 0; color: var(--n-text-muted); font-size: var(--n-step--1); }
/* …and the same for a KPI's caption, which carried "COGS R 0.00 · costs
   R 0.00" and "0 items · 0 tickets". _nOfKpi() takes an array now. */
.n-kpi-s > span { display: block; }
/* The KPI row and whatever is under it are two different objects, and on
   Staff & comps at 1440 they were touching: the last KPI's baseline box
   ended at 448 and the table's toolbar began at 448, so the day's figures
   read as a header ON the table rather than as the day's figures. One space
   step between them, at every width — the row itself carries no bottom
   margin, so this is the only place the separation can be said once. */
.n-kpis + * { margin-top: var(--n-space-4); }

/* ── 9. charts, all colour from tokens ────────────────────────────────── */
.n-chart { display: flex; flex-wrap: wrap; align-items: center; gap: var(--n-space-5); }
.n-donut { width: calc(var(--n-space-8) * 2 + var(--n-space-5)); height: auto; flex: 0 0 auto; }
.n-donut-seg {
  fill: none;
  stroke: var(--n-seg, var(--n-track));
  stroke-dasharray: var(--n-dash) var(--n-gap);
  stroke-dashoffset: var(--n-off);
}
.n-chart-total {
  margin: 0 0 var(--n-space-2);
  font-family: var(--n-font-display);
  font-size: var(--n-step-4);
  font-weight: var(--n-w-semibold);
}
.n-legend { list-style: none; margin: 0; padding: 0; }
.n-legend-i {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: baseline;
  gap: var(--n-space-2);
  padding: var(--n-space-1) 0;
  font-size: var(--n-step-0);
}
.n-legend-sw {
  width: var(--n-space-3);
  height: var(--n-space-3);
  border-radius: var(--n-radius-sm);
  background: var(--n-seg, var(--n-track));
  align-self: center;
}
.n-legend-p { color: var(--n-text-muted); }
.n-bars { display: grid; gap: var(--n-space-3); max-width: var(--n-read); }
.n-bar { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--n-space-2); align-items: baseline; }
.n-bar-l { color: var(--n-text-2); }
.n-bar-t { grid-column: 1 / span 2; display: block; height: var(--n-space-3); background: var(--n-track); border-radius: var(--n-radius-sm); }
.n-bar-t > i { display: block; height: 100%; width: calc(var(--n-pct) * 1%); background: var(--n-seg, var(--n-accent)); border-radius: var(--n-radius-sm); }
.n-sums { margin: 0; max-width: var(--n-read); }
/* Categories' "Sold today, all categories" total (js/n-office-views-b2.js)
   sits directly under the table it totals, not in a prose column of its
   own — so it takes the TABLE's width instead of the read width above, and
   its dd (already the grid's right-hand `auto` column, by the rule below)
   lands under the last column's own right edge. */
.n-tbl + .n-sums { width: 100%; max-width: none; }
.n-sum {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--n-space-3);
  padding: var(--n-space-2) 0;
  border-bottom: thin solid var(--n-border-soft);
}
.n-sum dt, .n-sum dd { margin: 0; }
.n-sum dt { color: var(--n-text-2); }
.n-sum-tot { font-weight: var(--n-w-bold); border-bottom: thin solid var(--n-rule); }
.n-sum-tot dt { color: var(--n-text); }

/* chips and check rows */
.n-flag {
  display: inline-block;
  padding: 0 var(--n-space-2);
  border-radius: var(--n-radius-pill);
  background: var(--n-surface-sunken);
  color: var(--n-text-2);
  font-size: var(--n-step--1);
  font-weight: var(--n-w-semibold);
}
.n-flag[data-tone="danger"], .n-flag[data-tone="action"] { background: var(--n-danger-soft); color: var(--n-danger); }
.n-flag[data-tone="warn"] { background: var(--n-warn-soft); color: var(--n-warn); }
.n-flag[data-tone="ok"] { background: var(--n-ok-soft); color: var(--n-ok); }
/* A flag inside the SELECTED row. Two translucent washes must not stack:
   the row wears --n-accent-soft (the TENANT's colour, at low alpha) and the
   flag wears --n-warn-soft/--n-ok-soft/--n-danger-soft on top of it, so the
   ink the flag was toned against is no longer what is behind it. Measured on
   Stock › Invoices with the two sample tenants: the "no copy" flag reads
   7.56:1 on an ordinary row and 3.61:1 on the selected one (Mulino, dark),
   5.91:1 and 3.63:1 (Kaapse Koffie, light) — under 4.5:1 in both, on the one
   row the owner is actually looking at.

   So on a selected row the flag puts an OPAQUE surface under itself and
   paints its own wash on top as a background IMAGE — which is the same two
   layers it has on every other row, in the same order, and therefore the
   same colour and the same reading. (Inventory's "low" and the reorder
   watch's "runs out today", both from batch B2a, are fixed by this too.) */
.n-tbl tbody tr[aria-selected="true"] .n-flag {
  background-color: var(--n-surface-sunken);
  background-image: none;
}
.n-tbl tbody tr[aria-selected="true"] .n-flag[data-tone] { background-color: var(--n-surface); }
.n-tbl tbody tr[aria-selected="true"] .n-flag[data-tone="warn"] { background-image: linear-gradient(var(--n-warn-soft) 0 100%); }
.n-tbl tbody tr[aria-selected="true"] .n-flag[data-tone="ok"] { background-image: linear-gradient(var(--n-ok-soft) 0 100%); }
.n-tbl tbody tr[aria-selected="true"] .n-flag[data-tone="danger"],
.n-tbl tbody tr[aria-selected="true"] .n-flag[data-tone="action"] { background-image: linear-gradient(var(--n-danger-soft) 0 100%); }

.n-quad {
  display: inline-block;
  padding: 0 var(--n-space-2);
  border-radius: var(--n-radius-pill);
  font-size: var(--n-step--1);
  font-weight: var(--n-w-semibold);
  background: var(--n-surface-sunken);
  color: var(--n-text-2);
}
.n-quad[data-q="star"] { background: var(--n-ok-soft); color: var(--n-ok); }
.n-quad[data-q="plowhorse"] { background: var(--n-warn-soft); color: var(--n-warn); }
.n-quad[data-q="puzzle"] { background: var(--n-accent-soft); color: var(--n-accent-text); }
.n-quad[data-q="dog"] { background: var(--n-danger-soft); color: var(--n-danger); }
.n-checks { list-style: none; margin: 0; padding: 0; }
.n-checkrow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--n-space-3);
  padding: var(--n-space-3) 0;
  border-bottom: thin solid var(--n-border-soft);
}
.n-check-dot {
  width: var(--n-space-3);
  height: var(--n-space-3);
  margin-top: var(--n-space-1);
  border-radius: var(--n-radius-pill);
  background: var(--n-text-muted);
}
.n-checkrow[data-tone="action"] .n-check-dot { background: var(--n-danger-fill); }
.n-checkrow[data-tone="warn"] .n-check-dot { background: var(--n-warn-fill); }
.n-checkrow[data-tone="ok"] .n-check-dot { background: var(--n-ok-fill); }

/* ── 10. empty states (spec §4) ───────────────────────────────────────── */
.n-empty {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-4);
  padding: var(--n-space-5) 0;
}
.n-empty-text { margin: 0; max-width: var(--n-read); font-size: var(--n-step-1); color: var(--n-text-2); }

/* ── 11. the table (spec §3) → css/n-table.css ────────────────────────── */
/* The whole of §11 now lives in its own file, linked BEFORE this one, so the
   component (js/n-table.js + css/n-table.css) can be lifted into the
   marketing site's proof sections without the office shell. Only the four
   rules that are about the OFFICE's treatment of a table went with it, and
   they are marked there. */

/* ── 11b. tabular figures — the token trio travels together ───────────── */
/* Scoped for the same reason (H1): .n-num is bare in css/n-floor.css as the
   number PAD's key, and this rule reached every one of them. */
[data-surface="office"] :is(.n-num, .n-onumber, .n-chart-total) {
  font-variant-numeric: var(--n-font-num-variant);
  font-feature-settings: var(--n-font-num-features);
}
.n-kpi-v, .n-legend-v, .n-legend-p, .n-bar-v, .n-sum dd { font-family: var(--n-font-num); }
/* THE GAP AFTER THE CURRENCY MARK (fix round, O10). fmtMoneyHtml() sets the
   amount as markup precisely so the space between "R" and the first digit is
   a margin this stylesheet can size, rather than the U+2009 fmtMoney() has
   to use in a plain string — that character measures 0.486 em in this face,
   nearly twice the ≤0.25 em the spec asks for, and it is why a total copied
   off the screen was never machine-readable. --n-space-1 is 4 px, which at
   the KPI's step 3 is 0.17 em and at the headline's step 6 is 0.09 em. The
   floor declares its own copy of this rule (css/n-floor.css) and the site
   inherits that one; this is the office's. */
[data-surface="office"] .n-cur { margin-inline-end: var(--n-space-1); }
/* In a TABLE cell the type is --n-step-0 (14 px), where the 4 px gap is
   0.286 em — over the spec's 0.25 em (critic 3). Three quarters of the token
   there: 3 px = 0.214 em. The KPI (0.174 em at 23 px) and the headline
   (0.091 em at 44 px) keep the full token. */
[data-surface="office"] .n-tbl .n-cur { margin-inline-end: calc(var(--n-space-1) * 0.75); }

/* .n-num is two different things in this product: the office's "this is a
   number" (here) and the floor's numpad KEY (css/n-floor.css). That key is a
   56 px bordered button at --n-step-3, and it was bleeding into every money
   cell in the back office — a bordered, over-sized <td> that broke the
   selected row's wash. Scoping under the surface attribute puts the office
   ahead of it on specificity whatever order the files load in, and takes the
   key's four properties back off. */
[data-surface="office"] .n-num {
  font-family: var(--n-font-num);
  font-variant-numeric: var(--n-font-num-variant);
  font-feature-settings: var(--n-font-num-features);
  min-height: 0;
  border: 0;
  background: none;
  font-size: inherit;
}
/* …and then the sizes the office actually wants. `inherit` above would
   otherwise flatten the three numbers that carry .n-num AND a size of their
   own (spec §3, §5): a money cell reads at the row's own size, the KPI row
   at step 3, the chart's total at step 4, and the one Fraunces number at
   step 6. */
[data-surface="office"] .n-kpi-v { font-size: var(--n-step-3); }
[data-surface="office"] .n-chart-total { font-size: var(--n-step-4); }
/* The one Fraunces number (spec §5) carries .n-num too — it IS a number — so
   the immunity rule above was handing it --n-font-num and the headline lost
   its display face. The size is not the only thing this rule has to say back:
   the family is repeated here, at the same specificity, so the office's one
   editorial number is the one number NOT set in the tabular UI face. */
[data-surface="office"] .n-onumber {
  font-family: var(--n-font-display);
  font-size: var(--n-step-6);
}
/* …and the spans fmtMoneyHtml() writes INSIDE it (critic 3, blocker 1):
   css/app.css's universal `* { font-family: var(--font-ui) }` matches
   .n-cur / .n-amt / .n-grp directly, and a matched rule beats an inherited
   value, so the headline's digits fell back to the UI face the moment the
   headline moved to the markup path. Named here, at the same specificity as
   the universal rule cannot reach. (`span`, not an `:is()` list of the four
   classes: tests/n-css-classes.test.mjs splits a prelude on commas, and
   `.n-cur` alone inside the list read as a bare declaration of the floor's
   class. Every span inside the headline is money markup.) */
[data-surface="office"] .n-onumber span {
  font-family: var(--n-font-display);
}

/* ── 12. the inspector (spec §1) ──────────────────────────────────────── */
.n-oinspector {
  grid-area: inspector;
  overflow: auto;
  background: var(--n-surface);
  border-left: thin solid var(--n-border);
}
.n-office[data-inspector="off"] .n-oinspector { border-left: 0; }
.n-ins { display: flex; flex-direction: column; gap: var(--n-space-3); padding: var(--n-space-4); }
.n-ins-head { display: flex; align-items: start; justify-content: space-between; gap: var(--n-space-2); }
.n-ins-title {
  margin: 0;
  font-family: var(--n-font-display);
  font-size: var(--n-step-2);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-display);
}
.n-ins-x {
  flex: 0 0 auto;
  min-width: var(--n-hit-button);
  min-height: var(--n-hit-button);
  background: transparent;
  border: 0;
  color: var(--n-text-muted);
  border-radius: var(--n-radius-sm);
  cursor: pointer;
}
.n-ins-x:hover { background: var(--n-surface-sunken); color: var(--n-text); }
.n-ins-sub { margin: 0; color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-ifacts { margin: 0; }
.n-ifact {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--n-space-3);
  padding: var(--n-space-2) 0;
  border-bottom: thin solid var(--n-border-soft);
}
.n-ifact dt { margin: 0; color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-ifact dd { margin: 0; text-align: right; }
.n-ins-acts { display: flex; flex-wrap: wrap; gap: var(--n-space-2); padding-top: var(--n-space-2); border-top: thin solid var(--n-rule); }
/* "Costing recipe" was wrapping to two lines while Edit and Remove either
   side stayed on one, so the row read as three different heights. The
   button keeps its own words on one line; the ROW is what wraps, whole
   buttons at a time, when three of them will not fit a narrow inspector. */
.n-ins-acts .n-act { white-space: nowrap; }
/* No rule of its own: its first child is an .n-ins-h, which now carries one.
   Two hairlines eight pixels apart is a printing error, not a section. */
.n-ins-receipt { padding-top: 0; }
/* A heading INSIDE the panel — the panel's title is the <h2>, so what the
   bill's lines and its totals are called sits a step below it. */
/* A heading inside the panel used to be a muted 12.5 px caption — smaller
   and paler than the rows it introduced, so "Lines" and "Totals" read as
   footnotes to the numbers rather than as the two halves of the bill. It
   takes the office's display face at body size, in full ink, over its own
   ink rule: the same editorial device the blocks in the content column use,
   at panel scale. */
.n-ins-h {
  margin: var(--n-space-4) 0 var(--n-space-2);
  padding-top: var(--n-space-2);
  border-top: thin solid var(--n-rule);
  font-family: var(--n-font-display);
  font-size: var(--n-step-0);
  font-weight: var(--n-w-semibold);
  color: var(--n-text);
}
.n-ins-h + .n-sums { margin-bottom: var(--n-space-3); }

/* the rail, as a sheet on a phone (nOfficeRailSheet) */
.n-raillist, .n-raillist-s { list-style: none; margin: 0; padding: 0; }
.n-raillist-g { padding: var(--n-space-2) 0; border-top: thin solid var(--n-border-soft); }
.n-raillist-g:first-child { border-top: 0; }
.n-raillist-h {
  display: flex;
  align-items: center;
  gap: var(--n-space-2);
  margin: 0 0 var(--n-space-1);
  color: var(--n-text-muted);
  font-size: var(--n-step--1);
  font-weight: var(--n-w-semibold);
}
.n-raillist-s .n-rg-item { width: 100%; min-height: var(--n-hit-button); }

/* the shortcut sheet */
.n-keys { margin: 0; }
.n-keyrow { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--n-space-3); padding: var(--n-space-2) 0; border-bottom: thin solid var(--n-border-soft); }
.n-keyrow dt, .n-keyrow dd { margin: 0; }
.n-keyrow kbd {
  display: inline-block;
  padding: 0 var(--n-space-2);
  background: var(--n-surface-sunken);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
  font-family: var(--n-font-num);
  font-size: var(--n-step--1);
  color: var(--n-text);
}

/* ── 12b. Menu and Stock (spec §5, js/n-office-views-b2.js) ────────────
   Everything these three screens draw that the shell did not already have.
   Nothing new is invented: the chips are the floor's own `.n-chips/.n-chip`
   atoms (css/n-floor.css), the buttons are `.n-obtn`, the sheets are nSheet,
   and only the pieces below had no rule anywhere.                        */

/* The category's colour as the till wears it — the swatch reads its hue from
   the --n-seg custom property the markup carries, which is the one kind of
   inline style this redesign allows. Round, because the till's tiles are
   filled shapes and a square would read as a checkbox. */
.n-chip-dot {
  display: inline-block;
  flex: 0 0 auto;
  width: var(--n-space-3);
  height: var(--n-space-3);
  margin-right: var(--n-space-1);
  border-radius: var(--n-radius-pill);
  background: var(--n-seg, var(--n-accent));
  vertical-align: baseline;
}
/* …and .n-chip, which on the floor is a 56 px pressed/unpressed pill (H1). */
[data-surface="office"] .n-chip { display: inline-flex; align-items: center; }

/* A Create tick, in the menu-import draft table: css/n-table.css's own
   .n-check is a 36 px row (--n-hit-office), the table's own hit floor, but
   this surface's buttons and other tap targets stand at 44 px
   (--n-hit-button) and the Create label was the one control on the surface
   that fell short of it — a 16 × 16 box in a 40 × 36 label. The label itself
   is made to carry the target, inline rather than the base rule's block
   flex, so it sizes to its own words and tick rather than stretching the
   cell (second critic pass, finding 2). The tick stays --n-tick — only the
   label's box grows. */
[data-surface="office"] .n-check {
  display: inline-flex;
  align-items: center;
  min-height: var(--n-hit-button);
  min-width: var(--n-hit-button);
}

/* The item editor, in the inspector. A form column, not a card: label,
   control, note, repeat — the same rhythm the blocks in the content column
   keep, at panel width. */
.n-ins-form { display: block; }
/* A text field, a select and a textarea fill the panel. A CHECKBOX and a
   RADIO do not — they are the size the browser draws them, beside their
   words (css/n-table.css, .n-check). They are excluded here by attribute
   rather than left to tie with `.n-check input` and be decided by which
   stylesheet was linked last: that tie is what made the Staff-only tick a
   287 × 44 empty rectangle with its label wrapped off the panel. */
.n-ins-form :is(input:not([type="checkbox"]):not([type="radio"]), select, textarea) {
  width: 100%;
  min-height: var(--n-hit-button);
}
.n-ins-form .n-lbl { margin-top: var(--n-space-3); }
.n-ins-form .n-note { margin: var(--n-space-1) 0 0; font-size: var(--n-step--1); }
/* A boxed aside inside the panel: the AI's missing ingredients, the weigh
   assistant. It is the one place in the office that gets a fill, because it
   is a machine's suggestion sitting inside the owner's own form and has to
   be told apart from it. */
.n-ins-note {
  margin-top: var(--n-space-3);
  padding: var(--n-space-3);
  background: var(--n-surface-sunken);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-sm);
}
.n-ins-note .n-note:first-child { margin-top: 0; }
.n-ins-rowacts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--n-space-2);
  margin-top: var(--n-space-3);
}

/* One editable line — a recipe ingredient, a portion, a weighed bottle, a
   duplicate pair's Keep control. It wraps rather than scrolls: the panel is
   320 px and three controls do not fit across it. */
.n-editrow {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--n-space-2);
  padding: var(--n-space-2) 0;
  border-bottom: thin solid var(--n-border-soft);
}
.n-editrow > :is(input, select) { flex: 1 1 var(--n-tbl-col); min-width: 0; }
/* The ingredient combobox (js/00-search.js) is an input inside its own
   positioned wrapper, so it is not a direct :is(input, select) child and
   needs the row's width rule said about the wrapper instead. */
.n-editrow > .spick { flex: 1 1 var(--n-tbl-col); min-width: 0; }
.n-editrow .n-lbl { margin: 0; flex: 0 0 auto; }
/* A number box with its own word over it. Two identical unlabelled boxes and
   a legend sentence underneath — "[55] [18] R 990.00", "Quantity, then cost
   each" — asked the owner to remember which was which on the screen where a
   restated invoice moves stock and The Books. The label is a micro-label:
   one step down, the secondary ink, hard against its field. (fix round, D2) */
[data-surface="office"] .n-editfield { display: flex; flex-direction: column; gap: var(--n-space-1); min-width: 0; }
[data-surface="office"] .n-editfield > .n-lbl { font-size: var(--n-step--1); color: var(--n-text-2); }
[data-surface="office"] .n-editfield > input { width: 100%; }
.n-editrow > .n-editfield { flex: 0 1 calc(var(--n-space-8) * 1.5); }
/* A quantity is short and numeric; it takes the number face and only the
   width a quantity needs, so the ingredient beside it keeps the line. */
.n-editrow > .n-editqty {
  flex: 0 1 calc(var(--n-space-8) * 1.5);
}
.n-editqty {
  font-family: var(--n-font-num);
  font-variant-numeric: var(--n-font-num-variant);
  font-feature-settings: var(--n-font-num-features);
  text-align: right;
}
.n-editx {
  flex: 0 0 auto;
  min-width: var(--n-hit-button);
  min-height: var(--n-hit-button);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: var(--n-radius-sm);
  color: var(--n-text-muted);
  cursor: pointer;
}
.n-editx:hover { background: var(--n-danger-soft); color: var(--n-danger); }

/* The audit trail — in the inspector (two columns: when, what) and in the
   History sheet (three: when, what, restore). The `when` column is the
   number face and a fixed measure, so the events line up down the page
   instead of ragging with the length of each date. */
.n-histrow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--n-space-3);
  align-items: baseline;
  padding: var(--n-space-2) 0;
  border-bottom: thin solid var(--n-border-soft);
}
.n-histrow-w {
  font-family: var(--n-font-num);
  font-variant-numeric: var(--n-font-num-variant);
  font-feature-settings: var(--n-font-num-features);
  font-size: var(--n-step--1);
  color: var(--n-text-muted);
  white-space: nowrap;
}
.n-histrow-t { min-width: 0; }
.n-histrow-t .n-note { display: block; margin: 0; font-size: var(--n-step--1); }
.n-histrow-a { justify-self: end; }

/* A duplicate pair, in the merge sheet. */
.n-duprow { padding: var(--n-space-3) 0; border-top: thin solid var(--n-rule); }
.n-duprow:first-child { border-top: 0; }
.n-duprow .n-editrow { border-bottom: 0; }

/* A note that carries a verdict. B2a's stocktake already wrote
   `data-tone="danger"` on an .n-note and nothing here answered it, so a
   failed read printed in the ordinary muted grey; the invoice draft's
   reconciliation banner needs the same three tones, so they are declared
   once, for both. Colour is never the only signal — every one of these
   sentences says in words what it says in ink. Scoped to this surface
   (second critic pass, finding 5): the floor never writes a toned .n-note —
   its own danger/ok/warn colour lives on .n-strip-sync and .n-change in
   css/n-floor.css, so this bare class was a hygiene risk rather than a live
   collision, and tests/n-css-classes.test.mjs now fails the build if a
   future toned .n-note (or .n-editfield) is declared unscoped here again. */
[data-surface="office"] .n-note[data-tone="ok"] { color: var(--n-ok); }
[data-surface="office"] .n-note[data-tone="warn"] { color: var(--n-warn); }
[data-surface="office"] .n-note[data-tone="danger"] { color: var(--n-danger); }

/* A line that has been struck off an applied invoice: still on the page,
   because taking it away would take away the way back. */
.n-editrow[data-tone="gone"] .n-lbl { text-decoration: line-through; }

/* ── the two AI drafts (Stock › Invoices, Menu › Menu import) ──────────
   A draft table is the one table on this surface whose cells are FIELDS: an
   invoice line's quantity, a proposed item's price. Three things follow, and
   each would be a defect if it were left unsaid:

     · a cell may not clip. .n-tbl tbody td is `nowrap` + `overflow: hidden`,
       so a long name ellipsises rather than break a column — which for a
       FIELD means the right-hand half of a number input is simply cut off;
     · a row may not be 36 px. It holds a control, and a control on this
       surface is --n-hit-button tall, with the line's own questions ("how is
       this measured out?") sitting under it;
     · the field takes the cell's width, not a width of its own, or the
       columns stop lining up the moment one name is longer than another. */
[data-n="table-wrap-invoice-draft"] tbody tr,
[data-n="table-wrap-menu-draft"] tbody tr { height: auto; }
[data-n="table-wrap-invoice-draft"] tbody td,
[data-n="table-wrap-menu-draft"] tbody td {
  padding: var(--n-space-2);
  overflow: visible;
  white-space: normal;
  vertical-align: top;
}
.n-cellin {
  width: 100%;
  min-width: 0;
  min-height: var(--n-hit-button);
}
/* A FIELD IN A CELL LOOKS LIKE A FIELD (fix round, D1). The payroll run's
   rate, hours, bonus and breakage boxes computed to no border and a
   transparent background — a number sitting in a cell, indistinguishable
   from the readings either side of it, on the four columns of the office
   that are typed into. They take the same edge and the same surface every
   other input on this surface wears (the focus ring is the office's own,
   §2), so a cell that can be typed into says so before it is tapped. Scoped
   to `input` so the <select> cells keep the sunken well and the chevron that
   tell a LIST from a box (above). */
[data-surface="office"] input.n-cellin {
  background: var(--n-surface);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
  padding: 0 var(--n-space-2);
}
/* "Maps to" is a <select> and it sat beside the Name text input wearing the
   same white box, the same border and the same radius — identical, on the one
   control in this office that decides WHICH stock item an invoice line's
   money and quantity land on. A select is now sunken and carries a chevron,
   so the eye can tell a list of your items from a box you type in without
   clicking it. The chevron is two token-coloured gradient wedges, not an
   image: no request, no data URI, and the colour is a token like every other
   value here. The right padding keeps the longest supplier name off it.
   (fix round, D3) */
[data-surface="office"] select.n-cellin {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--n-space-5);
  background-color: var(--n-surface-sunken);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--n-text-2) 50%),
    linear-gradient(135deg, var(--n-text-2) 50%, transparent 50%);
  background-size: var(--n-space-1) var(--n-space-1), var(--n-space-1) var(--n-space-1);
  background-position:
    right calc(var(--n-space-3) + var(--n-space-1)) center,
    right var(--n-space-3) center;
  background-repeat: no-repeat;
}
/* The two columns that hold a CONTROL rather than a value — Menu import's
   "Create" tick and the invoice draft's remove button — are as wide as the
   control and no wider. js/n-table.js lays its tables out `fixed` and sizes
   every column by --n-col-w — the column's declared `width`, with a drag or
   a saved view over it — so on a four-column draft the tick was taking a
   QUARTER of the screen and the item's name, which is the column the owner
   is reading, was squeezed to the same width as a checkbox. A control is not
   a proportion of a row, it is one tap target, so it is sized here in the
   tokens the target is built from rather than given a `width:` multiple, and
   named per table, because it is a fact about these two columns. */
[data-n="table-wrap-menu-draft"] :is(th, td):first-child,
[data-n="table-wrap-invoice-draft"] :is(th, td):last-child {
  width: calc(var(--n-hit-button) + var(--n-space-3));
}
/* One line's question, asked in the cell the answer belongs to: the AI was
   not sure how a product is measured out, or it read a bottle where the
   shelf counts crates. It wraps under the name rather than beside it — the
   controls are a select and a number, and neither fits a name column. */
.n-cellq {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-2);
  margin-top: var(--n-space-2);
  padding: var(--n-space-2);
  border-radius: var(--n-radius-sm);
  background: var(--n-warn-soft);
  color: var(--n-warn);
  font-size: var(--n-step--1);
}
.n-cellq select, .n-cellq input { min-height: var(--n-hit-button); }
/* In the 320 px inspector an edit row's NAME takes the whole line and the
   fields sit under it. A name, two number fields, a line total and a remove
   button do not fit across 320 px, and left to wrap where they fell they
   broke one invoice line across two visual rows with no way to tell which
   number belonged to which — "Beef patty each 40 / 18 R 720.00 ×". */
.n-oinspector .n-editrow > .n-lbl { flex: 1 1 100%; }
/* …and the numbers under it are narrower here than in a sheet, so the line
   total and the remove button come up ONTO their line instead of taking two
   more of their own. Measured on an applied invoice's lines: 137 px a line
   before, 89 px after, which is the difference between three lines of an
   invoice on the screen and five. */
.n-oinspector .n-editrow > .n-editqty { flex: 0 1 calc(var(--n-space-8) + var(--n-space-2)); }
.n-oinspector .n-editrow > .n-editfield { flex: 0 1 calc(var(--n-space-8) + var(--n-space-2)); }

/* An invoice's past corrections, in the inspector. */
.n-histlist .n-histrow { grid-template-columns: auto minmax(0, 1fr); }

/* The archived copy of an invoice, in a sheet: a PDF page or a photograph,
   scrolled inside the sheet rather than opened in a new window — inside the
   Android and iOS apps a blob in a new window replaces the webview and there
   is no way back to the till. */
.n-docview {
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
  overflow: auto;
  background: var(--n-surface-sunken);
}
.n-docview iframe {
  display: block;
  width: 100%;
  height: var(--n-docview-h);
  border: 0;
}
.n-docview img { display: block; width: 100%; height: auto; }

/* Drop a photo of the shelf here. Dashed, because a dashed edge is the one
   border in this system that means "not a thing yet — put one here". */
.n-drop {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-3);
  padding: var(--n-space-5);
  border: thin dashed var(--n-border);
  border-radius: var(--n-radius-md);
  background: var(--n-surface-sunken);
}
/* The drag-over state is js/90-boot.js's wireDropZone(), which paints an
   inline outline of its own — old code this batch does not touch, so there
   is deliberately no hover rule here to fight it. */
.n-drop .n-note { margin: 0; }

/* ── 12c. People (spec §5, js/n-office-views-people.js) ────────────────
   Employees and Staff & comps add five shapes the office did not have yet:
   a person's initials, a list row that is a name and one control, the tip
   rules' three switched rows, the claim offer, and the Danger rule the spec
   asks for at the foot of a staff card. Every one of them is built out of
   the tokens the rest of this file already uses. */

/* The staff card at the head of the inspector: the initials, then who they
   are. The initials wear the ROLE's tone as a wash, which is the same four
   decisions js/22-employees.js's ROLE_COLORS made, in the office's names. */
.n-ofp-card {
  display: flex;
  align-items: center;
  gap: var(--n-space-3);
  padding-bottom: var(--n-space-3);
  border-bottom: thin solid var(--n-rule);
}
.n-ofp-av {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--n-hit-button);
  height: var(--n-hit-button);
  margin: 0;
  border-radius: var(--n-radius-pill);
  background: var(--n-surface-sunken);
  color: var(--n-text-2);
  font-family: var(--n-font-ui);
  font-size: var(--n-step-0);
  font-weight: var(--n-w-bold);
}
.n-ofp-av[data-tone="accent"] { background: var(--n-accent-soft); color: var(--n-accent-text); }
.n-ofp-av[data-tone="ok"] { background: var(--n-ok-soft); color: var(--n-ok); }
.n-ofp-av[data-tone="warn"] { background: var(--n-warn-soft); color: var(--n-warn); }
.n-ofp-cardname {
  margin: 0;
  min-width: 0;
  font-family: var(--n-font-display);
  font-size: var(--n-step-2);
  line-height: var(--n-lh-display);
  color: var(--n-text);
}
.n-ofp-cardrole {
  display: block;
  font-family: var(--n-font-ui);
  font-size: var(--n-step--1);
  font-weight: var(--n-w-regular);
  color: var(--n-text-muted);
}

/* The Danger rule (spec §5): Remove is not another button beside
   Permissions, it is under a rule of its own at the foot of the card. The
   rule is drawn in the danger ink so the eye stops at it. */
.n-ofp-danger {
  margin-top: var(--n-space-5);
  padding-top: var(--n-space-4);
  border-top: thin solid var(--n-danger);
}

/* A list row: a name (with a second line under it), whatever flag it carries,
   and one control at the end. The on-clock board, the staff-card list and the
   tip-pool membership are all this shape. */
.n-ofp-list { list-style: none; margin: 0; padding: 0; }
.n-ofp-listrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-3);
  padding: var(--n-space-2) 0;
  border-bottom: thin solid var(--n-border-soft);
}
.n-ofp-listname { flex: 1 1 var(--n-tbl-col); min-width: 0; color: var(--n-text); }
.n-ofp-listsub {
  display: block;
  font-size: var(--n-step--1);
  color: var(--n-text-muted);
}
.n-ofp-sec { margin-top: var(--n-space-4); }
.n-ofp-secamt { margin: 0; }

/* The tip rules. Three rows of switch · name · percent, the same grid at
   every width so the three numbers line up under each other — which is the
   whole point of a set of percentages that has to add to 100. A row that is
   switched off keeps FULL opacity (§8 measures contrast on text as drawn)
   and says so in its ink instead, exactly as a disabled .n-obtn does. */
.n-ofp-rules { display: grid; gap: var(--n-space-2); max-width: var(--n-read); }
.n-ofp-rule {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--n-space-3);
  padding: var(--n-space-2) var(--n-space-3);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-sm);
}
.n-ofp-rule > .n-lbl { grid-column: 3; margin: 0; font-size: var(--n-step--1); color: var(--n-text-2); }
.n-ofp-rule > input[type="number"] { grid-column: 3; width: calc(var(--n-space-8) * 1.25); }
.n-ofp-rule > .n-ofp-rulename { grid-column: 2; min-width: 0; font-weight: var(--n-w-semibold); color: var(--n-text); }
/* The house-deduction row has no switch, so its name starts in column 1. */
.n-ofp-rule > .n-ofp-rulename:first-child { grid-column: 1 / 3; }
.n-ofp-rule:has(input[type="checkbox"]:not(:checked)) { background: var(--n-surface-sunken); }
.n-ofp-rule:has(input[type="checkbox"]:not(:checked)) .n-ofp-rulename { color: var(--n-text-2); }

/* An employee's own claim, on the day they clocked in — the one thing on
   this surface that is an OFFER rather than a record, so it is the one card
   with the accent's own line around it. */
.n-ofp-claim {
  margin: var(--n-space-3) 0;
  padding: var(--n-space-4);
  background: var(--n-accent-soft);
  border: thin solid var(--n-accent-line);
  border-radius: var(--n-radius-md);
}
.n-ofp-claim .n-summary { margin-top: 0; }

/* A QR is a picture of a key: it is centred, it is on paper-white whatever
   the screen is set to (a dark theme inverts a QR into something no camera
   reads), and it never prints from this page — printOverrideQr()/printIdCard()
   are the printed paths (css/n-print.css). */
.n-ofp-qrbox { margin: var(--n-space-4) 0; text-align: center; }
.n-ofp-qr {
  background: var(--n-paper);
  padding: var(--n-space-2);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
}
.n-ofp-qr-sm { padding: var(--n-space-1); }

/* An Inventory suggestion under an ingredient search: the name, and under it
   how it is measured and what it costs. The chip itself is the floor's
   (.n-chip, §12b); only the second line is new. */
.n-chip-sub {
  display: block;
  font-size: var(--n-step--1);
  font-weight: var(--n-w-regular);
  color: var(--n-text-muted);
}
[data-surface="office"] .n-chips .n-chip { flex-direction: column; align-items: flex-start; }

/* The one paragraph on these screens that explains the SCREEN rather than
   reporting on it. On a desk it is prose and this button is not on the page
   at all; below --n-bp-phone (§14) the prose closes behind it and the row
   costs one line instead of 144 px. The glyph is decoration — the state a
   screen reader reads is aria-expanded on the button and `hidden` on the
   paragraph — and the tap target is a full button height. */
.n-odisc {
  display: none;
  align-items: center;
  gap: var(--n-space-2);
  min-height: var(--n-hit-button);
  padding: 0;
  width: auto;
  background: none;
  border: 0;
  color: var(--n-accent-text);
  font-family: var(--n-font-ui);
  font-size: var(--n-step-0);
  font-weight: var(--n-w-semibold);
  text-align: left;
}
.n-odisc::after { content: "▸"; color: var(--n-text-muted); }
.n-odisc[aria-expanded="true"]::after { content: "▾"; }
/* The closed state is `data-closed`, not `hidden`: this file's own
   `[data-surface="office"] [hidden] { display: none !important }` (§1, the
   office's mirror of the floor's rule) cannot be outweighed by anything, and
   a desk has to be able to keep showing the paragraph the phone folded away.
   `display: none` takes it out of the accessibility tree exactly as `hidden`
   would, and the button's aria-expanded says which state it is in. */
.n-odisc-body[data-closed] { display: block; }

/* The day picker in the Today heading: ‹ Today › beside the date, so the
   day's first number is not pushed below the fold by a row of its own. The
   two arrows are as wide as a tap target and no wider. */
.n-ofp-daynav .n-oseg[data-n="sm-prev"],
.n-ofp-daynav .n-oseg[data-n="sm-next"] {
  min-width: var(--n-hit-button);
  padding-left: var(--n-space-2);
  padding-right: var(--n-space-2);
}

/* ── 12d. Money and Payroll (spec §5, js/n-office-views-people.js's payroll
      half and js/n-office-views-money.js) ─────────────────────────────────
   The lead's brief calls this section "§14 Money"; the file already has a
   §13 (the inspector's breakpoint) and a §14 (the phone), and both of those
   have to stay LAST in the cascade, so it takes the next number in the §12
   family the other two batches used — §12b Menu and Stock, §12c People. The
   name is the only thing that differs from the brief. Raised with the lead.

   The payroll run is the one table in the office whose cells are FIELDS from
   end to end: a tick, two lists, four numbers and three readings, on one row
   per person. It is the invoice draft's shape (§12b) and takes the same
   three concessions — a row is as tall as a control, a cell may not clip a
   field, and the field takes the cell's width — declared here per table,
   because that is a fact about these tables and not about nTable. */
[data-n="table-wrap-payroll-run"] tbody tr { height: auto; }
[data-n="table-wrap-payroll-run"] tbody td {
  padding: var(--n-space-2);
  overflow: visible;
  white-space: normal;
  vertical-align: top;
}
/* A person's name and the notes the run hangs under it — the own dates, the
   pro-rata warning, the overlap, the "figures unavailable" and the Paid ✓
   badge. Each is its own line, because each is a separate fact about whether
   this row is being paid at all. */
.n-ofpr-name { display: block; font-weight: var(--n-w-semibold); color: var(--n-text); }
[data-n="table-wrap-payroll-run"] td .n-note {
  display: block;
  margin: 0;
  font-size: var(--n-step--1);
}
[data-n="table-wrap-payroll-run"] td .n-note:empty { display: none; }
/* The quantity's unit and the "40 h worked, incl. 8 h Sun/holiday ×2" hint
   sit under the box rather than beside it — 96 px of column has room for a
   number or for a sentence, never both. */
[data-n="table-wrap-payroll-run"] td .n-cellin + .n-note { margin-top: var(--n-space-1); }
/* A read-only quantity — a monthly salary does not scale, so its box is a
   reading. The old screen said so with opacity .65, which §8 measures as
   failing text. Now that a typed cell wears a real field's edge (D1), the
   reading is the QUIET one: no border, no fill, the secondary ink — the same
   words in the same place, drawn as the reading they are. It is the exact
   opposite of what shipped before the fix round, when the one box on this
   table nobody could type into was the only one that looked like a box. */
[data-surface="office"] input.n-cellin[readonly] {
  background: transparent;
  border-color: transparent;
  color: var(--n-text-2);
  cursor: default;
}
/* The printed words of a typed cell (D5). On screen the box IS the value, so
   this is not drawn; css/n-print.css turns the pair round on paper, where an
   empty box would otherwise print its placeholder. */
.n-cellval { display: none; }

/* A locked row — someone already paid for these dates. Every control on it
   is disabled by prApplyRowLock(); this is what disabled LOOKS like in a
   table cell, and the text keeps full opacity for the same reason. */
[data-surface="office"] .n-cellin:disabled {
  background: var(--n-surface-sunken);
  color: var(--n-text-muted);
}
[data-n="table-wrap-payroll-run"] tbody tr:has(input[type="checkbox"]:not(:checked)) {
  background: var(--n-surface-sunken);
}
/* The tick that decides whether a person is in this run is one target, not a
   share of the row (the same rule the two draft tables' control columns
   take). */
[data-n="table-wrap-payroll-run"] :is(th, td):first-child {
  width: calc(var(--n-hit-button) + var(--n-space-3));
}

/* A payslip. It is a block like any other on screen; css/n-print.css is
   where it becomes a sheet of paper of its own. The rules either side of it
   are the block's own, so nothing is added here but the width the sums read
   at — a payslip is a column of eight figures, not a page-wide table. */
[data-n="pr-payslip"] .n-sums { max-width: var(--n-read); }

/* Books: the VAT201's own figure sits inside a block rather than at the head
   of the page, so it steps down one from the screen's one number — the page
   already has a Fraunces number and two of them at the same size would be
   two headlines. */
[data-surface="office"] #nOfB-bk-vat .n-onumber { font-size: var(--n-step-5); }

/* A <select> in a form ROW is as wide as its longest answer, not as wide as
   the row: `[data-surface="office"] :is(input, select, textarea)` is 100 %
   by default (right for the inspector's stacked form) and .n-formrow only
   ever said `width: auto` for `input`, so the run's Yes/No SDL question took
   a whole line of the period block to itself. Scoped to this block rather
   than to .n-formrow everywhere, so no screen that shipped before this batch
   moves a pixel. */
#nOfB-pr-period .n-formrow select { width: auto; }

/* Branches: the per-branch KPIs are a row of tills, and a till that did not
   answer says so in the warn tone rather than showing a nought. */
#nOfB-br-today .n-kpi[data-tone="warn"] .n-kpi-v { font-size: var(--n-step-2); }

/* ── 12e. Guests (spec §5, js/n-office-views-guests.js) ────────────────
   Customers & partners and Bookings are tables and forms the office already
   dresses; the two things that are new here are the MESSAGES pane — the only
   screen in the back office that is a conversation rather than a register —
   and the setup disclosure the WhatsApp connection opens with. §12b Menu and
   Stock, §12c People and §12d Money took the numbers before this one; §13
   and §14 have to stay last in the cascade. */

/* The performance report: the letter the owner sends a partner, shown as it
   will be read. A <pre> so the line breaks the report writer chose survive,
   in the UI face rather than a monospace one — it is prose, not code. */
.n-ofg-pre {
  margin: 0;
  padding: var(--n-space-4);
  background: var(--n-surface-sunken);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-sm);
  font-family: var(--n-font-ui);
  font-size: var(--n-step--1);
  line-height: var(--n-lh-prose);
  color: var(--n-text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The three-step WhatsApp guide. A real <details>, not the .n-odisc button
   pattern (§12c), because this one is CLOSED once the connection is made and
   open before it — a state the browser keeps for us — where the disclosure
   in People is a paragraph a desk always shows. */
.n-ofg-help {
  margin: var(--n-space-3) 0;
  padding: var(--n-space-3) var(--n-space-4);
  background: var(--n-surface-sunken);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-sm);
}
.n-ofg-help-s {
  min-height: var(--n-hit-office);
  display: flex;
  align-items: center;
  font-weight: var(--n-w-semibold);
  color: var(--n-text);
  cursor: pointer;
}
/* The ring is a BOX-SHADOW token (--n-focus-ring is two inset-less shadow
   layers), so it cannot ride in `outline` — Chrome drops the whole
   declaration and the disclosure took the keyboard with nothing drawn on
   it. Said the way §12f's guide heading says it, and the gap is inside the
   token already (fix round, T2). */
.n-ofg-help-s:focus-visible { outline: none; box-shadow: var(--n-focus-ring); }
.n-ofg-help-b { padding-top: var(--n-space-2); }
.n-ofg-steps {
  margin: 0;
  padding-left: var(--n-space-5);
  display: grid;
  gap: var(--n-space-3);
  max-width: var(--n-read);
  font-size: var(--n-step--1);
  line-height: var(--n-lh-prose);
  color: var(--n-text-2);
}

/* ── Messages ─────────────────────────────────────────────────────────
   Two panes: the conversations and the one that is open. The list is as wide
   as a name and a snippet need and no wider; the thread takes the rest. */
.n-msg {
  display: grid;
  grid-template-columns: minmax(0, var(--n-msg-list-w)) minmax(0, 1fr);
  gap: var(--n-space-5);
  align-items: start;
}
.n-msg-list { min-width: 0; }
.n-msg-thread {
  min-width: 0;
  padding-left: var(--n-space-5);
  border-left: thin solid var(--n-border-soft);
}
.n-msg-rows { list-style: none; margin: var(--n-space-3) 0 0; padding: 0; }
.n-msg-li { margin: 0; }

/* The thread is a TABLE in the markup and a column of bubbles on the screen
   — see js/n-office-views-guests.js's nOfMsgShell for why (a repeating head
   on paper). Every table box is unwound here, exactly as css/n-print.css
   unwinds the screen's scroll box the other way. */
.n-msg-t { display: block; width: 100%; border-collapse: collapse; }
.n-msg-head, .n-msg-head tr { display: block; }
/* css/app.css dresses every <th> and <td> in the app: uppercase, muted,
   10.5 px, a rule under it. None of that belongs on a conversation, and the
   two cells here are only cells so that PAPER can repeat the head — so both
   are reset to the surface's own type, in keywords rather than in values. */
.n-msg-headcell {
  display: block;
  padding: 0;
  border-bottom: 0;
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  font-size: inherit;
  font-weight: var(--n-w-regular);
  color: inherit;
}
.n-msg-bb { border-bottom: 0; }
.n-msg-headcell > .n-block-h,
.n-msg-headcell > .n-note,
.n-msg-headcell > span { display: block; }

/* A conversation. One control, the whole row, so the keyboard reaches every
   chat with Tab and the thumb has the whole width to land on. */
.n-msg-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 var(--n-space-3);
  width: 100%;
  min-height: var(--n-hit-button);
  padding: var(--n-space-2) var(--n-space-3);
  background: none;
  border: 0;
  border-radius: var(--n-radius-sm);
  text-align: left;
  color: var(--n-text);
  font: inherit;
  cursor: pointer;
}
.n-msg-row:hover { background: var(--n-accent-soft); }
/* The ring is the shell's own (§4: every button on this surface takes the
   halo) — a rule here would be dead weight AND a second answer to the same
   question. .n-ofg-help-s needs its own only because a <summary> is not one
   of the elements that rule names. */
.n-msg-row[aria-current="true"] {
  background: var(--n-accent-soft);
  box-shadow: inset var(--n-ring-w) 0 0 0 var(--n-accent-line);
}
.n-msg-who {
  display: flex;
  align-items: center;
  gap: var(--n-space-2);
  min-width: 0;
  font-weight: var(--n-w-semibold);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.n-msg-snip {
  grid-column: 1;
  min-width: 0;
  font-size: var(--n-step--1);
  color: var(--n-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.n-msg-when {
  grid-row: 1 / span 2;
  grid-column: 2;
  align-self: center;
  font-size: var(--n-step--1);
  color: var(--n-text-muted);
  white-space: nowrap;
}

/* The thread's own head, then the bubbles, then the reply. */
.n-msg-headcell { display: grid; gap: var(--n-space-2); }
/* Critic D3: this is a way BACK, not a section of the page. It used to be a
   grid item with nothing telling it otherwise, so it stretched the full
   width of the thread and read as a bordered box titled "Conversations". It
   is sized to its own words, sits at the left, and carries the ‹ that says
   which way it goes. */
/* The gap is not decoration: the arrow and the word are two flex items and
   the space between them in the markup is dropped, so without it the control
   reads "‹Conversations" (seen in this batch's own 390 shot). */
.n-msg-back { display: none; justify-self: start; gap: var(--n-space-1); }   /* only a phone needs it (§14) */
/* Critic D2: the guest's name, their number, the state pill, the two bot
   buttons and the 24-hour note were five stacked rows — measured at about
   190 px on a laptop and 230 px on a phone before the first bubble, on the
   one screen in the office whose whole content is the bubbles. What the chat
   IS (the pill) and what can be DONE to it (the two buttons) go on the
   name's own line, pushed to the far end of it; the sentences stay under it,
   one line each. It wraps rather than squeezing on a narrow thread. */
.n-msg-headline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-2) var(--n-space-3);
}
.n-msg-headwho {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--n-space-2);
  min-width: 0;
  margin-right: auto;
}
/* `.n-msg-headcell > span` above makes every direct child of the head cell a
   block; this row is the one that is not. The parent is named in the
   selector so it outranks that rule rather than racing it. */
.n-msg-headcell > .n-msg-headacts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-2);
}
/* P3 (B4a polish): two responsive-text patterns for the pill and the two bot
   buttons above --n-bp-phone (js/n-office-views-guests.js's nOfMsgHead). The
   short form is the default state — it is turned back on below in §14 — so
   `.n-ofg-short` starts hidden and the tail starts shown; both are plain
   inline text, so neither needs a display value at this width. */
.n-ofg-short { display: none; }
/* THE BOX IS THE SAME AT EVERY WIDTH (fixer round, blocker 2). O1 gave this
   box a phone-only `padding-bottom: calc(var(--n-space-8) * 7)` to hold the
   strip the sticky composer covers. A reserve inside the bubbles cannot do
   that job at any width: the composer is the thread's LAST child, so where
   the scroller ends it stands below the bubbles instead of over them, and
   the padding is a hole rather than a reservation (measured on the phone:
   the newest bubble came to rest 463.7 px clear of the composer). And while
   the scroller was NOT at its end the padding bought nothing — the newest
   bubble was still cut in half, at 1440 as well as at 390. The reserve is
   gone; js/n-office-views-guests.js's nOfMsgBubbles(toEnd) takes .n-ocontent
   to its end instead, which is the one place the composer is already clear.
   Top and bottom are the same token again, as they were before the round. */
.n-msg-bubbles {
  display: grid;
  margin: var(--n-space-4) 0;
  padding: var(--n-space-2) 0;
  gap: var(--n-space-3);
  max-height: var(--n-msg-h);
  overflow-y: auto;
}
.n-msg-b { display: flex; }
.n-msg-b[data-side="out"] { justify-content: flex-end; }
/* A MESSAGE IS BODY TEXT, AND THE WASH IS NOT A SELECTION (fix round, O4).
   Two things were wrong with the pair of bubbles. The guest's own words were
   set at step −1, the size this surface keeps for CAPTIONS, on the one
   screen whose entire content is somebody's sentences — so the smallest type
   in the office was the type the owner came to read. And the outgoing bubble
   was --n-accent-soft with --n-accent-line around it, which on this same
   screen is what a SELECTED conversation in the list beside it is drawn
   with: the same wash was saying "this row is open" on the left and "this
   line is ours" on the right. The two sides are told apart by where they sit
   and by their ground — the guest on the page's own surface, ours on the
   sunken one — and the accent pair goes back to meaning selection alone. */
.n-msg-bb {
  display: grid;
  gap: var(--n-space-1);
  max-width: var(--n-msg-bb-w);
  padding: var(--n-space-2) var(--n-space-3);
  border-radius: var(--n-radius-md);
  border: thin solid var(--n-border);
  background: var(--n-surface);
  color: var(--n-text);
  font-size: var(--n-step-0);
  line-height: var(--n-lh-prose);
}
.n-msg-b[data-side="out"] .n-msg-bb {
  background: var(--n-surface-sunken);
  border-color: var(--n-border-soft);
}
/* A line that is still going out, and one that never arrived. Neither is
   said with opacity — §8 measures contrast on text as drawn — so the ink and
   the border carry it and the words say it as well. */
.n-msg-b[data-state="pending"] .n-msg-bb { border-style: dashed; }
.n-msg-b[data-state="failed"] .n-msg-bb {
  background: var(--n-danger-soft);
  border-color: var(--n-danger);
}
.n-msg-txt { white-space: pre-wrap; overflow-wrap: anywhere; }
.n-msg-fail { font-weight: var(--n-w-semibold); color: var(--n-danger); }
/* …and the line under it — when it was sent, and whether it arrived — is a
   caption rather than a caption of last resort (step −1, not −2: the scale's
   floor is for a mark nobody has to read). */
.n-msg-meta {
  display: flex;
  align-items: center;
  gap: var(--n-space-1);
  justify-self: end;
  font-family: var(--n-font-num);
  font-variant-numeric: var(--n-font-num-variant);
  font-size: var(--n-step--1);
  color: var(--n-text-muted);
}
/* THE DELIVERY MARK (fix round, O6). It used to be the literal characters
   ◐ ✓ ✓✓ ✗ — four glyphs from four different parts of a font's coverage,
   which on a tablet's fallbacks rendered at four different weights and
   optical sizes, and which a screen reader read out as "heavy check mark".
   They are the icon set's own drawings now, sized by this rule rather than
   by a number passed into icon(), with the word beside each in .n-vh so the
   meta line is still a sentence when it is spoken. */
.n-msg-tick { display: inline-flex; align-items: center; }
.n-msg-tick svg { width: var(--n-step--1); height: var(--n-step--1); }
.n-msg-b[data-state="failed"] .n-msg-tick { color: var(--n-danger); }
/* THE 24-HOUR WINDOW WHEN IT IS SHUT (fix round, O5). WhatsApp's rule is the
   one thing on this screen that stops the owner doing what they came to do,
   and it was being said in the same grey as the sentence explaining what the
   bot answers. It takes the warn pair — the amber text tone on the amber
   wash, both already in the token file — which is the same treatment the
   masthead's shift chip uses for the same job. It is NOT --n-danger: nothing
   has gone wrong, and nothing is lost by waiting. */
[data-surface="office"] .n-note[data-n="msg-window"][data-tone="warn"] {
  padding: var(--n-space-2) var(--n-space-3);
  border-radius: var(--n-radius-sm);
  color: var(--n-warn);
  background: var(--n-warn-soft);
}
.n-msg-none { display: block; }
/* P3 (B4a polish): the reply box is the thread's own action, not a footer
   under it, so it stays on screen while the thread scrolls behind it — at
   EVERY width, not only the phone's (§14 used to be the only place this was
   true). With a long thread the reply used to fall after the bubbles at
   every other width and land below the fold — measured 215 px below it at
   1024×768, 20 px below it at 1440×900. `.n-ocontent` is the scrolling
   ancestor at every width (it always has been), so `position: sticky;
   bottom: 0` behaves the same way here as it always did on the phone. */
.n-msg-reply {
  display: grid;
  gap: var(--n-space-2);
  position: sticky;
  bottom: 0;
  padding-top: var(--n-space-2);
  background: var(--n-surface);
  border-top: thin solid var(--n-border-soft);
}
.n-msg-reply textarea { min-height: var(--n-hit-button); resize: vertical; }

/* ── 12f. Setup (spec §5, js/n-office-views-setup.js) ──────────────────
   Printers · Bill design · Drawer & till · Print jobs, Help, Reset and
   Benchmark. Batch B4b. Every size here is a multiple of the largest spacing
   token, the way §12e's message pane is, and every colour is semantic.

   The bill preview is the one thing on this surface set in a CHARACTER GRID
   — the layout the owner is choosing is "42 characters to the line", so a
   proportional face would draw a preview that lies about where the paper
   wraps. That face is now --n-font-mono in src/styles/tokens.css, beside the
   other three, rather than a literal typed here (fix round, T1).
   (§12b Menu and Stock, §12c People, §12d Money and §12e Guests took the
   numbers before this one; §13 and §14 have to stay last in the cascade.)  */
[data-surface="office"] {
  /* the bill preview is a paper roll: as wide as 42 characters read */
  --n-osu-slip-w: calc(var(--n-space-8) * 5);
  /* the peer map is a picture — tall enough to hold a city, short enough to
     leave the reading under it on the same screen */
  --n-osu-map-h: calc(var(--n-space-8) * 6);
}

/* Bill design: the switches and what they do to the paper, side by side, so
   a choice and its consequence are one glance apart. The heading and its ink
   rule are a row of their own across BOTH columns — the rule introduces the
   whole pairing, and one that stopped at the form column read as a rule
   somebody had drawn to the wrong length (fix round, D3). */
.n-osu-billgrid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--n-space-5);
  /* the heading's own margin already separates it from what it introduces;
     the column gap would say it a second time */
  row-gap: var(--n-space-2);
  align-items: start;
}
.n-osu-billgrid > .n-ins-h {
  grid-column: 1 / -1;
  margin-top: 0;
}
.n-osu-billcol { min-width: 0; }
/* The two file boxes. A raw <input type="file"> is the browser's own chrome —
   the one control on this surface drawn by the UA rather than by this file —
   so the box is visually hidden (it still carries the label, the accept list
   and the change event) and its LABEL is the button, saying which file has
   been chosen. A <label> is not one of the elements §4's ring rule names, so
   it says the ring itself, exactly as the guide heading below does. */
.n-osu-file { display: flex; flex-wrap: wrap; align-items: center; gap: var(--n-space-2); min-width: 0; }
.n-osu-filebtn { display: inline-flex; align-items: center; max-width: 100%; }
.n-osu-filebtn > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.n-osu-filebtn:focus-visible { outline: none; box-shadow: var(--n-focus-ring); }
/* The preview follows the switches down the column: every one of them is
   about this piece of paper, and a preview that has scrolled off the top is
   a preview of nothing. */
.n-osu-prev {
  margin: 0;
  position: sticky;
  top: var(--n-space-4);
}
.n-osu-slip {
  margin: var(--n-space-2) 0;
  padding: var(--n-space-4);
  width: var(--n-osu-slip-w);
  max-width: 100%;
  max-height: calc(var(--n-space-8) * 7);
  overflow: auto;
  white-space: pre;
  background: var(--n-surface-sunken);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-sm);
  font-family: var(--n-font-mono);           /* the character grid — see above */
  font-size: var(--n-step--1);
  line-height: var(--n-lh-ui);
  color: var(--n-text);
}
/* The logo and the review QR, as they will be printed — small, because what
   is being checked is that the right image is there. */
.n-osu-thumb {
  max-width: calc(var(--n-space-8) * 2);
  max-height: calc(var(--n-space-7) * 2);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-sm);
}
/* A slider is the one control on this surface that is a length rather than a
   box, so it takes the column it is in. */
.n-osu-range { width: 100%; }

/* A guide: a heading you can open, an ink rule under each, nothing else. A
   real <details> — the browser keeps the open state — with §12e's summary
   shape and the shell's own focus ring (a box-shadow, which is what
   --n-focus-ring is). */
.n-osu-guide { border-bottom: thin solid var(--n-border-soft); }
.n-osu-guide-h {
  display: flex;
  align-items: center;
  min-height: var(--n-hit-button);
  font-weight: var(--n-w-semibold);
  color: var(--n-text);
  cursor: pointer;
}
.n-osu-guide-h:focus-visible { outline: none; box-shadow: var(--n-focus-ring); }
.n-osu-guide-b {
  padding: 0 0 var(--n-space-4);
  max-width: var(--n-read);
  line-height: var(--n-lh-prose);
  color: var(--n-text-2);
}

/* The peer map (js/26-benchmark.js draws Leaflet into it) and a trend chart. */
.n-osu-map {
  height: var(--n-osu-map-h);
  border: thin solid var(--n-border-soft);
  border-radius: var(--n-radius-md);
  overflow: hidden;
}
.n-osu-trend { margin: var(--n-space-5) 0 0; max-width: var(--n-read); }
.n-osu-trend .n-lbl { display: flex; flex-wrap: wrap; gap: var(--n-space-2); align-items: baseline; }

/* THE DESTRUCTIVE PRIMARY IS A FILLED SURFACE (fix round, D1). On the typed
   sheets — erase the menu, erase the stock — the button that does the thing
   was a hairline crimson outline standing beside an equal white "Keep the
   menu", which made the irreversible action the least emphatic control in
   the room. It takes the danger GROUND and the ink that reads on that ground
   (--n-danger-ink: paper on the light scheme's pomegranate, ink on the dark
   scheme's pale one — 6.5:1 and 9.1:1), the way .n-act-go takes the accent
   and --n-accent-ink. The word "delete" is still typed by hand before it can
   be pressed at all: this is emphasis, not a shorter road.
   Disabled — which is how it starts, and how it stays until the phrase is
   right — it is not crimson at all, or a sheet would open showing a filled
   primary that cannot be pressed. */
.n-act-danger-go {
  background: var(--n-danger);
  border-color: var(--n-danger);
  color: var(--n-danger-ink);
}
.n-act-danger-go:disabled {
  background: var(--n-surface-sunken);
  border-color: var(--n-border-soft);
  color: var(--n-text-muted);
}
/* …and the way OUT of one of those sheets is a text button. It is the safe
   answer and the one the sheet is happy to be given, so it needs no weight
   at all — it needs to be legible and the size of a thumb, which it is. */
.n-act-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--n-text-2);
}
.n-act-quiet:hover { background: var(--n-surface-sunken); }

/* The printers' empty state offers the same thing the page header does, and
   two saffron primaries a hand's width apart read as two different actions
   (fix round, D5). The header keeps the primary; the one in the empty state
   is the secondary it always was in weight. Scoped to this table by its own
   wrap: every other empty state on the surface is the ONLY offer on its
   screen and stays a primary. */
[data-n="table-wrap-printers"] .n-tbl-emptyact {
  background: var(--n-surface);
  border-color: var(--n-border);
  color: var(--n-text);
}
[data-n="table-wrap-printers"] .n-tbl-emptyact:hover { background: var(--n-surface-sunken); }

/* Narrow — the same width at which the inspector stops being a column (§13):
   the preview stops being a second column and goes under the switches it is
   a preview of, at the width it actually needs. Declared here rather than in
   §13 so the whole of Setup is in one place; §13 and §14 still hold the
   shell's own two breakpoints. */
@media (max-width: 1199px) {
  .n-osu-billgrid { grid-template-columns: minmax(0, 1fr); }
}

/* ── 12g. Intelligence (spec §7, js/n-intel.js) ────────────────────────
   The question's answer in the inspector, Tonight's watch cards, and the
   briefing twin. Batch B5. (§12b Menu and Stock, §12c People, §12d Money,
   §12e Guests and §12f Setup took the numbers before this one; §13 and §14
   have to stay last in the cascade.)

   Nothing here is a card with a shadow. A watch card is a paragraph, a
   second paragraph, a figure and a button, separated from the next one by
   the same ink rule everything else on this surface is separated by — the
   spec's "blocks under ink rules, not cards" applies inside a block too.  */

/* THE THINKING STATE. A sentence, and a bar that fills ONCE over
   --n-dur-voice-max — the app's own ceiling on a local-model call, the same
   token js/n-floor-extras.js bounds a voice clip with. `1 forwards`: it
   runs one time and stays where it stopped. It is not a spinner and it does
   not loop, and it is not drawn at all for a reader who asked for less
   motion (the sentence beside it says everything the bar does). */
.n-intel-wait { margin: 0; color: var(--n-text-2); font-size: var(--n-step-0); }
.n-intel-bar {
  margin-top: var(--n-space-3);
  height: var(--n-space-1);
  border-radius: var(--n-radius-pill);
  background: var(--n-track);
  overflow: hidden;
}
.n-intel-bar i {
  display: block;
  height: 100%;
  border-radius: var(--n-radius-pill);
  background: var(--n-accent);
  transform-origin: left center;
  animation: n-intel-fill var(--n-dur-voice-max) linear 1 forwards;
}
@keyframes n-intel-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) {
  .n-intel-bar { display: none; }
}

/* The answer itself: prose at the panel's own measure, the figures inside it
   already re-set by js/n-intel.js and wearing .n-num. */
/* `pre-wrap`, because a model that answers in three short paragraphs wrote
   those line breaks on purpose and collapsing them turns a list into one
   run-on sentence. The text is escaped before it gets here, so the only
   thing being preserved is the model's own spacing. */
.n-intel-said {
  font-size: var(--n-step-0);
  line-height: var(--n-lh-prose);
  color: var(--n-text);
  white-space: pre-wrap;
}
.n-intel-said .n-num { font-family: var(--n-font-num); }
.n-intel-bad { margin: 0; font-size: var(--n-step-0); color: var(--n-danger); }
/* "How this was worked out" — a section of the panel, separated from the
   answer above it by ONE hairline, which is the one .n-ins-h already draws
   for every section heading in the inspector (`.n-ins-receipt` above says
   why: two hairlines eight pixels apart is a printing error, not a
   section). So this box draws none of its own.

   Its facts STACK — the term over the value, both left-aligned. The
   inspector's ordinary `.n-ifact` is a two-column grid with the value right-
   aligned, which is right for "Sold 142" and wrong here: these values are
   sentences ("None — the question is the whole request"), and a sentence in
   an `auto` column grows leftwards straight through the term beside it
   (measured: "Range" was printed over by its own value). */
.n-intel-how { margin-top: var(--n-space-2); }
.n-intel-how .n-ifact { grid-template-columns: minmax(0, 1fr); gap: 0; }
.n-intel-how .n-ifact dd { text-align: left; overflow-wrap: anywhere; }
/* …and the disclaimer under them (fix round, D2). It was the biggest block
   of prose in the panel — 98 px, seven lines at the panel's own body size —
   sitting between the heading and the four facts, which put all four below
   the fold at 1440. It is now the LAST thing in the block (js/n-intel.js)
   and one step down in the muted ink: still there to be read, no longer the
   thing standing in front of what it explains. */
.n-intel-how .n-note { font-size: var(--n-step--1); color: var(--n-text-muted); }

/* ── the watch cards ─────────────────────────────────────────────────── */
.n-intel-card {
  display: block;
  padding: var(--n-space-4) 0;
  border-top: thin solid var(--n-border-soft);
}
.n-intel-card:first-child { padding-top: 0; border-top: 0; }
.n-intel-what {
  margin: 0;
  max-width: var(--n-read);
  font-size: var(--n-step-1);
  line-height: var(--n-lh-prose);
  color: var(--n-text);
}
.n-intel-do {
  margin: var(--n-space-1) 0 0;
  max-width: var(--n-read);
  font-size: var(--n-step-0);
  color: var(--n-text-muted);
}
/* A model's own words are QUOTED, never restated as the app's. Nothing on
   this surface returns one today; the rule is here so that when an endpoint
   does, it reads as a quotation and not as a finding. */
.n-intel-quote {
  margin: var(--n-space-3) 0 0;
  padding-left: var(--n-space-3);
  max-width: var(--n-read);
  border-left: var(--n-space-1) solid var(--n-accent-line);
  color: var(--n-text-2);
  font-size: var(--n-step-0);
}
/* The figure is LAST, and it is the only thing on the card set in the
   number face. Its label is beside it, because a number alone is a riddle. */
.n-intel-fig {
  display: flex;
  align-items: baseline;
  gap: var(--n-space-2);
  margin: var(--n-space-3) 0 var(--n-space-3);
}
.n-intel-figl { color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-intel-fig .n-num {
  font-family: var(--n-font-num);
  /* THE THOUSANDS SEPARATOR IS A THIN SPACE (fix round, D3). fmtMoney sets
     "R 4 210.50" with U+2009 between the groups, and at this size, in
     proportional figures, that gap grew wide enough to read as a WORD BREAK
     — the card's one number looked like two numbers. The token turns the
     tabular figures on, which fixes every digit to one advance width and
     brings the group gap back to what it is meant to be. */
  font-variant-numeric: var(--n-font-num-variant);
  font-size: var(--n-step-3);
  font-weight: var(--n-w-semibold);
  color: var(--n-text);
}
.n-intel-card[data-tone="danger"] .n-intel-fig .n-num { color: var(--n-danger); }
.n-intel-card[data-tone="warn"] .n-intel-fig .n-num { color: var(--n-warn); }
/* The stale-shift nudge is adopted into this same block (js/n-office-views.js)
   and arrives with the old shell's own markup; it gets the card's separation
   so the watch reads as one list rather than as cards plus a stray box. */
#nIntelWatch + #staleShiftNudge,
#nOfBody-anomalies > #staleShiftNudge { margin-top: var(--n-space-4); }

/* ── the briefing twin ───────────────────────────────────────────────── */
.n-intel-when { margin: 0; color: var(--n-text-muted); font-size: var(--n-step--1); }
.n-intel-head {
  margin: var(--n-space-2) 0 0;
  max-width: var(--n-read);
  font-family: var(--n-font-display);
  font-size: var(--n-step-3);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-display);
  color: var(--n-text);
}
.n-intel-lines {
  margin: var(--n-space-3) 0 0;
  padding-left: var(--n-space-5);
  max-width: var(--n-read);
  font-size: var(--n-step-0);
  line-height: var(--n-lh-prose);
  color: var(--n-text);
}
.n-intel-line { margin-bottom: var(--n-space-1); }
.n-intel-act {
  margin: var(--n-space-3) 0 0;
  max-width: var(--n-read);
  font-size: var(--n-step-0);
  color: var(--n-text);
}
.n-intel-acts { display: flex; flex-wrap: wrap; gap: var(--n-space-2); margin-top: var(--n-space-4); }
[data-surface="office"] #nIntelBrief .n-sums { margin-top: var(--n-space-4); max-width: var(--n-read); }
[data-surface="office"] #nIntelBrief .n-num { font-family: var(--n-font-num); }

/* The mic in the command row (spec §7's voice entry). .n-cmd-icon already
   gives it the row's height; a square target is what makes it 44 px WIDE as
   well, which an icon-only button has to be. */
.n-cmd-mic { min-width: var(--n-hit-button); justify-content: center; }
/* The line's one-off note, hung under the field the way the results list is
   — same left edge, same width, under whatever is showing. */
.n-cmd-under {
  position: absolute;
  top: calc(100% + var(--n-space-1));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--n-space-2) var(--n-space-3);
  background: var(--n-surface-raised);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
  box-shadow: var(--n-shadow-2);
  color: var(--n-text-2);
  font-size: var(--n-step--1);
  z-index: 1;
}
/* The same sentence said again (fix round, D5): the second and every later
   ask of a session, where it is a reminder rather than news. Same box, same
   place, the muted ink. */
.n-cmd-under-q { color: var(--n-text-muted); }

/* THE INSPECTOR PANEL'S OWN FOCUS STOP (fix round, D4). js/n-intel.js moves
   the keyboard onto the panel when an answer lands (it carries tabindex="-1"
   for exactly that), and the panel is not a button, a field or a row — so it
   fell through §4's list and took the browser's own 1 px ring while every
   other stop on this surface measures the token's three. It gets the ring
   the office draws everywhere else.

   AND IT GETS IT AS AN OUTLINE AS WELL, for the reason the table row above
   does. Measured at 1440 with the answer's focus on the panel: the halo
   computes (getComputedStyle reports both shadow layers, the surface one at
   --n-ring-gap and the accent one at --n-ring-gap + --n-ring-w) and three of
   its four edges are NOT PAINTED — .n-oinspector is the scroll
   column (`overflow: auto`), the panel fills it, and a box-shadow drawn
   outside a box is clipped by the box that scrolls it. Photographed edge by
   edge: top no, left no, right no, bottom yes. A focus ring that shows one
   edge of four is the invisible-cursor bug in miniature, so the same ring is
   drawn INSIDE the panel too — --n-ring-w of --n-accent-line, at a negative
   offset of its own width — which is the one mechanism that cannot be
   clipped. Same two colours, same two widths, no new value. */
.n-oinspector:focus-visible,
[data-n="inspector-panel"]:focus-visible {
  outline: var(--n-ring-w) solid var(--n-accent-line);
  outline-offset: calc(-1 * var(--n-ring-w));
  box-shadow: var(--n-focus-ring);
}

/* ── 13. narrower than the inspector's home ───────────────────────────── */
/* Below 1200 px the inspector column goes; js/n-office.js reads that from the
   DOM and opens the same content as a sheet instead, so the breakpoint is
   written once, here. */
@media (max-width: 1199px) {
  .n-oinspector { display: none; }
  .n-office, .n-office[data-inspector="on"] { grid-template-columns: var(--n-rail-w) minmax(0, 1fr) 0; }
  .n-office[data-rail="wide"], .n-office[data-rail="wide"][data-inspector="on"] { grid-template-columns: var(--n-rail-wide-w) minmax(0, 1fr) 0; }
  .n-ohead, .n-obody { padding-left: var(--n-space-4); padding-right: var(--n-space-4); }
  [data-surface="office"] #main { padding-left: var(--n-space-4); padding-right: var(--n-space-4); }
}
/* ── 14. the phone ────────────────────────────────────────────────────── */
/* --n-bp-phone (600 px), not the 700 that used to be typed here: everything
   in this block is a phone compression — the wordmark and the user's name
   give the command line its width back, the title and the number step down,
   the page header stacks, and a table keeps only the columns a phone can
   honestly show. A custom property cannot live in an @media prelude, so the
   number is repeated here and nowhere else, exactly as css/n-floor.css
   repeats it. */
@media (max-width: 600px) {
  .n-cmd-word, .n-cmd-name { display: none; }

  /* THE COMMAND LINE GETS A ROW OF ITS OWN (fix round, O13). The previous
     answer to this — take the row's spare width back, drop a space token of
     padding — bought the field 75.2 px at 390, and 75 px is not a search
     box: the placeholder still painted as "Searcl", because the six other
     things on that row (the flame, ⋯, the shift chip, chat, the user, the
     rail button) are all flex: 0 0 auto and between them they had already
     spent the width. There is no arrangement of seven controls across
     390 px in which the one you type into is bigger than the ones you tap,
     so the row wraps: the six marks keep line one, the field takes the
     whole of line two, and the masthead becomes two rows tall.
     --n-cmd-h is restated to match — it is the grid's first row AND what
     everything sticky under it measures itself from, so a masthead that
     grew without it would have printed the page header underneath itself.
     Measured, not guessed: one row is --n-space-7 + --n-space-2 (56 px) and
     holds a --n-hit-button control with --n-space-1 either side; two of
     those rows is exactly twice that. */
  .n-cmdrow { flex-wrap: wrap; }
  .n-cmd-field {
    flex: 1 0 100%;
    order: 3;
    min-height: var(--n-hit-button);
  }
  .n-cmd-field input { padding-left: var(--n-space-3); padding-right: var(--n-space-3); }
  [data-surface="office"] { --n-cmd-h: calc((var(--n-space-7) + var(--n-space-2)) * 2); }

  /* The rail goes (spec §1). Eight unlabelled icons were taking a fifth of a
     390 px screen and squeezing every table into a column two of its own
     columns could not fit; the same eight groups open as a sheet from the
     button the command row grows at this width. Two grid columns now, so the
     areas template has to be restated with two names per row. */
  .n-orail { display: none; }
  .n-cmd-rail { display: inline-flex; }
  .n-office,
  .n-office[data-rail="wide"],
  .n-office[data-inspector="on"],
  .n-office[data-rail="wide"][data-inspector="on"] {
    grid-template-columns: minmax(0, 1fr) 0;
    grid-template-areas: "cmd cmd" "content inspector";
  }
  .n-otitle { font-size: var(--n-step-3); }
  [data-surface="office"] .n-onumber { font-size: var(--n-step-5); }
  /* A watch card's figure steps down with everything else (fix round, D3).
     At --n-step-3 "R 4 210.50" ran to within a few pixels of a 390 px
     card's width, which is the width that made the thin space read as a
     break in the first place; one step in, the figure is still the last and
     largest thing on the card and it is one number again. */
  .n-intel-fig .n-num { font-size: var(--n-step-2); }

  /* The page actions used to sit in a second column and squeeze the title to
     nothing; they get their own row under it instead. */
  .n-ohead { grid-template-columns: minmax(0, 1fr); }
  .n-oacts { grid-column: 1; grid-row: auto; justify-content: flex-start; }
  [data-surface="office"] #main { padding-left: var(--n-space-4); padding-right: var(--n-space-4); }

  /* A tab strip is ONE row that scrolls sideways, never two rows that push
     the screen's first number below the fold. Staff & comps carries five
     tabs; at 390 px they wrapped onto a second line, and with the page
     actions above them and the day picker below them the first KPI started
     at y ≈ 526 — 62 % of the screen spent on chrome before a number. Same
     pattern as the floor's category rail at this width (css/n-floor.css):
     a nowrap flex row with its own horizontal scroll, and cells that size to
     their words. */
  .n-osegs-tabs {
    position: relative;   /* the chevron below is placed against this box */
    display: flex;
    flex-wrap: nowrap;
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;
  }
  .n-osegs-tabs .n-oseg { flex: 0 0 auto; white-space: nowrap; }

  /* D3 (batch B3a): a scrolling row that cuts a tab dead at the edge with no
     hint that scrolling brings it back is the map's own bug (css/n-floor.css)
     in miniature — at 390 px Employees' "Activity log" clipped mid-word
     against the viewport edge and said nothing. Same two marks, same reason:
     the fade alone reads as a fading LABEL (which tab is this half-word?),
     so the persistent chevron is the affordance and the fade is only the
     run-up to it.

     One difference from the map's version, and it is layout, not intent: the
     map overlays a `display: grid` container whose direct children are the
     scrolling canvas and the chevron, so both share one grid cell. This strip
     IS the scrolling element (a flex row, not a wrapper around one), so the
     chevron is positioned against it directly instead — `position: relative`
     above, `position: absolute` below — rather than adding a wrapper element
     this batch does not otherwise need.

     Armed by nOfPTabsOverflow() (js/n-office-views-people.js) on render and
     on resize, the same two triggers nMapOverflow() uses and for the same
     reason: a strip's own content only changes on those two events, not on
     every frame. Disarmed the same way too — scrolling to the end (or a
     strip narrow enough that nothing overflows at all) removes the
     attribute, so a chevron never points past furniture, or a tab, that
     is not there. */
  .n-osegs-tabs[data-n-overflow="more"] {
    -webkit-mask-image: linear-gradient(to right, currentColor calc(100% - var(--n-space-6)), transparent);
    mask-image: linear-gradient(to right, currentColor calc(100% - var(--n-space-6)), transparent);
  }
  .n-osegs-tabs[data-n-overflow="more"]::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: var(--n-space-6);
    pointer-events: none;
    background-color: var(--n-text-muted);
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E");
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: var(--n-space-5) var(--n-space-5);
    mask-size: var(--n-space-5) var(--n-space-5);
  }

  /* The screen's own explanation collapses to one line (§12c). 144 px of a
     844 px screen went on a paragraph the owner has read once: with the
     command row, the header, the tabs and the day heading above it, Staff &
     comps' first number started at y = 474. Closed by default; the button
     is a real one, so Tab reaches it and Enter opens it. */
  .n-odisc { display: inline-flex; }
  .n-odisc-body[data-closed] { display: none; }
  /* One line does not need a block's own vertical padding around it: the
     button already carries a full tap target's height, and the 32 px this
     saves is 32 px of the screen the first number was below. */
  .n-block:has(> .n-block-body > .n-odisc) { padding-top: 0; padding-bottom: 0; }

  /* A LABEL SITS OVER ITS FIELD ON A PHONE (fix round, D4). .n-formrow is a
     baseline-aligned ROW — label, field, and whatever the row's own action is
     — which is right at a desk and wrong at 390 px: "Search the guides" took
     143 px of the row and left its input 190 px, a search box narrower than
     the words it was there to hold. Below this width the row stacks, the
     label sits over the field, and the field takes the column. Office-wide,
     because every screen's row has the same arithmetic (checked at 390 on
     Help, on Guests' send row and on People's date ranges); the ROW is what
     stacks, so a row that also carries a button keeps it under the field
     rather than beside a squeezed one.

     SCOPED, AND ONE LEVEL DEEP (closing polish). Two faults in the rules as
     they were written. `.n-formrow` was declared UNSCOPED while every other
     rule on this surface sits under [data-surface="office"] — a latent trap
     for the next surface that borrows the class name (today only the office's
     own view files emit it), so it is scoped like the rest. And
     `.n-formrow input` was a DESCENDANT
     selector: it reached past the row's own fields into the checkbox inside
     a `<label class="n-check">` that some rows carry (Staff & comps'
     entitlement ticks are the live case), stretching a 16 px tick across the
     column. The field that takes the column is the row's OWN child, so the
     child selector is what says so — and the tick keeps its 16 px. */
  [data-surface="office"] .n-formrow { flex-wrap: wrap; align-items: stretch; gap: var(--n-space-2); }
  [data-surface="office"] .n-formrow > .n-lbl { flex: 1 0 100%; }
  [data-surface="office"] .n-formrow > :is(input, select, textarea) { flex: 1 1 100%; width: 100%; min-width: 0; }

  /* A column's WIDTH is a desk decision. js/n-table.js gives each header its
     --n-col-w — the column's declared `width`, or the owner's drag over it —
     and at this breakpoint the two columns a phone keeps can ask for more
     than the phone has: Employees keeps Name (2) and Role (1), which is
     384 px of --n-tbl-col against 358 px of screen, so the table that is
     supposed to fit began to scroll sideways by 26 px (measured on Employees
     and on Items). At 390 px the office's contract is two columns and no
     sideways scroll, and that outranks any one column's share, so the
     columns that are left divide what there is. The two AI drafts are the
     exception here exactly as they are in css/n-table.css: they scroll on
     purpose, and their proportions are what make six review controls usable.

     Declared here rather than in css/n-table.css for the same reason the
     rule below is: that file is the shared component's — and the marketing
     site's own copy of it — and this is the OFFICE's reading of it. */
  [data-surface="office"] .n-tbl:not([data-n="table-wrap-invoice-draft"]):not([data-n="table-wrap-menu-draft"]):not([data-n="table-wrap-payroll-run"]) thead th { width: auto; }

  /* THE PAYROLL RUN SCROLLS SIDEWAYS (fix round, B4). It is a
     review-and-pay table — the same case as the two AI drafts, and the lead
     ruled it the same way: every column is priority 1
     (js/n-office-views-people.js), so nothing is hidden, and the table takes
     the width its thirteen columns ask for while the scroll box carries it.
     Hiding eleven of them and leaving "Mark period as paid — R 41 883.24"
     live underneath was the blocker: the tick that decides who is in the run,
     the hours that decide what they are paid and the deductions that come off
     were all off screen at 390. The Pay tick and the name are the first two
     columns, so a thumb starts where the row starts, and the whole row is
     one flick away. Declared here rather than in css/n-table.css because
     that file is the shared component's — and the marketing site's own copy
     of it — and this is the office's reading of it, exactly as the rule
     above is. */
  [data-n="table-wrap-payroll-run"] .n-tbl-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  [data-n="table-wrap-payroll-run"] table { width: max-content; min-width: 100%; }
  /* …and the one control in that scrolling row that a THUMB has to hit. Every
     checkbox in the app is 17 px (css/app.css), which is a mouse's target;
     the tick that decides whether a person is paid at all is the first thing
     a phone shows and the last thing anyone should have to aim at, so at this
     width it is a full --n-hit-button. The column is already that wide plus a
     space (§12d), so nothing else moves. */
  [data-n="table-wrap-payroll-run"] tbody td:first-child input[type="checkbox"] {
    width: var(--n-hit-button);
    height: var(--n-hit-button);
    min-width: var(--n-hit-button);
  }

  /* A cell that carries a flag WRAPS. Every table cell on this surface is
     `nowrap` + `text-overflow: ellipsis`, which is right for a column of
     names and wrong for a column of names AND flags: at 390 px the reorder
     watch's "Beef patty" plus "runs out today" had room for neither, so the
     ellipsis ate the cell and the row read "Beef patty …" — the name
     truncated and the warning, which is the only reason that row is on the
     screen at all, gone with it. The cell gets a second line instead. The
     flag is never hidden: a warning that disappears at the width most owners
     read on is not a warning.

     `.n-tbl td:has(.n-flag)` outweighs `.n-tbl tbody td` (a :has() takes the
     specificity of its argument), so it holds wherever the two meet. It is
     declared here rather than in css/n-table.css because that file is the
     shared component's and this is the office's reading of it. */
  .n-tbl td:has(.n-flag) { white-space: normal; }

  /* D2 (fix round): the run's tallest cell sat off in the table's own
     sideways scroll (the rule above) — a premium hint or an "Overlaps a
     paid period" warning forced the WHOLE row to its height even where the
     columns actually on screen (the Pay tick, the name) had nothing to say,
     leaving roughly 100 px of dead space under a one-line name. Below this
     breakpoint the run's own notes (under the name) and its Time-worked
     hint (under the quantity box) move into the row's inspector instead
     (nOfPrPhoneNotes, js/n-office-views-people.js) — the row stays the one
     line its visible content needs. The unit label beside the quantity box
     (phrsu_, "h" / "days") is not one of these — it is not a note, it is
     the box's own unit, and it stays. */
  [data-n="table-wrap-payroll-run"] tbody td [id^="pown_"],
  [data-n="table-wrap-payroll-run"] tbody td [id^="pmnote_"],
  [data-n="table-wrap-payroll-run"] tbody td [id^="pwarn_"],
  [data-n="table-wrap-payroll-run"] tbody td [id^="pstale_"],
  [data-n="table-wrap-payroll-run"] tbody td [id^="ppaid_"],
  [data-n="table-wrap-payroll-run"] tbody td [id^="phint_"] {
    display: none;
  }
  /* Messages: one pane at a time, with a way back (spec §5). At 390 px the
     two-column pane gave the thread about 150 px — narrower than one bubble —
     so the list and the thread take the whole width in turn and the thread
     grows a control that returns to the list. `data-pane` is written by
     js/n-office-views-guests.js on the pane itself, so the breakpoint is read
     from the DOM rather than duplicated in the script. */
  .n-msg { grid-template-columns: minmax(0, 1fr); }
  .n-msg[data-pane="list"] .n-msg-thread { display: none; }
  .n-msg[data-pane="thread"] .n-msg-list { display: none; }
  .n-msg-thread { padding-left: 0; border-left: 0; }
  .n-msg-back { display: inline-flex; }
  /* …and a bubble may take the column it is in: the token measure is a
     desk's, and on a phone it left a third of the width empty on both
     sides. The SIDE still says who spoke, and so does the visually-hidden
     name every bubble carries. */
  .n-msg-bb { max-width: 100%; }
  /* Critic D2, the phone half: the box the owner types into has to be ON the
     screen with the conversation — `.n-msg-reply`'s own rule is sticky at
     every width now (P3, B4a polish), so nothing phone-specific is left to
     say here. */

  /* P3 (B4a polish): the pill plus the two bot buttons need ~394 px on one
     line and this pane is 358 px wide, so they wrapped — 255 px of chrome
     above the first bubble at 390×844 (measured). Both patterns flip here:
     the tail a button keeps everywhere else is dropped, and the pill's short
     twin takes over from its long one. */
  .n-ofg-tail { display: none; }
  /* The FULL sentence is not `display:none` here: that would take it out of
     the accessibility tree, and a phone screen-reader user would be left
     with "bot on" and "Reply within the window" and no explanation (critic,
     closing polish). It is the visually-hidden atom instead — clipped to
     nothing, still announced — while the short twin is what the eye gets. */
  .n-ofg-full {
    position: absolute;
    width: var(--n-space-1);
    height: var(--n-space-1);
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .n-ofg-short { display: inline; }

  /* …and the two sentences under the name stop paying for their space
     twice (closing polish). The head cell is a grid with a --n-space-2 gap
     of its own, so the note inside it does not need its own margins as
     well, and the state note — which is EMPTY on the ordinary chat, where
     the bot is answering and nobody has asked us to stop — should not be
     spending a gap at all. Measured at 390×844, top of the head to the
     first bubble: 255 px before the batch, 215 with the window note folded
     to one line, 191 with these two. */
  .n-msg-headcell > span:empty { display: none; }
  .n-msg-headcell [data-n="msg-window"] { margin-block: 0; }
}
