/* ════════════════════════════════════════════════════════════════════════════
   n-floor.css — the floor surface (POS spec §4–§8)

   Every value in this file is a token or arithmetic over tokens. The spec's
   fixed sizes are expressed as the token sums that equal them, so the whole
   surface still answers to the density dial and to --n-hit-min:

     rail        96 = --n-space-8 + --n-space-6
     ticket     360 = --n-space-7 × 7.5      (docked, 601–900px: 320 = 64 × 5)
     tile min   132 = --n-space-8 × 2 + --n-space-1
     tile tall  112 = --n-hit-min × 2
     total bar   72 = --n-hit-min + --n-space-4
     tender      64 = --n-space-8
     table       96 = --n-space-8 + --n-space-6
     swipe       96 = --n-space-8 + --n-space-6

   Two things have no token and are written as CSS keywords rather than
   numbers, so this file holds no px at all:
     · a hairline is `thin` (the UA's 1px), never a literal;
     · the map's dot grid sizes its dot as a percentage of the token-sized
       tile, because there is no 1 px token.
   Both are listed for the lead.

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

/* ── 1. the shell gives way to the instrument panel ───────────────────── */
[data-surface="floor"] #shell {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  grid-template-areas: "main";
}
[data-surface="floor"] #side,
[data-surface="floor"] #top,
[data-surface="floor"] #tabbar { display: none; }
[data-surface="floor"] #main { padding: 0; overflow: hidden; }
/* the `hidden` attribute has to win over the display rules below (the whole
   surface toggles panels with it, and a `display:` rule would out-specify the
   UA's [hidden] default) */
[data-surface="floor"] [hidden] { display: none !important; }
/* The floating "Ask the app" helper sat on top of the ⋯ button and the total
   bar. On the floor it is summoned from the hub sheet instead — the same rule
   as the rest of the office chrome. */
[data-surface="floor"] #helpFab { display: none; }
/* The floating voice mic (js/32-voice.js) is a 52 x 52 button — under the
   floor's 56 px minimum — and on every floor screen it sat on top of the
   ticket. The floor's mic is the one in the STRIP (.n-strip-mic below), at
   --n-hit-min and only when the server can actually hear, so this one stays
   out of the way here. Off the floor it is untouched. */
[data-surface="floor"] #vcBtn { display: none; }
/* …and the stock-take panel it pins above that mic (#vcTakeBar, fixed, its own
   z-index) is drawn over the floor's own furniture with buttons under the
   56 px minimum. On the floor the take lives in the voice sheet instead — the
   count so far and the batch Confirm (D10, js/n-floor.js's nVoiceActions).
   Off the floor the bar is untouched. */
[data-surface="floor"] #vcTakeBar { display: none; }

/* ── 2. sizes derived once, on the surface, so sheets and toasts (which are
      children of <body>, not of .n-floor) inherit them too ────────────── */
[data-surface="floor"] {
  --n-rail-w: calc(var(--n-space-8) + var(--n-space-6));
  --n-ticket-w: calc(var(--n-space-7) * 7.5);
  --n-ticket-dock-w: calc(var(--n-space-8) * 5);
  --n-tile-min: calc(var(--n-space-8) * 2 + var(--n-space-1));
  --n-tile-h: calc(var(--n-hit-min) * 2);
  --n-total-h: calc(var(--n-hit-min) + var(--n-space-4));
  --n-tender-h: var(--n-space-8);
  --n-table-min: calc(var(--n-space-8) + var(--n-space-6));
  --n-swipe-w: calc(var(--n-space-8) + var(--n-space-6));
  --n-sheet-max-w: calc(var(--n-space-8) * 12);
}

/* ── 3. shared atoms ──────────────────────────────────────────────────────
   :where() so these carry the specificity of a bare element selector: they
   must beat app.css's `button {}` (later in the cascade, same weight) and lose
   to every component class below — otherwise `.n-floor button{background:
   transparent}` out-specifies `.n-total` and the total bar loses its fill. */
:where(.n-floor, .n-sheet, .n-toast) button {
  font-family: var(--n-font-ui);
  font-size: var(--n-step-1);
  font-weight: var(--n-w-semibold);
  color: var(--n-text);
  background: transparent;
  border: 0;
  border-radius: var(--n-radius-sm);
  transition: background var(--n-dur-fast) var(--n-ease), color var(--n-dur-fast) var(--n-ease);
}
:where(.n-floor, .n-sheet, .n-toast) :is(button, input):focus-visible {
  outline: none;
  box-shadow: var(--n-focus-ring);
}
:where(.n-floor, .n-sheet) input {
  font-family: var(--n-font-ui);
  font-size: var(--n-step-1);
  height: var(--n-hit-min);
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-3);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
  background: var(--n-surface);
  color: var(--n-text);
  width: 100%;
}
/* A placeholder is a LABEL until something is typed — "Search or scan" is
   how an operator knows what that box is for. The UA's default is the text
   colour at about 54 % opacity, which on this surface reads 2.6:1 against
   white: under the 4.5:1 the spec sets, and set with the one device the spec
   forbids on text (opacity). It is a real token here, at full opacity.
   --n-text-muted is the token whose job this is: measured at runtime it
   reads 6.31:1 on the light surface and 6.75:1 on the dark one.

   The rule reached the search box and the sheets and stopped there, which
   left exactly one floor placeholder behind: the ticket row's "Kitchen note",
   which is neither. It inherited app.css's `input::placeholder{color:
   var(--faint)}` — --n-text-faint, 3.66:1 on the light surface and 4.00:1 on
   the dark one, both under the 4.5:1 this spec sets. The selector is the
   SURFACE now, so a field added to the floor tomorrow is covered the day it
   is written; the two class selectors stay for the sheets, which nSheet()
   mounts outside [data-surface="floor"] when one is opened off the floor. */
[data-surface="floor"] :is(input, textarea)::placeholder,
.n-searchrow input::placeholder,
.n-sheet input::placeholder,
.n-sheet textarea::placeholder {
  color: var(--n-text-muted);
  opacity: 1;
}
:where(.n-floor, .n-sheet) :is(input[type="checkbox"], input[type="radio"]) {
  width: auto;
  height: auto;
  min-height: 0;
  padding: 0;
}
.n-num,
.n-money,
.n-money-l,
.n-money-xl,
.n-row-q,
.n-row-t,
.n-vcard .r > span:last-child,
.n-row-qty,
.n-splitrow-t,
.n-qrow-t,
.n-rc-a,
.n-table-no,
.n-table-min,
.n-total-money,
.n-tile-p {
  font-family: var(--n-font-num);
  font-variant-numeric: var(--n-font-num-variant);
  font-feature-settings: var(--n-font-num-features);
}
.n-lbl {
  display: block;
  margin: var(--n-space-3) 0 var(--n-space-1);
  font-size: var(--n-step-1);
  font-weight: var(--n-w-semibold);
  color: var(--n-text-2);
  /* app.css sets every <label> in UPPERCASE with tracking — office chrome
     that has no place on the floor */
  text-transform: none;
  letter-spacing: normal;
}
.n-note { margin: var(--n-space-2) 0; font-size: var(--n-step-1); color: var(--n-text-muted); line-height: var(--n-lh-ui); }
.n-summary { margin: 0 0 var(--n-space-3); font-size: var(--n-step-2); color: var(--n-text); line-height: var(--n-lh-ui); }
.n-rule { border: 0; border-top: thin solid var(--n-border); margin: var(--n-space-2) 0; }

/* ── 3b. money, as markup (spec §4) ───────────────────────────────────────
   fmtMoneyHtml() writes <span class="n-money"><span class="n-cur">R</span>
   <span class="n-amt">1<span class="n-grp">234.50</span></span></span>, and
   the SPACING LIVES HERE — no space character is printed inside an amount.

   The measurement behind that: U+2009, the thin space the text formatter
   uses, renders 9.23 px at the tile price's 19 px in this face — 0.486 em,
   where the spec allows a quarter of an em. --n-space-1 is 4 px, 0.21 em at
   19 px, and it is a token, so the gap follows the density dial instead of
   the font's idea of a thin space.

   nowrap on the whole amount: a price is one word. "R" and "1 234.50" on two
   lines is a different number to anyone reading it in a hurry. */
.n-money { white-space: nowrap; }
.n-cur { margin-inline-end: var(--n-space-1); }
.n-grp { margin-inline-start: var(--n-space-1); }

/* ── 4. the sale screen ───────────────────────────────────────────────── */
.n-floor {
  display: grid;
  grid-template-columns: var(--n-rail-w) 1fr var(--n-ticket-w);
  height: 100%;
  background: var(--n-bg);
  color: var(--n-text);
  font-size: var(--n-step-1);
  overflow: hidden;
}

