/*
  THE BRAND, in the book's own dark set: The Z Connect guidelines v1, transcribed from
  `web/src/styles/tokens.css` rather than chosen here. Five colours and no others — Lime
  #C9FF3B, Ink #141414, Z Green #1B4D3E, Volt #5B3DF5, Paper #F4F1EA — plus the derived
  ladder each has a measured ratio for, plus the separately-governed semantic three.

  THIS HEADER USED TO SAY "lime #C6FF64 on near-black, #262B24 hairlines, 12px radii, no
  elevation", and to claim the lime was "transcribed from web/src/styles/tokens.css:32
  (--lime), the value thezconnect.com serves". Every clause of that is now wrong, and the
  last one was wrong when it was written: #C6FF64 is `--card-lime`, the PRINTED CARD's
  colour, taken off physical artwork and deliberately outside the book. So this file and
  the admin dashboard it claims to match were rendering the card's green as the product's.
  Read tokens.css; do not trust a transcription, including this one.

  Radii are 0 and there is no elevation — `--r` is kept at 0px so that the two rules which
  name it keep reading one value, exactly as tokens.css keeps `--r`/`--r-lg`.

  The semantic colours are deliberately NOT the accent. A number that is good must
  not look like a brand moment — if "served" glowed lime it would read as the thing
  the page is selling, and the override count beside it would read as decoration.
  Lime is reserved for something you can press.

  In app.css rather than a <style> block for consistency with /verify/app.js, even
  though style-src does allow 'unsafe-inline' and a block would have worked. The
  script next door has no such choice.
*/

/*
  ── ARCHIVO, SELF-HOSTED ─────────────────────────────────────────────────────────────
  Copied byte for byte from `web/public/fonts/` into `./fonts/`, licence (OFL.txt)
  beside them, and declared exactly as `web/src/styles/tokens.css` declares them — the
  same eight subsets, the same measured unicode-ranges, the same metric-matched Arial
  fallbacks so a swap does not reflow.

  A manager is at a desk on a real browser, so the eight woff2 files are affordable here
  in a way they are not on the till next door: /verify/ keeps the system stack, and its
  own comment says why.

  RELATIVE `url()`, deliberately, and it is load-bearing for a different reason than in
  the HTML. This app is served from `/portal/` on TWO origins — partners.thezconnect.com
  and the API host — and a `url()` inside a stylesheet resolves against the STYLESHEET's
  address rather than the document's, so `fonts/…` beside this file is correct on both
  where a leading slash would depend on the mount.

  (This said the portal was "at the root of partners.thezconnect.com". It is NOT:
  `ops/add-partners-tls.ps1` proxies `/portal` explicitly and maps the bare hostname to
  `/verify/`, so the root of that host is the SCANNER. Found by codex. The conclusion
  about relative `url()` is unaffected, which is why only the reason changed.)
*/
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/archivo-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2190-2193, U+2212, U+2215, U+FEFF, U+FFFD; }
/* The latin-ext range was measured off the shipped subsets, not copied from Google's
   published range. Widening it would fetch a file that lacks the requested glyph and
   then fall through, wasting a request. */
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 400; font-display: swap;
  src: url('fonts/archivo-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+019D, U+01A0-01A1,
    U+01AF-01B0, U+01C4-01DC, U+01E6-01E7, U+01EA-01EB, U+01FA-021B, U+022A-022D,
    U+0230-0233, U+0237, U+0259, U+0272, U+1E24-1E25, U+1E62-1E63, U+1E6C-1E6D,
    U+1E80-1E85, U+1E9E, U+1EF2-1EF9, U+20A1, U+20A3-20A4, U+20A6-20A7, U+20A9,
    U+20AB, U+20AD, U+20B1-20B2, U+20B5, U+20B9-20BA, U+20BC-20BD, U+2113; }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('fonts/archivo-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2190-2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('fonts/archivo-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+019D, U+01A0-01A1,
    U+01AF-01B0, U+01C4-01DC, U+01E6-01E7, U+01EA-01EB, U+01FA-021B, U+022A-022D,
    U+0230-0233, U+0237, U+0259, U+0272, U+1E24-1E25, U+1E62-1E63, U+1E6C-1E6D,
    U+1E80-1E85, U+1E9E, U+1EF2-1EF9, U+20A1, U+20A3-20A4, U+20A6-20A7, U+20A9,
    U+20AB, U+20AD, U+20B1-20B2, U+20B5, U+20B9-20BA, U+20BC-20BD, U+2113; }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('fonts/archivo-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2190-2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('fonts/archivo-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+019D, U+01A0-01A1,
    U+01AF-01B0, U+01C4-01DC, U+01E6-01E7, U+01EA-01EB, U+01FA-021B, U+022A-022D,
    U+0230-0233, U+0237, U+0259, U+0272, U+1E24-1E25, U+1E62-1E63, U+1E6C-1E6D,
    U+1E80-1E85, U+1E9E, U+1EF2-1EF9, U+20A1, U+20A3-20A4, U+20A6-20A7, U+20A9,
    U+20AB, U+20AD, U+20B1-20B2, U+20B5, U+20B9-20BA, U+20BC-20BD, U+2113; }
