/* ═══════════════════════════════════════════════════════════════════════════
   customers.fitsupply.com — the whole stylesheet for the customer portal.

   ONE FILE, NO IMPORTS, NO CDN, NOTHING REMOTE. This is served as a Worker
   static asset behind a bearer-token URL. Every remote reference would be a
   third party learning that a named customer opened their statement.

   ─────────────────────────────── FONTS ───────────────────────────────
   THE FIVE WEBFONTS BELOW ARE SELF-HOSTED FROM /fonts/, SAME ORIGIN, latin
   subset, woff2. `font-src 'self'` in http.ts already allows them and was NOT
   widened; nothing here reaches a font CDN, so nobody outside this Worker
   learns that the page was opened.

   An earlier version of this file said "NO FONTS" and argued that a webfont
   would be a render-blocking request on a page whose job is to show six numbers
   quickly. That argument was right about the mechanism and is answered by
   `font-display: swap` rather than by going without: the figures paint
   IMMEDIATELY in the system fallback and reflow into the brand face when it
   lands. What the earlier version did not weigh is that an unbranded statement
   asking a customer for money is indistinguishable from a phishing page, and
   that is a worse failure than a swap.

   A page loads only the faces its own brand uses — the browser fetches nothing
   for a family no text is set in — so the real brand cost per statement is:
     Fit Supply .. Bricolage 41KB + Ubuntu 400/700 65KB + logo 65KB = ~171KB
     On Site ..... Days One 20KB + Roboto 400 66KB      + logo  8KB =  ~94KB
   The Fit Supply logo is the single largest asset on the page: its .svg is a
   65KB wrapper around an embedded base64 PNG, not a lean vector. Worth knowing
   before anyone adds a second copy of it somewhere.

   NEITHER BODY FACE HAS EVERY WEIGHT. Ubuntu ships 400 and 700 here, so a
   declared 800 resolves to the real 700 with no synthesis. DAYS ONE AND ROBOTO
   HAVE ONLY 400 — there is no bold face of either — so the On Site theme sets
   `font-synthesis: none` and gets its emphasis from the display face, from
   uppercase tracking and from borders. See the [data-brand="onsite"] block.

   WHAT COMES FROM render.ts: four custom properties on <html> — --accent,
   --accent-text, --font-display, --font-body — plus a `data-brand` attribute
   that selects one of the two theme blocks near the bottom of this file.
   Colours and font stacks are brand FACTS and live in env.ts; how the page uses
   them (corner radius, button case, which signature accent) is a design
   decision and lives here.

   ─────────────────────────────── CONTRAST ───────────────────────────────
   Assumption stated plainly: BODY COPY IS NEVER ON A BRAND COLOUR. It is
   --ink (#1c1f23) on white or on one of two near-white bands, measured:

     #1c1f23 on #ffffff ............ 16.54:1      (bar 4.5)
     #1c1f23 on #f6f7f8 ............ 15.42:1
     #54595f on #ffffff ............  7.07:1      muted labels, still small text
     #54595f on #f6f7f8 ............  6.59:1

   --accent-text is Fit Supply #dd1421 (5.00:1 on white) / On Site #a85f0c
   (4.88:1 on white). It is used as TEXT ONLY ON PURE WHITE. On the #f6f7f8
   bands those fall to 4.67:1 and ~4.55:1 — technically passes, but AGENTS.md is
   right that ~0.2 of headroom is inside rounding noise, so links inside a tinted
   band take --ink instead. THAT RULE IS NOW LOAD-BEARING FOR BOTH BRANDS, not
   just for Fit Supply: On Site's darkened orange has even less headroom on the
   band than the red does. Every link is underlined regardless, so colour is
   never the only thing distinguishing it (WCAG 1.4.1).

   --accent is the raw brand colour and the two brands are NOT interchangeable
   in it:

     Fit Supply #f31827 ... white on it is 4.22:1 — FAILS 1.4.3 for small text,
       PASSES the 3:1 large-text bar. So it carries a white label at 19px/700,
       over the >=18.66px-bold threshold. THE BUTTON LABEL SIZE IS LOAD-BEARING.
       Shrinking .ps-pay below 19px, or dropping its weight below 700, moves it
       to the 4.5:1 bar and it fails.
     On Site #F48F20 ...... white on it is 2.39:1 — FAILS EVEN THE LARGE-TEXT
       BAR, and On Site's own display face has no bold, so the 3:1 allowance was
       never available to it anyway (19px/400 is not WCAG large text; that needs
       >=24px unbolded). The marketing site sets white on this orange. THIS PAGE
       DOES NOT: the On Site pay button is #1c1f23 INK on #F48F20 at 6.93:1.
       Its hover state is the brand's secondary blue, DARKENED — #2085F4 itself
       is 3.67:1 under white and 4.51:1 under ink, neither with usable headroom
       at 19px/400, so the hover fill is #1767b0 with a white label at 5.83:1.

   BOTH ACCENTS ALSO APPEAR AS PURE DECORATION — the masthead rule, the footer
   rule, the card edge, the bar under an On Site h2. #F48F20 is 2.39:1 against
   white and would not clear 1.4.11's 3:1 bar, so it is worth being exact about
   why that is not a failure: 1.4.11 covers graphics REQUIRED TO UNDERSTAND the
   content and the boundaries of a control. None of these are. A rule under the
   masthead conveys nothing; the invoice-versus-order distinction the card edge
   supports is carried in text by the "Invoices" and "Sales orders" headings and
   by the badges; and the focus ring — which IS required — is --ink, never the
   accent. Anything that has to be seen to be understood is ink or a border.

   The past-due badge deliberately does NOT use the brand: a red that means
   "late" and a red that means "Fit Supply" are different jobs, and On Site's
   accent is orange. It is a fixed semantic pair, #8a1015 on #fdecea (8.48:1).

   ─────────────────────────────── MEASURE ────────────────────────────────
   --measure is the running-prose column. See the note on .ps-prose for what
   was measured and how. IT IS PER BRAND, because Ubuntu and Roboto are both
   narrower than the Arial metrics the original value was set from.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═════════════════════════════ @font-face ═════════════════════════════
   Order matters only in that a family must be declared before the first rule
   that uses it. `font-display: swap` on every one: the figures on this page
   are worth more painted immediately in a fallback than withheld for 100ms. */

/* Fit Supply display. VARIABLE — one file covers 200-800, which is why the
   headline weights (700, 800) cost nothing extra. */