/* rail */
.n-rail {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--n-surface);
  border-right: thin solid var(--n-border);
}
.n-hub {
  min-height: var(--n-hit-min);
  min-width: var(--n-hit-min);
  font-size: var(--n-step-4);
  border-bottom: thin solid var(--n-border);
}
/* The band that carries the mode toggle and the category list. It is a plain
   pass-through on a desk viewport — the rail stays the one flex column it has
   always been — and becomes the phone's bottom strip in the media query at
   the foot of this file. The categories come first in the markup and the
   toggle last (the desk's visual and Tab order); on the phone `order: -1`
   pins the toggle to the front of the strip. */
.n-railstrip { display: contents; }
.n-rail-cats { flex: 1; overflow-y: auto; overscroll-behavior: contain; }
.n-railcat {
  display: block;
  width: 100%;
  min-width: var(--n-hit-min);
  min-height: var(--n-hit-min);
  padding: var(--n-space-2);
  border-left: var(--n-space-1) solid transparent;
  border-radius: 0;
  text-align: left;
  color: var(--n-text);
}
.n-railcat-t {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--n-step-1);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-ui);
}
.n-railcat[aria-current="true"] {
  background: var(--n-accent-soft);
  color: var(--n-accent-text);
  border-left-color: var(--n-accent);
}
/* the last rail row on the map is an action rather than a place, so it reads
   in the secondary tone and carries a hairline above it */
.n-railcat-add { color: var(--n-text-2); border-top: thin solid var(--n-border); }
/* the Floor | Menu toggle stays at the FOOT of the rail even when the
   categories are hidden (map view) */
.n-rail-foot { display: flex; flex-direction: column; margin-top: auto; border-top: thin solid var(--n-border); }
/* On the desk the foot is last in the markup and last on screen — no `order`
   needed. (css/n-office.css:365 declares a bare `.n-rail-foot` for the OFFICE
   rail's own foot and loads after this file; the office lane owns that
   rename.) */
.n-railmode {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--n-space-1);
  min-height: var(--n-hit-min);
  /* The floor's text minimum, not the scale's smallest step: "Floor" and
     "Menu" are the two words that say WHICH HALF of the till is on screen,
     read at arm's length on a counter. --n-text-min is the one size on this
     surface that the density dial never scales below 16 px. */
  font-size: var(--n-text-min);
  color: var(--n-text-2);
}
.n-railmode[aria-current="true"] { background: var(--n-accent-soft); color: var(--n-accent-text); }

/* work area */
.n-work { display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
/* The icon is a SIBLING of the field, not a mark floating over it: the row
   lays the two out and the gap between them is one token. It used to be
   absolutely positioned at --n-space-4 with the input carrying a matching
   --n-space-8 of left padding — two numbers hand-fitted to each other, which
   at --n-pos-density 1.15 drifted apart (the padding scaled, the glyph's own
   width did not) and left 38 px of white between the magnifier and the word
   it belongs to. A flex gap cannot drift: it IS the distance. */
.n-searchrow {
  display: flex;
  align-items: center;
  gap: var(--n-space-3);
  padding: var(--n-space-2);
  padding-inline: var(--n-space-4);
  border-bottom: thin solid var(--n-border);
}
.n-searchic {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--n-text-muted);
  pointer-events: none;
}
/* `flex: 1` (basis 0) is what overrides the floor input's own width: 100% */
#nSearch { flex: 1; min-width: 0; padding-left: 0; }

.n-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--n-tile-min), 1fr));
  gap: var(--n-space-2);
  align-content: start;
  padding: var(--n-space-2);
  overflow-y: auto;
  min-height: var(--n-tile-h);
  flex: 1;
}
.n-tiles[hidden], .n-tile[hidden], .n-map[hidden] { display: none; }
/* the same tile grid, borrowed by the portion picker inside a sheet: there it
   is content in a body that already scrolls, not the work area's own scroller */
.n-tiles-sheet { flex: none; padding: 0; margin-top: var(--n-space-2); overflow: visible; }
.n-tiles-empty { padding: var(--n-space-5); color: var(--n-text-muted); font-size: var(--n-step-1); }
.n-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: stretch;
  gap: var(--n-space-2);
  min-height: var(--n-tile-h);
  /* the inline padding is the narrower token: at 1024 the tile area is four
     --n-tile-min columns wide, and the wider padding left a 12-letter name
     ("Cheeseburger") one or two pixels short of fitting on a line */
  padding: var(--n-space-3) var(--n-space-2);
  padding-top: calc(var(--n-space-3) + var(--n-space-1));
  /* clear of the spine: its own width plus the inline token beside it */
  padding-left: calc(var(--n-space-2) + var(--n-space-1));
  background: var(--n-surface);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
  text-align: left;
}
.n-tile:active { background: var(--n-surface-sunken); }
/* A SPINE down the left edge, never a cap across the top (00-do-not.md, the
   2026-09-11 rulings). Full width, the hue ran straight through the tile's
   own eye-line — the band a reader's eye travels to find the price — and ten
   of them across a grid read as ten headings. Down the left edge it is a
   margin mark: the same --n-tile-cat token, out of the way of every word. */
.n-tile-bar {
  position: absolute;
  top: var(--n-space-2);
  bottom: var(--n-space-2);
  left: 0;
  right: auto;
  width: var(--n-space-1);
  height: auto;
  border-radius: var(--n-radius-pill);
  background: var(--n-tile-cat, var(--n-border));
}
.n-tile-n {
  /* `anywhere` breaks INSIDE a word — the audit shot had "Cheeseburge / r" on
     a tile. A name breaks between words; only a word too long for the tile on
     its own is hyphenated, and it is hyphenated rather than guillotined. */
  overflow-wrap: break-word;
  hyphens: auto;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--n-step-1);
  font-weight: var(--n-w-semibold);
  line-height: var(--n-lh-ui);
}
.n-tile-p { align-self: flex-end; font-size: var(--n-step-2); font-weight: var(--n-w-bold); }
.n-tile-low { margin-left: var(--n-space-2); font-size: var(--n-step--1); color: var(--n-warn); }
.n-tile[data-86] { background-image: var(--n-void-hatch); }
.n-tile[data-86] .n-tile-n { color: var(--n-void); }
.n-tile[data-86] .n-tile-p { color: var(--n-void); font-weight: var(--n-w-semibold); }

/* the ten categorical tokens, chosen by index — no colour value in JS */
[data-cat="1"] { --n-tile-cat: var(--n-cat-1); }
[data-cat="2"] { --n-tile-cat: var(--n-cat-2); }
[data-cat="3"] { --n-tile-cat: var(--n-cat-3); }
[data-cat="4"] { --n-tile-cat: var(--n-cat-4); }
[data-cat="5"] { --n-tile-cat: var(--n-cat-5); }
[data-cat="6"] { --n-tile-cat: var(--n-cat-6); }
[data-cat="7"] { --n-tile-cat: var(--n-cat-7); }
[data-cat="8"] { --n-tile-cat: var(--n-cat-8); }
[data-cat="9"] { --n-tile-cat: var(--n-cat-9); }
[data-cat="10"] { --n-tile-cat: var(--n-cat-10); }

/* ── 5. the ticket ────────────────────────────────────────────────────── */
.n-ticket {
  display: grid;
  grid-template-rows: auto 1fr;
  min-width: 0;
  background: var(--n-surface);
  border-left: thin solid var(--n-border);
}
.n-tbody { display: grid; grid-template-rows: 1fr auto auto; min-height: 0; }
.n-strip {
  display: flex;
  align-items: center;
  gap: var(--n-space-2);
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-2);
  border-bottom: thin solid var(--n-border);
}
.n-strip-table { min-height: var(--n-hit-min); min-width: var(--n-hit-min); padding: 0 var(--n-space-2); font-weight: var(--n-w-bold); }
.n-strip-who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--n-text-2); font-size: var(--n-step-1); text-align: center; }
.n-strip-sync { min-height: var(--n-hit-min); min-width: var(--n-hit-min); padding: 0 var(--n-space-2); white-space: nowrap; }
.n-strip-sync[data-tone="ok"] { color: var(--n-ok); }
.n-strip-sync[data-tone="accent"] { color: var(--n-accent-text); }
.n-strip-sync[data-tone="warn"] { color: var(--n-warn); }
.n-strip-sync[data-tone="danger"] { color: var(--n-danger); font-weight: var(--n-w-bold); }
/* the mic, in the strip's right slot, and only when the server can hear
   (spec §9 — nVoiceAvailable in js/n-floor.js). js/02-icons.js carries no mic
   glyph, so the button wears the same 🎤 the app's own voice button has worn
   since it shipped; the accessible name is the words, as everywhere else. */
.n-strip-mic { flex: 0 0 auto; min-width: var(--n-hit-min); min-height: var(--n-hit-min); font-size: var(--n-step-2); }

/* The empty pane is CENTRED, not a sentence pinned to the top-left corner of
   360 px of blank column. A flex column with nothing in it but the stack, and
   the stack is two lines: which table this sale is for (when one is chosen)
   and the sentence that says what to do next. No icon, no button — the
   action the plan's empty state points at is the tile grid already on screen
   beside it. */