@font-face { font-family: 'Archivo Expanded'; font-style: normal; font-weight: 900; font-display: swap;
  src: url('fonts/archivo-expanded-900-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2190-2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Archivo Expanded'; font-style: normal; font-weight: 900; font-display: swap;
  src: url('fonts/archivo-expanded-900-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+019D, U+01A0-01A1,
    U+01AF-01B0, U+01C4-01DC, U+01E6-01E7, U+01EA-01EB, U+01FA-021B, U+022A-022D,
    U+0230-0233, U+0237, U+0259, U+0272, U+1E24-1E25, U+1E62-1E63, U+1E6C-1E6D,
    U+1E80-1E85, U+1E9E, U+1EF2-1EF9, U+20A1, U+20A3-20A4, U+20A6-20A7, U+20A9,
    U+20AB, U+20AD, U+20B1-20B2, U+20B5, U+20B9-20BA, U+20BC-20BD, U+2113; }

/* The book's own fallbacks, metric-matched so the swap does not reflow.
   Measured over the book's body and caps copy with fontTools:
   Archivo 500 / Arial 100.2%, Archivo 700 / Arial Bold 98.2%,
   Expanded 900 / Arial Black 118.1%. Archivo is UPM 1000, asc 878, desc 210, gap 0. */
@font-face { font-family: 'Archivo Fallback'; font-weight: 400 500; src: local('Arial'), local('ArialMT');
  size-adjust: 100.2%; ascent-override: 87.6%; descent-override: 21%; line-gap-override: 0%; }
@font-face { font-family: 'Archivo Fallback'; font-weight: 700; src: local('Arial Bold'), local('Arial-BoldMT');
  size-adjust: 98.2%; ascent-override: 89.4%; descent-override: 21.4%; line-gap-override: 0%; }
@font-face { font-family: 'Archivo Expanded Fallback'; font-weight: 900; src: local('Arial Black'), local('Arial-Black');
  size-adjust: 118.1%; ascent-override: 74.3%; descent-override: 17.8%; line-gap-override: 0%; }

:root {
  /* The one ground. `--card` and the three `*-bg` washes are GONE: a module is a 1px
     rule on the page's own ground, not a tint floating on it. */
  --bg: #141414;

  /* --line is the book's --rule: "1px at 20% inside modules", 1.78 on Ink, and NEVER
     the edge of a control. --edge is what a control gets — 3.77, which is what WCAG
     1.4.11 asks of anything that identifies one. Mixing them up is the whole reason
     --edge had to be added rather than reusing --line for the inputs. */
  --line: #41403F;
  --edge: #72716E;

  /* Paper, and the ladder under it. --fg was #FFFFFF and --dim was #AAAAAA; neither
     is in the book, and pure white on Ink is a harsher page than Paper. */
  --fg: #F4F1EA;    /* 16.33 on Ink */
  --dim: #9D9B97;   /* Paper 61% over Ink: 6.64 */
  --faint: #888783; /* Paper 52% over Ink: 5.12 — the book's disabled label */

  --accent: #C9FF3B;  /* --lime, 15.66 on Ink */
  --ink: #141414;     /* what a Lime block's own label is set in — the book's --on-lime */

  /* The 2px Volt outline, and nothing else on this page. Volt is never body text. */
  --focus: #5B3DF5;

  /* Semantic, deliberately NOT brand. These were the Tailwind 400 set (#4ADE80,
     #FBBF24, #F87171); these are the book's, each measured on Ink. */
  --ok: #5BD98A;    /* 10.30 */
  --warn: #F0B429;  /*  9.88 */
  --bad: #FF6B5E;   /*  6.59 */

  /* One family. Wide for shouting, normal for talking. */
  --sans: 'Archivo', 'Archivo Fallback', Arial, sans-serif;
  --disp: 'Archivo Expanded', 'Archivo Expanded Fallback', 'Arial Black', Arial, sans-serif;

  /* Corners 0px */
  --r: 0px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--fg);
  /* Body: Archivo 500 at 17px. Was `15px/1.5 Inter, …` — Inter is named by nothing
     that ships it, so every reader fell straight through to the system stack. */
  font: 500 17px/1.45 var(--sans);
  -webkit-font-smoothing: antialiased;
}
/* No italic cut is shipped and synthesis is off, so emphasis is weight. */
html { font-synthesis: none; }
em, i { font-style: normal; font-weight: 700; }

.hide { display: none !important; }
.gap { height: 12px; }
/* `.center` IS GONE, selector and class both. The book sets type flush left, and a
   rule named `.center` that sets `left` is worse than no rule — the three places that
   carried it (two `<section class="card center">` wrappers and two notes) no longer
   name it. Nothing in app.js writes the class; checked. */
.dim { color: var(--dim); }

/* ── chrome ─────────────────────────────────────────────── */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 5;
}
/* The partner's own name, written in by app.js off the login response. DATA, so it
   keeps the case it was entered in — the book's `.as-entered`: "iOS is not IOS". */