@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('/fonts/bricolage-grotesque-latin.woff2') format('woff2');
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* Fit Supply body. Two static faces — 400 and a REAL 700. */
@font-face {
  font-family: 'Ubuntu';
  src: url('/fonts/ubuntu-400-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Ubuntu';
  src: url('/fonts/ubuntu-700-latin.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* On Site display. 400 IS THE ONLY WEIGHT THAT EXISTS. Do not add a 700 rule
   here pointing at this same file — that is how a lie about a font gets into a
   stylesheet, and the browser would then stop synthesising and start scaling. */
@font-face {
  font-family: 'Days One';
  src: url('/fonts/days-one-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* On Site body. Also 400 only. */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Neutrals. */
  --ink: #1c1f23;
  --ink-muted: #54595f;
  --rule: #dde1e5;
  --rule-strong: #c3c9cf;
  --band: #f6f7f8;
  --paper: #ffffff;

  /* Semantic status pairs, brand-independent on purpose. */
  --late-bg: #fdecea;
  --late-ink: #8a1015;
  --clear-ink: #0f5132;

  /* Fallbacks. render.ts sets all four of these inline on <html>; these values
     are what a stylesheet-only preview (or a future page that forgets) gets. */
  --accent: #1c1f23;
  --accent-text: #1c1f23;
  --font-display: system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
  --font-body: system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;

  /* Shape tokens. Both theme blocks near the bottom of this file override every
     one of them; these are the neutral defaults, which is also what the page
     looks like if `data-brand` is ever absent. */
  --logo-h: 42px;
  --btn-radius: var(--radius);
  --btn-on: #ffffff;         /* label colour ON the accent fill */
  --btn-hover-bg: #1c1f23;
  --btn-hover-on: #ffffff;
  --btn-case: none;
  --btn-tracking: 0;

  /* The page column. Tables want width, prose does not — see .ps-prose. */
  --column: 1000px;
  --gutter: 20px;

  /* Running-prose measure. MEASURED, not guessed — see .ps-prose. */
  --measure: 440px;

  --radius: 8px;
}

/* ────────────────────────────── reset-ish ────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  /* The brand body face, with the old system stack still behind it inside
     --font-body (see env.ts) so a failed font fetch degrades to exactly what
     this page used to be rather than to Times. --measure is re-measured per
     brand in the theme blocks: Ubuntu and Roboto both fit MORE characters into
     the same column than the Arial metrics the 440px value was set from. */
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.6;
  /* THE PAGE BODY MUST NEVER SCROLL SIDEWAYS. Anything that can overflow is
     wrapped in .ps-scroll or given min-width:0; this is a last-resort belt on top
     of that, not the mechanism.

     AND IT BREAKS THE CHECK THAT WOULD CATCH A MISS. `overflow-x: hidden` turns
     body into a clipped scroll container, which CLAMPS
     `document.documentElement.scrollWidth` to the viewport — so the house check
     (`scrollWidth > innerWidth`, AGENTS.md) reports clean on a page that really
     does overflow, and the symptom on screen is an amount column that silently
     ends. Lift it before measuring:
       document.body.style.overflowX = 'visible'
     Verified that way at 390 / 560 / 640 / 768 / 1000 / 1440 / 1920 with the
     belt off — nothing overflows. The middle widths are not padding: below 559px
     .ps-pay goes full-width and above ~1000px the shell stops growing, so the
     band BETWEEN those is the only place an inline button can push the page out,
     and the On Site label is uppercase and tracked and therefore wider than the
     one this was first measured against. Re-run it there, not just at 390/1440.
     Covered: both brands, a long company name, a long verbatim payment-schedule
     string, the empty state and the error page. */
  overflow-x: hidden;
}

h1, h2, h3, h4, p, dl, dd, figure { margin: 0; }
dd { margin-inline-start: 0; }

a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; }

/* A visible, high-contrast focus ring on everything focusable. --ink rather than
   the accent, because it has to read on white, on the bands, and on the button. */
:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
  border-radius: 2px;
}

/* Visually hidden but present for assistive tech — table captions, the
   "opens in a new tab" hint. */
.ps-vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Present only on paper, and its opposite. `.ps-screenonly` needs no rule here —
   it is ordinary content until the print block hides it. */
.ps-printonly { display: none; }

/* ─────────────────────────── the content column ─────────────────────────── */

/* ONE column for the whole document, because this page has no host page to match.
   The AGENTS.md rule is "match the content column of the sections directly above
   and below" — here that resolves to a single deliberate width, and the constraint
   that actually binds is the prose measure below, not this outer number. 960px of
   content is what the widest thing on the page (a four-column line-item table with
   six-figure amounts) wants without feeling stretched. */
.ps-shell {
  max-width: var(--column);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.ps-main { padding-bottom: 8px; }

/* RUNNING PROSE, 45–75 REAL CHARACTERS.

   Measured, not asserted: the built page was loaded in Chromium at 1440px with
   font-family forced to "Liberation Sans" (metric-compatible with Arial, which is
   the widest realistic fallback in the stack above) and the rendered characters
   per line counted with a Range walked over the first text node — not divided out
   of a `ch` value, which measures the "0" glyph and runs ~1.5x narrow.

   440px at 17px/1.6 measured 65 characters on the longest full line and 53 on the
   shortest (7 full lines across the sample statement). That sits in the middle of
   the 45–75 band rather than at its ceiling, deliberately: system-ui resolves to
   SF Pro on macOS and Segoe UI on Windows, both NARROWER than Arial, so those
   platforms fit MORE characters into the same 440px. It takes a font ~13% narrower
   than Arial to push 65 to 75, which none of the stack is — so the ceiling holds on
   every platform rather than only on the one it was measured on.

   470px was the first value tried and measured 70. It passed, and it left the
   narrow-font platforms with almost no room; the number below is the second
   measurement, not the first guess.

   This applies to running prose only. Table cells, figures and address blocks are
   not prose and are not measured against it. */
.ps-prose {
  max-width: var(--measure);
  margin-block: 0 0.9em;
}
.ps-prose:last-child { margin-bottom: 0; }
.ps-muted { color: var(--ink-muted); font-size: 0.94em; }

/* Inside a TINTED band, links drop to --ink — the Fit Supply accent-text measures
   4.67:1 on #f6f7f8 and that margin is too thin to rely on. The underline is what
   carries the affordance in both cases.
   .ps-footer is NOT in this list and must not be added to it: the footer sits on
   white, it holds the only two real links on the page, and putting it here made
   the (0,1,1) selector beat the (0,0,1) `a` rule everywhere — which left
   --accent-text painting nothing at all while this file documented its contrast
   at length. */
.ps-summary a, .ps-remit a, .ps-staff a { color: var(--ink); }

/* ──────────────────────────────── headings ──────────────────────────────── */

/* h1 -> h2 -> h3 -> h4, properly nested: one h1 (the company name), an h2 per
   page section, an h3 per document, h4 for the blocks inside a document. No run
   of sibling h2s standing in for a hierarchy. */

/* h1/h2/h3 are the brand voice on this page: the display face carries most of
   the "is this really them?" recognition, and it costs no extra weight because
   Bricolage is variable and Days One is one 20KB file. h4 and the uppercase
   micro-labels stay in the BODY face — they are wayfinding, not identity, and a
   display face set at 13px and tracked is harder to read, not more branded. */
.ps-h1 {
  font-family: var(--font-display);
  font-size: clamp(28px, 4.2vw, 40px);
  line-height: 1.15;
  font-weight: 800;
  letter-spacing: -0.015em;
  /* Company names on this account run to 74 characters
     ("Northgate Student Living at University Commons Phase II Property Owner
     LLC"), which is a long word-set, not a long word — but a legal entity string
     can also contain an unbroken token, so it is allowed to break rather than
     push the masthead sideways. */
  overflow-wrap: anywhere;
}

.ps-h2 {
  font-family: var(--font-display);
  font-size: 25px;
  line-height: 1.25;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-bottom: 0.5em;
}

.ps-h3 {
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1.3;
  font-weight: 700;
}

.ps-h4 {
  font-size: 13px;
  line-height: 1.3;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 8px;
}

/* ──────────────────────────────── masthead ──────────────────────────────── */

.ps-masthead {
  padding-block: 24px 24px;
  /* PURE DECORATION. See the contrast note at the top of this file on why this
     is outside 1.4.11 and why On Site's 2.39:1 orange is allowed to draw it. */
  border-bottom: 4px solid var(--accent);
}

/* The real logo, at DOCUMENT scale. The marketing sites render these at 280x80
   (Fit Supply) and 270x48 (On Site); a statement is not a hero, so --logo-h
   pulls both down to roughly the height of the identity line beneath them. The
   customer's own company name in the h1 stays the largest thing in the block —
   branding sits above the account identification, it does not displace it.

   line-height:0 on the wrapper because a <p> holding one block image otherwise
   reserves a text line's worth of descender space under it. */
.ps-logo {
  line-height: 0;
  margin-bottom: 18px;
}
.ps-logo img {
  display: block;
  height: var(--logo-h);
  width: auto;
  /* Insurance only — at 390px the widest of the two lockups measures ~156px
     inside a ~358px column, so this never fires. It is here because a logo that
     silently pushes the masthead sideways is exactly the class of defect the
     house overflow check exists for. */
  max-width: 100%;
}

/* The document-type line: "Account statement" / "Customer portal".
   It is a <p> wrapping an INLINE-BLOCK <span>, and the span is not decorative
   markup — the Fit Supply signature is a hairline exactly as wide as the words,
   which a block-level element cannot draw. */
.ps-eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ps-eyebrow-t { display: inline-block; }

/* The identity line is what a customer checks first — "is this my account?" — so
   it sits directly under the h1 at full body size, not shrunk into fine print. */
.ps-ident {
  margin-top: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  color: var(--ink-muted);
  font-size: 16px;
}
.ps-ident-item { min-width: 0; }
/* THE SEPARATOR HANGS OFF THE ITEM BEFORE IT, not the one after.
   As a `+ ::before` it belonged to the SECOND item, so when the flex row wrapped
   at 390 the dot led the new line — "Account C1001" then a line opening with a
   stray middot. Trailing the first item instead reads the way a line ending in
   punctuation always does. `:not(:last-child)` rather than `+` because the dot
   now has to know it is not last, which a following-sibling combinator cannot
   express from the other side. */
.ps-ident-item:not(:last-child)::after {
  content: "\00b7";
  margin-left: 18px;
  color: var(--rule-strong);
}

/* ───────────────────────────── staff banner ───────────────────────────── */

/* Deliberately loud and deliberately inert. Dark slate rather than the brand,
   so it can never be mistaken for part of the customer's page, and bordered as
   well as filled so it survives a print with backgrounds off. */
.ps-staff {
  background: #24282e;
  color: #ffffff;
  border-bottom: 3px solid var(--accent);
  padding-block: 12px;
}
.ps-staff-title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}
.ps-staff-body {
  max-width: var(--measure);
  font-size: 15px;
  color: #e6e8ea; /* 13.0:1 on #24282e */
}

/* ─────────────────────────────── summary band ─────────────────────────────── */

.ps-summary {
  background: var(--band);
  border-bottom: 1px solid var(--rule);
  padding-block: 26px 28px;
}

.ps-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 16px 28px;
  margin-top: 4px;
}

/* min-width:0 on grid children is not optional — grid items default to
   min-width:auto exactly like flex items, and a long formatted amount is what
   pushes a track past its container. */
.ps-figure {
  min-width: 0;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 16px 18px;
}

.ps-figure-label {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* The two headline figures are the loudest thing on the page and are set in the
   display face for BOTH brands — this is the second-most recognisable surface
   after the logo, and on On Site it is also where a lost bold weight has to be
   paid back. Table cells are NOT in this list: they keep the body face and its
   tabular figures, which is what aligns a column of amounts. */
.ps-figure-value {
  font-family: var(--font-display);
  margin-top: 4px;
  font-size: clamp(28px, 3.4vw, 34px);
  line-height: 1.15;
  font-weight: 800;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.ps-figure-sub {
  margin-top: 2px;
  font-size: 14px;
  color: var(--ink-muted);
}

.ps-summary-note { margin-top: 16px; font-size: 15px; color: var(--ink-muted); }

/* ─────────────────────────────── sections ─────────────────────────────── */

.ps-section { padding-block: 34px 10px; }
.ps-section + .ps-section { padding-top: 26px; }

/* Invoices and sales orders must not read as one long list — they answer
   different questions. The order sections carry a tinted card and a different
   left rule so the eye can tell at a glance which kind of document it is on. */
.ps-doc {
  margin-top: 18px;
  padding: 20px 20px 22px;
  border: 1px solid var(--rule);
  border-left: 5px solid var(--accent);
  border-radius: var(--radius);
  background: var(--paper);
}

.ps-doc-so {
  border-left-color: var(--rule-strong);
  background: #fbfcfd;
}

.ps-doc-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--rule);
}
.ps-doc-head > * { min-width: 0; }

.ps-badges { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }

.ps-badge {
  display: inline-block;
  padding: 3px 9px;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  background: var(--band);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

/* Border AND text, not a tint alone: background colours do not print by default,
   and a past-due marker that vanishes on the printed copy is the one that most
   needs to survive it. */
.ps-badge-late {
  background: var(--late-bg);
  border-color: var(--late-ink);
  color: var(--late-ink);
  font-weight: 700;
}

.ps-badge-clear {
  background: var(--paper);
  border-color: var(--clear-ink);
  color: var(--clear-ink);
  font-weight: 700;
}

/* ───────────────────────── document meta (the dl) ───────────────────────── */

.ps-meta {
  display: grid;
  /* min() so this collapses to one column at 390px instead of forcing a
     360px track inside a ~350px container. */
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: 10px 24px;
  margin-block: 14px 20px;
}
.ps-meta-stack { grid-template-columns: minmax(0, 1fr); }

.ps-meta-item { min-width: 0; }
.ps-meta dt {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ps-meta dd {
  font-size: 16px;
  overflow-wrap: anywhere; /* PO numbers can be long unbroken strings */
}

/* The payment-schedule prose, quoted verbatim from NetSuite. Set apart so it
   reads as the customer's own agreed terms rather than as site copy. */
.ps-terms {
  margin-bottom: 20px;
  padding: 12px 14px;
  border-left: 3px solid var(--rule-strong);
  background: var(--band);
  font-size: 16px;
  /* A quoted block inside a card is not the page's running prose column; it is
     measured against the card it sits in — deliberately wider than .ps-prose,
     because it is a quotation set apart rather than running text, and narrower
     than the 960px card.

     620px WAS THAT NUMBER AND IS NOT ANY MORE. It predates the brand faces, and
     re-measured with a realistic (long, verbatim-from-NetSuite) payment-schedule
     string it ran 79 characters in Ubuntu and 84 in Roboto — over the 75 bar in
     both brands, on the one block whose content this repo does not control. It
     was invisible because the fixture's schedule string is 50 characters and
     fits on ONE line, and a check that drops single-line blocks as "ragged"
     never measured it at all. 520px measures 64 in Ubuntu and 68 in Roboto. */
  max-width: 520px;
}
.ps-terms-label {
  display: block;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* ──────────────────────────────── tables ──────────────────────────────── */

/* Every table lives in one of these. On a wide screen it does nothing; when a
   table cannot fit, IT scrolls and the page does not. */
.ps-scroll {
  overflow-x: auto;
  /* Keeps the focus ring from being clipped by the container it sits on. */
  padding-bottom: 2px;
  -webkit-overflow-scrolling: touch;
}

.ps-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 16px;
}

.ps-table th, .ps-table td {
  padding: 9px 10px;
  border-bottom: 1px solid var(--rule);
  text-align: left;
  vertical-align: top;
}

.ps-lines thead th {
  border-bottom: 2px solid var(--rule-strong);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
  white-space: nowrap;
}

.ps-lines tbody tr:last-child td { border-bottom: 0; }

/* Money and quantities right-aligned with tabular figures, so the digits stack in
   a column a reader can add up by eye. Without tabular-nums a proportional "1"
   is narrower than a "0" and the decimal points wander. */
.ps-c-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ps-c-sku {
  font-weight: 600;
  overflow-wrap: anywhere;
  /* One line item is a common case (single-line-single-doc.json). The table has
     no fixed row count and no minimum height, so n=1 renders as one row and the
     card closes around it — no reserved space, no hole. */
}

/* The six-row breakdown. It is not in a .ps-scroll because it is two columns and
   cannot overflow; it sits beside the payments column at width. */
.ps-breakdown { max-width: 380px; }
.ps-breakdown th { font-weight: 500; color: var(--ink-muted); }
.ps-breakdown td { white-space: nowrap; }

.ps-row-total th, .ps-row-total td {
  border-top: 2px solid var(--rule-strong);
  border-bottom: 0;
  padding-top: 11px;
  font-size: 18px;
  font-weight: 800;
  color: var(--ink);
}

.ps-paid { max-width: 420px; }
.ps-paid th { font-weight: 600; }

/* ───────────────────────── money block (2 columns) ───────────────────────── */

.ps-money {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 22px 34px;
  margin-top: 22px;
  padding-top: 4px;
}
.ps-money-col { min-width: 0; }

.ps-payments { margin-bottom: 16px; }
.ps-nopay { margin-bottom: 16px; }

.ps-balance {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 6px 16px;
  padding: 12px 14px;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
}
.ps-balance + .ps-balance { margin-top: 10px; }
.ps-balance > * { min-width: 0; }

.ps-balance-label {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.ps-balance-value {
  font-family: var(--font-display);
  font-size: 24px;
  font-weight: 800;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

/* Deposits already received are context, not a call to action. */
.ps-balance-quiet {
  border: 1px solid var(--rule);
  background: var(--band);
}
.ps-balance-quiet .ps-balance-value { font-size: 19px; font-weight: 700; }

/* ─────────────────────────────── pay buttons ─────────────────────────────── */

.ps-pay-group {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 22px;
}

/* SEE THE CONTRAST NOTE AT THE TOP OF THIS FILE BEFORE CHANGING ANY OF THIS.
   The two brands do NOT share a label colour and cannot be made to:
     Fit Supply  white on #f31827 = 4.22:1, a pass ONLY at 19px/700 (the
                 >=18.66px-bold large-text threshold, 3:1).
     On Site     white on #F48F20 = 2.39:1, a fail at every size, and Days One
                 has no bold so the large-text allowance was never reachable
                 anyway. It takes --ink on the orange instead, 6.93:1.
   --btn-on is that difference. It is a contrast decision, not a brand colour,
   which is why it lives in this file and not in env.ts.

   `background-color`, NOT the `background` shorthand: the Fit Supply theme
   paints a gradient over it with `background-image`, and a shorthand here would
   reset that back to none depending on which rule ran last. */
.ps-pay {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  padding: 13px 24px;
  border: 2px solid var(--accent);
  border-radius: var(--btn-radius);
  background-color: var(--accent);
  color: var(--btn-on);
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
  line-height: 1.3;
  text-transform: var(--btn-case);
  letter-spacing: var(--btn-tracking);
  /* A <button>-shaped control inherits UA `white-space: nowrap` in some contexts
     and a long label ("Pay balance on install $105,604.37") then refuses to wrap
     and pushes the page sideways at 390px. Reset it explicitly. */
  white-space: normal;
  min-width: 0;
}

/* The hover fill and its label move TOGETHER, because on On Site they must:
   the resting state is dark ink on orange and the hover state is white on a
   darkened blue, and swapping one without the other lands on 2.84:1. */
.ps-pay:hover {
  background-color: var(--btn-hover-bg);
  border-color: var(--btn-hover-bg);
  color: var(--btn-hover-on);
  text-decoration: none;
}
.ps-pay:focus-visible { outline-color: var(--ink); }

.ps-pay-label { font-weight: 700; }

/* Present ONLY when PayLink.amountCents is non-null. render.ts never synthesises
   this figure — see contract.ts on why a number beside a pay button that
   disagrees with the charge is a billing dispute. */
.ps-pay-amount {
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  white-space: nowrap;
}

.ps-pay-note { flex-basis: 100%; margin-top: -2px; }

/* ────────────────────────────── remittance ────────────────────────────── */

.ps-remit {
  margin-top: 32px;
  padding-block: 30px 34px;
  background: var(--band);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.ps-remit-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 22px 40px;
}
.ps-remit-col { min-width: 0; }

.ps-remit-label {
  margin-top: 12px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.ps-remit-payable {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.ps-remit-address {
  font-size: 17px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.ps-remit-note { margin-top: 20px; font-size: 15px; color: var(--ink-muted); }

/* ──────────────────────────── empty state ──────────────────────────── */

/* n=0 documents. A real panel with real sentences, not a blank column — and the
   remittance and contact blocks below it still render, so the page has a bottom. */
.ps-empty {
  padding-block: 34px 36px;
  background: var(--band);
  border-bottom: 1px solid var(--rule);
}

/* With no documents there is nothing between these two tinted bands, and the
   remittance block's own top margin opened a 32px white stripe across the page
   plus a double rule. They butt together instead. */
.ps-empty + .ps-remit { margin-top: 0; border-top: 0; }

/* ────────────────────────────── footer ────────────────────────────── */

.ps-footer {
  padding-block: 28px 40px;
  border-top: 4px solid var(--accent);
  margin-top: 32px;
}

/* ═════════════════════════════ BRAND THEMES ═════════════════════════════

   Two blocks, selected by `data-brand` on <html> (render.ts writes it from the
   SubsidiaryKey). Everything above this point is the document; everything here
   is which company's document it is.

   THE TWO BRANDS ARE DELIBERATELY NOT MIRROR IMAGES. Fit Supply is rounded and
   skewed — pill buttons, 10px cards, a hairline under a red eyebrow. On Site is
   square and rail-based — 0px buttons, a 4px left rail on a card, an orange bar
   under every h2. Those are the actual signatures of the two marketing sites and
   crossing them is what would make the portal read as neither.

   Both blocks are (0,2,0) against component rules at (0,1,0), so they win
   without !important — but that also means a theme rule can silently outrank a
   utility class. `.ps-vh` is the one that bites (it is (0,1,0) and it sets
   `position: absolute`), which is why the On Site h2 rule carries :not(.ps-vh).
   ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────────── Fit Supply ───────────────────────────── */

[data-brand="fitsupply"] {
  --radius: 10px;
  --btn-radius: 999px;
  --btn-on: #ffffff;
  --btn-hover-bg: #8d0e17;
  --btn-hover-on: #ffffff;
  --btn-case: none;          /* the site's buttons are sentence case, not caps */
  --btn-tracking: 0;
  --logo-h: 44px;
  /* MEASURED, not carried over: Ubuntu is narrower than the Arial metrics the
     440px default was set from. See the note on .ps-prose. */
  --measure: 430px;
}

/* THE FIT SUPPLY SIGNATURE. 700 uppercase in the darkened red (#dd1421, 5.00:1
   on white — this is TEXT, so the 4.5 bar applies and the raw #f31827 at 4.22
   would not do), with a 1px rule in the raw brand red under an inline-block, so
   the rule is exactly as wide as the words.

   Tracking is 0.26em where the marketing site uses 0.3em: on a page whose
   eyebrow reads "ACCOUNT STATEMENT" rather than a two-word marketing label, the
   larger value pushed the line most of the way across the masthead. This is a
   soft rule judged against its surroundings, not a fact. */
[data-brand="fitsupply"] .ps-eyebrow {
  font-size: 14px;
  letter-spacing: 0.26em;
  color: var(--accent-text);
}
[data-brand="fitsupply"] .ps-eyebrow-t {
  padding-bottom: 6px;
  border-bottom: 1px solid var(--accent);
}

/* The site's card is border-top 4px in the darkened red, 10px corners, and a
   shadow. The shadow here is 0 8px 20px/0.06 against the site's 0 16px 32px/0.12
   — a statement stacks ten of these down one column and the marketing value read
   as a pile of floating slabs. Same component, judged against its neighbours. */
[data-brand="fitsupply"] .ps-doc {
  border: 1px solid var(--rule);
  border-top: 4px solid var(--accent-text);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
}
/* The invoice/sales-order distinction survives the move from a left rail to a
   top edge — it is the same signal on a different side, and it is still not the
   only one (the section headings and the tinted card carry it in text). */
[data-brand="fitsupply"] .ps-doc-so {
  border-top-color: var(--rule-strong);
}

/* Pill, and a gradient rather than a flat fill. White stays >= 4.22:1 because
   the LIGHTEST stop is the brand red itself; every other point on the ramp is
   darker and safer. */
[data-brand="fitsupply"] .ps-pay {
  background-image: linear-gradient(100deg, var(--accent) 0%, #8d0e17 100%);
}
[data-brand="fitsupply"] .ps-pay:hover {
  background-image: linear-gradient(100deg, #8d0e17 0%, var(--accent) 100%);
}

/* ─────────────────────── On Site Fitness Service ─────────────────────── */

/* FONT-SYNTHESIS IS OFF FOR THIS WHOLE BRAND, AND THAT IS THE POINT.
   Days One and Roboto ship 400 and nothing else. Left alone, every
   `font-weight: 700` in this stylesheet — and there are twenty-odd — would be
   drawn by the browser smearing the 400 outline sideways. Faux bold on a money
   figure is exactly where it looks worst.

   So: synthesis off, and the elements that still need to read as LOUDER get it
   from something real — the display face, uppercase tracking, or a border. Each
   one is listed below rather than left to chance, because "it looked bold in
   the mock" is not a mechanism. */
[data-brand="onsite"] {
  --radius: 6px;
  --btn-radius: 0;                /* On Site is square-cornered. No pill. */
  --btn-on: #1c1f23;              /* ink on orange, 6.93:1 */
  --btn-hover-bg: #1767b0;        /* darkened #2085F4 — see the contrast note */
  --btn-hover-on: #ffffff;        /* 5.83:1 on #1767b0 */
  --btn-case: uppercase;
  --btn-tracking: 0.04em;
  --logo-h: 40px;
  /* MEASURED. Roboto is narrower still than Ubuntu. */
  --measure: 420px;

  font-synthesis: none;
  font-synthesis-weight: none;
}

/* THE ON SITE SIGNATURE: a 56x3 orange bar under the heading, left-aligned to
   the heading's own left edge.
   :not(.ps-vh) because this rule is (0,3,0) and would otherwise beat .ps-vh's
   `position: absolute` on the visually-hidden h2 in the error page, un-hiding
   it. Fourth specificity trap of its kind recorded in this repo. */
[data-brand="onsite"] .ps-h2:not(.ps-vh) {
  position: relative;
  padding-bottom: 15px;
  margin-bottom: 0.32em;
}
[data-brand="onsite"] .ps-h2:not(.ps-vh)::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 56px;
  height: 3px;
  border-radius: 2px;
  background: var(--accent);
}

/* The eyebrow is NOT given a second bar. One signature per page beats the same
   device stamped twice, and the h2 bars are where it reads. */
[data-brand="onsite"] .ps-eyebrow {
  letter-spacing: 0.14em;
  color: var(--accent-text);      /* #a85f0c on white, 4.88:1 */
}

/* Card: the site's white/1px-#e6e6e6/4px-orange-rail/6px-corner. */
[data-brand="onsite"] .ps-doc {
  border-left-width: 4px;
}

/* Section rhythm by hairline rather than by a bigger gap — the site does the
   same, and on a long statement it also gives the eye a place to resume. */
[data-brand="onsite"] .ps-section + .ps-section {
  border-top: 1px solid #e8e8e8;
  padding-top: 28px;
}

/* WHERE THE LOST BOLD IS PAID BACK.
   The display face already carries the headings, the two summary figures, a
   document balance, the payable-to name and the pay button label for both
   brands. What is left here is the tracking: those rules set a negative
   letter-spacing tuned to Bricolage, and Days One is a wider, rounder face that
   crowds at -0.02em. At 19px and up it reads as emphatic on its own. */
[data-brand="onsite"] .ps-figure-value,
[data-brand="onsite"] .ps-balance-value,
[data-brand="onsite"] .ps-h1,
[data-brand="onsite"] .ps-h2 {
  letter-spacing: 0;
}

/* Tabular figures are what keep a column of amounts aligned, and Days One does
   not carry the feature, so the TABLES stay in Roboto and get their emphasis
   from the rules already drawn around them. Named here so nobody later
   "finishes the job" by putting the display face on a table cell. */
[data-brand="onsite"] .ps-table th {
  letter-spacing: 0.04em;
}

/* The breakdown's TOTAL row is the other thing that went flat: .ps-row-total is
   font-weight 800, which is inert here, so it is separated from the Subtotal and
   Tax rows above it by its top border alone. That is left as it is on purpose —
   the figure a customer acts on is the balance due, which is .ps-balance-value
   and does take the display face. Recorded rather than fixed so the next person
   does not have to re-derive whether it was missed. */

/* The badges are the one place where losing bold could cost meaning rather than
   just presence — "Past due · 34 days" has to out-shout a neutral badge. It
   keeps its own semantic colour pair and border (8.48:1) and gains tracking and
   a slightly heavier border, none of which depend on a weight that does not
   exist. The print block widens that border again. */
[data-brand="onsite"] .ps-badge-late {
  letter-spacing: 0.03em;
  border-width: 2px;
}

/* ═══════════════════════════════ RESPONSIVE ═══════════════════════════════ */

@media screen and (max-width: 700px) {
  :root { --gutter: 16px; }
  body { font-size: 16px; }
  /* The measure comes down with the type so the character count stays put. A
     custom property must be redeclared on a SELECTOR — a bare declaration at the
     top of a media block is not a declaration at all, it is parsed as the start
     of a qualified rule and silently swallows the rule that follows it. CSS has
     no syntax gate; it drops what it cannot read and reports nothing. */
  :root { --measure: 410px; }
  .ps-doc { padding: 16px 14px 18px; }
  .ps-money { gap: 18px; }
}

/* THE LINE-ITEM TABLE ON A PHONE.

   Four columns (item, quantity, rate, amount) cannot be read at 390px as a
   table, and the three obvious answers are all wrong:
     * shrink the type   — puts the numbers below a legible size;
     * scroll it sideways — the reader cannot see the SKU and its amount at the
       same time, which is the ONLY comparison this table exists to support;
     * drop a column     — that is deleting data the customer is being billed
       for, and contract.ts already narrowed these lines to four fields.

   So each row becomes a small card: the header row is removed and every cell
   carries its column name from data-label. Nothing is hidden, nothing is
   truncated, and the SKU sits directly above its own amount.

   Two mechanical notes. `display: block` on <tr>/<td> destroys the implicit
   table roles in Chrome and Safari, which is why render.ts writes role="table",
   role="row" and role="cell" explicitly — they are redundant at desktop width
   and load-bearing here. And this query is scoped to `screen`: a US Letter page
   is ~816px CSS pixels, so an unscoped max-width query would not fire on
   paper — but scoping it is what guarantees that stays true if the numbers
   ever move. */
@media screen and (max-width: 559px) {
  .ps-lines thead { display: none; }

  .ps-lines,
  .ps-lines tbody,
  .ps-lines tr,
  .ps-lines td {
    display: block;
    width: 100%;
    min-width: 0;
  }

  .ps-lines tr {
    border: 1px solid var(--rule);
    border-radius: 6px;
    padding: 6px 12px;
    margin-bottom: 10px;
    background: var(--paper);
  }
  .ps-lines tr:last-child { margin-bottom: 0; }

  .ps-lines td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px 16px;
    padding: 5px 0;
    border-bottom: 1px dotted var(--rule);
    text-align: right;
  }
  .ps-lines tr td:last-child { border-bottom: 0; }

  .ps-lines td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-muted);
    text-align: left;
  }

  /* The item code is the card's title: it reads left, at body weight, and wraps
     rather than forcing the card wide. */
  .ps-lines td.ps-c-sku {
    font-weight: 700;
    border-bottom: 1px solid var(--rule);
  }

  .ps-c-num { white-space: normal; }

  .ps-breakdown, .ps-paid { max-width: none; }
  .ps-balance-value { font-size: 21px; }
  .ps-pay { width: 100%; justify-content: space-between; }
}


/* ════════════════════════════ REDUCED MOTION ════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ═══════════════════ payment schedule & account credits ═══════════════════
   Both tables reuse .ps-lines, which is what collapses a wide table into
   stacked label/value cards below 640px — they are as wide as the line-item
   table and would otherwise be the only two that do not. What is left here is
   the small amount each one does NOT share.
   ───────────────────────────────────────────────────────────────────────── */

/* The schedule sits between the terms prose and the line items, so it needs its
   own air rather than the line table's, which is introduced by an h4. */
.ps-schedule { margin: 14px 0 18px; }

/* The percentage is the row's handle — a customer scanning for "the 40%" finds
   it here — so it reads as a heading rather than as another cell. Tabular
   figures keep 50/40/10 aligned down the column. */
.ps-ms-step {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

/* The deposit references hang under the step's own label, because they answer a
   question about THAT step. Block, so several references wrap as one unit
   instead of trailing off the end of the label's last line. */
.ps-ms-refs {
  display: block;
  margin-top: 2px;
  font-size: 14px;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* A state is a word, and it must not wrap mid-phrase into the amount column. */
.ps-ms-state { white-space: nowrap; }

/* PAID AND REQUESTED NOW CARRY A BORDER AND A WORD, NEVER A TINT ALONE.
   Background colours do not print, and this table is one a customer prints and
   takes to their A/P department — a state distinguished only by fill would come
   out of the printer identical on every row. Same reasoning as .ps-badge-late.
   The colours are semantic, not brand: "paid" and "wanted now" mean the same
   thing on both companies' statements, and On Site's accent is orange, which
   would read as a warning on a row that is settled. */
.ps-ms-paid,
.ps-ms-due {
  display: inline-block;
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 1px 9px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.02em;
}
/* MEASURED, not estimated: #1c6b3a on white is 6.53:1 and #8a4b00 is 6.80:1.
   Both clear 4.5:1 as small bold text, which 13px/700 is — nowhere near the
   18.66px bold that would let the 3:1 allowance apply. The first draft of this
   comment carried 5.42 and 5.74, written before anyone computed them; they were
   wrong in the safe direction, which is exactly how a wrong "measured" figure
   survives review. */
.ps-ms-paid { color: #1c6b3a; }
.ps-ms-due { color: #8a4b00; }

/* The credits section is shell-width with no card, matching the remittance
   section directly below it rather than the document cards above — measured
   against both neighbours, and the block it reads as a sibling of is the one
   that follows it. */
.ps-credits { padding-block: 26px 4px; }
.ps-credits .ps-table-wrap { margin-top: 14px; }
.ps-credits tfoot th,
.ps-credits tfoot td {
  border-top: 2px solid var(--rule-strong);
  border-bottom: 0;
  font-weight: 700;
}

/* The third summary figure. It is the answer to "did you get my money", so it
   is marked as different from the two amounts owed — a left rule in the brand
   accent, which is decoration and carries no meaning the text does not. */
.ps-figure-credit { border-left: 3px solid var(--accent); }

/* ═══════════════════════════ the print button ═══════════════════════════

   The only control on this page that is not a link, and the only reason the
   portal serves any JavaScript. It sits on the masthead's bottom row beside the
   "Account … as of …" line, which wraps to its own line below ~560px.

   `hidden` until /print.js reveals it — see printControl(). The explicit
   [hidden] rule is REQUIRED: the UA sheet's `[hidden] { display: none }` is
   (0,1,0) and loses to the display below, so without it the button would be
   visible to a visitor whose browser never ran the script and would do nothing
   when clicked.
   ───────────────────────────────────────────────────────────────────────── */

.ps-masthead-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 20px;
}
.ps-masthead-foot .ps-ident { margin-top: 10px; }

.ps-print {
  flex: none;
  display: inline-block;
  padding: 8px 16px;
  border: 1px solid var(--rule-strong);
  /* Brand vars, so it is square on On Site and a pill on Fit Supply like every
     other button here — but OUTLINED, never filled. The filled treatment is the
     pay button's, and a print control must not compete with the one control on
     this page that moves money. */
  border-radius: var(--btn-radius);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  text-transform: var(--btn-case);
  letter-spacing: var(--btn-tracking);
  cursor: pointer;
}
.ps-print:hover { border-color: var(--ink); background: var(--band); }
.ps-print:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.ps-print[hidden] { display: none; }

/* ═══════════════════ the internal multi-subsidiary view ═══════════════════

   One <div class="ps-statement" data-brand="…"> per subsidiary a customer
   transacts under, all inside ONE document. The brand attribute is on this
   wrapper rather than on <html> — see renderStaffPage for why concatenating
   whole documents cannot work — and the theme blocks above are all descendant
   selectors and custom properties, so they apply from here exactly as they do
   from the root.

   Present on the staff route only. A customer's own page has one statement and
   no wrapper at all, so none of this renders for them.
   ───────────────────────────────────────────────────────────────────────── */

/* NO RULE HERE, and that is a correction rather than an omission. A 6px black
   border was added first, on the reasoning that the block below is a DIFFERENT
   COMPANY's statement with its own bank details and needs more than a footer
   hairline between them. Looked at in the browser it was invisible: every
   fragment on this page begins with the dark full-bleed staff banner — the
   route always passes staffView — so the divider was drawn against a block of
   the same colour and the banner was doing the whole job already. The white gap
   is the separation; the banner is the label. */
.ps-statement + .ps-statement { margin-top: 44px; }

/* THE WRAPPER HAS TO SET font-family ITSELF, and the reason is worth keeping.

   `body` sets `font-family: var(--font-body)`, and on a customer's own page that
   resolves correctly because `documentShell` puts `data-brand` on `<html>` — the
   brand's token is above `body`, so `body` sees it.

   The INTERNAL view renders `themed: false`: there is no `data-brand` on the root,
   because one document there can hold two companies and a root theme would paint
   both in the first one's identity. The brand tokens live on each `.ps-statement`
   wrapper instead, which is BELOW `body`. So `body` resolved `--font-body` against
   `:root`, got the system-stack fallback, and — because `font-family` inherits as a
   COMPUTED value, not as the unresolved `var()` — every element inside the themed
   wrapper inherited that already-decided system stack and never consulted the
   wrapper's own token.

   The office view therefore rendered its body copy in the system stack while the
   customer's own page rendered Ubuntu or Roboto, which quietly contradicts that
   view's whole premise: what the office prints is supposed to be byte-for-byte what
   the customer would print. Nothing looked broken, which is why it survived.

   Re-declaring it here makes the wrapper's own `--font-body` the one that applies,
   for it and everything under it. Same family as the `--measure` bug already
   recorded in CLAUDE.md: an inherited custom property is resolved where the
   PROPERTY is declared, not where the token is. */
.ps-statement[data-brand] {
  font-family: var(--font-body);
}

/* ═══════════════════════ the internal customer search ═══════════════════════

   /staff — the page a rep lands on to find a customer, and the toolbar that
   links back to it from a statement. Internal only: nothing below renders on a
   customer's own page, and the whole search page is unthemed (see
   renderStaffSearch) because one result list can hold statements from more than
   one subsidiary and no single brand would be honest on it.

   EVERY COLOUR HERE IS A NEUTRAL TOKEN, not the accent. On an unthemed document
   --accent falls back to --ink (#1c1f23), so an accent-coloured control would be
   black here and brand-coloured nowhere — a rule that looks deliberate and is
   actually the fallback. The one place that matters is the result card, which
   reuses .ps-doc and would otherwise draw .ps-doc's 5px accent rail in black.
   ───────────────────────────────────────────────────────────────────────── */

.ps-stafftools {
  background: #24282e;
  padding-block: 10px;
  /* The staff banner directly below is the SAME slate, so without a hairline the two
     read as one block and the link looks like part of the banner's own sentence. */
  border-bottom: 1px solid rgba(255, 255, 255, 0.18);
}
.ps-stafflink {
  /* On the dark bar, and #ffffff on #24282e is 13.9:1. The underline stays —
     colour is never the only thing marking a link on this site (WCAG 1.4.1),
     and here there is no second colour to mark it with. */
  color: #ffffff;
  font-size: 15px;
  font-weight: 700;
}
.ps-stafftools + .ps-statement { margin-top: 0; }

/* The bar carries two things now: the way back to the search, and the one
   page-level print control. `space-between` rather than a gap, so the print
   action sits at the far end and reads as an action ON this page rather than as
   a second link in the same sentence. It wraps to two lines below ~430px. */
.ps-stafftools-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
}

/* THE PRINT BUTTON'S OWN COLOURS ARE FOR A LIGHT MASTHEAD, and this bar is
   #24282e. Left alone it would draw --ink (#1c1f23) text inside a
   --rule-strong border on near-black: a control that is genuinely invisible,
   with a clean build and a green suite. (0,2,0) beats .ps-print's (0,1,0).

   White on #24282e is 13.9:1, the same figure as the link beside it. The border
   composites to about #868a8f on this bar — 4.3:1, over the 3:1 that WCAG 1.4.11
   asks of a non-text control boundary. */
.ps-stafftools .ps-print {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.45);
  background: transparent;
  font-size: 14px;
  padding: 6px 14px;
}
.ps-stafftools .ps-print:hover {
  border-color: #ffffff;
  background: rgba(255, 255, 255, 0.12);
}
.ps-stafftools .ps-print:focus-visible { outline-color: #ffffff; }

/* Sign out sits at the far end of the bar on every staff page, whatever else is on
   it — the auto margin takes the free space before space-between does. */
.ps-stafftools-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-left: auto;
}
.ps-signout { margin: 0 0 0 auto; }
.ps-signout-go {
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  color: #ffffff;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.45);
  border-radius: var(--btn-radius, 4px);
  padding: 6px 14px;
  cursor: pointer;
}
.ps-signout-go:hover { border-color: #ffffff; background: rgba(255, 255, 255, 0.12); }
.ps-signout-go:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }

.ps-searchband {
  background: var(--band);
  border-bottom: 1px solid var(--rule);
  padding-block: 22px 24px;
}

.ps-search-label {
  display: block;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 8px;
}

/* The box and its button are ONE row that wraps as a unit. `flex-wrap` plus a
   min-width on the input is what stops the button being squeezed to its own
   text on a phone: below ~430px the two stack instead, which is the only
   behaviour that keeps a 44px tap target on both. */
.ps-search-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: stretch;
}

.ps-search-input {
  flex: 1 1 260px;
  /* An input does not inherit the page font, and without this the one control
     on the page renders in the UA's own 13px sans while everything around it is
     the brand body face at 17px. */
  font: inherit;
  font-size: 17px;
  min-width: 0;
  padding: 10px 12px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
}
.ps-search-input:focus-visible { outline: 3px solid var(--ink); outline-offset: 1px; }

/* OUTLINED, NOT FILLED, and for the same reason the print button is: the filled
   treatment on this site belongs to the control that moves money. This page has
   no such control, but a rep moves between the two all day and a search button
   that looks like a pay button teaches the wrong thing about both. */
.ps-search-go {
  flex: none;
  padding: 10px 20px;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
.ps-search-go:hover { background: var(--ink); color: var(--paper); }
.ps-search-go:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }

/* A result card IS a .ps-doc — same border, radius, padding and head rule — with
   the rail softened, because .ps-doc's rail is var(--accent) and this document
   is unthemed. */
.ps-result { border-left-color: var(--rule-strong); }
.ps-result .ps-h3 { min-width: 0; overflow-wrap: anywhere; }
/* The company name is the link and the largest thing in the card, so it takes
   --ink rather than --accent-text: on an unthemed page those are the same value,
   and stating it here means a future theme cannot repaint a heading. */
.ps-result-link { color: var(--ink); }
.ps-result .ps-scroll { margin-top: 14px; }

/* ═══════════════════ office-issued magic links ═══════════════════

   Two surfaces, both internal and neither ever on paper:

     .ps-issue    the "send this customer a link" panel, one per statement on
                  /staff/c/:id, between that statement's staff banner and its
                  masthead.
     .ps-linkout  the page /staff/c/:id/link returns — the ONE place a freshly
                  minted token exists.

   NEUTRAL TOKENS ONLY, for the reason the search block above already records:
   /staff/c/:id themes each statement on its own wrapper and the issued-link page
   is unthemed outright, so --accent resolves to --ink (#1c1f23) on one of them
   and to a brand colour on the other. An accent-coloured control here would be
   black on one page and red on the next with nothing in the markup to say why.
   ───────────────────────────────────────────────────────────────────────── */

/* A BAND, NOT A CARD, and it is the seam between the internal chrome above it
   and the customer's own document below. The banner directly above is #24282e
   and the masthead below is white, so a third value would read as a third
   region; --band is the same near-white the summary and search bands use, with
   a rule under it doing the separating. */
.ps-issue {
  background: var(--band);
  border-bottom: 1px solid var(--rule);
  padding-block: 20px 22px;
}

.ps-issue-h {
  font-size: 20px;
  margin-bottom: 4px;
}

/* THE COMPANY NAME IS THE LARGEST THING IN THE PANEL. A rep with nine tabs open
   is about to create a credential, and the failure this panel has to prevent is
   creating it for the wrong customer — so the name is set at heading weight even
   though it is a paragraph, and the entity rides beside it rather than under it,
   where the two are read as one phrase. */
.ps-issue-for {
  font-size: 19px;
  font-weight: 700;
  line-height: 1.3;
  max-width: none;
  margin-bottom: 10px;
}

.ps-issue-sub {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 9px;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  background: var(--paper);
  /* --ink-muted (#54595f) on --paper is 7.3:1, and this is 14px so 4.5:1 applies
     rather than the large-text allowance. */
  color: var(--ink-muted);
  font-size: 14px;
  font-weight: 600;
  vertical-align: 2px;
}

.ps-issue-form { margin-top: 14px; }

/* OUTLINED, NOT FILLED, the third control on this site to take that treatment
   and for the same stated reason: the filled button belongs to the one control
   that moves money, and a rep moves between these pages all day. It is a real
   <button type="submit"> in a real <form> and needs no JavaScript — unlike the
   print controls it is NOT rendered hidden, because there is nothing to reveal. */
.ps-issue-go {
  display: inline-block;
  padding: 10px 20px;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
.ps-issue-go:hover { background: var(--ink); color: var(--paper); }
.ps-issue-go:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }

/* ── the issued-link page ──────────────────────────────────────────────── */

.ps-linkout { padding-block: 28px 8px; }

/* The input and its button are ONE row that wraps as a unit, the same shape as
   the search box — and for the same reason: below ~430px the two stack, which is
   the only arrangement that keeps a 44px tap target on both. */
.ps-copy-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: stretch;
  margin-block: 16px 10px;
}

/* A READONLY INPUT RATHER THAN A PARAGRAPH, because that is what makes the page
   work when the copy button is blocked: a click selects, a keystroke copies. A
   143-character URL in prose has to be dragged across three wrapped lines.

   MONOSPACE, and it is not decoration. What is in here is transcribed or
   verified character by character when something goes wrong, and a proportional
   face renders `l`, `1` and `I` — all of which occur in base64url — close enough
   to be misread. `font-size: 16px` is the iOS zoom floor: anything smaller makes
   Safari zoom the page on focus, which on this page yanks the layout sideways at
   the exact moment a rep is trying to select something. */
.ps-copy-input {
  flex: 1 1 320px;
  min-width: 0;
  font-family: ui-monospace, 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
  font-size: 16px;
  padding: 10px 12px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--band);
  color: var(--ink);
}
.ps-copy-input:focus-visible { outline: 3px solid var(--ink); outline-offset: 1px; }

.ps-copy-go {
  flex: none;
  padding: 10px 20px;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
.ps-copy-go:hover { background: var(--ink); color: var(--paper); }
.ps-copy-go:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }
/* REQUIRED, exactly as .ps-print[hidden] is: the UA sheet's `[hidden]` rule is
   (0,1,0) and loses to the display above, so without this the button is visible
   to a rep whose browser never ran /copy-link.js and does nothing when clicked. */
.ps-copy-go[hidden] { display: none; }

/* The live region. It has a fixed height so the paragraph below it does not jump
   when a message appears — a layout shift under the cursor at the moment somebody
   clicks is how a second click lands on the wrong thing. */
.ps-copy-status {
  min-height: 1.6em;
  margin-bottom: 8px;
  font-weight: 600;
  /* #0f5132 on white is 8.6:1. It is the same green the "Paid in full" badge
     uses, so a confirmation reads as a confirmation across the whole portal. */
  color: var(--clear-ink);
}

/* ─────────────────────────────────────────────────────────────────────────
   PRINT IS LAST IN THIS FILE ON PURPOSE, and moving it breaks it silently.

   A media query adds no specificity, so a plain rule of equal weight beats a
   print rule by SOURCE ORDER alone. This block used to sit ~80 lines above the
   payment-schedule and account-credit rules, and every print override it made
   for those (`.ps-ms-refs`, `.ps-ms-paid`, `.ps-schedule`, `.ps-credits`) was
   dead on arrival — declared, never applied, and invisible in a diff. Found by
   a test asserting no `px` font-size inside the block, which caught the two
   rules that were being read as part of it.

   New rules go ABOVE this comment. A test pins it last.
   ───────────────────────────────────────────────────────────────────────── */

/* ═══════════════════ jump lists and card separation (2026-09-24) ═══════════════════
   Owner: a clickable list of the open documents in the summary boxes, and "make the
   breaks / space between the items more clear". SCREEN ONLY, both of them: the jump
   list is .ps-screenonly in the markup, and the card treatment below sits in a screen
   query so the printed sheet — which the office mails — is unchanged. */

.ps-jump {
  list-style: none;
  margin: 12px 0 0;
  padding: 8px 0 0;
  border-top: 1px solid var(--rule);
  max-height: 15.5em;
  overflow-y: auto;
  font-size: 15px;
}
.ps-jump-link {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 4px 2px;
  color: var(--ink);
  text-decoration: none;
  border-radius: 3px;
}
.ps-jump-link:hover, .ps-jump-link:focus-visible { background: var(--band); }
.ps-jump-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.ps-jump-doc { font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }
.ps-jump-note {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-muted);
}
.ps-jump-amt { margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; }

@media screen {
  .ps-doc {
    margin-top: 34px;
    border-color: var(--rule-strong);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 6px 18px rgba(0, 0, 0, .06);
  }
  .ps-doc:first-of-type { margin-top: 20px; }
  /* A tinted band across the top of every card, so where one document ends and the
     next begins is visible at a glance rather than inferred from a hairline. */
  .ps-doc-head {
    margin: -20px -20px 16px;
    padding: 14px 20px 12px;
    background: var(--band);
    border-bottom: 1px solid var(--rule-strong);
    border-radius: var(--radius) var(--radius) 0 0;
  }
  /* Arriving from a jump link: stop short of the edge, and mark the card briefly. */
  .ps-doc h3[id] { scroll-margin-top: 90px; }
  .ps-doc:has(h3:target) { outline: 3px solid var(--accent); outline-offset: 3px; }
}

/* ═══════════════════ the visit log (office only) ═══════════════════
   Sits under .ps-issue on /staff/c/:id and borrows its band treatment and
   neutral tokens for the same reasons recorded there. */
.ps-visits {
  background: var(--band);
  border-bottom: 1px solid var(--rule);
  padding-block: 16px 18px;
}
.ps-visits-h { font-size: 20px; margin-bottom: 4px; }
.ps-visits-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  max-height: 16em;
  overflow-y: auto;
  font-size: 15px;
}
.ps-visits-list li { padding: 3px 0; border-top: 1px solid var(--rule); }
.ps-visits-list li:first-child { border-top: 0; }
.ps-visits-src { color: var(--ink-muted); margin-left: 8px; }
.ps-visits-more { margin-top: 6px; color: var(--ink-muted); font-size: 14px; }

/* ═════════════════════════════════ PRINT ═════════════════════════════════

   Customers print this and staple it to a check. That is a real workflow and
   the sheet has to stand on its own.  */

@page {
  size: letter;

  /* 0.55in top and bottom, 0.6in sides — a 7.3in text block. It was 0.5in all
     round, which is inside every printer's unprintable area but reads as raw:
     the summary panel and the document cards are full-bleed boxes, so at 0.5in
     their rules run within a half inch of the sheet edge and the page looks
     like a screenshot rather than a document. The sides carry the extra because
     that is the edge a stapled or punched sheet loses. */
  margin: 0.55in 0.6in;
}

@media print {
  :root {
    --column: none;
    --gutter: 0;
    /* The 45-75 character bar applies on paper too. A Letter page gives a 7.3in
       text block, which at body size runs about 90 characters — wider than
       anything on screen. Counted off a rendered page with a Range rather than
       derived from the screen value: print resolves at a different
       px-per-inch, and the arithmetic that gave 70 on paper gave 50.

       RE-MEASURED TWICE, and both times it was something else that moved it.
       First the brand faces: 5in was right for the old system stack, and Ubuntu
       and Roboto are both narrower, so the same 5in column measured 76-81 — over
       the bar on every page, in both brands, while the screen numbers were still
       comfortably inside it. Then the type ladder below dropped the body from
       10.5pt to 9.5pt, and a smaller face fits MORE characters in the same
       column: 4.15in went from 67 to 74, a character off the bar.

       A measure is a property of the column AND the font, so changing either
       invalidates it — measure again, do not scale the old number. Both brands
       are within a character of each other on paper so one value serves; the
       screen values are per brand, in the theme blocks. */
    --measure: 3.8in;
    --rule: #9aa0a6;
    --rule-strong: #6b7076;
  }

  /* AND THE LINE ABOVE DOES NOT REACH THE OFFICE VIEW, which is where this was
     found (2026-09-17, measuring the per-subsidiary print). `--measure` is a
     custom property, so it INHERITS — and each brand sets its own screen value
     on `[data-brand="…"]`. On a customer's own page that attribute is on <html>
     itself, so the two declarations tie at (0,1,0) and this block wins on source
     order. On the office view the attribute is on the `.ps-statement` WRAPPER
     instead (see renderStaffPage), which is a DESCENDANT of the element `:root`
     matched — and an inherited value always loses to one declared on the element
     itself, whatever its specificity.

     So the office printed every statement at the brand's SCREEN measure. Counted
     with a Range on a rendered sheet: the widest paragraph ran 80 characters
     against the same paragraph's 70 on the customer's own sheet — over the 75
     bar, on the copy the office mails out, while the customer's copy was inside
     it. The internal view's whole premise is that what prints is byte-for-byte
     the customer's own statement; this was the one thing about it that was not.

     Declared against the same attribute selector, so it ties at (0,1,0) and wins
     on source order — the same mechanism the eyebrow and card-border restatements
     further down use. It is NOT a bare `.ps-statement` rule: that would fix the
     office view and leave the customer's page resolving through a different
     selector, which is two behaviours again. */
  [data-brand="fitsupply"], [data-brand="onsite"] { --measure: 3.8in; }

  /* ── THE TYPE LADDER ──────────────────────────────────────────────────
     Owner, 2026-08-26: the print version "needs some refining, esp around page
     margins, font size".

     It was the SCREEN ladder with the body knocked down one step, and screen
     sizes on paper read enormous: a 12pt table cell is larger than the 10.5pt
     prose above it, and a one-line invoice card ran 1.3 pages and stranded 70%
     of the second sheet. Measured across the fixtures, the whole ladder is now
     set in pt against a 9.5pt body, and every step that was inherited from a
     screen px value is restated here rather than left to resolve at 96dpi.

     THE BAND FOR SMALL TEXT IS 8pt AND NOTHING GOES BELOW IT. That is the floor
     for the uppercase tracked labels — 7pt is where tracked small caps stop
     being legible in a photocopy, and this sheet gets photocopied by A/P
     departments. Body prose does not go below 9.5pt for the same reason.
     ───────────────────────────────────────────────────────────────────── */

  body {
    font-size: 9.5pt;
    line-height: 1.42;
    color: #000;
    background: #fff;
    overflow-x: visible;
  }

  .ps-shell { max-width: none; padding-inline: 0; }

  /* .ps-terms carries its own screen max-width in px, which on paper resolves
     against a different px-per-inch and lands wider than the running prose it
     sits beside. On a sheet it is just another paragraph. */
  .ps-terms { max-width: var(--measure); }

  /* THE TRAP: .ps-scroll clips on paper. A table wider than the text block would
     lose its right-hand columns — the amount column — on the printed sheet, and
     the reader would have no way to know. Let it overflow visibly instead; the
     stacked-card query above is `screen`-scoped so it cannot rescue this. */
  .ps-scroll { overflow: visible !important; }

  /* ONE DOCUMENT PER SHEET (owner, 2026-08-26). Every document after the first
     starts a new page, so an invoice and the order beside it never share paper
     and a sheet can be filed, scanned or handed to one approver on its own.

     `break-before`, NOT `break-inside: avoid`. That was tried first and is a
     different rule with a worse failure: it keeps a card whole, so a document
     taller than the remaining space jumps forward and leaves whatever was left
     of the previous sheet blank — measured at roughly half a page on a
     two-invoice statement. Here the break is deliberate and lands in a known
     place, and a document longer than a sheet is still allowed to run onto the
     next one rather than being clipped or thrown a page forward.

     THE ADJACENT-SIBLING FORM IS LOAD-BEARING, twice over.

     `.ps-doc` alone would break BEFORE the first card too, giving the summary
     a cover sheet of its own. That was built and measured against this one
     rather than argued about, printing four fixtures to Letter: it cost a
     sheet on two of them (2 documents 4 -> 5 pages, 3 documents 6 -> 7) and
     saved one on none. A cover page is not free and buys nothing here,
     because the masthead already sits above the first document.

     The second rule catches the seam the first cannot see: invoices and sales
     orders are rendered as two separate <section>s, so the first order is
     nobody's sibling. It matches only when both sections exist, because a
     text node between them is not an element. */
  .ps-doc + .ps-doc { break-before: page; }
  .ps-section-inv + .ps-section-so { break-before: page; }

  /* A second subsidiary's statement starts its own sheet. It is a different
     company with different bank details; it does not share paper. */
  .ps-statement + .ps-statement { break-before: page; margin-top: 0; }

  /* ── PRINTING ONE SUBSIDIARY OUT OF SEVERAL ───────────────────────────
     The office sends one company one sheet. /print.js puts .ps-printing-one on
     <html> and .ps-print-target on the wanted statement immediately before
     window.print(), and takes both off afterwards; nothing here has a screen
     rule, so a state left behind by a dialog that never fired `afterprint` is
     invisible until the next print — which is why that script clears the scope
     on the way IN as well as on the way out.

     `display: none` rather than `visibility: hidden`: a hidden box still
     occupies its page, so three statements would print as one statement and two
     blank sheets.

     AND THE BREAK HAS TO BE UNDONE. `.ps-statement + .ps-statement` above puts a
     page break before every statement after the first, and it still matches when
     the earlier sibling is display:none — so printing the SECOND subsidiary
     alone opened with a blank sheet. (0,3,0) against that rule's (0,2,0). */
  .ps-printing-one .ps-statement:not(.ps-print-target) { display: none !important; }
  .ps-printing-one .ps-statement + .ps-statement { break-before: auto; }

  /* The company name repeated on every card, so a sheet separated from the rest
     still says whose account it is. Hidden on screen — see .ps-printonly. */
  .ps-doc-for {
    margin: 0 0 2pt;
    font-size: 9pt;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: #000;
  }

  /* What must not be split across sheets — a balance box, a breakdown row, the
     remittance details. The document card itself is not on this list, for the
     reason given above. */
  .ps-remit, .ps-figure, .ps-balance, .ps-doc-head, .ps-money-col { break-inside: avoid; }

  /* The footer joined this list once documents started forcing page breaks. It
     is seven lines, and it was landing with its last sentence alone on a sheet
     — which reads as a printer fault rather than as a statement. Measured both
     ways across the four print fixtures: the page count is identical, so
     keeping it whole costs white space and never a sheet. */
  .ps-footer { break-inside: avoid; }
  .ps-table tr { break-inside: avoid; }
  .ps-h2, .ps-h3, .ps-h4 { break-after: avoid; }

  /* A SPLIT TABLE REPEATS ITS <tfoot> ON EVERY PAGE, and that is per spec —
     browsers repeat header and footer row groups across a table's fragments.
     For a heading row that is exactly right. For the credits table it printed
     "Total on your account $30,876.40" underneath a single visible row of
     $12,500.00, and then again above the remaining rows on the next sheet: a
     total that does not add up to what a reader can see, twice, in the one
     block on this page whose whole job is accounting for money we are holding.
     The continuation sheet also lost the block's heading, so the second copy
     arrived with no idea what it was totalling.

     There is no way to say "do not repeat this footer", so the table is kept
     whole instead. It is a handful of rows in practice. A credits list taller
     than a sheet defeats `break-inside: avoid` and the repeat comes back — if
     that ever shows up, the fix is to drop the <tfoot> and put the total in a
     <p> after the table, where nothing can repeat it. */
  .ps-credits .ps-table-wrap, .ps-credits .ps-table { break-inside: avoid; }

  /* ── THE PAY LINKS PRINT, AS ADDRESSES ────────────────────────────────
     Owner, 2026-09-17: the office needs to "print those statements with the
     paylinks". Until now this block said `.ps-pay-group { display: none }` on
     the reasoning that a printed button does nothing — true of the BUTTON and
     wrong about the block, which also holds the unknown-amount note and the
     card convenience fee note. Hiding the group hid the fee disclosure with it,
     so a printed sheet showed a link charging $86.36 and never said the card
     rail adds 3% on top. That is the "billing dispute, not a layout bug" this
     codebase keeps naming, arriving on paper.

     So the button stops being a button and becomes a labelled address: the
     label and the amount on one line, the URL beneath it, printed from the
     href the same way the footer's links are. A real pay URL on this account is
     143 characters, so it WILL wrap — `overflow-wrap: anywhere` is what makes
     it wrap instead of running off the sheet, because a URL has no spaces to
     break at and the default `normal` would push a 143-character unbreakable
     token straight through the page box.

     8.5pt, not 8pt: the 8pt floor in this block is for tracked uppercase
     labels, and this is a lowercase string somebody has to TRANSCRIBE from a
     photocopy. It is the one piece of text on the sheet where a misread
     character is a failed payment.
     ───────────────────────────────────────────────────────────────────── */
  /* ── AND SINCE THE QR CODES, IT IS A TWO-COLUMN GRID ──────────────────
     143 characters printed under a button solved the wrong half: the address
     is on the sheet and nobody is going to type it. Each link now also prints
     a QR code (src/qr.ts, server-rendered SVG, no script and no network), and
     the group carries them in a column of their own to the left of the labels
     and addresses they belong to.

     GRID RATHER THAN A WRAPPER PER LINK, because a wrapper would have changed
     which element is the flex item on SCREEN — and the screen layout of this
     block is not what anybody asked to change. render.ts emits one
     `.ps-qr-slot` before each `.ps-pay`, and the slot is `.ps-printonly`, so
     on screen it is `display: none`, generates no box, and the anchors are
     still the direct flex children they were yesterday. The screen rules
     above are untouched and this block does the whole job.

     THE SLOT IS ALWAYS EMITTED, EMPTY OR NOT. Grid auto-placement pairs items
     by position: drop one code and every button after it moves a column left,
     against a label that is not its own. An always-present slot makes the
     pairing structural. The first column is `max-content`, so a group whose
     codes are all absent collapses it to nothing and prints exactly what it
     printed before the codes existed.

     The notes span both columns. `.ps-pay-note` is where the card convenience
     fee disclosure lives, and this file already records at length what it cost
     to have that sentence hidden on paper once — full width, under the codes
     and the addresses, is the one place a grid cannot separate it from them.
     ───────────────────────────────────────────────────────────────────── */
  .ps-pay-group {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    column-gap: 10pt;
    row-gap: 6pt;
    align-items: start;
    margin-top: 8pt;
    /* A label on one sheet and its URL on the next is an address nobody can
       use. The group is a few lines; keeping it whole costs white, never a
       page. */
    break-inside: avoid;
  }

  /* `.ps-qr-slot.ps-printonly`, NOT `.ps-qr-slot`, and the reason is the sixth
     specificity trap recorded on this stylesheet. `span.ps-printonly { display:
     inline }` sits further down this block at (0,1,1) — it exists so a print-only
     clause spliced into a sentence does not break the line it lives in — and it
     outranks a bare `.ps-qr-slot` at (0,1,0) wherever either one is written. The
     slot is a <span>, so it would resolve to `inline`.

     It happens to render correctly anyway, because a grid item is blockified
     whatever its own `display` says, and that is exactly the kind of accident
     this file keeps having to unpick: the rule is wrong and the layout is right,
     until someone changes the group's display and both stop being true at once.
     (0,2,0) states the intent instead of relying on the blockification. */
  .ps-qr-slot.ps-printonly { display: block; }

  /* 1.2in, and the number is derived in src/qr.ts rather than chosen here —
     PRINT_QR_WIDTH_IN carries the arithmetic. A pay link is a version 8 symbol,
     49 modules plus 4 of quiet zone a side, so 57 across: 30.48mm / 57 is
     0.535mm a module against the ~0.4mm a phone wants off paper. Change this
     and the encoder's constant together, or the printed code silently drops
     below the size that scans.

     The quiet zone is INSIDE the SVG (a white rect the full span of the
     viewBox), so there is no padding here and none is wanted: a QR without its
     light border is the most common way a hand-rolled one fails to scan while
     looking perfect. */
  .ps-qr {
    display: block;
    width: 1.2in;
    height: 1.2in;
  }

  .ps-pay-note { grid-column: 1 / -1; }

  .ps-pay {
    display: block;
    /* Was `0 0 5pt`, which separated one address from the next while the group
       was a plain block. The group is a grid now and `row-gap` does that job;
       leaving the margin as well stacked 11pt between rows while the QR column
       beside them sat at 6pt, so the two columns drifted out of step down the
       block. Set to 0 on both axes deliberately — there was never a horizontal
       margin here to lose. */
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    color: #000;
    font-family: inherit;
    font-size: 9.5pt;
    font-weight: 700;
    /* On Site's buttons are uppercase and tracked on screen. A shouted line
       above a URL is not a button, it is just loud. */
    text-transform: none;
    letter-spacing: 0;
  }

  /* THE GAP WAS DOING REAL WORK AND `display: block` THREW IT AWAY. On screen
     `.ps-pay` is an inline-flex with `gap: 4px 12px`, which is what separates the
     label from the amount; as a block it has no gap and the two spans butt
     together — the first printed sheet read "Pay this invoice$89,968.82". Found
     by rasterizing the PDF and looking at it, not by any rule in this file. */
  .ps-pay-amount { margin-left: 8pt; }

  /* A THEME BLOCK OUTRANKS THE RULE ABOVE — the same (0,2,0)-beats-(0,1,0) trap
     this file has recorded four times. Fit Supply paints the button with
     `background-image: linear-gradient(...)`, which `background: none` at
     (0,1,0) cannot reach, and a gradient does not print anyway: what it leaves
     is a white-on-white label. Restated at matching weight. */
  [data-brand="fitsupply"] .ps-pay,
  [data-brand="onsite"] .ps-pay {
    background: none;
    background-image: none;
    border: 0;
    color: #000;
    box-shadow: none;
  }

  .ps-pay::after {
    content: attr(href);
    display: block;
    /* The measure bar is about running prose and a URL is not prose, but the
       block still has to sit inside the text column beside it. Measured on a
       rendered sheet rather than derived — see the note on --measure above. */
    max-width: var(--measure);
    margin-top: 1pt;
    font-size: 8.5pt;
    font-weight: 400;
    letter-spacing: 0;
    overflow-wrap: anywhere;
  }

  /* The unknown-amount note and the convenience fee note. `margin-top: -2px` is
     a screen value tucking them under a flex row that no longer exists here. */
  .ps-pay-note { margin-top: 0; margin-bottom: 0; }
  /* The 3pt that used to separate the two notes is `row-gap` on the group now.
     Restating it here would put 9pt between the notes and 6pt everywhere else
     in the same block. */
  .ps-pay-note + .ps-pay-note { margin-top: 0; }

  .ps-screenonly { display: none !important; }
  .ps-printonly { display: block; }
  /* (0,1,1) — beats the block rule above, for a print-only clause spliced into a
     sentence. A block-level span would break the line it lives in. */
  span.ps-printonly { display: inline; }

  /* Backgrounds do not print by default, so everything that carried meaning in a
     fill gets a border instead. */
  .ps-summary, .ps-remit, .ps-empty, .ps-terms, .ps-doc-so, .ps-badge,
  .ps-balance-quiet, .ps-figure {
    background: transparent !important;
  }
  .ps-summary, .ps-remit, .ps-empty { border: 1px solid var(--rule-strong); }
  .ps-summary, .ps-remit, .ps-empty { padding: 9pt; margin-block: 8pt; }
  .ps-badge { border: 1pt solid #000; color: #000; }
  .ps-badge-late { border-width: 2pt; font-weight: 800; }
  .ps-doc { border: 1pt solid var(--rule-strong); border-left-width: 3pt; }

  /* THE STAFF BANNER IS HIDDEN ON PAPER, reversing what this rule said until
     2026-08-26. It read "the staff banner stays — if somebody prints the
     internal view, the paper had better say so", which is the right instinct
     about a screen and the wrong one about a sheet.

     Owner: hide "the internal staff view box if the staff print the online
     statement". The reason it is safe is that `staffView` adds the banner AND
     NOTHING ELSE — renderStatement's own comment says so, and there is no
     staff-only figure anywhere on the page — so the printed sheet is already
     identical to what the customer would print. The banner was not marking
     privileged content; it was announcing a route. On paper it read as a
     black-boxed disclaimer stamped above the logo on a document being sent TO
     the customer, which is worse than saying nothing.

     If a staff-only FIGURE is ever added to this page, this rule becomes wrong
     and the banner has to come back. */
  .ps-staff { display: none !important; }

  .ps-masthead { border-bottom: 1.5pt solid #000; padding-block: 0 6pt; }

  /* THE LOGO PRINTS. A statement gets stapled to a check and mailed back, and
     the sheet has to say who it is from without the browser's own header. It is
     an <img>, so it survives `print-color-adjust` (that rule governs BACKGROUND
     painting, not images). Sized in pt so it does not depend on the screen
     value of --logo-h. */
  .ps-logo { margin-bottom: 6pt; }
  .ps-logo img { height: 19pt; }

  /* A THEME BLOCK OUTRANKS THIS ONE. [data-brand="…"] .ps-doc is (0,2,0) and
     the print rule above is (0,1,0) — a media query adds no specificity — so
     the card borders have to be restated at matching weight or the screen
     treatment (and a shadow that prints as nothing) survives onto paper. */
  [data-brand="fitsupply"] .ps-doc {
    border: 1pt solid var(--rule-strong);
    border-top: 3pt solid #000;
    box-shadow: none;
  }
  [data-brand="onsite"] .ps-doc { border-left-width: 3pt; }

  /* The orange bar under an On Site h2 is a background, and backgrounds do not
     print. Reclaim the space it was reserving rather than leaving a gap under
     every heading on the sheet. */
  [data-brand="onsite"] .ps-h2:not(.ps-vh) { padding-bottom: 0; margin-bottom: 0.5em; }
  [data-brand="onsite"] .ps-h2:not(.ps-vh)::after { display: none; }

  /* Same reason, opposite direction: the past-due badge's weight is inert on
     On Site (font-synthesis is off and Roboto has no bold), so on paper its
     border is doing the whole job. */
  [data-brand="onsite"] .ps-badge-late { border-width: 2pt; }
  .ps-footer { border-top: 1pt solid #000; padding-block: 7pt 0; margin-top: 10pt; }

  /* Headings. The h1 is the company name, and at 20pt it was competing with the
     logo directly above it for the top third of the sheet. */
  .ps-h1 { font-size: 15pt; line-height: 1.15; }
  .ps-h2 { font-size: 11.5pt; }
  .ps-h3 { font-size: 10.5pt; }
  .ps-h4 { font-size: 8pt; }

  /* Everything the screen sets in px and print inherited at 96dpi. Each of these
     was LARGER than the body prose beside it: a 12pt table cell under a 10.5pt
     paragraph is the tell that a screen ladder is being printed. */
  .ps-table { font-size: 9.5pt; }
  .ps-meta dd, .ps-remit-address { font-size: 9.5pt; }
  .ps-summary-note, .ps-remit-note, .ps-ms-refs { font-size: 8.5pt; }
  .ps-figure-value { font-size: 15pt; }
  .ps-balance-value { font-size: 13pt; }
  .ps-balance-quiet .ps-balance-value { font-size: 11pt; }
  .ps-remit-payable { font-size: 11pt; }

  /* The 8pt floor: uppercase tracked labels, and the badges. */
  .ps-lines thead th, .ps-meta dt, .ps-balance-label, .ps-remit-label,
  .ps-figure-label, .ps-terms-label, .ps-doc-for {
    font-size: 8pt;
  }

  /* THE EYEBROW NEEDS BOTH BRAND SELECTORS, not `.ps-eyebrow`. Each theme sets
     its own size at (0,2,0) and a print rule at (0,1,0) loses to it — a media
     query adds no specificity. Setting the bare class here looked correct, was
     silently ignored, and the eyebrow printed at 10.5pt beside a 15pt h1. This
     is the fourth time that trap has been recorded on this codebase; the tell
     is always a computed value that will not move. */
  [data-brand="fitsupply"] .ps-eyebrow,
  [data-brand="onsite"] .ps-eyebrow { font-size: 8.5pt; }

  /* The identity line ("Account C1001 · as of …") is deliberately full body
     size on screen — it is what a customer checks first. Same intent on paper,
     which now means 9.5pt rather than the screen's 16px. */
  .ps-ident { font-size: 9.5pt; }

  /* The quoted payment terms. It is the one block whose text comes verbatim
     from NetSuite, and at the screen's 16px it printed LARGER than the heading
     of the card it sits inside. Its width is already pinned to --measure above,
     so only the size and the padding are left. */
  .ps-terms { font-size: 9.5pt; padding: 6pt 8pt; margin-bottom: 8pt; }

  /* The breakdown's Total row. 18px on screen makes it the loudest number in
     the card, which is right there and wrong here — on paper the balance box
     beside it is the figure a reader is looking for. */
  .ps-row-total th, .ps-row-total td { font-size: 11pt; padding-top: 5pt; }
  .ps-badge, .ps-ms-paid, .ps-ms-due { font-size: 8pt; padding: 0.5pt 5pt; }
  .ps-figure-sub { font-size: 8.5pt; }

  /* ── DENSITY ──────────────────────────────────────────────────────────
     The paddings are screen values too, and they compound: a card's own
     padding, plus the head's rule, plus the meta grid's margin, plus the money
     grid's, is most of an inch of white before the first number. Roughly halved
     across the board. Measured, not eyeballed: the fixture whose one-line
     invoice ran onto a second sheet now closes on the first.
     ───────────────────────────────────────────────────────────────────── */
  .ps-section { padding-block: 14pt 4pt; }
  .ps-section + .ps-section { padding-top: 12pt; }
  /* (0,3,0) — the On Site theme sets this at that weight and would otherwise
     outrank the line above. A media query adds no specificity. */
  [data-brand="onsite"] .ps-section + .ps-section { padding-top: 12pt; }

  .ps-doc { margin-top: 10pt; padding: 9pt 10pt 10pt; }
  .ps-doc-head { padding-bottom: 5pt; gap: 3pt 8pt; }
  .ps-meta { gap: 4pt 14pt; margin-block: 7pt 8pt; }
  .ps-table th, .ps-table td { padding: 3.5pt 5pt; }
  .ps-money { gap: 8pt 18pt; margin-top: 9pt; padding-top: 0; }
  .ps-payments, .ps-nopay { margin-bottom: 7pt; }
  .ps-balance { padding: 5pt 7pt; border-width: 1.5pt; }
  .ps-balance + .ps-balance { margin-top: 6pt; }
  .ps-schedule { margin: 7pt 0 9pt; }
  .ps-credits { padding-block: 12pt 2pt; }
  .ps-figures { gap: 8pt; }
  .ps-figure { padding: 7pt 9pt; }
  .ps-terms { margin-block: 7pt; }

  a { color: #000; }
  /* Print the destination of a real link. Scoped to the footer because that is
     the only other place one appears: `mailto:` needs no expansion (the visible
     text is already the address) and the pay links have their own treatment
     above, on their own line rather than in parentheses — a 143-character URL
     spliced into a sentence is unreadable, and unlike this one it is meant to be
     typed rather than merely identified. */
  .ps-footer a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}