.n-rowswrap { display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; }
.n-rows { min-height: calc(var(--n-hit-min) * 2); }
/* …but not when there is nothing in it: the two rows' worth of reserved
   height pushed the centred empty stack (below) 56 px down the pane, so the
   sentence sat under the middle rather than in it. nList leaves this
   container genuinely childless when the ticket is empty. */
.n-rows:empty { min-height: 0; }
.n-rowsempty {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--n-space-2);
  padding: var(--n-space-4);
  text-align: center;
}
.n-rowsempty[hidden] { display: none; }
.n-rows-emptyt { font-size: var(--n-step-2); font-weight: var(--n-w-semibold); }
.n-rows-emptyt[hidden] { display: none; }
.n-rows-empty { margin: 0; color: var(--n-text-muted); font-size: var(--n-step-1); }

.n-row { position: relative; border-bottom: thin solid var(--n-border-soft); overflow: hidden; }
.n-row-main {
  touch-action: pan-y;              /* the list still scrolls; sideways is the swipe */
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--n-space-2);
  width: 100%;
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-3);
  text-align: left;
  border-radius: 0;
}
.n-row-main:active { background: var(--n-surface-sunken); }
/* The focus ring goes INSIDE this one. --n-focus-ring is a halo drawn outside
   the border box, and a row lives in an .n-row with overflow:hidden (the
   swipe-to-remove panel is parked out there), so the halo was cut off on all
   four sides — the keyboard user's most-used stop on the floor showed nothing
   at all. --n-focus-ring-inset is the same accent line, drawn inward.
   Every other control inside a clipped or scrolling box gets the same
   treatment further down (§8c). */
.n-row-main:focus-visible { box-shadow: var(--n-focus-ring-inset); }
.n-row-q { min-width: var(--n-space-5); font-size: var(--n-step-1); font-weight: var(--n-w-bold); }
.n-row-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--n-step-1); }
.n-row-t { font-size: var(--n-step-1); font-weight: var(--n-w-semibold); }
.n-row-sent { color: var(--n-text-muted); }
.n-row-tag { margin-left: var(--n-space-2); font-size: var(--n-step--1); font-weight: var(--n-w-semibold); color: var(--n-text-muted); }
.n-row-note-txt { display: block; font-style: normal; font-size: var(--n-step--1); color: var(--n-text-muted); }
/* the sent state, in words, on its own line under the name. --n-step-0 is one
   notch under the floor's 16 px minimum, which is the rule for a state label
   rather than a thing you read a price off — and it keeps the row inside its
   56 px (16 × 1.4 + 14 × 1.4 = 42). */
.n-row-state { display: block; font-size: var(--n-step-0); font-weight: var(--n-w-regular); color: var(--n-text-muted); line-height: var(--n-lh-ui); }
/* a refused row keeps its danger treatment right through the state word: the
   colour rules above reach .n-row-n, and a child with its own colour would
   otherwise stay muted inside a row the server said no to */
.n-row[data-err] .n-row-state { color: var(--n-danger); }
.n-row[data-sent] .n-row-n, .n-row[data-sent] .n-row-t { color: var(--n-text-muted); }
.n-row[data-comped] .n-row-n { text-decoration: line-through; }
.n-row[data-pending] .n-row-main { color: var(--n-accent-text); }
.n-row[data-err] { border-left: var(--n-space-1) solid var(--n-danger-fill); }
.n-row[data-err] .n-row-n, .n-row[data-err] .n-row-t { color: var(--n-danger); }
.n-row-why { margin: 0; padding: 0 var(--n-space-3) var(--n-space-1); font-size: var(--n-step--1); color: var(--n-danger); }

.n-row-x { display: none; }
.n-row[data-sel] .n-row-x, .n-row .n-row-retry {
  display: flex;
  align-items: center;
  gap: var(--n-space-2);
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-3) var(--n-space-2);
}
.n-qty {
  flex: 0 0 auto;
  width: var(--n-hit-min);
  height: var(--n-hit-min);
  font-size: var(--n-step-3);
  border: thin solid var(--n-border);
  background: var(--n-surface-sunken);
}
.n-row-qty { min-width: var(--n-space-6); text-align: center; font-size: var(--n-step-2); font-weight: var(--n-w-bold); }
.n-row-note { flex: 1; min-width: 0; }
.n-row-act {
  flex: 0 0 auto;
  min-height: var(--n-hit-min);
  min-width: var(--n-hit-min);
  padding: 0 var(--n-space-3);
  border: thin solid var(--n-border);
}
.n-row-void { color: var(--n-danger); border-color: var(--n-danger); }
.n-row-act:disabled { color: var(--n-text-faint); border-color: var(--n-border-soft); }
.n-row-swipe {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: var(--n-swipe-w);
  background: var(--n-danger-fill);
  color: var(--n-inverse-text);        /* the same pair as LATE — see .n-table */
  border-radius: 0;
  visibility: hidden;
  translate: 100% 0;                /* `translate`, not transform — see .n-table */
  transition: translate var(--n-dur-fast) var(--n-ease);
}
.n-row[data-swiped] .n-row-swipe { visibility: visible; translate: 0 0; }

.n-second { display: grid; grid-template-columns: 1fr 1fr 1fr; border-top: thin solid var(--n-border); }
.n-second button {
  min-height: var(--n-hit-min);
  border-radius: 0;
  border-right: thin solid var(--n-border);
  color: var(--n-text);
}
.n-second button:last-child { border-right: 0; }
.n-second button:active { background: var(--n-surface-sunken); }

.n-total {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  /* the tighter pair of tokens: the total now carries a thin space between the
     symbol and the number (n-format), which at --n-step-5 is about a tenth of
     the money column — enough, at a 360 px ticket, to push "9 items" onto a
     second line. The count belongs on one line. */
  gap: var(--n-space-2);
  width: 100%;
  min-height: var(--n-total-h);
  padding: 0 var(--n-space-3);
  background: var(--n-accent);
  color: var(--n-accent-ink);
  border: thin solid var(--n-accent-line);
  border-radius: 0;
}
.n-total:active { background: var(--n-accent-active); }
/* An empty sale is not an error and the bar is not "greyed out": it is a
   sunken well with muted text, which is the whole disabled meaning. app.css's
   global `button:disabled{opacity:.5}` on top of that took "Sale is empty"
   down to 2.06:1 — a state message nobody can read. Opacity is never how this
   surface says disabled (spec §4: never opacity on text). */
.n-total:disabled { background: var(--n-surface-sunken); color: var(--n-text-muted); border-color: var(--n-border); opacity: 1; }
.n-total-count { font-size: var(--n-step-1); font-weight: var(--n-w-semibold); text-align: left; color: inherit; }
.n-total-money { font-size: var(--n-step-5); font-weight: var(--n-w-bold); }
/* Small capitals that have to be read at a glance, from across a counter —
   the plan's own capitals BUTTON, not an eyebrow over a heading (the thing
   00-do-not.md bans). --n-track-wide is the token for exactly this job. */
.n-total-cta { font-size: var(--n-step-1); font-weight: var(--n-w-bold); letter-spacing: var(--n-track-wide); }
.n-cta-open { display: none; }
/* An empty sale has no action, so the bar does not offer one: "Sale is empty
   R 0.00 CHARGE" promised a charge the button refuses to take. The count and
   the total stay — they are the STATE — and the CTA is simply absent until
   there is a line to charge for. (Specificity beats the phone block's
   `.n-floor .n-cta-open { display: inline }` below.) */
.n-total:disabled .n-total-cta { display: none; }

/* how far a changed total grows is a design value: --n-bump-scale */
@keyframes n-bump { from { transform: scale(1); } 40% { transform: scale(var(--n-bump-scale)); } to { transform: scale(1); } }
.n-bump { animation: n-bump var(--n-dur-base) var(--n-ease); }

/* ── 6. floor map ─────────────────────────────────────────────────────── */
/* A grid, not a flex column, for one reason: the edge rail below has to sit
   exactly over the CANVAS's box (the tools row underneath it must stay
   clear), and a grid area is how a pseudo-element says that without any
   absolute positioning or a second measurement. */
.n-map { display: grid; grid-template-rows: 1fr auto; min-height: 0; height: 100%; }
/* (the map header's own chip row is gone: the venue's sections are rail rows
   now — see .n-railcat and nFloorRailRender() — and the plan has that band) */