.brand b { display: block; font: 700 17px/1.3 var(--sans); text-transform: none; letter-spacing: 0; }
.brand span { display: block; font-size: 13px; color: var(--dim); }

/*
 * USE THE SCREEN. This was 940px, so on a 1920 laptop the portal drew itself
 * inside 49% of the window with a manager's own staff list squeezed into a
 * column while half the desk sat empty.
 *
 * WIDENING ALONE WOULD HAVE MADE IT WORSE, which is why the sections below pair
 * up rather than just stretching: a paragraph set to 1560px is 190 characters a
 * line and unreadable, and a two-row staff list stretched that wide is mostly
 * whitespace with the Suspend button a mouse-journey from the name it belongs
 * to. So the CONTAINER grows, prose stays at a readable measure, and the
 * short-but-tall blocks sit side by side instead of stacking.
 */
.wrap { max-width: 1560px; margin: 0 auto; padding: 20px 24px 64px; }

/* Prose keeps a readable measure no matter how wide the container gets. ch is
   the right unit here: it tracks the font, so this stays ~80 characters if the
   type ever changes size. Applied to the explanatory copy only — a TABLE must
   still be free to use the whole width. */
/* `.wrap > p` MATCHED NOTHING and the cap silently did not apply — the three
   explanatory paragraphs under Scans are children of #viewPortal, not of .wrap,
   so they rendered 1512px wide: a 190-character line. Measured, not reviewed;
   the rule looked right and did nothing. Descendant selectors, scoped to the
   blocks that hold prose, so a new paragraph inherits the cap by default. */
.wrap p:not(.err):not(.flash):not(.k):not(.v),
.card .note { max-width: 82ch; }

/* Staff and the listing are both short and tall, so they stack into a long
   scroll on a wide screen for no reason. Paired, they fill the row.
   Single column below 1100px — two columns on a laptop half-screen would put
   the Suspend button and the account name in a 300px box. */
.cols2 { display: grid; grid-template-columns: 1fr; gap: 0 28px; align-items: start; }
@media (min-width: 1100px) { .cols2 { grid-template-columns: 1fr 1fr; } }
.cols2 .col { min-width: 0; }
/* The h2 inside a column must not keep the top margin it uses when it follows a
   card, or the two columns start at different heights and read as misaligned. */
.cols2 .col > .h2:first-child { margin-top: 0; }
.narrow { max-width: 460px; margin-left: auto; margin-right: auto; }

/* ── type ───────────────────────────────────────────────── */

/* Four roles and nothing between them. Headline and Title are Archivo Expanded 900
   in caps at -.04em; Body and Caption are Archivo. `.h1` was 22px and `.h2` 15px,
   both in the gap between roles. */
.h1 { font: 900 28px/.95 var(--disp); letter-spacing: -.04em; text-transform: uppercase;
      font-feature-settings: 'case'; margin: 0 0 8px; overflow-wrap: break-word; }
@media (min-width: 480px) { .h1 { font-size: 34px; } }
.h2 { font: 900 17px/1.1 var(--disp); letter-spacing: -.04em; text-transform: uppercase;
      font-feature-settings: 'case'; margin: 28px 0 10px; }
.card .h2 { margin-top: 0; }

.note { font-size: 13px; line-height: 1.6; color: var(--dim); margin: 10px 0 0; }
.warnnote { color: var(--warn); }
.err { font-size: 13px; color: var(--bad); margin: 10px 0 0; min-height: 1.2em; }
.flash { font-size: 13px; color: var(--ok); margin: 10px 0 0; min-height: 1.2em; }

/* ── surfaces ───────────────────────────────────────────── */

/* No cards floating on tint: a module is a 1px rule on the page's own ground.
   tokens.css's `.card` verbatim, and the reason `--card` is gone. */
.card {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: none;
  padding: 18px;
  margin-bottom: 12px;
}

/* ── controls ───────────────────────────────────────────── */

/* The Caption role: 13px, 700, caps, +0.1em tracking. */
label { display: block; font-size: 13px; font-weight: 700; text-transform: uppercase;
        letter-spacing: .1em; color: var(--dim); margin: 0 0 6px; }
.note + label { margin-top: 18px; }

