/* ═══════════════════════════════════════════════════════════════════════════
   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. */
.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; }
.ps-ident-item + .ps-ident-item::before {
  content: "\00b7";
  margin-right: 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; }
}

/* ═════════════════════════════════ 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;
  margin: 0.5in;
}

@media print {
  :root {
    --column: none;
    --gutter: 0;
    /* The 45-75 character bar applies on paper too. A 0.5in-margin Letter page
       gives a 7.5in text block, which at 10.5pt runs about 94 characters — wider
       than anything on screen. Counted off a rendered Letter PDF 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 WHEN THE BRAND FACES LANDED, and this is the value that
       actually moved. 5in was right for the old system stack; Ubuntu and Roboto
       are both narrower, and the same 5in column measured 76-81 characters —
       OVER the bar on every page, in both brands, while the screen numbers were
       still comfortably inside it. A measure is a property of the column AND the
       font, so changing the font invalidates it. 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: 4.15in;
    --rule: #9aa0a6;
    --rule-strong: #6b7076;
  }

  body {
    font-size: 10.5pt;
    line-height: 1.45;
    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; }

  /* WHAT TO KEEP WHOLE, AND WHAT NOT TO. `break-inside: avoid` on the whole
     .ps-doc card was tried first and is wrong: a document taller than the
     remaining space jumps to the next sheet and leaves most of a page blank —
     measured at roughly half of page 1 empty on a two-invoice statement, which
     is both a hole and a wasted sheet on a document people print in volume.
     So a document is allowed to break, and the pieces that must not be split
     across sheets — a balance box, a breakdown row, the remittance details —
     are the ones that get the rule. */
  .ps-remit, .ps-figure, .ps-balance, .ps-doc-head, .ps-money-col { break-inside: avoid; }
  .ps-table tr { break-inside: avoid; }
  .ps-h2, .ps-h3, .ps-h4 { break-after: avoid; }

  /* A printed button does nothing. The remittance block is what replaces it, and
     the print-only note in that block says where the online links live. */
  .ps-pay-group { display: none !important; }
  .ps-printonly { display: block; }

  /* 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: 12pt; margin-block: 10pt; }
  .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 stays. If somebody prints the internal view, the paper had
     better say so. */
  .ps-staff { background: transparent !important; color: #000; border: 2pt solid #000; padding: 6pt; }
  .ps-staff-body { color: #000; }

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

  /* 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: 10pt; }
  .ps-logo img { height: 26pt; }

  /* 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: 8pt 0; margin-top: 12pt; }

  .ps-h1 { font-size: 20pt; }
  .ps-h2 { font-size: 14pt; }
  .ps-h3 { font-size: 12pt; }

  a { color: #000; }
  /* Print the destination of a real link — except the pay links, which are
     hidden, and mailto:, where the visible text is already the address. */
  .ps-footer a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}

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