.n-mapcanvas {
  position: relative;
  /* every child of .n-map is placed by hand: the rail below is an explicitly
     placed pseudo-element, and an auto-placed sibling would be pushed into a
     column of its own beside it */
  grid-area: 1 / 1;
  min-height: 0;   /* a grid item's min-height:auto would push the row past the map */
  /* clip, not hidden: an `overflow:hidden` box is still a scroll container, so
     a table sitting near the far edge let the canvas be scrolled out from
     under a finger (and under a click). `clip` cannot scroll. (The phone
     block at the foot of this file swaps this for `auto` — there the plan is
     deliberately bigger than the screen.) */
  overflow: clip;
  background-color: var(--n-map-base);
}
/* A plan wider than the canvas is cut dead at the right edge, and a hard cut
   says nothing: on a 390 px phone a 576 px floor plan lost its far tables
   with no hint that scrolling would bring them back. Two things say it now,
   and they say different halves of it:

     · the last --n-space-6 of the VISIBLE box fades out — a mask on the
       canvas, so the fade stays at the edge while the plan scrolls under it
       rather than travelling with the furniture the way an absolutely
       positioned pseudo-element would. It was --n-space-8 for one round, and
       64 px is most of a 72 px table: the table under it was not "carrying
       on past the edge", it was DIMMED, name and all, and a table you cannot
       read is worse than a table you can see is cut (critic D5b). A ramp
       shorter than half a table dims no label enough to matter — measured;
     · a persistent chevron rail at the right edge, --n-space-6 wide, which is
       what actually says "there is more this way". The affordance is the
       chevron, not the length of the fade, which is why the fade can go back
       to being short.

   The gradient's first stop only has to be OPAQUE (a mask reads alpha, not
   hue); currentColor is the surface's own --n-text, so no colour is spelled
   out here. The attribute is set by nMapOverflow() only while the canvas
   actually overflows, so a plan that fits has no fade, no mask and no rail at
   all. It is set on the MAP because both of them hang off it.

   It carries a VALUE, not just its presence: "more" while there is plan to the
   right and "end" once the operator has scrolled to it (D9). A chevron still
   pointing right at the far edge of the map is a promise the map cannot keep,
   and the ramp dims the last column of tables for nothing — so both rules
   below are on "more" alone, and scrolling back re-arms them. */
.n-map[data-n-overflow="more"] .n-mapcanvas {
  -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);
}
/* The rail: a reserved strip over the canvas's own grid area, carrying one
   chevron in the muted ink. It never takes a tap (pointer-events) and it is
   drawn OUTSIDE the mask, so the one mark that means "scroll" is the one mark
   the fade cannot dim. */
.n-map[data-n-overflow="more"]::after {
  content: "";
  grid-area: 1 / 1;
  justify-self: end;
  align-self: stretch;
  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 plan itself. It fills the canvas at every desk viewport, so nothing
   about the map changes there; on a phone it keeps its size and the canvas
   scrolls over it. The furniture is positioned against THIS box — see
   nMapStage() in js/n-floor.js. */
.n-mapstage {
  position: relative;
  width: 100%;
  height: 100%;
  /* the existing dot grid, in tokens: the tile is a spacing token and the dot
     a proportion of it (there is no 1 px token, and this file holds no px).
     It sits on the stage rather than the canvas so it scrolls WITH the
     furniture instead of staying still under it. */
  background-image: radial-gradient(var(--n-border) 6%, transparent 6%);
  background-size: var(--n-space-5) var(--n-space-5);
}
.n-table {
  position: absolute;
  left: var(--x);
  top: var(--y);
  /* the standalone `translate` property, NOT transform: app.css's global
     `button:active { transform: scale(.98) }` replaces a transform outright,
     which threw the table 48 px sideways between pointerdown and mouseup —
     every tap landed on the canvas instead of the table. */
  translate: -50% -50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--n-space-1);
  min-width: var(--n-table-min);
  min-height: var(--n-table-min);
  padding: var(--n-space-2);
  border-radius: var(--n-radius-md);
  touch-action: none;
}
.n-table-no { font-size: var(--n-step-5); font-weight: var(--n-w-bold); line-height: var(--n-lh-display); }
.n-table-word { font-size: var(--n-step-1); font-weight: var(--n-w-bold); }
.n-table-min { font-size: var(--n-step-1); }
.n-table[data-state="free"] { border: thin solid var(--n-border); color: var(--n-text-2); }
.n-table[data-state="open"] { background: var(--n-surface-raised); border: thin solid var(--n-accent-line); color: var(--n-text); }
/* --n-inverse-text, not --n-paper. On the light scheme the two tokens hold
   the same cream and the pair audits at 5.46:1 — but --n-paper does not move
   with the scheme, and the dark danger fill is LIGHTER than the light one, so
   on dark it left LATE and the minutes at 3.84:1. Nothing pale can pass there:
   even pure white tops out at 4.18:1. --n-inverse-text IS "the ink for a
   strong filled ground" — it flips to near-black on dark, and the pair then
   reads 4.58:1. Found by the runtime contrast sweep, both tenants. */
.n-table[data-state="late"] { background: var(--n-danger-fill); border: thin solid var(--n-danger-fill); color: var(--n-inverse-text); }
/* Paid and still sitting. The pair a floor most needs to tell apart from two
   metres is "owes everything" and "already settled", so this is the ok line
   plus the WORD — never colour alone (spec §5, plan §6.3). */
.n-table[data-state="paid"] { background: var(--n-surface-raised); border: thin solid var(--n-ok-fill); color: var(--n-text); }
.n-table[data-state="paid"] .n-table-word { color: var(--n-ok); }
/* the spec asks for a 3 px selected outline; --n-space-1 (4) is the nearest token */
.n-table[data-sel] { outline: var(--n-space-1) solid var(--n-accent-line); outline-offset: 0; }
/* Floor furniture. --n-surface-well is a SURFACE token: on dark it is the
   same near-black as the map base, so every wall vanished. A wall has to read
   as a solid object against the map at a glance and carries no text, so it is
   drawn in --n-text-faint — the 3:1 non-text UI token, which is exactly what
   "a shape you must be able to see, but never read" is for — and it needs no
   outline once it is solid. */
.n-elem {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  height: var(--h);
  translate: -50% -50%;
  background: var(--n-text-faint);
  border: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* a pillar is a round wall; a door is the gap in one, so it takes the accent
   pair rather than the furniture tone */
.n-elem[data-kind="pillar"] { border-radius: 50%; }
.n-elem[data-kind="door"] { background: var(--n-accent-soft); border: thin solid var(--n-accent-line); border-radius: var(--n-radius-sm); }
/* a zone label is words on the map itself: no fill, and text at the floor's
   minimum size and a body-text tone */
.n-elem[data-kind="label"] {
  background: transparent;
  color: var(--n-text-2);
  font-size: var(--n-step-1);
  font-weight: var(--n-w-bold);
  text-align: center;
}
.n-maptools {
  grid-area: 2 / 1;    /* the row under the canvas — see .n-map's grid */
  display: flex;
  gap: var(--n-space-2);
  padding: var(--n-space-2);
  padding-bottom: calc(var(--n-space-2) + env(safe-area-inset-bottom));
  border-top: thin solid var(--n-border);
  background: var(--n-surface);
}
.n-maptools[hidden] { display: none; }

/* ── 7. sheets ────────────────────────────────────────────────────────── */
#nSheetRoot { position: relative; z-index: 130; }
.n-sheet-wrap {
  position: fixed;
  /* the spec's 92 vh, expressed as "the viewport less a token" */
  inset: var(--n-space-7) 0 0 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* THE scrim. A sheet is the one live thing on the screen while it is open,
     and what says so is the surface behind it going dim — the total bar
     included. It was an --n-inverse-bg layer at opacity .55, a colour and a
     dimming hand-mixed at the point of use; --n-scrim is that mix as a token,
     with its own value in each scheme. No transition: the wrap does not fade,
     only the panel slides (--n-dur-slow), exactly as before. */
  background: var(--n-scrim);
  z-index: 130;
}
/* The scrim's colour moved up to the wrap (above); this element stays as
   what it always also was — the tap-outside target that covers the whole
   wrap. It draws nothing of its own. */
.n-sheet-scrim {
  position: absolute;
  inset: 0;
}
.n-sheet {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: var(--n-sheet-max-w);
  max-height: 100%;
  background: var(--n-surface);
  color: var(--n-text);
  border-radius: var(--n-radius-lg) var(--n-radius-lg) 0 0;
  box-shadow: var(--n-shadow-e);
  transform: translateY(100%);
  transition: transform var(--n-dur-slow) var(--n-ease);
}
.n-sheet-wrap.n-in .n-sheet { transform: translateY(0); }
.n-sheet:focus { outline: none; }
.n-sheet-head {
  display: flex;
  align-items: center;
  gap: var(--n-space-2);
  padding: var(--n-space-2) var(--n-space-3);
  border-bottom: thin solid var(--n-border);
}
.n-sheet-title { flex: 1; margin: 0; font-size: var(--n-step-3); font-weight: var(--n-w-semibold); }
.n-sheet-x { width: var(--n-hit-min); height: var(--n-hit-min); flex: 0 0 auto; }
/* min-height: 0 — a flex item's automatic minimum is its CONTENT, so without
   this a tall body refuses to shrink, the sheet overflows its own max height
   and the action row is pushed off the bottom of the screen. */
.n-sheet-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--n-space-3); }
/* The row is opaque --n-surface with a hairline top edge, so body text that
   scrolls under it is CUT at a line the eye can see rather than fading into
   the buttons; flex: 0 0 auto keeps it at its own height however tall the
   body gets (a shrunk row would let its 56 px buttons hang out past that
   opaque background — the guillotine). */