/* NOT checkboxes or radios. The native box is the one round shape the brand's own
   allow-list keeps, and this block would otherwise hand it a padding, a border and a
   ground — `.rowlabel input` only ever reset its width. Scoped exactly as
   tokens.css:152 scopes the same element rule, for the same reason at a different
   property. */
input:not([type="checkbox"]):not([type="radio"]) {
  width: 100%;
  padding: 13px;
  font: inherit;
  /* Body is 17px, which clears the 16px floor under which iOS Safari zooms the whole
     page on focus. */
  font-size: 17px;
  border-radius: var(--r);
  /* --edge, not --line: this border is what identifies the field as a field. */
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--fg);
}
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }
/* The book's 2px Volt outline. Volt is at most one thing per screen, and a focus ring
   is on one element at a time by construction. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 0; }

/* A CTA is a flat Lime block with a Caption label in caps, and press swaps the
   polarity — the book's own press state, and what replaces every shadow.

   Deliberately NOT tokens.css's `white-space: nowrap`: that exists to stop a label
   breaking inside a 44px header box, and these are full-width block buttons whose
   labels include "Sign in with a different account". */
button, .btn {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 14px;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: .1em;
  font-feature-settings: 'case';
  text-align: left;
  text-decoration: none;
  border: 2px solid var(--accent);
  border-radius: var(--r);
  background: var(--accent);
  color: var(--ink);
  cursor: pointer;
}
button:active, .btn:active { background: var(--bg); color: var(--accent); }
@media (hover: hover) { button:hover, .btn:hover { background: var(--bg); color: var(--accent); } }
button.ghost {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--edge);
}
button.ghost:active { background: var(--accent); color: var(--ink); border-color: var(--accent); }
@media (hover: hover) { button.ghost:hover { background: var(--accent); color: var(--ink); border-color: var(--accent); } }
/* border-color too, or a red fill keeps the lime border the base rule gives it. */
button.danger { background: var(--bad); color: var(--ink); border-color: var(--bad); }
button.danger:active { background: var(--bg); color: var(--bad); }
@media (hover: hover) { button.danger:hover { background: var(--bg); color: var(--bad); } }
button.small { width: auto; min-height: 36px; padding: 8px 14px; }
/* The book's disabled state. `opacity: .5` was the old one and it washes the label and
   the border together, which reads as a colour rather than as an unavailable control. */
button:disabled, button:disabled:hover {
  cursor: default;
  background: transparent;
  color: var(--faint);
  border: 1px solid var(--line);
}

/* range picker */
.rowhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.rowhead .h2 { margin: 28px 0 10px; }
.seg { display: flex; gap: 6px; flex-wrap: wrap; }
.seg button {
  width: auto;
  min-height: 36px;
  padding: 8px 12px;
  background: transparent;
  color: var(--dim);
  border: 1px solid var(--edge);
  border-radius: var(--r);
}
@media (hover: hover) { .seg button:hover { background: transparent; border-color: var(--fg); color: var(--fg); } }
.seg button.on { background: var(--accent); color: var(--ink); border-color: var(--accent); }

/* ── the numbers ────────────────────────────────────────── */

/* Four buckets, because four is what `GET /partner/stats` returns and they sum to
   every scan in the window. Two rows of two before one column, rather than four
   straight to one: a 34px number in a full-width card is a lot of scrolling on a
   phone before the branch table, and the pairs still read left to right. */
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
@media (max-width: 860px) { .stats { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px) { .stats { grid-template-columns: 1fr; } }

.stat { margin-bottom: 0; }
.stat .k { font-size: 13px; font-weight: 700; color: var(--dim); text-transform: uppercase; letter-spacing: .1em; }
/* Four across, "Served without a valid scan" is the only label that wraps, and a
   wrapped label pushes its number a line below the three beside it — four figures
   meant to be compared at a glance, sitting on two different baselines. Reserving
   the second line costs nothing and holds them level. Scoped above the two-column
   breakpoint because from there down the cards are wide enough that nothing wraps,
   and the reservation would just be a gap under every label. */
@media (min-width: 861px) { .stat .k { min-height: 3em; } }
/* A figure that stands alone is Headline, not a size of its own. 40px was between
   roles; this is tokens.css's own `.stat`. */
.stat .v {
  font: 900 34px/.92 var(--disp);
  letter-spacing: -.04em;
  margin: 8px 0 6px;
  font-variant-numeric: tabular-nums lining-nums;
}
.stat .cap { font-size: 13px; color: var(--dim); line-height: 1.5; }

/* Bands for the override and waiting counts. Zero overrides is genuinely good
   news; the rest is a question, not a failure — so amber and red, never the accent.
   Waiting never takes the green: two green tiles in one row halve the signal of the
   one this page exists to make loud, and a quiet stretch with no unanswered prompts
   is ordinary rather than an achievement.

   THE WASHES ARE GONE and the border carries the whole band, which is the book's
   `.notice` and its state modifiers: a 1px rule plus a 2px edge, in ONE semantic
   token, over the page's own ground. The four `rgba(…, .4)` borders and the three
   `--*-bg` tints went with them. */
.stat.ok   { border-color: var(--ok);   border-left-width: 2px; }
.stat.ok   .v { color: var(--ok); }
.stat.warn { border-color: var(--warn); border-left-width: 2px; }
.stat.warn .v { color: var(--warn); }
.stat.bad  { border-color: var(--bad);  border-left-width: 2px; }
.stat.bad  .v { color: var(--bad); }

/* ── tables ─────────────────────────────────────────────── */

.scroll { overflow-x: auto; }
/* min-width grew with the Waiting column: four numeric columns and a branch name
   no longer fit a phone, and a table allowed to squeeze breaks the headers onto
   three lines each instead. `.scroll` above it is what makes that acceptable.
   It grew again with the type: Body is 17px where this was 14. */
table { width: 100%; min-width: 620px; border-collapse: collapse; font-size: 17px; }
th {
  text-align: left;
  font-size: 13px;
  font-weight: 700;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: .1em;
  padding: 0 0 10px;
}
td { padding: 12px 0; border-top: 1px solid var(--line); vertical-align: top; }
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; padding-left: 14px; }
td.num.warn { color: var(--warn); }
td.num.bad { color: var(--bad); }
.sub { font-size: 13px; color: var(--dim); line-height: 1.5; }
td .sub { display: block; margin-top: 3px; }