.n-sheet-actions {
  display: flex;
  flex: 0 0 auto;
  gap: var(--n-space-2);
  padding: var(--n-space-3);
  padding-bottom: calc(var(--n-space-3) + env(safe-area-inset-bottom));
  border-top: thin solid var(--n-border);
  background: var(--n-surface);
}
.n-sheet-head { flex: 0 0 auto; }
.n-sheet-actions[hidden] { display: none; }
.n-act {
  flex: 1;
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-4);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
  background: var(--n-surface);
}
.n-act:active { background: var(--n-surface-sunken); }
.n-act-go { background: var(--n-accent); color: var(--n-accent-ink); border-color: var(--n-accent-line); }
.n-act-go:active { background: var(--n-accent-active); }
.n-act-danger { color: var(--n-danger); border-color: var(--n-danger); }
.n-act:disabled { color: var(--n-text-faint); border-color: var(--n-border-soft); }
.n-acts { display: flex; flex-wrap: wrap; gap: var(--n-space-2); margin-top: var(--n-space-3); }

.n-menu { display: flex; flex-direction: column; gap: var(--n-space-1); }
.n-menuitem {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--n-space-2);
  min-height: var(--n-hit-min);
  padding: var(--n-space-2) var(--n-space-3);
  border: thin solid var(--n-border);
  text-align: left;
}
.n-menuitem:active { background: var(--n-surface-sunken); }
.n-menuitem-sub { flex-basis: 100%; font-size: var(--n-step--1); font-weight: var(--n-w-regular); color: var(--n-text-muted); }

.n-field { display: block; }
.n-field input { margin-bottom: var(--n-space-2); }
.n-cam { width: 100%; border-radius: var(--n-radius-md); background: var(--n-inverse-bg); }
.n-cam[hidden], #qrCamWrap[hidden] { display: none; }

/* the reused auth ladder (voidAuthHtml) is old markup — element selectors
   only, scoped to this block, so nothing outside the floor is restyled */
.n-auth { margin-top: var(--n-space-3); }
.n-auth label { font-size: var(--n-step-1); color: var(--n-text-2); text-transform: none; letter-spacing: normal; }
.n-auth input { min-height: var(--n-hit-min); font-size: var(--n-step-1); }
.n-auth button { min-height: var(--n-hit-min); padding: 0 var(--n-space-3); font-size: var(--n-step-1); }

/* ── 8. charge sheet ──────────────────────────────────────────────────── */
/* The cash pad is 455 px tall and the sheet's body at 1024x768 is 550: with
   the total, the tabs, the tenders and the tip stacked above it in one
   column, the numpad started below the fold on every viewport — an operator
   had to scroll a modal to take cash. Wherever the sheet is wider than a
   phone the choices (tender, tip) and the cash pad stand side by side, and
   nothing on the pay tab needs scrolling. */
.n-charge-cols { display: flex; flex-direction: column; gap: var(--n-space-3); }
.n-charge-col { min-width: 0; }
.n-charge-col[hidden] { display: none; }
@media (min-width: 601px) {
  .n-charge-cols { flex-direction: row; align-items: flex-start; }
  .n-charge-col { flex: 1 1 0; }
}
.n-charge-total { display: flex; align-items: baseline; justify-content: space-between; gap: var(--n-space-3); padding-bottom: var(--n-space-3); border-bottom: thin solid var(--n-border); }
.n-charge-lbl { font-size: var(--n-step-1); color: var(--n-text-muted); }
.n-money-xl { font-size: var(--n-step-5); font-weight: var(--n-w-bold); }
.n-money-l { font-size: var(--n-step-3); font-weight: var(--n-w-bold); }

.n-tabs { display: flex; gap: var(--n-space-1); margin: var(--n-space-3) 0; }
.n-tabs[hidden] { display: none; }
/* Tabs are UNDERLINED, not filled. Pay|Split wore the same soft-accent pill
   as the tender tiles sitting under it, so "Split" read as a fifth way to
   pay — a tab and a radio are different promises and they may not look the
   same. A tab now has no fill and no box: the active one carries an accent
   bar along its bottom edge, the inactive ones are secondary text. The bar is
   --n-space-1, the same token the rail's active indicator uses for exactly
   this job (there is no line-width token in the file — reported to the lead). */
.n-tab {
  flex: 1;
  min-height: var(--n-hit-min);
  min-width: var(--n-hit-min);
  padding: 0 var(--n-space-3);
  border: 0;
  border-bottom: var(--n-space-1) solid transparent;
  border-radius: 0;
  background: transparent;
  color: var(--n-text-2);
}
.n-tab[aria-selected="true"] { background: transparent; color: var(--n-text); border-bottom-color: var(--n-accent); }

/* the track is two tenders wide, not one tile: at four across a 366 px
   column "Uber Eats" was the only tender that wrapped, and a tender that
   wraps reads as a different kind of thing from the three beside it. Where
   the row IS wide (the split pane) auto-fit still puts them all on one line. */
.n-tenders { display: grid; grid-template-columns: repeat(auto-fit, minmax(calc(var(--n-space-8) * 2), 1fr)); gap: var(--n-space-2); }
/* plan §5: a tender is a TILE (--n-radius-md); the tip and quick-cash rows
   are chips (--n-radius-pill); the numpad's keys stay --n-radius-sm, the
   default every control on this surface starts from. Three shapes, three
   kinds of thing, decided by the plan rather than by which rule was written
   last. */
.n-tender { min-height: var(--n-tender-h); border: thin solid var(--n-border); border-radius: var(--n-radius-md); overflow-wrap: break-word; hyphens: auto; }
.n-tender[aria-checked="true"] { background: var(--n-accent-soft); color: var(--n-accent-text); border-color: var(--n-accent-line); }
/* the tighter gap: five tiles across a 366 px column leaves "Custom" one
   pixel short at --n-space-2, and the row reads as one segmented control
   anyway.

   The four percentages are equal tracks; the fifth is `minmax(max-content,
   1fr)`, so "Custom" gets AT LEAST the width of the word and shares the rest.
   break-word/hyphens is gone from the tile: it is the right rule for a menu
   item's name, which can genuinely be longer than its tile, but on a control
   whose label is one short word it hyphenated "Custo / m" the moment the
   track was a pixel short — a word broken inside itself on a money screen.
   A label now decides the track's width instead of the track breaking the
   label. */
.n-tips { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--n-hit-min), 1fr)); gap: var(--n-space-1); }
/* the charge sheet has five tip tiles; the fifth ("Custom") sizes to its
   label so the word never breaks. The four-tile Tip sheet keeps auto-fit. */
#nPayPane .n-tips { grid-template-columns: repeat(4, 1fr) minmax(max-content, 1fr); }

.n-tip { min-height: var(--n-hit-min); border: thin solid var(--n-border); border-radius: var(--n-radius-pill); }
.n-tip[aria-pressed="true"] { background: var(--n-accent-soft); color: var(--n-accent-text); border-color: var(--n-accent-line); }
/* Custom is the fifth tile and the tallest, so the whole tip row stands at
   the tender's 64 px (a grid row is as tall as its tallest cell). It is a
   disclosure: one field appears under it, and only while it is chosen. */
.n-tip-custom { min-height: var(--n-tender-h); }
.n-field[hidden] { display: none; }

.n-numout { font-size: var(--n-step-3); font-weight: var(--n-w-bold); font-family: var(--n-font-num); text-align: right; }
/* P1 (polish 7): an empty .n-numout still drew its placeholder ("Tendered",
   "0.00", "0.350") in the VALUE register — large, bold, right-aligned — so
   the field read as though it already held an amount. A placeholder is a
   hint, not a value: it drops to the hint register (--n-step-0, the sheet's
   ordinary body weight, left-aligned — nothing here claims to be a figure a
   cashier can trust) while whatever is actually typed keeps .n-numout's own
   register untouched. Colour is repeated here (rather than left to the
   floor-wide input::placeholder rule above) so this one rule is the whole
   proof: a hint, in every property that told it apart from a value. */
.n-numout::placeholder { font-size: var(--n-step-0); font-weight: var(--n-w-regular); color: var(--n-text-muted); text-align: left; }
/* quick cash: Exact and the three notes a South African guest actually hands
   over, the same 64 px tile as a tender */
/* Quick cash is ONE ROW of its own, above the field it fills — never a column
   inside the numpad's grid, where R100 sat in the 4's place (critic D6). The
   gap below it is the whole point: it is what says "these are notes, those are
   digits". */
/* The heading is the group's accessible NAME (aria-labelledby) and it is on
   screen at every width (D8) — but it is the row's FIRST CELL rather than a
   line above it (D12). Drawn as a block heading it cost 43 px of a sheet that
   has to keep both the numpad and the change due above the fold at 1024x768,
   and batch 6c bought that back by taking the numpad's keys down to
   --n-hit-min — which is 56 px and NOT what the spec asks of a numpad (§6: a
   3x4 pad of 64 px keys, the size the tenders are). The keys go back to 64 px
   and the label costs nothing instead: in the row, at the tiles' own height,
   where "Quick cash" sits beside the notes it names.

   Measured at 1024x768 with the keys back at 64: the numpad ends at 590, the
   change due runs 602-659, and the sticky action row starts at 671 — the
   number the cashier reads out is above the fold AND above the bar, with the
   heading drawn. */
.n-quicks { display: grid; grid-template-columns: auto repeat(4, 1fr); align-items: center; gap: var(--n-space-2); margin-top: var(--n-space-2); }
/* D2 (batch 6h): the row sits under the field it fills now, not first in the
   column, so the field's own margin (.n-lbl + the input) is what separates
   it from "Cash received" above — only the gap to the numpad below stays a
   deliberate override, the one that says "these are notes, those are digits"
   (critic D6). */
#nCashWrap .n-quicks { margin-bottom: var(--n-space-4); }
#nQuickLbl { margin: 0; white-space: nowrap; }
.n-quick { min-height: var(--n-hit-min); border: thin solid var(--n-border); border-radius: var(--n-radius-pill); background: var(--n-surface); }
.n-quick:active { background: var(--n-surface-sunken); }
.n-numpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--n-space-2); margin-top: var(--n-space-2); }
/* spec §6: 64 px keys, the tender tile's own height (--n-tender-h) */
.n-num { min-height: var(--n-tender-h); font-size: var(--n-step-3); border: thin solid var(--n-border); background: var(--n-surface); }
.n-num:active { background: var(--n-surface-sunken); }
.n-change { margin: var(--n-space-3) 0 0; padding: var(--n-space-3); border-radius: var(--n-radius-md); font-size: var(--n-step-3); font-weight: var(--n-w-bold); }
.n-change[hidden] { display: none; }
.n-change[data-tone="ok"] { background: var(--n-ok-soft); color: var(--n-ok); }
.n-change[data-tone="warn"] { background: var(--n-warn-soft); color: var(--n-warn); }
.n-total-act { border-radius: var(--n-radius-sm); grid-template-columns: 1fr auto; }

.n-splitlist { border: thin solid var(--n-border); border-radius: var(--n-radius-md); overflow: hidden; }
.n-splitrow {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: var(--n-space-2);
  width: 100%;
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-3);
  border-bottom: thin solid var(--n-border-soft);
  border-radius: 0;
  text-align: left;
  background: transparent;
}
.n-splitrow:last-child { border-bottom: 0; }
.n-splitrow[data-paid] .n-splitrow-n { color: var(--n-text-muted); text-decoration: line-through; }
.n-splitpick[aria-pressed="true"] { background: var(--n-accent-soft); }
.n-splitrow-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.n-splitrow-tick { min-width: var(--n-space-5); color: var(--n-accent-text); font-weight: var(--n-w-bold); }
.n-splitrow-pick { display: flex; gap: var(--n-space-1); }
.n-pick { min-width: var(--n-hit-min); min-height: var(--n-hit-min); border: thin solid var(--n-border); }
.n-pick[aria-pressed="true"] { background: var(--n-accent); color: var(--n-accent-ink); border-color: var(--n-accent-line); }
.n-sumrow { display: flex; align-items: baseline; justify-content: space-between; gap: var(--n-space-3); margin-top: var(--n-space-3); }

.n-modes, .n-chips { display: flex; flex-wrap: wrap; gap: var(--n-space-2); }
.n-mode, .n-chip { min-height: var(--n-hit-min); padding: 0 var(--n-space-3); border: thin solid var(--n-border); }
.n-mode[aria-pressed="true"], .n-chip[aria-pressed="true"] { background: var(--n-accent-soft); color: var(--n-accent-text); border-color: var(--n-accent-line); }

.n-receipt { border: thin solid var(--n-border); border-radius: var(--n-radius-md); padding: var(--n-space-3); }
.n-rc-r { display: flex; justify-content: space-between; gap: var(--n-space-3); padding: var(--n-space-1) 0; font-size: var(--n-step-1); }
.n-rc-tot { font-weight: var(--n-w-bold); font-size: var(--n-step-2); }
.n-rc-tax { text-align: center; font-weight: var(--n-w-bold); letter-spacing: var(--n-track-wide); }
/* P2 (polish 7): Change is the payment block's own emphasised row — the
   figure that leaves the drawer, one step past TOTAL's --n-step-2 (that is
   the guest's number; this is the till's). Paid by / Cash received either
   side of it stay at .n-rc-r's own body weight. */
.n-rc-change { font-weight: var(--n-w-bold); font-size: var(--n-step-3); }

/* ── 8b. partner QR and staff switch, hosted (spec §3: one modal) ──────
   Both were still showModal() — a centred card with no dialog semantics, no
   focus trap and nothing behind it inert. The sheets below host the SAME
   bodies: partnerQrResultHtml()'s scan card and the till's own staff list,
   so every inner flow (refusal cards, item picker, PIN pad) is unchanged.
   The result card is old markup, so the only rules that reach into it are
   scoped to .n-pqres and do one thing: put its controls on the floor's
   minimum target. */
.n-pqres :is(button, .btn, .ghost, input, select) { min-height: var(--n-hit-min); }
.n-pqres .sumrow { display: flex; align-items: center; gap: var(--n-space-2); min-height: var(--n-hit-min); }
/* the camera preview is revealed by scanQrWithCamera() writing an inline
   display, so it hides by CLASS here: the surface's own [hidden] rule is
   !important (the panels depend on that) and would win over the inline one */
.n-camwrap { display: none; }

/* ── 8c. focus inside a clipped box (spec §10.3) ───────────────────────────
   --n-focus-ring is a halo: two rings drawn OUTSIDE the border box, which is
   the right thing on open ground and useless inside a box that clips. Every
   clipping box on this surface is listed here, each with what it clips:

     .n-row        overflow:hidden — the swipe-to-remove panel is parked
                   outside the row, so the row must clip; the halo around a
                   focused row, its steppers, its note field and its Remove
                   went with it
     .n-splitlist  overflow:hidden — the rounded list corners
     .n-qlist      overflow:hidden — same, for stored sales
     .n-toast      overflow:hidden — the countdown bar is clipped to the
                   rounded corners, and the Undo button sits against them
     .n-mapcanvas  overflow:clip (auto on a phone) — the map scrolls or is
                   clipped, and a table can sit hard against either edge

   Inside those, the same accent line is drawn INWARD. It is the same token
   pair, so a tenant's accent still owns the ring. Verified by pixel-diff:
   every stop on the floor, in the sheets, on the map and in the toast shows
   changed pixels when it takes focus. */
.n-row :is(button, input):focus-visible,
.n-splitlist :is(button, input):focus-visible,
.n-qlist :is(button, input):focus-visible,
.n-toast button:focus-visible,
.n-mapcanvas :is(button, input):focus-visible {
  box-shadow: var(--n-focus-ring-inset);
}

.n-staff { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--n-tile-min), 1fr)); gap: var(--n-space-2); }
.n-staffbtn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--n-space-1);
  min-height: var(--n-tile-h);
  padding: var(--n-space-2);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-md);
}
.n-staffbtn:active { background: var(--n-surface-sunken); }
.n-staff-av {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--n-hit-min);
  height: var(--n-hit-min);
  border-radius: 50%;
  background: var(--n-accent-soft);
  color: var(--n-accent-text);
  font-size: var(--n-step-2);
  font-weight: var(--n-w-bold);
}
.n-staff-n { font-size: var(--n-step-1); font-weight: var(--n-w-semibold); text-align: center; }
.n-staff-r { font-size: var(--n-step--1); color: var(--n-text-muted); text-align: center; text-transform: capitalize; }
.n-pin { font-size: var(--n-step-4); text-align: center; }
/* a setting that lives in a sheet is a full-width row, not a stray tickbox */
.n-switchrow {
  display: flex;
  align-items: center;
  gap: var(--n-space-3);
  min-height: var(--n-hit-min);
  margin: var(--n-space-3) 0 0;
  padding: 0 var(--n-space-3);
  border: thin solid var(--n-border);
  border-radius: var(--n-radius-sm);
  font-size: var(--n-step-1);
  font-weight: var(--n-w-regular);
  text-transform: none;
  letter-spacing: normal;
}

/* ── 8d. the voice card, hosted (spec §9) ─────────────────────────────────
   vcShowProposal() (js/32-voice.js) works out the proposal — the order it
   heard, the count it would set, the answer to a question — and on the floor
   that card is the BODY of the voice sheet rather than a modal on top of it.
   Its own Confirm/Cancel row is hidden, so exactly one "Do it" is on screen:
   the sheet's sticky action, which is the one the spec says must be tapped
   before anything is written.

   B7: for the five intents the floor MODELS (order_items, add_stock,
   check_stock, ask, recipe) the markup is now the floor's own, built from the
   proposal in js/n-floor-extras.js — so a proposed order is set in the same
   .n-row the ticket beside it uses and its amounts come through the same
   fmtMoneyHtml parts as the tile they were priced from, instead of arriving
   at the inline twelve-pixel size that file writes into its own markup.

   The rules below are in three parts: the card's HEADING, which both kinds of
   card wear; the FLOOR'S OWN card; and the FALLBACK — .r / .inline / .muted,
   which still dress js/32-voice.js's markup for every card this file does not
   model (add_recipe, stock_low, chat, action, an unknown intent, and the
   stock-take batch card). No part is allowed a value of its own. */