/* ── chips ──────────────────────────────────────────────── */

/* tokens.css's `.tag`: transparent, one 1px --edge rule, 0 radius, Caption in caps,
   and a state modifier that moves the border and the text together. */
.chip {
  display: inline-block;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.6;
  text-transform: uppercase;
  letter-spacing: .1em;
  padding: 1px 8px;
  border-radius: var(--r);
  border: 1px solid var(--edge);
  color: var(--dim);
  margin: 4px 4px 0 0;
  white-space: nowrap;
}
.chip.ok { color: var(--ok); border-color: var(--ok); }
.chip.warn { color: var(--warn); border-color: var(--warn); }
.chip.bad { color: var(--bad); border-color: var(--bad); }

/* ── staff ──────────────────────────────────────────────── */

.staff { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; flex-wrap: wrap; padding: 14px 0; border-top: 1px solid var(--line); }
.staff:first-child { border-top: 0; padding-top: 4px; }
.staff .id { min-width: 220px; flex: 1 1 240px; }
/* A person's name. DATA, so it keeps its own case. */
.staff .id b { font: 700 17px/1.3 var(--sans); text-transform: none; letter-spacing: 0; display: block; }
.staff .id .sub { font-size: 13px; color: var(--dim); line-height: 1.6; word-break: break-word; }
/* Must be allowed to shrink. The two "you cannot do this" explanations are whole
   sentences, and `flex: 0 0 auto` sizes to max-content — which pushed the page
   itself sideways on a phone instead of wrapping the text. */
/* FLUSH LEFT, and the column was ALREADY half-way there: the last rule in this file
   re-declares `.staff .act` with `align-items: flex-start`, so the buttons have been
   packed left at every width while the sentence under them was right-aligned. D14
   settles it — type sets flush left — and it also makes the two agree. The
   `max-width: 560px` override that used to say exactly this is gone with it. */
.staff .act {
  flex: 1 1 200px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
}
.staff .act button { width: auto; min-height: 36px; padding: 9px 14px; }
.staff .act .why { font-size: 13px; color: var(--dim); line-height: 1.5; text-align: inherit; }

/* ── facts ──────────────────────────────────────────────── */

.facts { margin: 0; display: grid; grid-template-columns: 160px 1fr; gap: 10px 16px; font-size: 17px; }
@media (max-width: 560px) { .facts { grid-template-columns: 1fr; gap: 2px 0; } .facts dd { margin-bottom: 10px; } }
/* The term is a label; the definition is data. */
.facts dt { color: var(--dim); font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; }
.facts dd { margin: 0; word-break: break-word; }
.facts dd.unset { color: var(--dim); }
.facts dd.num { font-variant-numeric: tabular-nums; }

/* ── confirm ────────────────────────────────────────────── */

/* Ink at 80%, which is the one scrim the record carries — `web/src/components/
   PhotoCropper.css`'s `.crop-back`. It was `rgba(0, 0, 0, .7)`, and black is not
   one of the five. */