/* — the FALLBACK's own heading. The floor's card has none: its heading is the
     sheet's title, and a bold sentence under a bold title was the same thing
     said twice (critic D1). js/32-voice.js's markup brings its own 🎤 line,
     which is the fallback's signature — "this is the model's own card" — so it
     keeps this rule and the sheet keeps its own title above it. — */
.n-vcard h3 { margin: 0 0 var(--n-space-2); font-size: var(--n-step-2); font-weight: var(--n-w-bold); }

/* — the floor's own card — */
/* The proposal's rows are ticket rows, so §5's typography is inherited rather
   than restated. Two things are not a ticket's: a row here is READ, never
   tapped, so it does not need the 56 px target the ticket's does; and a
   reason ("not on the menu, will be skipped") is a sentence, so the name cell
   wraps here where the ticket's ellipsises. */
.n-vcard .n-row-main { min-height: 0; padding: var(--n-space-2) 0; }
.n-vcard .n-row-n { white-space: normal; overflow: visible; text-overflow: clip; }
.n-vcard .n-vwhy, .n-vcard .n-vmeta { color: var(--n-text-muted); }
.n-vcard .n-vsub { color: var(--n-text-muted); font-weight: var(--n-w-regular); }
/* The words the till heard — FIRST on the card and in the primary ink at the
   floor's own minimum (critic D2). They were the smallest, lightest text on
   the card and sat below the model's louder paraphrase of them: the evidence
   set as a footnote to the guess. It is the other way round now. The quote
   marks are the same ones js/32-voice.js has always printed. */
.n-vlbl {
  margin: 0;
  font-size: var(--n-step-0);
  font-weight: var(--n-w-regular);
  line-height: var(--n-lh-ui);
  color: var(--n-text-muted);
}
.n-vquote {
  margin: 0 0 var(--n-space-2);
  font-size: var(--n-step-1);
  font-style: normal;
  color: var(--n-text);
}
/* …and the model's sentence under it, where it is shown at all: muted, regular
   weight, one step down — a gloss on the rows rather than a second heading. */
.n-vsay {
  margin: 0 0 var(--n-space-2);
  font-size: var(--n-step-0);
  font-weight: var(--n-w-regular);
  line-height: var(--n-lh-ui);
  color: var(--n-text-muted);
}
/* An answer is read, not checked line by line, so it is body text at the
   floor's minimum rather than a row. */
.n-vanswer { margin: 0; font-size: var(--n-step-1); font-weight: var(--n-w-semibold); }
/* What Do it would put on the ticket. The sale's total bar is the loudest
   thing on the floor at --n-step-5, and this is the same number one step
   before it exists — so it is set at the same size, in the display face the
   till reserves for a figure somebody decides on. */
.n-vtotal {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--n-space-3);
  padding: var(--n-space-2) 0 0;
}
.n-vtotal-l { font-size: var(--n-step-1); font-weight: var(--n-w-semibold); }
.n-vtotal-m { font-size: var(--n-step-5); font-weight: var(--n-w-bold); }
/* …and the display face reaches the digits themselves: §3's numeral rule sets
   font-family on .n-money, which is more specific than anything an ancestor
   can say. The tabular figures and the thin-space grouping are untouched. */
.n-vtotal-m .n-money { font-family: var(--n-font-display); }

/* — the fallback: js/32-voice.js's own markup, for the cards this file does
     not model. Old markup, so the rules are scoped to .n-vcard and do one
     thing each. — */
.n-vcard .inline { display: none; }
/* The same metrics the floor's own rows have inside this card: .n-row-main's
   block padding (above), the ticket's text size, the ticket's hairline. The
   amount cell takes the numeral face and weight of .n-row-t through §1's
   shared rule, so a fallback card's figures are set exactly like a built
   card's (critic D3). */
.n-vcard .r {
  display: flex;
  justify-content: space-between;
  gap: var(--n-space-3);
  padding: var(--n-space-2) 0;
  font-size: var(--n-step-1);
  border-bottom: thin solid var(--n-border-soft);
}
.n-vcard .r:last-child { border-bottom: 0; }
.n-vcard .r > span:last-child { font-weight: var(--n-w-semibold); }
.n-vcard .muted { color: var(--n-text-muted); }
/* js/32-voice.js's own transcript line, inside its own markup: a 12 px
   inline-styled <p class="muted">Heard: “…”</p>. The floor has already lifted
   those words into the quote block at the top of the card, so the original is
   taken off the screen rather than shown twice at a size the type scale does
   not have. The `[style]` is what tells it apart from that file's OTHER bare
   muted paragraph — "Nothing counted yet." on an empty stock-take card, which
   must still be read. (That inline style is the signature we have; if the
   markup ever loses it, this selector is the one line to change, exactly as
   the card-matching regex in js/n-floor-extras.js is.) */
.n-vcard > p.muted[style] { display: none; }

/* ── 9. offline queue ─────────────────────────────────────────────────── */
.n-qlist { display: flex; flex-direction: column; border: thin solid var(--n-border); border-radius: var(--n-radius-md); overflow: hidden; }
.n-qlist-bad { border-color: var(--n-danger); background: var(--n-danger-soft); }
.n-qrow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--n-space-2); min-height: var(--n-hit-min); padding: var(--n-space-2) var(--n-space-3); border-bottom: thin solid var(--n-border-soft); }
.n-qrow:last-child { border-bottom: 0; }
.n-qrow-n { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: var(--n-space-1); }
.n-qrow-t { font-weight: var(--n-w-bold); }
.n-qrow-acts { display: flex; gap: var(--n-space-2); }
.n-qrow-acts .n-act { flex: 0 0 auto; }

/* ── 10. toast ────────────────────────────────────────────────────────── */
.n-toastroot {
  position: fixed;
  left: 0; right: 0;
  bottom: calc(var(--n-total-h) + var(--n-space-3));
  display: flex;
  justify-content: center;
  padding: 0 var(--n-space-3);
  pointer-events: none;
  z-index: 135;
}
.n-toast {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--n-space-3);
  max-width: var(--n-sheet-max-w);
  padding: var(--n-space-2) var(--n-space-3);
  background: var(--n-inverse-bg);
  color: var(--n-inverse-text);
  border-radius: var(--n-radius-md);
  box-shadow: var(--n-shadow-e);
  overflow: hidden;
  pointer-events: auto;
}
.n-toast-text { font-size: var(--n-step-1); }
.n-toast-undo {
  flex: 0 0 auto;
  min-width: var(--n-hit-min);
  min-height: var(--n-hit-min);
  padding: 0 var(--n-space-3);
  color: var(--n-inverse-text);
  border: thin solid var(--n-inverse-text);
}
.n-toast-bar {
  position: absolute;
  left: 0; bottom: 0;
  width: 100%;
  height: var(--n-space-1);
  background: var(--n-accent);
  transform-origin: left;
  transition: transform var(--n-toast-ms) linear;
}
.n-toast-bar.n-run { transform: scaleX(0); }