.scrim {
  position: fixed;
  inset: 0;
  background: #141414;   /* the fallback matters: a DROPPED scrim leaves the dialog
                            floating over readable page content. The record
                            (PhotoCropper.css) carries no fallback; a manager's
                            browser is less known than a member's. */
  background: color-mix(in srgb, #141414 80%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  z-index: 20;
}
/*
 * A dialog over a scrim is the ONE place a `.card` may not be transparent — it would
 * read straight through to the page it is interrupting. So it takes the record's own
 * overlay treatment, from `.crop-panel` beside the scrim above: an opaque Ink ground,
 * and 2px of Paper framing all four edges because a centred panel exposes all four.
 */
.confirm { max-width: 420px; width: 100%; margin: 0; background: var(--bg); border: 2px solid var(--fg); }

/*
 * Anchors were unstyled, so "Open the scanner" rendered as default browser blue
 * on this near-black surface — the same defect the verify page had, found the
 * same way: by looking at it. Neither page had ever contained a link before the
 * cross-links between them were added, so neither stylesheet had a rule.
 *
 * They were then the accent, which reads as a control — lime here belongs to
 * something you can press. The book sets a link in body colour with a 1px underline
 * and moves the UNDERLINE to Volt on hover, so this is tokens.css copied rather than
 * a choice of ours. Underline kept either way: colour alone is not an affordance.
 */
a { color: var(--fg); text-decoration: underline 1px; text-underline-offset: 3px; }
@media (hover: hover) { a:hover { text-decoration-color: var(--focus); text-decoration-thickness: 2px; } }

/*
 * The button link keeps its own treatment ENTIRELY — it is a control, not prose.
 *
 * COLOUR, not just text-decoration, and that omission was a real regression:
 * `a:hover` above matches `<a class="btn">` too, so hovering the scanner button
 * repainted its text in the link hover colour on the lime background it keeps.
 * Restoring only the underline left the contrast broken, so the override has to
 * restate every property the prose rule touches. The hover rule in the button block
 * above owns the actual hover state.
 */
a.btn { color: var(--ink); text-decoration: none; }
@media (hover: hover) { a.btn:hover { text-decoration: none; } }
a.btn:focus-visible { text-decoration: none; }


/* ── creating and editing, added 2026-09-01 ──────────────────────────────────
 *
 * The portal could already read everything and change almost nothing: five
 * working endpoints (create staff, edit staff, create branch, edit branch) had
 * no front end at all, so a manager's only button was Suspend.
 */

/* Transparent on the page's own ground with an --edge border, exactly as `input`
   above. An earlier version of this rule invented `var(--field)`, which is defined
   NOWHERE — so the declaration was discarded and the control fell back to
   transparent — and paired it with `--ink`, the colour meant for text ON the lime
   accent, which on a dark ground is black on black. */
select {
  width: 100%;
  min-height: 44px;
  padding: 11px 12px;
  border-radius: var(--r);
  border: 1px solid var(--edge);
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 17px; /* Body, and it clears the 16px floor under which iOS Safari zooms */
}

/* THE SAME BUG THE COMMENT ABOVE DESCRIBES WAS STILL LIVE ONE RULE BELOW IT:
   `.rowlabel` set `color: var(--ink)` — #0D0F0C then, #141414 now, i.e. exactly the
   page's own ground — on "Show this branch to members", the checkbox label in the
   branch form. `.rowlabel` is (0,1,0) and beats the `label` element rule, so nothing
   rescued it. Invisible text, and it is `--fg` now. */
.rowlabel {
  display: flex; align-items: center; gap: 9px;
  font-size: 17px; font-weight: 500; text-transform: none; letter-spacing: 0;
  color: var(--fg); margin: 0;
}
.rowlabel input { width: auto; margin: 0; }

/* The one-time password. Deliberately loud and deliberately NOT a toast — it is
   the only moment this value exists in readable form anywhere.
   `var(--good, #7ee787)` was the third undefined-token bug in this file: `--good` is
   declared nowhere, so the fallback is what rendered — a green from no palette here.
   It is the semantic --ok, which is the only green in the record. */
.secret { border-color: var(--ok); border-left-width: 2px; margin: 12px 0; }
/* An identifier is copied, so nothing may change its case — and the brand retires
   monospace for one. tokens.css's `.code` is the page's own face at 700 with +.04em
   and SLASHED ZEROS, which is what tells an O from a 0 in a password read aloud. */
.secretval {
  display: block;
  font-family: var(--sans);
  font-size: 17px; font-weight: 700; letter-spacing: .04em;
  font-variant-numeric: tabular-nums slashed-zero;
  padding: 12px 14px; margin-top: 10px;
  border-radius: var(--r);
  /* The SAME bug lived here and mattered more: this is the only readable copy of
     a live credential that exists anywhere, and an undefined background plus
     near-black text rendered it invisible. Checking textContent proved the value
     was SET; it never proved it could be READ. */
  background: transparent; color: var(--accent);
  border: 1px solid var(--edge);
  user-select: all;            /* one tap selects the whole thing */
  overflow-wrap: anywhere;
}

/* The dialogs scroll: the branch form is seven fields and a phone in landscape
   is shorter than it. Without this the Save button is unreachable. */
.scrim .confirm { max-height: 88vh; overflow-y: auto; }

.staff .act { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; }

/* ── the scan limit, added 2026-10-01 ─────────────────────────────────────────
 *
 * Karl: mandatory, and "done super nicely" and easy to adjust from a phone. So it is
 * one card, read top to bottom on a 390px screen: what the rule is, the two controls
 * that set it, one button, then a switch per branch. No table anywhere in it.
 *
 * Motion is state feedback only and every value is the shared set below: a switch
 * knob that translates, a "Saved" that fades, an editor that fades up into place,
 * and a press scale on the controls that are tapped. Transitions, never keyframes,
 * so a double tap retargets instead of restarting. Reduced motion keeps the fades
 * and drops every movement.
 */
:root {
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

.sl { margin-bottom: 28px; }
.sl-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sl-head .h2 { margin: 0; }

/* The rule itself, in the book's Headline role: the one figure on this card. */
.sl-rule {
  font: 900 28px/.95 var(--disp);
  letter-spacing: -.03em;
  text-transform: uppercase;
  font-feature-settings: 'case';
  font-variant-numeric: tabular-nums lining-nums;
  margin: 14px 0 0;
  overflow-wrap: break-word;
}
@media (min-width: 480px) { .sl-rule { font-size: 34px; } }
.sl-rule.unset { color: var(--bad); }

/* The book's `.notice`: a 1px rule plus a 2px edge in ONE semantic colour, over the
   page's own ground — the same treatment the stat bands use. */
.sl-banner {
  border: 1px solid var(--warn);
  border-left-width: 2px;
  padding: 12px 14px;
  margin: 14px 0 0;
  font-size: 15px;
  line-height: 1.5;
}
.sl-banner b { display: block; font-weight: 700; margin-bottom: 2px; }
.sl-banner.bad { border-color: var(--bad); }

.sl-editor { margin-top: 18px; max-width: 460px; }
.sl-editor.entering { opacity: 0; transform: translateY(-4px); }
.sl-editor { transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out); }

/* − [ 1 ] + at 48px: a thumb target on a phone, and the number is the thing read. */
.sl-stepper { display: grid; grid-template-columns: 48px 1fr 48px; gap: 0; max-width: 240px; }
.sl-stepper input:not([type="checkbox"]):not([type="radio"]) {
  text-align: center;
  font: 700 22px/1 var(--sans);
  font-variant-numeric: tabular-nums;
  min-height: 48px;
  border-left: 0; border-right: 0;
  -moz-appearance: textfield;
}
.sl-stepper input::-webkit-outer-spin-button,
.sl-stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.sl-step {
  width: 48px; min-height: 48px; padding: 0;
  text-align: center; font: 700 22px/1 var(--sans);
  background: transparent; color: var(--fg);
  border: 1px solid var(--edge);
}
.sl-step:disabled, .sl-step:disabled:hover { color: var(--faint); border-color: var(--line); }

/* Four equal cells, always one row: a window that wraps WEEK onto a second line on
   a phone reads as two controls. */
.sl-window { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.sl-window button { min-width: 0; padding-left: 4px; padding-right: 4px; text-align: center; }
.sl-actions { display: grid; gap: 10px; }
@media (min-width: 480px) { .sl-actions { grid-template-columns: auto auto; justify-content: start; } .sl-actions button { width: auto; min-width: 200px; } }
.sl-window button[aria-checked="true"] { background: var(--accent); color: var(--ink); border-color: var(--accent); }

/* Press feedback on the controls of this card, and only those: the rest of the
   portal keeps its own press state untouched. */
.sl-step, .sl-window button, .sl-press, .sl-switch {
  transition: transform 120ms var(--ease-out), background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}
.sl-step:active:not(:disabled), .sl-window button:active, .sl-press:active:not(:disabled),
.sl-switch:active:not(:disabled) { transform: scale(0.97); }

.sl-sub {
  font: 700 13px/1.2 var(--sans); letter-spacing: .1em; text-transform: uppercase;
  color: var(--dim); margin: 26px 0 0;
}

.sl-branches { list-style: none; margin: 10px 0 0; padding: 0; max-width: 560px; }
.sl-branch {
  display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 12px;
  min-height: 56px; padding: 8px 0; border-top: 1px solid var(--line);
}
.sl-branch:first-child { border-top: 0; }
.sl-bname { font: 700 17px/1.3 var(--sans); overflow-wrap: anywhere; }
.sl-bstate { display: block; font-size: 13px; font-weight: 500; color: var(--dim); }

/* "Saved", beside the switch it confirms. Fades in fast, leaves faster. */
.sl-saved {
  font-size: 13px; font-weight: 700; color: var(--ok); letter-spacing: .02em;
  opacity: 0; transition: opacity 150ms ease;
}
.sl-saved.on { opacity: 1; transition: opacity 200ms var(--ease-out); }

/* A square switch: the book's 0px radius, Lime when on (something you can press),
   a 1px --edge when off. The knob TRANSLATES; it never resizes. */
.sl-switch {
  position: relative; width: 52px; min-height: 30px; height: 30px; padding: 0;
  background: transparent; border: 1px solid var(--edge);
}
/* 30px to look at, 46px to hit: the row is a thumb target on a phone. */
.sl-switch::before { content: ''; position: absolute; inset: -8px -6px; }
.sl-switch::after {
  content: ''; position: absolute; top: 5px; left: 5px; width: 18px; height: 18px;
  background: var(--dim);
  transition: transform 160ms var(--ease-out), background-color 160ms ease;
}
.sl-switch[aria-checked="true"] { background: var(--accent); border-color: var(--accent); }
.sl-switch[aria-checked="true"]::after { transform: translateX(22px); background: var(--ink); }
.sl-switch:disabled { cursor: default; opacity: 1; background: transparent; border-color: var(--line); }
.sl-switch:disabled::after { background: var(--faint); }
.sl-switch[aria-checked="true"]:disabled { background: transparent; border-color: var(--line); }
/* The base `button:hover` swaps polarity; a switch must not look like it flipped. */
@media (hover: hover) and (pointer: fine) {
  .sl-switch:hover { background: transparent; border-color: var(--fg); }
  .sl-switch[aria-checked="true"]:hover { background: var(--accent); border-color: var(--fg); }
}

/* The scan log's member cell: a name is DATA and keeps its own case. */
td .who { display: block; font-weight: 700; text-transform: none; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .sl-editor.entering { transform: none; }
  .sl-editor, .sl-switch::after { transition-property: opacity, background-color; }
  .sl-step:active, .sl-window button:active, .sl-press:active, .sl-switch:active { transform: none; }
}
/* An empty message line holds no space on this card: the Save button and Branches
   would otherwise sit a blank line apart for no visible reason. */
.sl .err:empty, .sl .flash:empty { min-height: 0; margin: 0; }

/* ── a branch's discount limit (2026-10-12) ────────────────────────────────────
   The branch rows of the card above, with the cap in place of the switch, and an
   editor that opens under the one row being changed. The same controls, the same
   press scale and the same fade-up as the limit's own editor; no new motion. */
.cap-list { list-style: none; margin: 14px 0 0; padding: 0; max-width: 560px; }
.cap-row {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px;
  min-height: 56px; padding: 8px 0; border-top: 1px solid var(--line);
}
.cap-row:first-child { border-top: 0; }
.cap-row .sl-bname { min-width: 0; }
/* A branch at its limit says so in the colour its tills are refusing in. */
.cap-row.full .sl-bstate { color: var(--warn); }
.cap-row .cap-edit { white-space: nowrap; }
.cap-editor { list-style: none; margin: 0 0 14px; padding: 4px 0 0; }
.cap-editor input:not([type="checkbox"]):not([type="radio"]) {
  max-width: 200px; font: 700 22px/1 var(--sans); font-variant-numeric: tabular-nums; min-height: 48px;
}
.cap-period { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; max-width: 240px; }
.cap-period button { min-width: 0; text-align: center; }
.cap-period button[aria-checked="true"] { background: var(--accent); color: var(--ink); border-color: var(--accent); }
.cap-period button { transition: transform 120ms var(--ease-out), background-color 120ms ease, color 120ms ease, border-color 120ms ease; }
.cap-period button:active { transform: scale(0.97); }

/* ── insights (2026-10-12) ─────────────────────────────────────────────────────
   Numbers as text: a count and what it counts, flush left, no bars. */
#insights { margin-bottom: 28px; }
.ins-stats { margin-top: 4px; }
.ins-cols { margin-top: 12px; }
.ins-cols .col { margin-bottom: 12px; }
.sl-sub.ins-first { margin-top: 0; }
.ins-list { list-style: none; margin: 10px 0 0; padding: 0; }
.ins-list li {
  display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline;
  padding: 8px 0; border-top: 1px solid var(--line); font-size: 15px;
}
.ins-list li:first-child { border-top: 0; }
.ins-list .n { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ins-list .what { min-width: 0; overflow-wrap: anywhere; }
.ins-list .sub { display: block; font-size: 13px; color: var(--dim); }
.ins-download { width: auto; min-width: 200px; }
@media (max-width: 479px) { .ins-download { width: 100%; } }
#insights .err:empty, #insights .flash:empty, #capCard .err:empty, #capCard .flash:empty { min-height: 0; margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .cap-period button:active, .ins-download:active { transform: none; }
}