/* ── 11. tablet portrait (601–900) and phone (≤600) — spec §7 ─────────── */
@media (max-width: 900px) {
  .n-floor { grid-template-columns: var(--n-rail-w) 1fr var(--n-ticket-dock-w); }
}
@media (max-width: 600px) {
  /* The spec's phone minimum: 72 px of table (--n-space-8 + --n-space-2) with
     its number at --n-step-4, rather than the furniture shrinking to fit the
     screen. A 96 px table on a 390 px map does not fit — the seed layout's
     tightest pair of column centres is 78 px apart, so 80 px and up intersect
     — but 72 leaves 6 px of daylight between them, and the AABB sweep at
     390x844 is clean.

     The canvas SCROLLS instead of clipping (overflow: auto below, where the
     desk viewport has `clip`): --n-table-min is a minimum, so a table named
     "Bar 2" is as wide as its label at --n-step-4, and furniture near the far
     edge is reached by scrolling rather than being guillotined by the box.
     Nothing shrinks to make room. */
  [data-surface="floor"] { --n-table-min: calc(var(--n-space-8) + var(--n-space-2)); }
  .n-table { padding: var(--n-space-1); }
  /* Stacked columns: the notes sit directly under the tip tiles here, and a
     label beside four money buttons on a 390 px screen leaves each of them
     too narrow to read. The heading takes the whole row of its own instead —
     the sheet scrolls at this width, so the line costs nothing (D8/D12). */
  .n-quicks { grid-template-columns: repeat(4, 1fr); }
  #nQuickLbl { grid-column: 1 / -1; }
  .n-table-no { font-size: var(--n-step-4); }
  /* The canvas FILLS the column between the strip and the total bar. It was
     square (aspect-ratio 1/1, the map scrolling under it), which on a 390 px
     phone stopped the floor plan at ~60 % of the height available and shrank
     every table to fit a box two thirds the size of the space it had. A
     percentage now means a different distance across than down — the price of
     using the whole screen, and the right trade on the one viewport where the
     map is held in the hand rather than read from across the room. */
  .n-mapcanvas {
    flex: 1;
    min-height: 0;
    /* the phone's map is held in the hand: the furniture keeps its size and
       the canvas scrolls over a plan that is bigger than the screen (`clip`
       cannot scroll, which is exactly what it is for on the desk viewport) */
    overflow: auto;
    overscroll-behavior: contain;
  }
  /* The plan's own size on a phone: eight tables across and eight down. A
     390 px screen puts the seed floor's tightest pair of column centres 78 px
     apart, which two 72 px tables with names on them ("Bar 1", "Bar 2") do
     not clear — the AABB sweep found them 1.8 px into each other. The fix the
     spec names is not smaller furniture but a plan that keeps its size, so
     the stage is sized in the table token itself: below eight tables across,
     furniture starts touching whatever the screen is. Every centre then sits
     ~112 px from its neighbour and the sweep is clean. */
  .n-mapstage {
    min-width: calc(var(--n-table-min) * 8);
    min-height: calc(var(--n-table-min) * 8);
  }
  /* The choices column, COMPACTED so the pad it sits above is on screen.
     Two tenders across put the four of them on two rows of 64 px tiles, and
     those 136 px were what pushed the cash block's first numpad row off the
     bottom of the sheet (row 1 ended at 824 against a body ending at 747).
     One row of four at the floor's own 56 px minimum buys 80 px of it back,
     and the pad's first row now ends at 744 — inside the body, with the sheet
     at the top of its scroll and nothing reordered. The tiles keep the 56 px
     every control on this surface is guaranteed; "Uber Eats" wraps to two
     lines inside its tile and neither line clips (measured: scrollWidth ==
     clientWidth and scrollHeight == clientHeight on all four). */
  .n-tenders { grid-template-columns: repeat(auto-fit, minmax(var(--n-hit-min), 1fr)); }
  .n-tender { min-height: var(--n-hit-min); }
  .n-floor {
    grid-template-columns: var(--n-hit-min) 1fr;
    /* the rail row is `auto`, not a fixed --n-hit-min: it carries the menu's
       categories on the tile view and the venue's sections on the map, and a
       floor with only one section leaves it holding a single row. The row is
       still 56 px whenever it has content, because .n-railcat's own
       min-height is --n-hit-min. */
    grid-template-rows: auto 1fr auto auto;
    grid-template-areas: "hub strip" "work work" "cats cats" "total total";
  }
  /* The rail and the ticket dissolve into the page grid. Every rule here is
     scoped to .n-floor, so the moment nTicketSheet() moves the ticket into a
     sheet it stops matching and the ticket is a plain column again — one
     element, one renderer, no duplicated markup. (Unscoped `grid-area`s
     followed the element into the sheet and squeezed the rows to 24 px.) */
  .n-floor > .n-rail,
  .n-floor > .n-ticket { display: contents; }
  .n-floor > .n-rail > .n-hub { grid-area: hub; border-right: thin solid var(--n-border); }
  .n-floor > .n-ticket > .n-strip { grid-area: strip; background: var(--n-surface); }
  .n-floor > .n-ticket > .n-tbody { display: contents; }
  .n-floor > .n-work { grid-area: work; }
  /* The bottom strip. The rail dissolves into the page grid (display:contents
     above), so the STRIP is the grid item: two 56 px mode buttons pinned
     left, then the categories/sections scrolling past them under the thumb.
     Both parts are in one horizontal band because they are one band on the
     desk rail too — an operator learns one control, in one place. */
  .n-floor > .n-rail > .n-railstrip {
    grid-area: cats;
    display: flex;
    flex-direction: row;
    min-width: 0;
    border-top: thin solid var(--n-border);
    background: var(--n-surface);
  }
  /* `padding` and `gap` are reset here, not inherited: css/n-office.css:365
     also declares a bare `.n-rail-foot` (its own rail's foot) with a padding
     and a gap, and it loads after this file, so the floor's foot has been
     wearing the office's box. That collision is the office lane's to name —
     it is left alone on the desk rail, where it is the shipped look — but the
     phone strip has 56 px to live in and cannot carry someone else's 8 px. */
  .n-floor .n-railstrip > .n-rail-foot {
    order: -1;                      /* first two cells: see .n-railstrip above */
    flex: 0 0 auto;
    flex-direction: row;
    gap: 0;
    padding: 0;
    margin-top: 0;
    border-top: 0;
    border-right: thin solid var(--n-border);
  }
  .n-floor .n-railstrip > .n-rail-foot > .n-railmode {
    min-width: var(--n-hit-min);    /* 56 px each, the floor minimum */
    padding: 0 var(--n-space-1);
  }
  .n-floor .n-railstrip > .n-rail-cats {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;
  }
  .n-floor .n-tbody > .n-rowswrap,
  .n-floor .n-tbody > .n-second { display: none; }
  .n-floor .n-tbody > .n-total {
    grid-area: total;
    padding-bottom: env(safe-area-inset-bottom);
    min-height: calc(var(--n-total-h) + env(safe-area-inset-bottom));
  }
  .n-floor .n-cta-charge { display: none; }
  .n-floor .n-cta-open { display: inline; }
  .n-railcat {
    width: auto;
    white-space: nowrap;
    display: flex;
    align-items: center;
    padding: 0 var(--n-space-3);
    border-left: 0;
    border-bottom: var(--n-space-1) solid transparent;
  }
  .n-railcat[aria-current="true"] { border-left-color: transparent; border-bottom-color: var(--n-accent); }
  /* Seven cells in the ~278 px left over beside the two mode buttons, each
     still carrying the desk rail's flex-shrink of 1: the strip did not
     scroll, it COMPRESSED, and every label was guillotined mid-word ("Mair",
     "Drin", "Star", "Des"). A category that cannot be read is not a category.
     The cells size to their own labels and the strip scrolls instead (it
     already has overflow-x: auto); the ceiling is two --n-space-8 of label,
     after which a genuinely long category ellipsises — a mark that says "cut"
     rather than a word that looks like another word. */
  .n-floor .n-railstrip > .n-rail-cats > .n-railcat {
    flex: 0 0 auto;
    width: auto;
    min-width: var(--n-hit-min);
    padding-inline: var(--n-space-3);
  }
  .n-floor .n-railstrip .n-railcat-t {
    display: block;
    -webkit-line-clamp: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: calc(var(--n-space-8) * 2);
  }
  /* "+ Floor…" separates itself from the places with a hairline. In the
     vertical rail that hairline is ABOVE it, which is where a separator goes
     when the list runs down the page; in this strip the list runs across, so
     the same rule read as an underline along the top of one cell (critic D4).
     Turn it a quarter-turn with the list. */
  .n-floor .n-railstrip .n-railcat-add { border-left: thin solid var(--n-border); border-top: 0; }
  .n-railcat-t { -webkit-line-clamp: 1; }
  .n-sheet { max-width: 100%; }

  /* The ticket sheet's total bar sat --n-space-3 inside the sheet on three
     sides — a square-cornered bar floating in a rounded panel, with a strip
     of surface showing under it. It is the same bar as the one on the page
     behind it and it belongs to the same edge: pulled flush through the
     body's own padding. No bottom radius: this sheet's bottom edge is the
     viewport's (measured at 390x844 — sheet.bottom 844, border-radius
     "16px 16px 0 0"), so there is no corner there to follow. */
  .n-sheet-body > .n-in-sheet > .n-total {
    /* `width: auto` is what makes the negative margins WIDEN the bar rather
       than slide it: .n-total carries width: 100%, which resolves against the
       padded content box and would leave the bar its old width, moved 12 px
       to the left. As a stretched grid item with an auto width it takes the
       margins into its own box and reaches both edges of the sheet. */
    width: auto;
    margin: 0 calc(var(--n-space-3) * -1) calc(var(--n-space-3) * -1);
  }

  /* The charge sheet, taking cash, carries NO `order` — and this is the rule
     that must not come back. The numpad below the fold was once fixed by
     lifting the cash column with `order: -1`, which moves the PAINT and
     leaves the DOM where it was: Tab and a screen reader still ran Pay/Split
     → tenders → tips → cash, the exact reverse of the screen, and "Total
     due" and the Pay | Split tabs ended up UNDER the numpad. Paint order and
     DOM order agree on this sheet, and the total is read first. The room the
     pad needed came out of the choices column instead (.n-tenders above).

     There is deliberately no padding-bottom reserve under the body either.
     "Take payment" is .n-sheet-actions, a flex: 0 0 auto SIBLING of this
     scroll container rather than a bar laid over it, so the two can never
     overlap: measured at 390x844 with Cash, body.bottom 747 and actions.top
     747. A calc(--n-total-h + --n-space-5) reserve here only added 84 px of
     empty sheet under the last key row. */
}
