/* portal/portal.css
 *
 * SELF-CONTAINED ON PURPOSE. The portal's token values are duplicated from
 * theme.css rather than imported, because in production `portal/` is its OWN
 * DOCROOT for pay.portalexplore.com — a `../theme.css` reference would resolve
 * during local testing and 404 in production, which is the worst kind of bug to
 * find late. Values kept identical to theme.css so the two look like one family.
 *
 * Mobile-first is not decoration here: an owner-operator checks pay on a phone.
 * The rule the layout enforces is that the page body never scrolls sideways —
 * wide content scrolls inside its own container.
 */

:root {
  /* ── THE SURFACE LADDER ────────────────────────────────────────────────
     --panel used to measure 1.030:1 against --bg. That is not a card sitting
     on a page, it is a card the same colour as the page, and every screen in
     the portal is built out of cards - so the whole app read as one flat
     sheet and the only thing suggesting depth was a border at 1.30:1 that
     you could not see either. Both halves of that are fixed: the border ladder
     below, and these.

     --bg is deliberately UNTOUCHED. It is the brand navy, it is what
     --glass is derived from, and it is the theme-color the phone paints its
     status bar with (theme-boot.js). The separation is bought by stepping the
     surfaces UP instead, which also keeps every light-on-dark text ratio the
     same or better against --bg.

       panel   / bg     1.030 -> 1.121
       panel-2 / bg     1.135 -> 1.295
       panel-2 / panel  1.102 -> 1.154
       input   / panel  1.185 -> 1.236

     The ceiling is --red on --input-bg, the lightest surface: at these values
     it holds 4.65:1, and one more step up on --input-bg would push a negative
     settlement figure under AA. That is the constraint, not taste. */
  --bg:          #0F172A;
  --panel:       #17213C;
  --panel-2:     #1F2C4C;
  --input-bg:    #25314F;
  /* ── THE BORDER LADDER ─────────────────────────────────────────────────
     Three weights, because a hairline between two table rows and the edge of
     a text field are not the same job and were sharing one value.

     The old --border was rgba(148,163,184,.16), which composites to 1.30:1
     against --bg - below the ~1.2:1 where an edge stops reading at all on a
     phone in daylight, and well under the 3:1 WCAG SC 1.4.11 asks for the
     boundary that IDENTIFIES a control. Fifty-one rules shared that one
     value: a card's decorative edge and the only thing telling you where the
     search box is. So dividers stay quiet, surfaces get an edge you can see,
     and anything you type in or click gets --border-control, which clears 3:1
     against the lightest surface it ever sits on (--input-bg, the worst case).

     Measured worst-case across --bg / --panel / --panel-2 / --input-bg:
       --border-2        1.15 -> 1.36   internal rules, row separators
       --border          1.30 -> 1.86   card edges, table frames, header
       --border-control  (new)  3.06    inputs, buttons, chips, toggles
     Disabled controls deliberately KEEP --border: 1.4.11 exempts them, and
     the softer edge is how you tell at a glance that one is dead. */
  --border:      rgba(148, 163, 184, 0.36);
  --border-2:    rgba(148, 163, 184, 0.18);
  --border-control: rgba(148, 163, 184, 0.66);
  --accent:      #F59E0B;
  --accent-soft: rgba(245, 158, 11, 0.16);
  /* One step lighter, for the hover of a filled amber button. Written as a
     token because the primary button is not the only thing that will ever want
     it, and a raw hex here is the first crack in a palette. */
  --accent-hi:   #FBBF24;
  --accent-ink:  #0A0F1A;
  --good:        #10B981;
  --warn:        #F59E0B;
  --info:        #60A5FA;
  /* Red-400, not red-500. #EF4444 measured 4.74:1 on --bg and passed, which is
     how it survived review - but the places --red is actually USED are a
     deduction total inside a card and an error under a form field, so the
     surfaces that matter are --panel-2 (4.18:1) and --input-bg (3.89:1), and
     it failed both. A negative number on a settlement is the last thing on
     this page that should be hard to read. #F87171 clears 5.2:1 everywhere. */
  --red:         #F87171;
  --text:        #F1F5F9;
  --muted:       #94A3B8;
  --radius:      6px;
  --radius-md:   10px;
  --font-head:   'Barlow Condensed', 'Arial Narrow', Impact, sans-serif;
  --font-body:   'Barlow', 'Helvetica Neue', Arial, sans-serif;
  --font-mono:   ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;

  /* ── TYPE SCALE ────────────────────────────────────────────────────────
     Until now every size was written inline at the point of use - .84rem here,
     .85rem there, .8rem somewhere else - so three things meant to look alike
     drifted apart by a hundredth of a rem and nobody could tell whether that
     was deliberate. These are the only sizes the portal uses. A value that is
     not on this scale is a bug, not a nuance. */
  --fs-xs:       0.72rem;   /* table meta, timestamps, column labels */
  --fs-sm:       0.8rem;    /* secondary text, help notes */
  --fs-base:     0.875rem;  /* body copy, table cells */
  --fs-md:       1rem;      /* emphasised body, card values */
  --fs-lg:       1.25rem;   /* brand, section headings */
  --fs-xl:       1.6rem;    /* view titles */
  --fs-2xl:      2.1rem;    /* the settlement total - the one number that matters */
  --lh-tight:    1.2;
  --lh-base:     1.55;
  --tracking-caps: .12em;   /* the condensed all-caps treatment used for labels */

  /* ── SPACING SCALE (4px base) ──────────────────────────────────────────
     Same story: .35rem / .4rem / .45rem / .85rem were all in use and all meant
     "a small gap". Rhythm is what makes a dense money table readable, and you
     cannot have rhythm with sixteen different gaps. */
  --sp-1:        0.25rem;
  --sp-2:        0.5rem;
  --sp-3:        0.75rem;
  --sp-4:        1rem;
  --sp-5:        1.5rem;
  --sp-6:        2rem;
  --sp-8:        3rem;

  /* ── ELEVATION ─────────────────────────────────────────────────────────
     One shadow, one z-index for anything that floats over the page. The menu
     must clear the sticky header and the table's own stacking context.

     The card/raise pair below are LAYERED on purpose: one big soft blur reads
     as fog, while a tight contact shadow under a wider ambient one reads as an
     object resting on a surface. Both are pure black at low alpha so they sit
     over --panel and --panel-2 alike without tinting either. */
  --shadow-pop:   0 10px 30px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .3);
  --shadow-card:  0 1px 2px rgba(0, 0, 0, .28), 0 1px 1px rgba(0, 0, 0, .18);
  --shadow-raise: 0 4px 12px rgba(0, 0, 0, .34), 0 1px 3px rgba(0, 0, 0, .24);
  --z-pop:        60;
  --z-header:     50;

  /* ── MOTION ────────────────────────────────────────────────────────────
     Two durations and one curve, so every control accelerates the same way.
     ease-out rather than ease-in-out because these are RESPONSES to a hover or
     a click: the movement should look like it started the instant the input
     did, then settle. --dur-press is the shorter of the two because a release
     should be quieter than the entrance that earned the attention.

     WHY THESE ARE TOKENS AND NOT LITERALS. Before this, thirteen rules changed
     colour on :hover and only a handful named a transition, so half the portal
     snapped and half eased - which reads as the slow half being broken. One
     curve, applied everywhere, is most of the difference between "a web page"
     and "an app".

     Every transition in this file names its PROPERTIES. `transition: all` on
     a page holding a settlement table means the browser watches layout
     properties it will never animate, and it puts an accidental animation of
     something like `height` one character away. */
  --dur-press:   90ms;
  --dur-base:    150ms;
  --ease:        cubic-bezier(.2, .6, .35, 1);

  /* ── FOCUS ─────────────────────────────────────────────────────────────
     The ring was written out at some twenty call sites as `2px solid
     var(--accent)`, with the offset drifting between 1px, 2px and -2px for no
     reason anyone recorded. One token: a keyboard user sees the SAME ring on
     every control, and changing it is one edit rather than twenty. */
  --focus-ring:  2px solid var(--accent);

  /* ── THEME-DEPENDENT INKS ────────────────────────────────────────
     Colours that used to be written inline, because only one theme existed.
     Now that a light palette overrides tokens, anything that must flip with
     the theme has to BE a token - an inline rgba() is invisible to the
     override and ships a dark-theme artifact onto a light page. */
  --glass:         rgba(15, 23, 42, .86);      /* sticky header over content */
  --row-hover:     rgba(148, 163, 184, .05);   /* table row under the pointer */
  --row-driver:    rgba(148, 163, 184, .10);   /* driver group heading, detail table */
  /* Hover has to beat the control's RESTING edge, not the old .16 hairline.
     The old .34 sat at the same weight as the resting card edge, so hovering
     a field changed nothing you could see. .86 reads 4.13:1 on --input-bg
     against the control's 3.06:1 there - a 1.35x step. */
  --border-hover:  rgba(148, 163, 184, .86);   /* input border, pointer over it */
  /* The ghost button's hairline is that button's ONLY boundary - there is no
     fill behind it - so 1.4.11 applies at full force. .45 measured 2.45:1 on
     --input-bg; .63 clears 3.23:1 and still reads as a hairline, not a fill. */
  --accent-border: rgba(245, 158, 11, .63);    /* ghost button hairline */
  --accent-soft-2: rgba(245, 158, 11, .22);    /* ghost button hover wash */
  /* STATUS WASHES, and the reason they are tokens now.
     .err, .req-chip and .earn-tag each wrote their own rgba() of a status
     colour inline - rgba(239,68,68,.08), rgba(16,185,129,.35) - which is the
     exact pattern the note at the top of this block warns about: an inline
     rgba is invisible to the light override, so a dark-theme wash shipped
     onto a light page. On white, rgba(239,68,68,.28) composites to 1.28:1 -
     an error box with no edge at all. They were also pinned to the OLD red;
     --red is #F87171 now, so the border and the text it framed had drifted
     apart into two different reds. One pair per tone, both themes. */
  --red-soft:      rgba(248, 113, 113, .10);
  --red-border:    rgba(248, 113, 113, .45);   /* 2.24:1 - a notice edge, not a control */
  --good-soft:     rgba(16, 185, 129, .14);
  --good-border:   rgba(16, 185, 129, .35);
  /* The thumb was a hardcoded slate that composites to 1.26:1 on a white
     page: on light, the scrollbar simply vanished - on the one page whose
     whole job is long lists and a wide money table. */
  --scroll-thumb:    rgba(148, 163, 184, .32);
  --scroll-thumb-hi: rgba(148, 163, 184, .45);
  --shadow-bar:    0 -8px 20px rgba(2, 6, 23, .45);  /* the fixed action bar */
  /* A whisper of light across the top of a card - what separates "a rectangle
     with a border" from "a surface". A token so the light theme, where
     white-over-white has nothing to whisper, switches it off instead of
     fighting it. */
  --surface-grad:  linear-gradient(180deg, rgba(148, 163, 184, .055), rgba(148, 163, 184, 0) 46%);
}

/* ── LIGHT THEME ─────────────────────────────────────────────────────────
   The print block at the end of this file proved the point early: the palette
   lives in tokens, so a second theme is one attribute and one block. Dark
   stays the default - no attribute, dark page - because a driver checks pay
   at night in a cab; light exists for the 3PL at a desk by a window.
   theme-boot.js applies the saved choice from <head>, before first paint.

   Amber-500 on white is 2.2:1 - decorative, not readable - so the accent
   steps down to amber-800 - the 700 read fine on plain surfaces but failed
   4.5:1 sitting on its own soft amber washes - and the hover goes DARKER,
   the way light-theme hovers do. The muted ink clears 4.5:1 on --bg, --panel and --panel-2. */
:root[data-theme="light"] {
  color-scheme:  light;
  /* Light separates the other way round: --panel is #FFFFFF and cannot go up,
     so the PAGE steps down. #F5F7FB against white was 1.073:1 - a white card
     on an almost-white page. #EAEEF5 makes it 1.164:1, and --panel-2 drops to
     #DFE5F0 so a hovered week row and a table head still read as a change
     against the card they sit on (1.124 -> 1.265).

     --bg is the light theme's theme-color too: theme-boot.js and app.js both
     write it to the meta tag, and both had #F5F7FB spelled out. They move
     with this value or the phone's status bar stops matching the page. */
  --bg:          #EAEEF5;
  --panel:       #FFFFFF;
  --panel-2:     #DFE5F0;
  --input-bg:    #FFFFFF;
  /* The same ladder in navy-on-white. Light needs slightly LESS alpha than
     dark for the same ratio, because the surfaces it composites over are all
     near-white and the ink is near-black: 1.40 -> 1.91 for --border, and .49
     puts --border-control at 3.13:1 worst-case (3.31:1 on #FFFFFF).
     --input-bg IS --panel here (both #FFFFFF, 1.000:1 apart) and that is
     correct - a field is the brightest thing in a light UI - which leaves the
     border as the ONLY thing identifying it. Hence 3:1 is not optional. */
  --border:      rgba(15, 23, 42, .30);
  --border-2:    rgba(15, 23, 42, .17);
  --border-control: rgba(15, 23, 42, .49);
  --accent:      #92400E;
  --accent-soft: rgba(180, 83, 9, .10);
  --accent-hi:   #78350F;
  --accent-ink:  #FFFFFF;
  /* Emerald-800, forced by the surface step above. #047857 was the tightest
     ink in the light palette at 4.88:1 on the old --panel-2, so darkening
     --panel-2 to buy card separation would have dropped positive money below
     AA - the one number a carrier opens this page to read. #065F46 clears
     6.4:1 on the new --panel-2. The wash and hairline below follow it. */
  --good:        #065F46;
  /* Amber-800, not 700. #B45309 cleared 4.5:1 on --bg and --panel but landed
     4.47:1 on --panel-2 - a miss by three hundredths, which is still a miss on
     the surface the summary cards use. Matches --accent, exactly as the dark
     palette already has --warn === --accent. */
  --warn:        #92400E;
  --info:        #1D4ED8;
  --red:         #B91C1C;
  --text:        #0F172A;
  --muted:       #475569;
  --glass:         rgba(234, 238, 245, .86);   /* tracks --bg, or the sticky header tints wrong */
  --row-hover:     rgba(15, 23, 42, .04);
  --row-driver:    rgba(15, 23, 42, .055);     /* driver group heading, detail table */
  /* .60, not the .52 that merely cleared 3:1. Against light's resting
     --border-control (.49, 3.13:1 worst-case) a .52 hover measured 3.63:1 -
     which is the same trap the old dark .34 fell into: technically a different
     colour, visually the same edge. .60 gives 4.33:1, a 1.38x step. */
  --border-hover:  rgba(15, 23, 42, .60);
  /* .85 rather than the .75 that first cleared the bar: on --panel-2 that
     measured 3.01:1, and a threshold you pass by a hundredth is one rounding
     away from failing. .85 gives 3.22:1 there. */
  --accent-border: rgba(180, 83, 9, .85);
  --accent-soft-2: rgba(180, 83, 9, .16);
  /* The status washes in their light inks, keyed to the light --red/--good
     (#B91C1C / #065F46) rather than the dark tones, so a wash never disagrees
     with the text sitting inside it. */
  --red-soft:      rgba(185, 28, 28, .07);
  --red-border:    rgba(185, 28, 28, .38);
  --good-soft:     rgba(6, 95, 70, .09);
  --good-border:   rgba(6, 95, 70, .50);
  --scroll-thumb:    rgba(15, 23, 42, .40);
  --scroll-thumb-hi: rgba(15, 23, 42, .55);
  --shadow-pop:   0 10px 30px rgba(15, 23, 42, .16), 0 2px 8px rgba(15, 23, 42, .10);
  --shadow-card:  0 1px 2px rgba(15, 23, 42, .08), 0 1px 1px rgba(15, 23, 42, .05);
  --shadow-raise: 0 4px 12px rgba(15, 23, 42, .12), 0 1px 3px rgba(15, 23, 42, .08);
  --shadow-bar:   0 -8px 20px rgba(15, 23, 42, .14);
  --surface-grad: none;
}

* { box-sizing: border-box; }

/* The -moz- half was missing, so the type was smoothed on Chrome and Safari
   and left alone on Firefox/macOS - the same page rendering a shade heavier on
   one browser. Both halves, on the root, is the whole fix. */
html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Paints the overscroll gutter and the scrollbar trough in our own dark, so
     a rubber-band scroll on a settlement does not flash white. */
  background: var(--bg);
  color-scheme: dark;
}

/* ── [hidden] MUST WIN, ONCE, FOR EVERYTHING ────────────────────────────────
   `[hidden] { display: none }` lives in the USER-AGENT stylesheet, and the
   cascade puts every author rule above every UA rule REGARDLESS of specificity.
   So any author `display` on an element - even a bare `display: block` on a <p> -
   silently re-shows something the markup says is hidden. It is not a specificity
   bug and cannot be fixed by making the selector stronger; only `!important`
   here outranks an author declaration.

   This file has already been patched for that same hazard three separate times
   (`table.detail tr[hidden]`, `.appmenu[hidden]`, `.acting-bar[hidden]`), each
   time after the fact. The fourth was `.signin-err { display: block }`, which
   showed an empty red error strip to every carrier reaching the sign-in screen
   before they had done anything. One global rule ends the class of bug; the
   three older element-specific ones are left in place as harmless duplicates. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  /* Native controls - the multi-week checkboxes, the search field's own
     clear affordance - pick their tint up from here rather than each
     restating it. */
  accent-color: var(--accent);
}

/* Amber at low alpha rather than the UA's blue, which on this navy is both
   off-brand and, over muted grey text, genuinely hard to read. */
::selection { background: rgba(245, 158, 11, .3); color: var(--text); }

/* The scrollbars are part of the furniture on a page whose whole job is long
   lists and a wide money table. Left default they are the one bright
   platform-grey element on an otherwise composed dark page. */
* { scrollbar-color: var(--scroll-thumb) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border: 2px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hi); background-clip: content-box; }

.wrap {
  max-width: 1240px; margin: 0 auto; padding: 0 1rem 4rem;
  padding-left: max(1rem, env(safe-area-inset-left));
  padding-right: max(1rem, env(safe-area-inset-right));
}

/* ── THE COLUMN AND THE WINDOW (desktop) ─────────────────────────────────
   Up to 1240px the column IS the window: .wrap fills it with a 1rem gutter
   each side and nothing below applies. Past that, the cards, notices and
   tabs stay in the 1240px column - that is the width a two-card summary
   reads best at - but a settlement detail table has sixteen columns and was
   being asked to fit them into 1208px on a 1900px monitor, so it either
   scrolled sideways or wrapped Line and Job into four-line cells while a
   third of the screen sat empty on each side. Walt: "horz scrolling isn't
   ideal in this type of view since we have the realestate."

   HOW. .wrap lets go of its max-width and the column moves down one level -
   the tabs and the view carry it - so a view that opts in (#view.view-wide,
   which show() sets for the week detail) can hand its top-level
   .table-scroll the whole window while everything else in it keeps the
   column. Done this way rather than with the usual
   `width:100vw; margin-left:calc(50% - 50vw)` breakout because 100vw
   includes the vertical scrollbar on Windows: that idiom paints 17px past
   the body and the page scrolls sideways - the one thing this file's header
   says the layout never does. Every width here derives from the real body
   width, so the gutters are exactly the 1rem .wrap already keeps.

   THE HEADER GOES WITH THE TABLE, not the column. It is sticky from 720px
   up, and a sticky bar narrower than the content scrolling under it shows
   the rows past its edges as they pass - a 1240px header over a 1900px
   table looks like a table tucked behind a floating strip. So the header
   spans the window too. Its search box was flex-grow filling the column;
   uncapped it would become a metre of empty field, so it is capped and the
   freed space sits between it and the product-line toggle, which keeps the
   controls at the right edge where they have always been. */
@media screen and (min-width: 1241px) {
  .wrap { --col-max: calc(1240px - 2rem); max-width: none; }   /* 2rem = .wrap's own gutters */
  .wrap > .tabs, .wrap > .acting-bar, #view {
    max-width: var(--col-max); margin-left: auto; margin-right: auto;
  }
  #view.view-wide { max-width: none; }
  #view.view-wide > :not(.table-scroll) {
    max-width: var(--col-max); margin-left: auto; margin-right: auto;
  }
  header.top .search-wrap { max-width: 36rem; margin-right: auto; }
}

/* ── tester banner ────────────────────────────────────────────────────────
   Loud on purpose. This build talks to fixtures, and nobody should mistake it
   for real settlement data. */
.tester-bar {
  background: var(--accent-soft);
  border-bottom: 1px solid var(--accent);
  padding: .5rem 1rem;
  font-family: var(--font-head);
  font-size: .8rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent);
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
}
.tester-bar select {
  background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--radius);
  padding: .25rem .5rem; font-family: var(--font-body); font-size: .85rem;
  text-transform: none; letter-spacing: 0;
}
.tester-bar .spacer { flex: 1; }

/* ── header / search ─────────────────────────────────────────────────────── */
header.top {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
  padding: 1rem 0; border-bottom: 1px solid var(--border); margin-bottom: 1.25rem;
}

/* STICKY FROM 720px UP, and deliberately not below it. On a phone this header
   stacks to three rows - brand, search, side toggle - and pinning that would
   spend a third of a 667px screen on chrome to save a scroll. On a desktop it
   is one row, and keeping the search box and the way back permanently in reach
   is most of what makes this read as an application rather than a document.

   The blur needs a translucent background to have anything to do, and the
   solid --bg fallback is what a browser without backdrop-filter gets - opaque
   either way, because a settlement table scrolling UNDER legible-but-clear
   glass is a page you cannot read. */
@media (min-width: 720px) {
  header.top {
    position: sticky; top: 0; z-index: var(--z-header);
    background: var(--bg);
    margin-bottom: 1.25rem;
  }
  @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    header.top {
      background: var(--glass);
      -webkit-backdrop-filter: blur(12px) saturate(1.4);
      backdrop-filter: blur(12px) saturate(1.4);
    }
  }
}

/* THE LOCKUP. The header said "EXPLORE" and nothing else, so the only place
   the product was named was the browser tab - a carrier arriving from an
   emailed link saw a company name and had to infer the rest. Company mark,
   hairline, product name: the arrangement every bank statement portal uses,
   for the reason they use it.

   CENTRED, not baseline-aligned: the company half is an IMAGE now, and a
   replaced element has no baseline of its own - the browser synthesises one at
   its bottom edge, which drops CARRIER PAY to the foot of the plate. */
.brand-lock { display: flex; align-items: center; gap: .55rem; min-width: 0; }

/* THE MARK ON ITS OWN PLATE.

   The logo is a blue wordmark whose O is a BLACK map pin. Black on --bg
   (#0F172A) is not a contrast problem so much as a disappearance - the pin
   vanishes and the mark reads "EXPL RE" with a globe floating in the gap. So
   it sits on white, which is the same treatment every internal Explore header
   uses and the reason the plate is a literal #fff and not a token: it is the
   artwork's safe area, not a themed surface. On the light palette #fff against
   --bg (#F5F7FB) is all but invisible, and on paper it is white on white - so
   ONE rule serves both themes and print, with nothing to keep in sync.

   28px keeps the header row at the 40px the theme button already sets: the
   letterforms are 97 of the artwork's 150 rows, so this lands an 18px cap
   height against the 20px of the text it replaced. line-height:0 is what stops
   the inline image's descender slot from padding the plate unevenly. */
.brand {
  /* h1 carries a 0.67em UA margin. Unreset, it was padding this flex row taller
     than the 1rem the rule above asks for, and no other header child agreed
     with it - the misalignment you could see but not name. */
  margin: 0;
  display: flex; align-items: center;
  background: #FFFFFF; border-radius: var(--radius);
  padding: 3px 8px; line-height: 0;
}
.brand img { display: block; height: 28px; width: auto; }
.brand-sub {
  font-family: var(--font-head); font-weight: 600; font-size: var(--fs-sm);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  color: var(--muted); white-space: nowrap;
  padding-left: .55rem; border-left: 1px solid var(--border);
}
.who { font-size: var(--fs-sm); color: var(--muted); min-width: 0; overflow-wrap: anywhere; }
.who b { color: var(--text); font-weight: 600; }

/* HEADER ONLY. flex-basis is a MAIN-AXIS size: a width here, because
   header.top is a flex ROW. Left unscoped it also applied inside
   .filter-bar, which is a flex COLUMN - see the note at the search-field
   block below for the screen that shipped as a result. */
/* The flex-basis lives with the other .search-wrap sizing further down, where
   the comment explaining WHY it is scoped to the header also lives. This rule
   used to restate `flex: 1 1 260px` here and be silently overridden by that
   one - two numbers for one box, and the losing one read first. */
header.top .search-wrap { min-width: 200px; }
#search {
  width: 100%; padding: .55rem .75rem .55rem 2rem;
  background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--radius);
  font-family: var(--font-mono); font-size: .88rem;
}

/* 16px MINIMUM, unconditionally. iOS zooms and sticks on focus for anything
   smaller. The original fix lived inside a max-width:719px block, so a phone
   rotated to landscape (~844px logical) fell straight out of it and the bug came
   back - on the very orientation someone uses to read a wide table. */
#search { font-size: 16px; }

/* Visually hidden, still present for assistive tech. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

.skip-link {
  position: absolute; left: .5rem; top: -3rem; z-index: 100;
  padding: .55rem .9rem; border-radius: 6px;
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--font-head); font-size: .82rem;
  letter-spacing: .08em; text-transform: uppercase; text-decoration: none;
  transition: top .12s ease-out;
}
.skip-link:focus { top: .5rem; }

/* The magnifier used to be attempted TWICE: a U+2315 glyph here, and the drawn
   circle-and-handle further down. The drawn one wins on document order, so
   this one had never rendered - it was only still here to be read as the
   explanation for something it was not doing. The live one is in the search
   field block below. */

/* ── side toggle: rendered only when a principal has more than one side ──── */
.sides { display: flex; gap: .4rem; }
.side-btn {
  font-family: var(--font-head); font-size: .8rem; letter-spacing: .08em; text-transform: uppercase;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border-control); border-radius: 999px; padding: .3rem .85rem; cursor: pointer;
}
.side-btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* The side this account does not hold. Visibly present, visibly not yours -
   the point is to answer "where is crude?" before it becomes a question. */
.side-btn[disabled] {
  opacity: .4; cursor: not-allowed; border-style: dashed;
}
.side-btn[disabled]:hover { background: transparent; color: var(--muted); }
.side-btn:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
/* The pill had no hover at all, so the only way to learn it was pressable was
   to press it. */
.side-btn:not([disabled]):hover { border-color: var(--accent); color: var(--accent); }
.side-btn {
  transition-property: background-color, border-color, color, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.side-btn:not([disabled]):active { transform: translateY(1px); transition-duration: var(--dur-press); }

/* ── badges + status ─────────────────────────────────────────────────────── */
.badge {
  display: inline-block; font-family: var(--font-head); font-weight: 600;
  font-size: .7rem; letter-spacing: .09em; text-transform: uppercase;
  padding: .14rem .5rem; border-radius: 999px; border: 1px solid currentColor; white-space: nowrap;
}
.badge-good { color: var(--good); } .badge-warn { color: var(--warn); } .badge-info { color: var(--info); }
.status-note { font-size: .84rem; color: var(--muted); margin: .35rem 0 1rem; }
.status-note.tone-good { color: var(--good); }

/* ── summary cards ───────────────────────────────────────────────────────── */
.sum-grid { display: grid; grid-template-columns: 1fr; gap: .85rem; margin-bottom: .85rem; }
@media (min-width: 720px) { .sum-grid { grid-template-columns: 1fr 1fr; } }

/* CONCENTRIC RADIUS. The card is --radius-md (10px) with ~14px of padding, and
   everything nested inside it - the metric strip, the balance panel - is
   --radius (6px). Outer = inner + padding is the rule; at this padding the two
   scale steps already satisfy it, which is why the strip inside a card does not
   look like a sticker on top of one. */
.sum-card {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: .9rem 1rem;
  box-shadow: var(--shadow-card);
}
.sum-card h3 {
  font-family: var(--font-head); font-size: .78rem; letter-spacing: .11em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 .6rem;
}
.card-count { color: var(--accent); }
.sum-line { display: flex; justify-content: space-between; gap: 1rem; padding: .2rem 0; font-size: .92rem; }
.sum-label { color: var(--text); }
.sum-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.sum-context { font-size: .78rem; color: var(--muted); padding: 0 0 .25rem; }
.sum-total { border-top: 1px solid var(--border); margin-top: .45rem; padding-top: .45rem; font-weight: 600; }
/* The pay ladder. A subtotal (gross, net) sits under a dashed rule so it reads
   as the sum of the rows above it; the deductions between subtotals are plain
   rows in muted type, so the figures a carrier is paid stand out from the ones
   taken off. Total pay keeps its solid rule and accent colour below. */
.sum-sub { border-top: 1px dashed var(--border); margin-top: .3rem; padding-top: .3rem; font-weight: 600; }
.sum-value.less { color: var(--muted); }
.sum-total .sum-value { color: var(--accent); }
.sum-card.ded .sum-total .sum-value { color: var(--red); }

/* ── dashboard stat tiles ────────────────────────────────────────────────
   The headline strip above a list: a figure, its label, one quiet note.
   Values wear text ink, never an accent - the one exception is negative
   money, which uses the same red the net strip does. A tile that routes
   somewhere is a real <button> and says so on hover/focus; a plain figure is
   a <div> and never pretends to be pressable. */
.stat-row {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-2); margin: 0 0 var(--sp-4);
}
@media (min-width: 720px) { .stat-row { grid-template-columns: repeat(4, 1fr); } }
.stat {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius-md); padding: var(--sp-3) var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-1);
  min-width: 0; text-align: left;
}
.stat-label {
  font-family: var(--font-head); font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
  color: var(--muted);
}
.stat-value {
  font-family: var(--font-mono); font-size: var(--fs-lg); font-weight: 600;
  color: var(--text); line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.stat-value.tone-neg { color: var(--red); }
.stat-note { font-size: var(--fs-xs); color: var(--muted); }
.stat { box-shadow: var(--shadow-card); }
button.stat {
  cursor: pointer;
  transition-property: background-color, border-color, box-shadow, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
/* Only the tiles that ROUTE somewhere react. A plain figure is a <div> and is
   left inert on purpose - the comment above this block is explicit that a tile
   must never pretend to be pressable, and a hover state is exactly that
   pretence. */
button.stat:hover { border-color: var(--accent); background: var(--panel-2); box-shadow: var(--shadow-raise); }
button.stat:active { transform: translateY(1px); box-shadow: var(--shadow-card); transition-duration: var(--dur-press); }
button.stat:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

/* ── filter bar + chips ──────────────────────────────────────────────────
   The box FILTERS a list already on screen; the chips narrow it by state.
   aria-pressed carries the on state - the same contract the side toggle
   uses - and forced-colors mode thickens the border because a background
   alone vanishes there. */
.filter-bar {
  display: flex; flex-direction: column; gap: var(--sp-2);
  margin: 0 0 var(--sp-3);
}
/* Belt and braces behind the fix above: whatever the container's axis, the
   filter box is a normal-height control. A search field can never again be a
   third of a screen tall. */
.filter-bar .search-wrap { flex: 0 0 auto; width: 100%; }
@media (min-width: 720px) {
  /* Chips beside the box rather than stacked under it - the filter and the
     facets are one control, and side by side they fit above the fold. */
  .filter-bar { flex-direction: row; align-items: center; }
  .filter-bar .search-wrap { flex: 1 1 22rem; width: auto; }
  .filter-bar .chip-row { flex: 0 0 auto; }
}
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.chip {
  background: var(--input-bg); color: var(--muted);
  border: 1px solid var(--border-control); border-radius: 999px;
  padding: .3rem .8rem; font-family: var(--font-body);
  font-size: var(--fs-sm); cursor: pointer;
}
.chip[aria-pressed="true"] {
  background: var(--accent-soft); border-color: var(--accent);
  color: var(--accent); font-weight: 600;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
.chip {
  /* A facet chip is one of the smallest targets on the page and sits in a row
     of them. 32px keeps a mis-tap from landing on the neighbouring facet. */
  min-height: 32px; display: inline-flex; align-items: center;
  transition-property: background-color, border-color, color, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.chip:active { transform: translateY(1px); transition-duration: var(--dur-press); }
.chip .chip-n { opacity: .75; margin-left: .35rem; font-variant-numeric: tabular-nums; }
@media (forced-colors: active) {
  .chip[aria-pressed="true"] { border-width: 2px; }
}

/* The admin fee sits BELOW the deductions total and visually outside it,
   because it is already inside every row amount. Styling it like a normal
   line would invite someone to add it into the total - which is the bug the
   first cut shipped. Dashed rule and muted type say "for information". */
.fee-info {
  display: flex; justify-content: space-between; gap: 1rem;
  margin-top: .5rem; padding-top: .45rem;
  border-top: 1px dashed var(--border);
  font-size: .82rem; color: var(--muted);
}

.net-strip {
  display: flex; justify-content: space-between; align-items: center;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: .7rem 1rem; margin-bottom: 1.25rem;
  box-shadow: var(--shadow-card);
}
.net-strip span { font-family: var(--font-head); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.net-strip b { font-family: var(--font-mono); font-size: 1.35rem; font-variant-numeric: tabular-nums; color: var(--accent); }
/* An ESTIMATE reads quieter than a settlement. Same layout so the two stay
   comparable, muted colour so nobody mistakes one for the other at a glance,
   and a dashed edge that says "not final" without putting a red alarm on a
   page about somebody pay. */
.net-strip.est { border-style: dashed; }
.net-strip.est b { color: var(--muted); }
.wk-total.est { color: var(--muted); }
.est-notice { border-color: var(--accent); }
/* A name another carrier already holds. Blocking, not a warning: no
   button can resolve it, because one normalised name belongs to exactly
   one carrier. */
.tag-blocked {
  display: inline-block; padding: 0 .4rem; border-radius: 3px;
  font-size: .66rem; letter-spacing: .07em; text-transform: uppercase;
  color: var(--red, #d9534f); border: 1px solid currentColor;
}
.map-choice { margin-bottom: .4rem; }
.map-choice .btn { white-space: normal; text-align: left; }

/* ── detail table ────────────────────────────────────────────────────────── */
.table-scroll {
  overflow-x: auto;                     /* wide content scrolls HERE, never the body */
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--panel);
  box-shadow: var(--shadow-card);
}
table.detail { border-collapse: collapse; width: 100%; font-size: .84rem; }
table.detail th, table.detail td { padding: .45rem .6rem; text-align: left; border-bottom: 1px solid var(--border-2); }
table.detail thead th {
  font-family: var(--font-head); font-size: .7rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); background: var(--panel-2); white-space: nowrap; position: sticky; top: 0;
  /* A border-bottom on a STICKY cell is painted with the cell's own box and
     several engines drop it once the row detaches and floats - so the header
     loses its underline exactly when it is doing its job. An inset shadow is
     drawn as background and stays put. z-index keeps it over the rows it is
     now overlapping rather than being scrolled through. */
  z-index: 1;
  box-shadow: inset 0 -1px 0 var(--border);
}
/* Reading a wide money table means tracking one row across twelve columns. The
   tint is deliberately quiet - it guides the eye without striping the table. */
table.detail tbody tr.row-line:hover td { background: var(--row-hover); }
.ta-r { text-align: right; }
.mono, table.detail td.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
table.detail td.ta-r { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.row-carrier { background: var(--panel-2); font-weight: 600; }

/* The week still being driven. A left accent rather than a colour swap: the row
   has to read as the same KIND of thing as the settled weeks under it, just the
   one that is not finished. */
.week-row.wk-live { border-left: 3px solid var(--accent); }

/* Collapsible driver groups. The caret is drawn, not a glyph, so it cannot
   fall back to tofu on an older device. */
.row-driver.can-fold { cursor: pointer; }
/* ONLY THE NAME CELL WARMS. This was every td in the row, which meant that
   hovering a driver recoloured that driver's TOTAL into the same amber the
   settlement total uses. A money figure changing colour under the cursor of the
   person reading it is the one thing this page must not do: it reads as the
   number itself having changed. The affordance still lands - the pointer is on
   the name, which is what folds. */
.row-driver.can-fold:hover td:first-child { color: var(--accent); }
.row-driver.can-fold:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
.row-driver.can-fold td { transition: color var(--dur-base) var(--ease); }
.fold-caret {
  display: inline-block; width: 0; height: 0; margin-right: .45rem;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  border-left: 6px solid currentColor;
  transition: transform .12s ease;
}
.row-driver[aria-expanded="true"] .fold-caret { transform: rotate(90deg); }

/* The mobile card view re-displays every tr as block, which OUTRANKS the UA's
   [hidden] rule - without this, collapsed rows would hide on desktop and show
   on a phone. */
table.detail tr[hidden] { display: none !important; }

/* A correction posted into a locked week. Visibly a revision, not an ordinary
   charge - "shows revised ... since we are in a no delete env". */
.tag-revised {
  display: inline-block; padding: 0 .35rem; margin-left: .3rem;
  font-size: .62rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--warn, #d9a514); border: 1px solid currentColor; border-radius: 3px;
  vertical-align: middle;
}
.row-driver td { padding-left: 1.2rem; font-weight: 600; }
/* A DRIVER ROW IS A HEADING, NOT A LINE. Walt, 2026-09-15: "add some contrast
   for the driver rows vs the rest of the rows ... they kinda all look the same
   right now". They did. .row-carrier has --panel-2 under it and .row-line is
   muted and indented, but the driver row between them carried nothing except
   bold text at a 1.2rem indent - and at .84rem in a sixteen-column table that
   is not a level in a hierarchy, it is a slightly darker line.

   Two quiet cues rather than one loud one, because this row sits between two
   others that already spend surface and indent:
     - its own surface, deliberately BETWEEN the carrier's --panel-2 and the
       table's --panel, so the ladder reads carrier > driver > line downward;
     - a top rule, which is what actually says "a new group starts here" - the
       eye reads a horizontal break as a boundary far faster than it reads a
       three-percent change in background.

   Desktop only. Below 720px the table is already stacked cards and the driver
   row is restyled wholesale down there (see the max-width: 719px block, which
   zeroes these borders); adding a surface to it as well would put a grey band
   behind a heading that is doing fine without one. */
@media (min-width: 720px) {
  .row-driver td {
    background: var(--row-driver);
    border-top: 1px solid var(--border);
  }
}

.row-line td:first-child { padding-left: 2.1rem; color: var(--muted); }
.row-line.is-negative td.ta-r:last-child { color: var(--red); }
.muted { color: var(--muted); font-weight: 400; }

/* ── week list + search results ──────────────────────────────────────────── */
.week-list, .results { display: flex; flex-direction: column; gap: .4rem; margin-bottom: 1.25rem; }
/* The home view stacks the balances card directly over this list, and
   .sum-card carries no margin of its own (it grew up inside grids that
   spaced it). Margin-top rather than margin on the card: it collapses away
   in every composition that already spaces itself. */
.week-list { margin-top: var(--sp-3); }
.week-row, .result {
  display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; text-align: left;
  background: var(--panel); border: 1px solid var(--border-control); border-radius: var(--radius);
  padding: .6rem .8rem; color: var(--text); font-family: var(--font-body); font-size: .9rem; cursor: pointer;
}
.week-row:hover, .result:hover { border-color: var(--accent); background: var(--panel-2); }
.week-row:focus-visible, .result:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
/* These rows ARE the navigation - a year of settlements is a list of them - so
   they get the contact shadow that separates a card from the page, and a
   ground shift on hover rather than a hairline nobody notices.

   No lift on hover and no scale on press. A list of amounts that bobs as the
   pointer crosses it looks unserious, and this is the screen where someone
   checks what they were paid. */
.week-row, .result {
  box-shadow: var(--shadow-card);
  transition-property: background-color, border-color, box-shadow;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.week-row:active, .result:active { background: var(--input-bg); transition-duration: var(--dur-press); }
.wk-date { font-weight: 600; }
.wk-side, .r-kind {
  font-family: var(--font-head); font-size: .7rem; letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
}
.wk-total { margin-left: auto; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.r-label { font-family: var(--font-mono); font-size: .85rem; }
.r-sub { color: var(--muted); font-size: .8rem; width: 100%; }
@media (min-width: 720px) { .r-sub { width: auto; margin-left: auto; } }

/* ── contract terms + actions ────────────────────────────────────────────── */
.contract { display: flex; gap: 1.5rem; flex-wrap: wrap; margin: 0 0 1rem; }
.contract > div { display: flex; flex-direction: column; }
.contract span { font-family: var(--font-head); font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.contract b { font-family: var(--font-mono); font-size: 1rem; }

.actions { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0; }
.btn {
  /* inline-flex + centring so a button with an icon, a count or a wrapped
     label still centres its contents. As a plain block it relied on the text
     being one short line, which "Download Excel" at 320px is not. */
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: 36px;
  font-family: var(--font-head); font-size: .8rem; letter-spacing: .07em; text-transform: uppercase;
  background: transparent; color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--radius); padding: .45rem .9rem; cursor: pointer;
  transition-property: background-color, border-color, color, box-shadow, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
/* Hover moved the border colour and nothing else, which on a 1px hairline is
   almost invisible on a phone-bright screen. The faint amber wash is what
   actually reads as "this one". */
.btn:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.btn:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

/* PRESS. One pixel down, and the duration is the short one - a press should
   land immediately and release quietly. Not a scale(): these buttons sit in a
   fixed bottom bar on a phone, shoulder to shoulder, and scaling one nudges
   the optical spacing of its neighbours. Translating does not.

   :active fires on touch too, so this is also the only feedback an iPhone user
   gets that a tap registered before the request comes back. There was none. */
.btn:active { transform: translateY(1px); transition-duration: var(--dur-press); }

.btn.primary {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600;
  box-shadow: var(--shadow-card);
}
/* The generic :hover above would wash the primary button out to the same
   ghost treatment as its neighbours - it has to stay the loud one. */
.btn.primary:hover {
  background: var(--accent-hi); border-color: var(--accent-hi); color: var(--accent-ink);
  box-shadow: var(--shadow-raise);
}
.btn.primary:active { box-shadow: var(--shadow-card); }

/* There was no disabled face at all, so a button the controller had switched
   off looked exactly like one that worked and reported nothing when pressed. */
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: .45; cursor: not-allowed;
  background: transparent; border-color: var(--border); color: var(--muted);
  box-shadow: none; transform: none;
}

h2.view-title {
  font-family: var(--font-head); font-size: 1.3rem; letter-spacing: .01em;
  margin: 0 0 var(--sp-2); display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  /* Stops "Settlement for week ending 23 August" breaking with one orphaned
     word on line two. balance is for headings and short titles only - it is
     capped at a few lines and would be ignored on a paragraph anyway. */
  text-wrap: balance;
}

/* pretty, not balance, for running copy: it only prevents the last-line orphan
   and leaves the rest of the ragged edge alone, which is what you want on the
   explanatory notes this portal leans on to say why a screen is empty. */
.empty, .status-note, .stat-note, .bar-note, .sum-context, .adj-hint, .notice p, .whose {
  text-wrap: pretty;
}
/* Sits between the week title and the status note. Quieter than the title
   because it is not what the reader came for, louder than the note because it
   is identification rather than a caption. */
.whose { margin: 0 0 .55rem; font-size: .95rem; color: var(--muted); }
.whose b { color: var(--text); font-weight: 600; }

.empty { color: var(--muted); font-size: .9rem; }
/* AN EMPTY STATE, NOT A FAILURE. .empty alone is one muted sentence, which on a
   dark page is indistinguishable from a view that did not load - the worst
   reading for somebody checking whether they have been paid. The <b> becomes a
   real headline so focusHeading() and a screen reader have something to land
   on, and the sentence under it says what will change. */
.empty-state { color: var(--text); }
.empty-state b {
  display: block;
  margin-bottom: var(--sp-1, .35rem);
  font-family: var(--font-head);
  font-size: 1.05rem;
  letter-spacing: .02em;
}
/* THE TAB STOP HAS TO BE VISIBLE. render.js makes the wide detail table a
   focusable region so its off-screen columns can be reached from a keyboard
   (SC 2.1.1); without a ring, that tab stop is somewhere a keyboard user
   cannot see they have arrived. */
.table-scroll:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
/* The only <b> inside .empty is the signed-in address on the no-access and
   request screens - 50 unbreakable characters at 320px. */
.empty b { overflow-wrap: anywhere; }
/* Was red body text, which on a page that also prints negative money in red is
   not distinct enough to be read as "something went wrong". Given the same
   left-rule treatment .notice uses, so a failure looks like a piece of the
   interface rather than a stray sentence. */
.err {
  color: var(--red); font-size: .9rem;
  background: var(--red-soft);
  border: 1px solid var(--red-border);
  border-left: 3px solid var(--red);
  border-radius: var(--radius);
  padding: .6rem .75rem; margin: .5rem 0;
  text-wrap: pretty;
}

/* ── the tab row ─────────────────────────────────────────────────────────── */
.tabs {
  display: flex; gap: .15rem;
  margin: 0 0 1.1rem;
  border-bottom: 1px solid var(--border);
  /* Scrolls rather than wraps: a wrapped tab row reflows the whole page when a
     badge count changes from 9 to 10. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  flex: 0 0 auto;
  background: none; border: 0; cursor: pointer;
  padding: .7rem .95rem;
  font-family: var(--font-head); font-size: .82rem;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted);
  /* Transparent border on every tab, coloured only on the active one, so the
     text never shifts by a pixel when the selection moves. */
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.tab:hover { color: var(--text); border-bottom-color: var(--border); }
.tab.on { color: var(--text); border-bottom-color: var(--accent); }
.tab:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
/* Both of the above read var(--fg), a token this file has never declared. An
   unresolvable var() makes the property `unset`, and for an inherited property
   that means inherit - so the ACTIVE tab was taking its colour from <body> and
   landing on the right value by luck. It survived every visual check and would
   have broken the moment body colour changed. */
.tab {
  transition-property: color, border-color, background-color;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}

.tab-badge {
  /* The count goes 9 -> 10 while you are looking at it. Proportional digits
     re-measure the badge and nudge every tab to its right. */
  font-variant-numeric: tabular-nums;
  display: inline-block; margin-left: .4rem;
  min-width: 1.15rem; padding: .05rem .3rem;
  border-radius: 999px;
  background: var(--accent); color: var(--accent-ink);
  font-size: .72rem; letter-spacing: 0; text-align: center;
}
/* A zero badge is noise - it says "nothing here" in the most attention-grabbing
   way available. Handled in render.tabs(), where `0` is falsy so no badge is
   emitted at all; :has-text() is not a real selector and cannot do this. */
.tab-badge:empty { display: none; }

/* ── "coming soon" is not an error ──────────────────────────────────────── */
.notice {
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  background: var(--panel);
  padding: .9rem 1rem;
  margin: 1rem 0;
  box-shadow: var(--shadow-card);
}
.notice-tag {
  display: inline-block; margin-bottom: .4rem;
  font-family: var(--font-head); font-size: .72rem;
  letter-spacing: .11em; text-transform: uppercase;
  color: var(--accent);
}
.notice p { margin: 0; color: var(--muted); line-height: 1.5; }

/* ── the search field ──────────────────────────────────────────────────── */
/* Was a bare input wedged into the header. The magnifier sits inside the field
   so the control reads as a search box rather than a text box. */
/* NO FLEX SIZING HERE. `flex: 1 1 22rem` on this shared class is what broke
   the Carriers screen: .filter-bar lays out in a COLUMN, so the 22rem
   flex-basis became a 352px-tall search box. The drawn magnifier below is
   pinned to top:50%, so it was stranded ~180px beneath the input - the
   "floating logo" - and the carrier list was pushed off the bottom of the
   screen, which read as "typing a name searches nothing". The box was
   filtering correctly the whole time; nobody could see the result.

   Sizing belongs to the CONTAINER, which knows its own axis. */
.search-wrap { position: relative; min-width: 0; }
header.top .search-wrap { flex: 1 1 22rem; }

/* THE MAGNIFIER IS DRAWN, not a glyph and not an SVG: a glyph falls back to
   tofu on an older device, and a data: URI background would need img-src to be
   loosened in a CSP that is deliberately tight on a page showing money.

   OPTICALLY CENTRED, not geometrically. The lens and the handle are two
   elements, so `top: 50%` on each centres each one and leaves the COMPOSITE
   sitting high - the handle hangs below the lens and nothing accounts for it.
   The lens is lifted to -8px (rather than half its own 13px box) so that the
   ink of the two together straddles the middle of the field. It is a pixel and
   a half; it is also the difference between an icon that looks placed and one
   that looks dropped in.

   Both tint with the field: muted at rest, accent once the box has focus, so
   the control confirms it is listening. */
.search-wrap::before {
  content: '';
  position: absolute; left: .7rem; top: 50%;
  width: 13px; height: 13px; margin-top: -8px;
  border: 2px solid var(--muted); border-radius: 50%;
  pointer-events: none;
  transition: border-color var(--dur-base) var(--ease);
}
.search-wrap::after {
  content: '';
  position: absolute; left: 1.42rem; top: 50%;
  width: 7px; height: 2px; margin-top: 2px;
  background: var(--muted);
  transform: rotate(45deg);
  pointer-events: none;
  transition: background-color var(--dur-base) var(--ease);
}
.search-wrap:focus-within::before { border-color: var(--accent); }
.search-wrap:focus-within::after  { background: var(--accent); }

#search { padding-left: 2.1rem; }

/* EVERY box inside a .search-wrap wears the same face. The header's #search
   was styled BY ID, so the in-view boxes (the carrier filter, Link a sign-in,
   the approval flow) rendered as bare UA inputs - a white native field on a
   dark page with the magnifier glyph floating over it. Walt: "search bar
   looks weird". The header keeps its mono face via the #search rules, which
   win on specificity. */
.search-wrap input[type="search"] {
  width: 100%; padding: .55rem .75rem .55rem 2.1rem;
  background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border-control); border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 16px;   /* the iOS zoom floor, same as #search */
}

/* ── what a search box does when you touch it ───────────────────────────
   These are on the SHARED selector plus #search, so the header box and the
   four in-view boxes (carrier filter, Link a sign-in, the approval flow, the
   guest grant) behave identically. They did not before: the header field was
   styled by id and the rest inherited nothing, which is the "search bar looks
   weird" the block above was written for. Same field, same manners. */
.search-wrap input[type="search"],
#search {
  transition-property: border-color, background-color, box-shadow;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.search-wrap input[type="search"]:hover:not(:focus),
#search:hover:not(:focus) { border-color: var(--border-hover); }

/* The field lifts off the panel while it holds focus - a lighter ground and an
   amber edge under the ring. The ring alone is the accessibility contract; this
   is the part that makes the box feel switched on. */
.search-wrap input[type="search"]:focus-visible,
#search:focus-visible {
  outline: var(--focus-ring); outline-offset: 1px;
  border-color: var(--accent);
  background: var(--panel-2);
}

/* Placeholders were inheriting the UA's near-black on a dark field in some
   builds. Stated, so the hint reads as a hint on every browser. */
.search-wrap input[type="search"]::placeholder,
#search::placeholder { color: var(--muted); opacity: 1; }

/* WebKit draws its own clear button as a dark grey X, which on this navy is a
   smudge you cannot see and cannot aim at. Redrawn as a muted glyph that
   brightens on hover, and given a real target rather than the 8px default. */
.search-wrap input[type="search"]::-webkit-search-cancel-button,
#search::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; margin-left: .3rem;
  cursor: pointer; opacity: .55;
  background: var(--muted);
  /* The mask is a shape, not a fetched asset - no img-src to loosen. */
  -webkit-mask: linear-gradient(45deg, transparent 44%, #000 44%, #000 56%, transparent 56%),
                linear-gradient(-45deg, transparent 44%, #000 44%, #000 56%, transparent 56%);
          mask: linear-gradient(45deg, transparent 44%, #000 44%, #000 56%, transparent 56%),
                linear-gradient(-45deg, transparent 44%, #000 44%, #000 56%, transparent 56%);
  transition: opacity var(--dur-base) var(--ease), background-color var(--dur-base) var(--ease);
}
.search-wrap input[type="search"]::-webkit-search-cancel-button:hover,
#search::-webkit-search-cancel-button:hover { opacity: 1; background: var(--accent); }

/* ── results ───────────────────────────────────────────────────────────── */
.results { display: flex; flex-direction: column; gap: .35rem; margin-top: .75rem; }
.result {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center; gap: .75rem;
  width: 100%; text-align: left; cursor: pointer;
  padding: .7rem .85rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
}
.result:hover { border-color: var(--accent); }
.result:focus-visible { outline: var(--focus-ring); outline-offset: 1px; }
.result .r-kind {
  font-family: var(--font-head); font-size: .68rem;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent);
  padding: .15rem .4rem; border: 1px solid var(--border); border-radius: 4px;
}
.result .r-label { font-variant-numeric: tabular-nums; min-width: 0; overflow-wrap: anywhere; }
.result .r-sub { color: var(--muted); font-size: .85rem; white-space: nowrap; }

@media (max-width: 719px) {
  /* Three columns in 320px turns every result into three cramped lines. Stack. */
  .result { grid-template-columns: 1fr; gap: .25rem; }
  .result .r-sub { white-space: normal; }
  .tab { padding: .85rem .8rem; font-size: .8rem; }
}

/* ── admin + the request form ────────────────────────────────────────────── */
.actions-inline { display: flex; gap: .5rem; margin-top: .6rem; }
.actions-inline .btn { flex: 1; }

.req-form { display: flex; flex-direction: column; gap: .35rem; margin: 1rem 0; }
.req-form label {
  font-family: var(--font-head); font-size: .78rem; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
/* select is styled alongside input, not left to the UA. An unstyled control
   keeps its platform background - white - which on this panel is a light box in
   a dark form, and in the light theme loses the border the inputs beside it
   have. Same tokens, so it follows both themes for free. */
.req-form input,
.req-form select {
  /* 16px so iOS does not zoom and stick on focus. */
  font: inherit; font-size: 16px; padding: .6rem .7rem;
  background: var(--input-bg); color: var(--text);
  border: 1px solid var(--border-control); border-radius: 6px;
}
.req-form input,
.req-form select {
  transition-property: border-color, background-color;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.req-form input:hover:not(:focus),
.req-form select:hover:not(:focus) { border-color: var(--border-hover); }
.req-form input:focus-visible,
.req-form select:focus-visible {
  outline: var(--focus-ring); outline-offset: 1px;
  border-color: var(--accent); background: var(--panel-2);
}
.req-form input::placeholder { color: var(--muted); opacity: 1; }

/* ── account: savings progress ──────────────────────────────────────────── */
.bar { height: 6px; border-radius: 999px; background: var(--panel-2);
       overflow: hidden; margin: .35rem 0 .3rem; }
.bar > span { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.bar > span.full { background: var(--good); }
.bar-note { font-size: .78rem; color: var(--muted); margin-bottom: .45rem;
            overflow-wrap: anywhere; /* carries refs and emails inside a 254px card */ }
/* Louder than a note, quieter than an error: this one marks an access request
   whose email address nobody proved, on the screen where a click links an
   address to a carrier's settlements. Weight as well as colour, and the sentence
   itself says "unverified" - the colour is the third channel, not the first. */
.bar-note.tone-warn { color: var(--warn); font-weight: 600; }
.net-strip b.neg { color: var(--red); }
h3.sub { font-family: var(--font-head); font-size: .82rem; letter-spacing: .11em;
         text-transform: uppercase; color: var(--muted); margin: 1.5rem 0 .5rem; }

/* Export stub output — shows the request the real endpoint would receive. */
pre.stub {
  background: var(--panel-2); border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: .7rem .85rem; overflow-x: auto;
  font-family: var(--font-mono); font-size: .78rem; color: var(--muted); margin: 0 0 1.25rem;
}

/* ── MOBILE ───────────────────────────────────────────────────────────────
   An owner-operator checks pay on a phone, so this is the primary case, not a
   fallback. Three things matter and each is a real bug if missed. */
@media (max-width: 719px) {

  /* 1. Columns the spec marks hideOnMobile drop out entirely.

        SPECIFICITY MATTERS HERE and it bit once already. The card rules below use
        `table.detail tr.row-line td` (0,2,3), which outranks a bare `.hide-sm`
        (0,1,0) — so `display: flex` won and every hidden column reappeared on the
        phone, which is the exact opposite of the intent. These selectors carry the
        class alongside the same element chain so they win outright, rather than
        reaching for !important. */
  .hide-sm { display: none; }
  table.detail thead th.hide-sm,
  table.detail tr.row-line td.hide-sm,
  table.detail tr.row-carrier td.hide-sm,
  table.detail tr.row-driver td.hide-sm { display: none; }

  /* 2. iOS ZOOMS THE WHOLE PAGE when an input with font-size < 16px is focused,
        and then leaves it zoomed. The search box is the first thing anyone
        touches, so it must be exactly 16px or larger here. */
  #search { font-size: 16px; padding-top: .65rem; padding-bottom: .65rem; }
  .tester-bar select { font-size: 16px; }

  /* Tile figures step down one size: at phone widths the 2-col grid gives a
     tile ~134px, and a 13-char YTD figure at 20px mono needs ~156px - it was
     wrapping mid-digit. 16px fits; the hero keeps the display size. */
  .stat-value { font-size: var(--fs-md); }

  /* 3. Tap targets. A 30px row is a mis-tap; 44px is the accepted floor. */
  .week-row, .result { min-height: 44px; padding: .7rem .8rem; }
  .side-btn, .btn { min-height: 44px; padding-left: 1rem; padding-right: 1rem; }

  /* ── the detail table becomes cards ────────────────────────────────────
     A twelve-column financial table on a 375px screen is a horizontal scroll
     nobody discovers. Reflowed with CSS alone, reading the field name from the
     data-label the renderer already emits — so there is exactly one markup and
     no second render path that can drift from the first. */
  table.detail, table.detail tbody, table.detail tr, table.detail td { display: block; width: 100%; }
  table.detail thead { display: none; }
  table.detail { font-size: .9rem; }

  /* Carrier and driver rows stay single-line bars: name left, total right. */
  table.detail tr.row-carrier, table.detail tr.row-driver {
    display: flex; justify-content: space-between; align-items: baseline; gap: .75rem;
    padding: .55rem .7rem;
  }
  table.detail tr.row-carrier td, table.detail tr.row-driver td { padding: 0; border: 0; width: auto; }
  table.detail tr.row-carrier td::before, table.detail tr.row-driver td::before { content: none; }
  table.detail tr.row-driver { padding-left: 1.1rem; }

  /* Each line becomes a bordered card. */
  table.detail tr.row-line {
    border: 1px solid var(--border); border-radius: var(--radius);
    margin: .45rem .5rem; padding: .25rem 0; background: var(--panel-2);
  }
  table.detail tr.row-line td {
    display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
    border: 0; padding: .28rem .7rem; text-align: left;
  }
  table.detail tr.row-line td::before {
    content: attr(data-label);
    font-family: var(--font-head); font-size: .68rem; letter-spacing: .09em;
    text-transform: uppercase; color: var(--muted); flex: 0 0 auto;
  }
  /* Undo the desktop indent, which would compound with the card padding. */
  table.detail tr.row-line td:first-child { padding-left: .7rem; color: var(--text); font-weight: 600; }

  /* An accessorial leaves its unit-of-work cells genuinely empty. Hide them
     rather than printing a labelled blank — same "absent, not zero" rule the
     summary cards follow. */
  table.detail tr.row-line td:empty { display: none; }

  /* The amount is what someone opened the app for: make it the anchor. */
  table.detail tr.row-line td:last-child { font-size: 1rem; font-weight: 600; }

  /* Nothing scrolls sideways any more, so drop the scroller's chrome. */
  .table-scroll { overflow-x: visible; border: 0; background: transparent; border-radius: 0; }
  /* EXCEPT the balances roster: .bal-table does not reflow to cards and its
     22rem minimum is wider than the card. Without this, the roster painted
     ~45px past the card edge and the whole page panned sideways - the one
     rule this file's header says the layout never breaks. */
  .sum-card .table-scroll { overflow-x: auto; }

  /* ── the compact header ────────────────────────────────────────────────
     Was four stacked rows - brand / search / identity / product line - which
     measured 196px of chrome before the first dollar figure. Now one utility
     row: menu, brand, search button, theme button. The search FIELD folds
     behind its own button (the field still exists and still answers to
     showSearch(); the button only unfolds it), identity moves into the app
     menu where the session already paints it, and the side toggle keeps its
     full-width row because switching product lines deserves a fat target.

     The :not([hidden]) guards matter: showSearch() and the session gate work
     through the hidden attribute, and a bare display rule here would outrank
     the UA's [hidden] behaviour and un-hide what the session hid. */
  header.top { gap: .5rem .4rem; }
  /* The plate is wider than the word it replaced - about 103px against 80px -
     and the phone utility row has no 23px to spare: waffle, lockup, search and
     theme come to 347px of the 358px a 390px screen leaves after .wrap padding,
     and one pixel over that wraps the theme button onto a row of its own. 24px
     buys the margin back and still reads as the mark rather than a favicon. */
  .brand img { height: 24px; }
  /* Gone from the LAYOUT, still in the accessibility tree: the header is
     where a screen reader expects the signed-in identity, and the app menu
     copy of it is a label inside role="menu" that menu navigation skips. */
  .who {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip: rect(0 0 0 0); clip-path: inset(50%);
    overflow: hidden; white-space: nowrap;
  }
  .search-toggle:not([hidden]) { display: inline-flex; order: 1; margin-left: auto; }
  /* When the search button is gated away the theme button inherits the job
     of holding the right edge, or it would sit orphaned beside the brand. */
  .search-toggle[hidden] ~ .theme-btn { margin-left: auto; }
  /* Scoped under header.top for SPECIFICITY, not location: the base rules
     for these two live later in the file and would win a bare-class tie. */
  header.top .theme-btn { order: 2; width: 44px; height: 44px; }
  header.top .search-wrap { display: none; }
  header.top.search-open .search-wrap:not([hidden]) {
    display: block; flex: 1 1 100%; order: 5;
  }
  header.top .waffle { width: 44px; height: 44px; }
  .sides { order: 6; width: 100%; }
  .side-btn { flex: 1; }
  /* The LIVE header carries an injected Sign out button (bootUi appends it
     under MSAL). It cannot fit the phone's utility row, and whenever the app
     menu is visible the menu already holds Sign out - so it yields to the
     menu. When the menu is hidden (a signed-in principal the session cannot
     build a menu for) it stays, because it is then the only way out. */
  #appmenu-wrap:not([hidden]) ~ .btn { display: none; }

  /* -- actions pinned to the bottom -------------------------------------
     On a phone the buttons are what you came for, and having to reach the end
     of a nine-load settlement to find them is friction. Pinned, they stay in
     thumb reach - and once this is added to a home screen it reads like an app
     rather than a web page that happens to fit.

     safe-area-inset-bottom keeps the bar clear of the iPhone home indicator. It
     only reports a real value because the viewport meta carries viewport-fit=cover. */
  .actions {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 40;
    margin: 0;
    display: flex; gap: .5rem;
    padding: .6rem .8rem calc(.6rem + env(safe-area-inset-bottom, 0px));
    background: var(--panel);
    border-top: 1px solid var(--border);
    box-shadow: var(--shadow-bar);
  }
  .actions .btn { flex: 1; margin: 0; }

  /* The bar is fixed, so the page needs room to scroll clear of it - otherwise
     the last row of the settlement hides behind it, which is the classic version
     of this bug. */
  /* Measured, not guessed: app.js trackBarHeight() writes --bar-h from the
     bar's real height, so a wrapped four-button bar cannot hide the last row.
     The 5.5rem fallback applies only before the first measurement. */
  .wrap { padding-bottom: calc(var(--bar-h, 5.5rem) + 1rem + env(safe-area-inset-bottom, 0px)); }

  /* Standalone has no browser chrome, so the page would sit against the status
     bar. Push it clear. */
  @media (display-mode: standalone) {
    .tester-bar { padding-top: calc(.5rem + env(safe-area-inset-top, 0px)); }
    header.top  { padding-top: calc(1rem + env(safe-area-inset-top, 0px)); }
  }

  .net-strip b { font-size: 1.45rem; }
  .view-title { font-size: 1.15rem; }
}

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

/* Windows High Contrast collapses backgrounds to system colours, which would
   render the savings bar as a blank track with fill and trough indistinguishable.
   The figure is printed as text directly below either way, so this is about the
   bar not becoming a lie rather than about it being the only source. */
@media (forced-colors: active) {
  .bar { border: 1px solid CanvasText; }
  .bar > span { background: Highlight; forced-color-adjust: none; }
  .skip-link { border: 1px solid CanvasText; }
}

/* ── BREADCRUMB / BACK ────────────────────────────────────────────────────
   Above the title, quiet until you look for it. Deliberately NOT inside
   .actions: Download and Back are different kinds of thing, and putting them in
   one row is what made "All weeks" invisible at the bottom of a long
   settlement. */
.crumb { margin: 0 0 var(--sp-2); }
.back-link {
  padding-left: var(--sp-2);
  padding-right: var(--sp-3);
  background: transparent;
  border-color: transparent;
  color: var(--muted);
  font-size: var(--fs-sm);
}
/* Reasserts the transparent ground the generic .btn:hover would otherwise wash
   amber. This control is meant to stay quiet until it is looked for; the colour
   change alone is enough to answer "is this a button". */
.back-link:hover { color: var(--accent); border-color: var(--border); background: transparent; }
.back-link:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

/* ── THE APP MENU (waffle) ────────────────────────────────────────────────
   Why this exists: once internal staff picked a carrier there was NO way back.
   The header held a brand, a name and a sign-out button, so the only exit from
   "viewing someone else's settlements" was to end the session entirely. The
   waffle is the way back, and it is also where the sections live so the tab row
   is not the only navigation on a narrow screen.

   It is shown to BOTH audiences with different contents - staff get switch-
   carrier and the admin areas, a carrier gets their own short list. Same control
   in the same place either way, so there is one thing to learn. */
.waffle {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: repeat(3, 3px);
  grid-template-rows: repeat(3, 3px);
  gap: 2px;
  place-content: center;
  width: 40px; height: 40px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  cursor: pointer;
}
.waffle span { width: 3px; height: 3px; border-radius: 1px; background: var(--muted); }
.waffle:hover { border-color: var(--accent); }
.waffle:hover span { background: var(--accent); }
.waffle[aria-expanded="true"] { background: var(--accent-soft); border-color: var(--accent); }
.waffle[aria-expanded="true"] span { background: var(--accent); }
.waffle:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
.waffle { transition-property: background-color, border-color, transform; transition-duration: var(--dur-base); transition-timing-function: var(--ease); }
.waffle span { transition: background-color var(--dur-base) var(--ease); }
.waffle:active { transform: translateY(1px); transition-duration: var(--dur-press); }

.appmenu-wrap { position: relative; flex: 0 0 auto; }

.appmenu {
  position: absolute;
  top: calc(100% + var(--sp-2));
  left: 0;
  min-width: 232px;
  padding: var(--sp-2);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop);
  z-index: var(--z-pop);
  /* Never wider than the screen it is on, never taller - a long internal
     menu on a short landscape phone scrolls inside itself instead of
     running off the bottom. */
  max-width: calc(100vw - 2rem);
  max-height: calc(100vh - 80px);
  overflow-y: auto;
}
.appmenu[hidden] { display: none; }
/* A menu that appears instantly at full opacity reads as a repaint; the same
   menu arriving over 150ms reads as a menu opening. Down 4px and in, from the
   corner it is anchored to. Exit is not animated: [hidden] removes it, and a
   fade-out on a menu you have already decided to leave is just latency. */
.appmenu { animation: menu-in var(--dur-base) var(--ease); transform-origin: top left; }
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.appmenu-label {
  display: block;
  /* The identity can be a 50-char guest email. Unbreakable, it forced a
     sideways scrollbar on the menu; anywhere also shrinks the label's
     min-content so the panel keeps its natural width. */
  overflow-wrap: anywhere;
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  font-family: var(--font-head);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--muted);
}

.appmenu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  background: transparent;
  border: 0;
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-base);
  text-align: left;
  cursor: pointer;
}
.appmenu-item:hover { background: var(--panel-2); }
.appmenu-item:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }
.appmenu-item { transition: background-color var(--dur-base) var(--ease), color var(--dur-base) var(--ease); }
.appmenu-item[aria-current="true"] { color: var(--accent); }
.appmenu-item[aria-current="true"]::after { content: '\2022'; margin-left: auto; }
.appmenu-item .mi-badge {
  margin-left: auto;
  padding: 0 var(--sp-2);
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: var(--fs-xs);
  font-weight: 700;
}
.appmenu-sep { height: 1px; margin: var(--sp-2) var(--sp-1); background: var(--border); }

/* ── "you are viewing someone else's money" ───────────────────────────────
   Staff acting as a carrier see the carrier's name where their own would be,
   which is exactly the state where a mistaken edit is most plausible. This bar
   says whose books are open and gets out in one click. Amber, not red: it is a
   normal supported mode, not an error. */
.acting-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin: 0 0 var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  font-size: var(--fs-sm);
}
.acting-bar[hidden] { display: none; }
.acting-bar .acting-tag {
  font-family: var(--font-head);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--accent);
}
.acting-bar .acting-name { font-weight: 600; color: var(--text); }
/* The chip that says these rows came from a NAME match and not from a mapping.
   Outlined rather than filled: it qualifies the bar it sits in, and a second
   solid block beside the amber one reads as a competing banner. */
.acting-bar .acting-unmapped {
  font-family: var(--font-head);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  padding: 2px var(--sp-2);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  color: var(--accent);
  cursor: help;
}
.acting-bar .acting-exit { margin-left: auto; }

/* A closed company, said on the row. Muted rather than red: an inactive
   carrier is a normal thing to be looking at in an internal tool - their
   settlement history is still owed an answer - so this labels, it does not
   warn. */
.crow-inactive {
  margin-left: var(--sp-2);
  padding: 1px var(--sp-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-head);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

/* ── THE ROW YOU CAME HERE FOR ─────────────────────────────────────────────
   Set by revealLine() after a jump from the global search lands on a week.
   Two states in one class on purpose: a brief wash that CATCHES the eye on
   arrival, decaying to a permanent left-edge marker that still POINTS at the
   row ten minutes later. A highlight that disappears leaves somebody who
   scrolled away with nothing to come back to.

   The marker is a box-shadow inset rather than a border so the row does not
   change height and shove the rest of the table down as it lands. */
@keyframes found-wash {
  from { background: var(--accent-soft); }
  to   { background: transparent; }
}
tr.is-found {
  box-shadow: inset 3px 0 0 0 var(--accent);
  animation: found-wash 2.4s ease-out 1;
}
/* Respect a person who has asked the interface to stop moving: they keep the
   marker and the wash, and lose only the fade. */
@media (prefers-reduced-motion: reduce) {
  tr.is-found { animation: none; background: var(--accent-soft); }
}

/* "Still looking", wherever an answer is about to appear. Deliberately quiet -
   it is a statement about the page, not a warning about it - and it carries no
   spinner, because a spinner in a text line reads as a stuck control. */
.is-working {
  color: var(--accent);
  font-style: italic;
}

/* ── INTERNAL ACCESS LIST (admin centre) ───────────────────────────────────
   A list of PEOPLE, built from cards rather than reusing table.detail. That
   table's mobile reflow styles its LAST cell as an amount and indents its FIRST
   as a settlement line under a driver; neither is true here, and inheriting
   them would mean overriding more than writing.

   Every value comes off the type and spacing scales at the top of this file -
   "a value that is not on this scale is a bug, not a nuance". Separation is by
   panel colour rather than a border for the same reason: there is no
   border-width token, so a hairline here would be the one off-scale number in
   the block. */
.int-list {
  list-style: none;
  margin: 0 0 var(--sp-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.int-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--panel-2);
  border-radius: var(--radius-md);
}
/* min-width:0 so a long address wraps inside the card instead of forcing the
   page to scroll sideways — the one layout rule this portal never breaks. */
.int-who { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.int-email { font-size: var(--fs-base); font-weight: 600; overflow-wrap: anywhere; }
.int-meta { font-size: var(--fs-xs); color: var(--muted); overflow-wrap: anywhere; }

/* WHERE the access comes from, said out loud on every row. An owner or an admin
   keeps the internal portal whatever this screen does, so their row has to
   explain itself rather than just quietly lacking a Revoke button. */
.int-src {
  font-family: var(--font-head);
  font-size: var(--fs-xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.int-act { margin-left: auto; flex: 0 0 auto; }

@media (max-width: 640px) {
  /* The menu is deliberately NOT re-anchored here any more. right:0 against
     the 44px wrap - which sits at the far LEFT of the header - hung the
     229px panel 172px off the left edge of the screen, leaving a 57px
     sliver: the "waffle renders off screen" bug. The default left:0 fits
     every width down to 320px, and the max-width guard on the panel itself
     keeps it honest on anything narrower. */
  /* The row stacks on a phone, so "push me right" would leave the control
     floating under a half-width column. */
  .int-act { margin-left: 0; }
}

@media (forced-colors: active) {
  .waffle, .theme-btn, .search-toggle { border: 1px solid CanvasText; }
  .waffle span { background: CanvasText; forced-color-adjust: none; }
  /* The half-fill and the magnifier handle are backgrounds, which forced
     colors strips - leaving two identical empty circles. Restored in
     CanvasText, the same treatment the waffle dots get. */
  .theme-ic { forced-color-adjust: none; }
  .search-toggle > span::after { background-color: CanvasText; forced-color-adjust: none; }
  .appmenu { border: 1px solid CanvasText; }
  .acting-bar { border: 1px solid CanvasText; }
}

/* ── picking several weeks to download at once ────────────────────────────
   The checkbox is a SIBLING of the row button, not a child and not wrapped in a
   label with it: a checkbox inside a button is invalid markup, and a label
   around both would make "open this week" toggle the tick instead. */
.week-item { display: flex; align-items: center; gap: var(--sp-3); }
.week-item .week-row { flex: 1; min-width: 0; }
.wk-pick {
  flex: 0 0 auto;
  width: 20px; height: 20px; margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
/* Disabled means the cap is reached, which is a rule and not a fault - dimmed,
   not reddened. */
.wk-pick:disabled { cursor: not-allowed; opacity: .4; }
.wk-pick:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

/* Sticky, because the whole point of the bar is that "Download 3 weeks" stays in
   reach while you scroll a year of settlements looking for the fourth. */
/* --head-h is written by app.js from the header's REAL height, the same way
   --bar-h is measured for the fixed action bar. Guessed, this bar would slide
   under the sticky header the moment the header wrapped to two rows. The 0px
   fallback is what a phone gets, where the header is not sticky at all. */
.pick-bar {
  position: sticky; top: var(--head-h, 0px); z-index: 5;
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin: 0 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* Painted after the list, so it is empty for one frame - an empty bar must not
   leave a stray box on the page. */
.pick-bar:empty { display: none; }
.pick-count {
  font-family: var(--font-head);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  letter-spacing: .06em;
  text-transform: uppercase;
}
.pick-note, .pick-status { font-size: var(--fs-sm); color: var(--muted); }
.pick-bar .btn { margin-left: auto; }
.pick-bar .btn ~ .btn { margin-left: 0; }

.fail-list {
  margin: var(--sp-2) 0 0;
  padding-left: 1.1rem;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.fail-list li { margin-bottom: var(--sp-1); }

@media (max-width: 719px) {
  /* WCAG 2.2 SC 2.5.8 puts the floor at 24px, and the row this sits in is already
     44px tall, so the tick gets a full-height band to be hit in. */
  .wk-pick { width: 24px; height: 24px; }
  /* The phone header scrolls away, so the bar belongs at the real top.
     Its measured --head-h offset is for the sticky desktop header only -
     left in place here it pinned the bar mid-air, 85-129px down, which is
     the "headers oddly persist" report. */
  .pick-bar { top: 0; }
  /* Both buttons share the width rather than one of them being pushed to the far
     edge of a wrapped row, where it reads as belonging to nothing. */
  .pick-bar .btn { margin-left: 0; flex: 1 1 auto; }
  .pick-count { width: 100%; }
}

@media (forced-colors: active) {
  .pick-bar { border: 1px solid CanvasText; }
}

/* ── settlement blocks: pay / accessorials / balances ────────────────────────
   Replaces the two-card .sum-grid. Three stacked blocks, because the third
   (balances) has no natural partner and a 2x2 grid left a hole. */
.sum-stack { display: grid; grid-template-columns: 1fr; gap: var(--sp-3, .85rem); margin-bottom: .85rem; }
/* 720px, matching every other breakpoint in this file - 900px was an outlier
   that left tablets on one column while the rest of the page had gone wide.
   align-items: stretch, not start: the two cards carry different numbers of
   lines, and ragged card bottoms beside each other is the "weird alignment". */
@media (min-width: 720px) {
  .sum-stack { grid-template-columns: 1fr 1fr; align-items: stretch; }
  .sum-stack > .sum-card { display: flex; flex-direction: column; }
  /* Pin the total to the bottom edge so both cards' totals sit on one line
     however many charges the week happened to carry. */
  .sum-stack > .sum-card > .sum-total { margin-top: auto; }
}

.sum-card h3 { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
.as-of {
  font-family: var(--font-body); text-transform: none; letter-spacing: 0;
  font-size: var(--fs-xs); color: var(--muted); font-weight: 400; white-space: nowrap;
}

/* Quantities. Figure over label, never a money row with an empty value. */
.metric-strip {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; margin: 0 0 .75rem;
}
.metric { background: var(--panel); padding: .5rem .6rem; min-width: 0; }
.m-val {
  font-family: var(--font-mono); font-size: var(--fs-md); font-variant-numeric: tabular-nums;
  color: var(--text); display: block; line-height: 1.15;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.m-lab {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: var(--tracking-caps);
  font-size: var(--fs-xs); color: var(--muted); display: block; margin-top: .1rem; font-weight: 600;
}

/* Accessorial group headings, with their subtotal. */
.sub-head {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: var(--tracking-caps);
  font-size: var(--fs-xs); color: var(--muted); font-weight: 600;
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin: .7rem 0 .3rem; padding-top: .55rem; border-top: 1px solid var(--border);
}
.sub-head.first { margin-top: 0; padding-top: 0; border-top: 0; }
.sub-head .s-amt { font-family: var(--font-mono); letter-spacing: 0; }
.earn-tag {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: .09em;
  font-size: var(--fs-xs); font-weight: 600; color: var(--good);
  background: var(--good-soft); padding: .08em .4em;
  border-radius: 3px; margin-left: .4rem;
}

/* Escrow / maintenance / negative balance rows. */
.bal-row { padding: .35rem 0; }
.bal-row + .bal-row { border-top: 1px solid var(--border); margin-top: .35rem; }
.bal-head { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.bal-foot {
  display: flex; justify-content: space-between; gap: 1rem;
  font-size: var(--fs-xs); color: var(--muted); margin-top: .1rem;
}
.bal-foot .wk { font-family: var(--font-mono); color: var(--red); }
.bal-table { width: 100%; border-collapse: collapse; }
/* The gap lives on the WRAPPER: inside the scroll box it cannot collapse out
   (overflow makes a BFC), which left dead space above the header row and
   none below the last. */
.sum-card .table-scroll { margin-top: .4rem; }
.bal-table th {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: .1em;
  font-size: var(--fs-xs); color: var(--muted); font-weight: 600;
  text-align: left; padding: .3rem .4rem; border-bottom: 1px solid var(--border);
}
.bal-table td { padding: .35rem .4rem; font-size: var(--fs-sm); border-bottom: 1px solid var(--border-2); }
.bal-table td.ta-r, .bal-table th.ta-r {
  text-align: right; font-family: var(--font-mono);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Where the gross went. */
.split-wrap { margin-top: .6rem; margin-bottom: var(--sp-4); }
.split-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; gap: 1px; background: var(--border); }
.split-bar i { display: block; height: 100%; }
.split-key {
  display: flex; flex-wrap: wrap; gap: .1rem 1rem; margin-top: .4rem;
  font-size: var(--fs-xs); color: var(--muted);
}
.split-key span { display: inline-flex; align-items: center; gap: .3rem; }
.split-key i { width: 7px; height: 7px; border-radius: 2px; display: inline-block; }
.sg-k { background: var(--accent); }
.sg-w { background: var(--red); }
.sg-b { background: var(--info); }

/* Request a balance adjustment. Opens a ticket; changes no money. */
.adj-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: .6rem; flex-wrap: wrap;
  border-top: 1px dashed var(--border); margin-top: .6rem; padding-top: .6rem;
}
.adj-hint { font-size: var(--fs-xs); color: var(--muted); }
.adj-btns { display: flex; gap: .4rem; flex-wrap: wrap; }
.btn-ghost {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: .1em;
  font-size: var(--fs-xs); font-weight: 600; color: var(--accent);
  background: var(--accent-soft); border: 1px solid var(--accent-border);
  border-radius: var(--radius); padding: .4rem .7rem; cursor: pointer;
}
.btn-ghost {
  min-height: 32px;
  transition-property: background-color, border-color, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.btn-ghost:hover { background: var(--accent-soft-2); border-color: var(--accent); }
.btn-ghost:active { transform: translateY(1px); transition-duration: var(--dur-press); }
.btn-ghost:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
.adj-form { border: 1px solid var(--border); border-radius: 8px; background: var(--panel-2); padding: .7rem .8rem; margin-top: .6rem; }
.adj-form .f-lab {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: .1em;
  font-size: var(--fs-xs); color: var(--muted); font-weight: 600; display: block; margin-bottom: .3rem;
}
.adj-note {
  width: 100%; box-sizing: border-box; background: var(--input-bg);
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); font-family: var(--font-body); font-size: var(--fs-base);
  padding: .45rem .55rem; resize: vertical;
  transition-property: border-color, background-color;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.adj-note:hover:not(:focus) { border-color: var(--border-hover); }
.adj-note:focus-visible {
  outline: var(--focus-ring); outline-offset: 1px;
  border-color: var(--accent); background: var(--panel-2);
}
.adj-note::placeholder { color: var(--muted); opacity: 1; }
.attached { font-size: var(--fs-xs); color: var(--muted); border-left: 2px solid var(--border); padding-left: .55rem; margin: .5rem 0; }
.adj-actions { display: flex; gap: .5rem; align-items: center; }
.btn-solid {
  font-family: var(--font-head); text-transform: uppercase; letter-spacing: .1em;
  font-size: var(--fs-xs); font-weight: 700; color: var(--accent-ink);
  background: var(--accent); border: 0; border-radius: var(--radius);
  padding: .45rem .85rem; cursor: pointer;
}
.btn-solid {
  min-height: 32px;
  transition-property: background-color, box-shadow, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
  box-shadow: var(--shadow-card);
}
.btn-solid:hover { background: var(--accent-hi); box-shadow: var(--shadow-raise); }
.btn-solid:active { transform: translateY(1px); box-shadow: var(--shadow-card); transition-duration: var(--dur-press); }
.btn-solid:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

.btn-plain {
  background: none; border: 0; color: var(--muted); font-size: var(--fs-sm);
  cursor: pointer; padding: .45rem .2rem;
  transition: color var(--dur-base) var(--ease);
}
.btn-plain:hover { color: var(--text); }
.btn-plain:focus-visible { outline: var(--focus-ring); outline-offset: 2px; border-radius: var(--radius); }
.req-chip {
  display: inline-flex; align-items: center; gap: .4rem; font-size: var(--fs-sm);
  color: var(--good); background: var(--good-soft);
  border: 1px solid var(--good-border); border-radius: var(--radius);
  padding: .35rem .6rem; margin-top: .5rem;
}
.adj-error { color: var(--red); font-size: var(--fs-sm); margin-top: .4rem; }


/* ── the blocks on a phone ───────────────────────────────────────────────────
   Everything above is written mobile-first, so these are the places where the
   desktop shape genuinely does not survive a 360px screen rather than a
   wholesale second layout. */
@media (max-width: 719px) {
  /* Three metrics across a phone leaves ~100px each, and "1,100.8" under
     "TONS" starts truncating. Two columns wrap to a second row instead. */
  .metric-strip { grid-auto-flow: row; grid-template-columns: 1fr 1fr; }
  .metric { padding: .45rem .55rem; }

  /* The card head carries a title AND a total. On a narrow screen they collide,
     so the total drops beneath the title and keeps its own line. */
  .sum-card h3 { flex-wrap: wrap; row-gap: .15rem; }
  .sum-card h3 > .sum-value { font-size: var(--fs-base); }

  /* A charge label like "Insurance & compliance" and its amount must not end up
     on one squeezed line with the amount wrapping under a hyphen. */
  .sum-line { gap: .5rem; align-items: baseline; }
  .sum-label { min-width: 0; overflow-wrap: anywhere; }
  .sum-value { flex: none; }

  /* Full-width targets rather than two buttons crammed side by side. */
  .adj-row { flex-direction: column; align-items: stretch; gap: .5rem; }
  .adj-btns { flex-direction: column; }
  .btn-ghost, .btn-solid { width: 100%; padding: .6rem .7rem; }
  .adj-actions { flex-direction: column-reverse; align-items: stretch; gap: .4rem; }
  .adj-actions .btn-plain { text-align: center; }

  /* The per-driver roster is the one part that cannot usefully shrink. Let it
     scroll on its own rather than squeezing three money columns into 360px. */
  .bal-table { min-width: 22rem; }

  .bal-foot { flex-wrap: wrap; row-gap: .1rem; }
}

/* Fine-grained: below ~380px even two metrics are tight, so stack them. */
@media (max-width: 379px) {
  .metric-strip { grid-template-columns: 1fr; }
  /* Same story one strip down: two 20px-mono money columns cannot hold a
     seven-figure YTD at this width, and overflow-wrap:anywhere breaks money
     BETWEEN DIGITS - which on a pay portal reads as a corrupted amount. */
  .stat-row { grid-template-columns: 1fr; }
  /* The lockup - plate plus "CARRIER PAY" - is about 175px here, and at this
     width that wraps the utility row and the header balloons. The product half
     yields; the mark and the tab title still say whose page this is. */
  .brand-sub { display: none; }
}

/* ── work-in-progress bar ────────────────────────────────────────────────────
   A three-pixel strip across the top while a request is in flight.

   IT MUST NOT FLASH. The tester bar taught this the hard way - it appeared for
   as long as a token took and read as a glitch in front of carriers. So app.js
   holds this back for a beat before showing it, and a call that finishes inside
   that window never paints at all. A bar that blinks on every fast click makes
   a fast app feel broken.

   Fixed rather than sticky so it cannot be scrolled away from, and above the
   header without displacing it - a bar that pushed the page down would reflow
   the whole layout on every click, which is the opposite of feeling responsive. */
.busybar {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  z-index: 100; pointer-events: none;
  /* PAINTED BY A BACKGROUND OF ITS OWN, not only by the creeping span inside
     it. The reduced-motion rule below stops that span moving, and an indicator
     that is only a moving highlight is an indicator that is sometimes nothing
     at all. This faint track is the half that always survives. */
  background: var(--accent-soft);
  /* visibility as well as opacity: an element at `opacity: 0` is still laid
     out and still reported as shown, and the pending state is asserted off the
     real cascade rather than off a class list. */
  visibility: hidden;
  opacity: 0; transition: opacity .18s ease-out;
}
.busybar > span {
  display: block; height: 100%; width: 0;
  background: var(--accent);
  /* The glow is what separates it from a static rule at a glance. */
  box-shadow: 0 0 8px rgba(245, 158, 11, .75);
  transition: width .25s ease-out;
}
/* TWO OWNERS, ONE BAR, and this is the only place they meet.

   `.on` belongs to the ADAPTER: any request in flight, held back for a beat
   first so a call that finishes quickly never paints at all (the note above).

   `data-busy` belongs to NAV(): a navigation or a write this page is
   deliberately holding a control for. That one shows immediately and on
   purpose - it exists to answer the tap that would otherwise be repeated, and
   a delayed answer to "did my click register" is the silence that produced
   the repeats.

   Two selectors rather than one shared class, so neither owner can put the bar
   down while the other still wants it up. */
.busybar.on,
:root[data-busy] .busybar { opacity: 1; visibility: visible; }

/* Creeping toward 90% and waiting there is the honest shape: we know work is
   happening, not how much is left. The last 10% is closed only when it really
   finishes, so the bar never sits full while the page is still working. */
.busybar.on > span { animation: busy-creep 12s cubic-bezier(0, .6, .3, 1) forwards; }
.busybar.done > span { width: 100% !important; animation: none; transition: width .16s ease-out; }

@keyframes busy-creep {
  0%   { width: 0; }
  12%  { width: 34%; }
  40%  { width: 68%; }
  70%  { width: 84%; }
  100% { width: 92%; }
}

/* Motion is the whole mechanism here, so with it suppressed the bar becomes a
   plain steady strip: still says "working", says it without movement. */
@media (prefers-reduced-motion: reduce) {
  .busybar { transition: none; }
  .busybar.on > span,
  .busybar.done > span { animation: none; transition: none; width: 100%; }
}

/* ── carrier roster ──────────────────────────────────────────────────────────
   This started as .week-row (a class shaped for a WEEK - date, side, badge,
   total), became a full-width list when 200 name-shaped pills turned out to be
   unscannable, and is now a TABLE, because the row stopped being a name and
   became a record: users, sign-ins, opens, downloads, last seen, staff views.
   Walt: "it could be a table of carriers and the signins number could be a
   column along with a bunch of other metrics".

   It rides on table.detail rather than a new table system, so it inherits the
   hairlines, the header face, the .ta-r numeric alignment and - the part worth
   the reuse - the under-720px rule that turns a table.detail row into a card by
   reading each cell's data-label. */
.carrier-table { min-width: 46rem; }        /* below this the columns collide; .table-scroll scrolls */

/* The header cell IS the sort control. A button rather than a click handler on
   the th, so it is tabbable and announces itself; styled back down to the th's
   own face so the table does not sprout eight chrome buttons. */
.th-sort {
  display: inline-flex; align-items: center; gap: .3rem;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  cursor: pointer; white-space: nowrap;
}
.th-sort:hover { color: var(--text); }
.th-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.th-sort.is-on { color: var(--accent); }
/* Reserved whether or not it is showing, so changing sort column does not
   shuffle every header sideways by the width of a caret. */
.sort-caret { display: inline-block; min-width: .6em; font-size: .8em; }
th.ta-r .th-sort { justify-content: flex-end; }

/* The carrier name is the row's action. The whole <tr> is clickable too, but
   this is what the keyboard reaches and what a screen reader reads as the
   control - a <tr> is not focusable. */
.crow-pick {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; font-weight: 600; color: var(--text); text-align: left;
  cursor: pointer;
  /* The factoring prefixes ($ATLAS_FACTORING ...) make these names long and
     front-loaded with noise; wrapping beats truncating, because the part that
     identifies the carrier is at the END. */
  overflow-wrap: anywhere;
}
.crow-pick:hover { color: var(--accent); }
.crow-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

.crow-row { cursor: pointer; }
.carrier-table tbody tr.crow-row:hover td { background: var(--row-hover); }

/* A zero is a REAL zero - most carriers in Dataverse have never opened the
   portal - but a column of two hundred bold zeros is a wall that hides the
   handful of numbers that are not zero. Dimmed, not hidden: hiding would read
   as "no data", which is a different and wrong claim. */
.carrier-table td.is-zero { color: var(--muted); }
/* "never" is a word in a column of dates and counts, so it is set apart from
   them rather than dressed up to look like one. */
.carrier-table .none { font-style: italic; color: var(--muted); }

.crow-state {
  font-family: var(--font-head); text-transform: uppercase;
  letter-spacing: .08em; font-weight: 600; font-size: var(--fs-xs);
  color: var(--good); white-space: nowrap;
}
/* Not mapped is the one state that blocks the carrier seeing anything, so it is
   the one that gets a colour. */
.crow-state.warn  { color: var(--warn); }
.crow-state.muted { color: var(--muted); }

/* The roster's state cell is a BUTTON now - it opens the carrier's identity
   profile, and "Not mapped" is exactly the phrase somebody wants to click to
   find out why. It has to keep looking like the label it replaced, so the UA
   button chrome is stripped and only the affordances come back: a pointer, an
   underline on hover, and a real focus ring. Colour still comes from the .warn /
   .muted modifiers above, hence `color: inherit` rather than a value - naming
   one here would override them and paint every state the same.

   NOT styled as a .btn. Two hundred buttons is a wall, and this one is a link
   into a detail screen rather than an action on the row. The <span> form of
   .crow-state is still used - by the off-roster rows, which know nothing about
   the carrier to look one up, and by the profile's own badges. */
button.crow-state {
  background: none; border: 0; padding: 0; margin: 0;
  color: inherit; font: inherit; text-transform: inherit;
  letter-spacing: inherit; cursor: pointer; text-align: left;
}
button.crow-state:hover { text-decoration: underline; }
button.crow-state:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px;
}

@media (max-width: 719px) {
  /* Each carrier becomes a card, on the same data-label mechanism the detail
     table already uses - one markup, no second render path. The min-width that
     forces the desktop scroller has to go, or the card would still be 46rem
     wide inside it. */
  .carrier-table { min-width: 0; }
  .carrier-table, .carrier-table tbody, .carrier-table tr, .carrier-table td {
    display: block; width: 100%;
  }
  .carrier-table thead { display: none; }

  .carrier-table tr.crow-row {
    border: 1px solid var(--border); border-radius: var(--radius);
    margin: .45rem .5rem; padding: .25rem 0; background: var(--panel-2);
  }
  .carrier-table tr.crow-row td {
    display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
    border: 0; padding: .28rem .7rem; text-align: left;
  }
  .carrier-table tr.crow-row td::before {
    content: attr(data-label);
    font-family: var(--font-head); font-size: .68rem; letter-spacing: .09em;
    text-transform: uppercase; color: var(--muted); flex: 0 0 auto;
  }
  /* The name is the card's heading, so it loses the label and takes the width. */
  .carrier-table tr.crow-row td:first-child { padding: .4rem .7rem .2rem; }
  .carrier-table tr.crow-row td:first-child::before { content: none; }
  .carrier-table tr.crow-row td:first-child .crow-pick { font-size: 1rem; }
}

/* A LANDSCAPE PHONE clears the 720px width gate carrying ~390px of height,
   and a sticky 125px header is a third of that, pinned forever - the other
   half of "headers oddly persist". Short viewports scroll the header away
   the way a phone does, and the pick-bar goes back to the real top with it,
   because its --head-h offset only means something under a pinned header. */
@media (min-width: 720px) and (max-height: 500px) {
  header.top { position: static; }
  .pick-bar { top: 0; }
}

/* ── SURFACES ─────────────────────────────────────────────────────────────────
   The gradient is declared as background-IMAGE so it layers over each card's
   own background-color; the light theme sets the token to none and these all
   quietly flatten. Hover rules that set the background shorthand reset the
   image for the duration of the hover, which is fine - the ground shift is
   the louder signal. */
.sum-card, .stat, .net-strip, .notice, .hero-pay { background-image: var(--surface-grad); }

/* ── THE HERO - what someone opened this app to learn ─────────────────
   The carrier home led with a stat tile the same size as its neighbours, so
   the latest settlement - the number the whole page exists for - dressed like
   a footnote. One card, one display-size figure, tappable straight into the
   week it names: it carries the same data-side/data-week pair a week row
   does, so the one delegated handler routes it and there is no second path. */
.hero-pay {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-1);
  width: 100%;
  margin: 0 0 var(--sp-3);
  padding: var(--sp-4) var(--sp-5) var(--sp-4) calc(var(--sp-5) + 3px);
  background-color: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-raise);
  color: var(--text);
  font-family: var(--font-body);
  text-align: left;
  overflow: hidden;
}
/* The spine is painted inset rather than as border-left: a 3px border joins
   the radius geometry and visibly lopsides the top-left corner. */
.hero-pay::before {
  content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 3px;
  background: var(--accent);
}
.hero-label {
  font-family: var(--font-head); font-size: var(--fs-xs);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  color: var(--muted);
}
.hero-value {
  font-family: var(--font-mono); font-size: var(--fs-2xl); font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: var(--lh-tight);
  color: var(--text); overflow-wrap: anywhere;
}
.hero-value.tone-neg { color: var(--red); }
/* The running week's figure is an estimate - same muted contract as
   .net-strip.est, because a mid-week total is incomplete, not final. */
.hero-value.tone-est { color: var(--muted); }
.hero-note { font-size: var(--fs-sm); color: var(--muted); }
/* The status wears the same badge the week row wears, pinned clear of the
   figure. "Processing" on the front page beats a surprise inside. */
.hero-pay .badge { position: absolute; top: var(--sp-3); right: var(--sp-3); }
button.hero-pay {
  cursor: pointer;
  transition-property: background-color, border-color, box-shadow, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
button.hero-pay:hover { border-color: var(--accent); box-shadow: var(--shadow-pop); }
button.hero-pay:active { transform: translateY(1px); transition-duration: var(--dur-press); }
button.hero-pay:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }

/* ── THEME TOGGLE ───────────────────────────────────────────────────────
   The glyph is drawn - a half-filled circle, the universal contrast mark -
   because an emoji sun renders as tofu-or-colour depending on the device and
   an SVG asset would be the only external image on a page whose CSP
   deliberately allows none. */
.theme-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border-control); border-radius: var(--radius);
  cursor: pointer;
  transition-property: background-color, border-color, color, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.theme-btn:hover { border-color: var(--accent); color: var(--accent); }
.theme-btn:active { transform: translateY(1px); transition-duration: var(--dur-press); }
.theme-btn:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
.theme-ic {
  width: 15px; height: 15px;
  border: 2px solid currentColor; border-radius: 50%;
  background: linear-gradient(90deg, currentColor 0 50%, transparent 50%);
}

/* ── SEARCH, folded behind its button on a phone ─────────────────────
   Desktop never sees this control - from 720px up the field itself lives in
   the header. display:none here IS the desktop rule; the phone block above
   un-hides it with :not([hidden]). */
.search-toggle {
  display: none;
  flex: 0 0 auto;
  align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border-control); border-radius: var(--radius);
  cursor: pointer;
  transition-property: background-color, border-color, color, transform;
  transition-duration: var(--dur-base);
  transition-timing-function: var(--ease);
}
.search-toggle[aria-expanded="true"] {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent);
}
.search-toggle:hover { border-color: var(--accent); color: var(--accent); }
.search-toggle:active { transform: translateY(1px); transition-duration: var(--dur-press); }
.search-toggle:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
/* The same drawn magnifier the field wears, one size up for the fat target. */
.search-toggle > span { position: relative; width: 18px; height: 18px; }
.search-toggle > span::before {
  content: ''; position: absolute; left: 1px; top: 1px;
  width: 10px; height: 10px;
  border: 2px solid currentColor; border-radius: 50%;
}
.search-toggle > span::after {
  content: ''; position: absolute; left: 11px; top: 12px;
  width: 6px; height: 2px; background: currentColor;
  border-radius: 1px; transform: rotate(45deg);
}

/* ── DEPARTURE ─────────────────────────────────────────────────────────────
   The tapped row says it heard you. The busybar alone is 3px at the far edge
   of the screen - invisible in sunlight, and "nothing happened" earns a
   second tap. The global reduced-motion rule freezes the spin into a static
   ring, which still reads as "busy". */
.week-row.is-loading, button.hero-pay.is-loading { opacity: .65; }
.week-row.is-loading::after, button.hero-pay.is-loading::after {
  content: ''; flex: 0 0 auto;
  width: 14px; height: 14px;
  border: 2px solid var(--muted); border-top-color: var(--accent);
  border-radius: 50%; margin-left: .5rem;
  animation: row-spin .7s linear infinite;
}
@keyframes row-spin { to { transform: rotate(360deg); } }

/* ── ARRIVAL ─────────────────────────────────────────────────────────────
   A view swap that repaints in one frame reads as a flicker; the same content
   fading in over 150ms reads as navigation. Opacity ONLY: a transform here
   would re-parent the fixed action bar and break position:sticky in the table
   header for the length of the animation - both invisible until someone
   scrolls at the wrong moment. show() restarts the class; showQuiet() stays
   out of it because it repaints under a live caret. The reduced-motion rule
   above already zeroes every animation with !important. */
#view.view-anim { animation: view-in var(--dur-base) var(--ease); }
@keyframes view-in { from { opacity: 0; } to { opacity: 1; } }

/* ── PRINT ───────────────────────────────────────────────────────────────────
   A settlement is a document people print and file, and until now printing one
   produced a navy page: most browsers drop background colours by default, so
   the panels vanished and the near-white body text printed near-white ON white.
   The result was a sheet of blank paper with a few amber words on it.

   Rather than restate a hundred rules, the TOKENS are redefined for paper and
   the whole stylesheet follows. That is the payoff for the palette living in
   one place - the light theme this file never had is nine declarations.

/* ── the internal home's find results ──────────────────────────────────────
   Only two ideas here, because everything else reuses .result / .chip: a
   driver's block, and the INERT variant of a row or chip. A hit with nowhere to
   go must not wear a pointer and an accent hover - that pairing is the whole
   visual language for "this is a button", and honouring it on something that
   does nothing is how a screen starts feeling broken. */
.find-driver { padding: .55rem 0; border-bottom: 1px solid var(--border); }
.find-drivers .find-driver:last-child { border-bottom: 0; }
.find-name { margin: 0 0 .4rem; font-weight: 600; }
.find-code {
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--muted); font-weight: 400; margin-left: .4rem;
}
.result.is-off { cursor: default; }
.result.is-off:hover { border-color: var(--border); }
.chip.is-off { cursor: default; opacity: .75; }
.chip.is-off:hover { border-color: var(--border); color: var(--muted); }

   The Download PDF button remains the better route and is not going anywhere;
   this is for the person who hits Ctrl+P anyway, which is most people. */
@media print {
  /* Matched to the [data-theme] blocks' specificity, or a saved light theme
     would outrank the paper palette and print its screen colours. */
  :root, :root[data-theme="light"], :root[data-theme="dark"] {
    --bg:          #FFFFFF;
    --panel:       #FFFFFF;
    --panel-2:     #F4F6F9;
    --input-bg:    #FFFFFF;
    --border:      #94A3B8;
    --border-2:    #CBD5E1;
    /* Not optional, and not cosmetic: an UNDEFINED custom property makes
       `border: 1px solid var(--border-control)` invalid at computed-value
       time, which drops the whole shorthand to its initial values - and
       border-color's initial is currentColor. Leave this out and every
       control that survives the print hide-list prints a border in the
       colour of its own text. The translucent screen value would also
       composite to near-white on paper. Slate-500 is 4.76:1 on white. */
    --border-control: #64748B;
    --text:        #0F172A;
    --muted:       #475569;
    --accent:      #8A5A00;   /* amber is illegible on white; this is its ink */
    --accent-soft: #FDF3E0;
    --accent-ink:  #0F172A;
    --shadow-pop:  none;
    --shadow-card: none;
    --shadow-raise: none;
    --surface-grad: none;
    --glass:        #FFFFFF;
    /* Status tones in their paper inks, or a saved theme decides how a
       printed badge reads - and dark's bright tones are illegible on white. */
    --good:         #047857;
    --warn:         #8A5A00;
    --info:         #1D4ED8;
    --red:          #B91C1C;
  }

  html, body { background: #FFFFFF; color: #0F172A; }

  /* Chrome for operating the app, none of which means anything on paper. */
  .busybar, .skip-link, .tabs, .appmenu-wrap, .sides, .actions, .pick-bar,
  .wk-pick, .filter-bar, .crumb, .adj-row, .adj-form, .tester-bar,
  .search-wrap, .theme-btn, .search-toggle { display: none !important; }

  /* The header stops being sticky - a fixed element repeats on every sheet in
     some engines and is clipped in others. */
  header.top { position: static; background: #FFFFFF; border-bottom: 1px solid var(--border); }

  .wrap { max-width: none; padding: 0; }

  /* The horizontal scroller has no meaning on paper, and leaving it means the
     columns past the fold are simply not printed. */
  .table-scroll { overflow: visible; border: 1px solid var(--border); }
  table.detail { font-size: 10pt; }
  table.detail thead th { position: static; box-shadow: none; border-bottom: 1px solid var(--border); }

  /* A driver's block should not be split across sheets when it can be helped,
     and a settlement total stranded alone on page three is the classic version
     of this. */
  tr, .sum-card, .net-strip, .bal-row { break-inside: avoid; page-break-inside: avoid; }
  thead { display: table-header-group; }   /* repeat column headings per sheet */

  /* Backgrounds carry meaning here - the carrier/driver banding, the amber on
     the net strip - so ask for them rather than letting the UA flatten it. */
  .row-carrier, .row-driver, .net-strip, table.detail thead th {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
}

/* == THE CONTROL THAT WAS ACTIVATED ========================================
   The bar above answers "is the app doing anything". It cannot answer "is it
   doing what I just asked" - with four tabs and a roster of carriers on
   screen, a page-level strip does not say WHICH tap is being worked on. So the
   activated control is marked for the length of its own request.

   Held back and marked as WORKING rather than merely greyed: `disabled` alone
   reads as "not available to you", which is a different and more
   discouraging claim. aria-busy is the hook because app.js sets it on every
   control it holds, and because assistive tech reads the same attribute. */
[aria-busy="true"].week-row,
/* .crow-pick, not .crow: the roster is a table now, and the control nav() marks
   is the name button inside the row - a <tr> is not activated and never carries
   the attribute. */
[aria-busy="true"].crow-pick,
[aria-busy="true"].result,
[aria-busy="true"].btn,
[aria-busy="true"].tab,
[aria-busy="true"].chip,
[aria-busy="true"].side-btn,
[aria-busy="true"].appmenu-item {
  cursor: progress;
  opacity: .55;
}

/* Colour is not available in High Contrast, so the held control is named by an
   outline instead of by being dimmed, and the bar by an edge of its own. */
@media (forced-colors: active) {
  .busybar { border-bottom: 2px solid Highlight; forced-color-adjust: none; }
  [aria-busy="true"] { outline: 1px dashed CanvasText; }
}

/* == THE SMART CARRIER LIST ===============================================
   Walt: "the profiles that display should only be recent profiles with data
   behind them not just a list of all historical carriers". The ORDERING now
   lives in the table's own sort - carrierSort() plus the "You opened" column -
   so rankCarriers() and its .crow-recent badge are gone. What has to stay is
   the part that must be VISIBLE: a carrier this person opened is marked on the
   row itself, because a position means nothing the moment the roster is sorted
   by some other column.

   On the first cell rather than the <tr>: a border on a table row is not
   painted reliably once the cells have their own backgrounds, which the
   carrier/driver banding gives them. */
.crow-row.is-recent td:first-child { border-left: 3px solid var(--accent); }

/* Why the list is shorter than the whole. A filtered list that does not say so
   reads as a complete one, and staff have to be able to tell "not here" from
   "filtered out". The ORDER half of this sentence moved to the column headers,
   which carry aria-sort and a caret - see listNote() in app.js. */
.list-note {
  margin: 0 0 var(--sp-3);
  color: var(--muted);
  font-size: var(--fs-sm);
}

/* ── THE SIGN-IN SCREEN ───────────────────────────────────────────────────
   This screen is what a carrier meets before anything else, and until now it
   was one button that handed straight to Microsoft's account picker. It now
   carries a field, so it needs the things a field needs.

   Measured at 320px, because an owner-operator on an old phone is the realistic
   worst case and every control here has to stay reachable there. */
.signin-form { max-width: 26rem; }

/* The help text under the field. Quieter than the label, but it is REAL
   guidance - "which of my addresses" is the question a carrier actually has -
   so it is muted rather than tiny: --muted at --fs-sm clears 4.5:1 on this
   background, and shrinking it further would not help anybody read it. */
.signin-hint {
  margin: calc(var(--sp-1) * -1) 0 var(--sp-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
  color: var(--muted);
}

/* Shown exactly when the "I cannot sign in" form is suppressed — see offersHelp()
   in app.js. It carries the only bare <a> on this screen, and portal.css has
   never had an `a` rule (every other link in the portal is a .btn), so without
   this the UA's default #0000EE lands on a near-black page at 1.90:1 — on the
   one screen where this mailto is the carrier's last route to a human.
   --accent is 8.3:1 there, and the underline keeps the affordance from being
   carried by colour alone (SC 1.4.1).

   AN ID SELECTOR, BECAUSE THE PAGE WRITES AN ID. This shipped as
   `.signin-nohelp a` while app.js emits `id="signin-nohelp"`, and a class
   selector cannot match an id: the rule was inert and the link rendered at the
   browser default the whole time. It went unnoticed because the test asserted
   the rule's SOURCE TEXT and its fixture repeated the same mistake. The
   selector now names the id the page actually renders, and
   portal-signin-css.test.js measures the computed colour of that element
   against a stand-in for the UA default rather than reading this line. */
#signin-nohelp a { color: var(--accent); text-decoration: underline; }

/* THE ERROR. Colour is not the signal - it is one of three, alongside the mark
   and the border - because SC 1.4.1 forbids colour as the only channel, and
   because this page is read in a truck, in sunlight, one-handed. */
.signin-err {
  /* NO `display` DECLARATION. A <p> is already block, so it bought nothing - and
     it OUTRANKED the UA's [hidden] rule, so every carrier saw an empty red strip
     under the field before they had typed anything. See the [hidden] rule at the
     top of this file. */
  margin: var(--sp-2) 0 0;
  padding: var(--sp-2) var(--sp-3);
  border-left: 3px solid var(--red);
  border-radius: 0 var(--radius) var(--radius) 0;
  /* --red-soft, NOT a literal rgba. A raw rgba is invisible to the light-theme
     override (see the token's own note), so the hardcoded dark wash this
     shipped with would have followed a carrier into light mode unchanged. The
     token is per-theme and is what portal-contrast enforces. */
  background: var(--red-soft);
  line-height: var(--lh-base);
  /* SC 1.4.3. The sentence inherited `.err { color: var(--red) }`, and the .08
     red tint over --bg composites to rgb(33,27,44) - which puts #EF4444 at
     4.44:1 at .9rem, under the 4.5:1 floor. (On the untinted page background the
     same red was 4.74:1 and passed; the tint is what pushed it under.) --text is
     15.2:1 there, and the red still carries the signal three other ways: the
     left border, the "!" mark, and the tint itself. */
  color: var(--text);
}
/* tabindex=-1 makes this focusable so a failure can MOVE focus here. Without an
   outline, a keyboard user then stands somewhere they cannot see.

   `:focus`, NOT `:focus-visible`. Focus arrives here PROGRAMMATICALLY, and after
   a mouse click on Continue Chrome does not consider the resulting focus
   "visible" - so a sighted mouse user would get no ring on the element focus had
   just been moved to. */
.signin-err:focus { outline: 2px solid var(--red); outline-offset: 2px; }
.err-mark {
  display: inline-block;
  width: 1.15rem; height: 1.15rem;
  margin-right: var(--sp-1);
  border-radius: 999px;
  background: var(--red);
  color: var(--accent-ink);
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.15rem;
  text-align: center;
  vertical-align: -.15rem;
}
/* The plain sentence, in body colour rather than red: it is the thing to READ,
   and full-contrast text reads better than coloured text does. */
.err-title { color: var(--text); }
/* The code, last and quiet. Nobody acts on it; somebody quotes it. */
.err-code {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}

/* NOT .actions. On a phone .actions is position:fixed at the bottom of the
   viewport, which is right for "download this settlement" and wrong for the
   button that submits the field directly above it. */
.signin-actions,
.signin-alt { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-4) 0 0; }
.signin-actions { margin-top: var(--sp-3); }
.signin-actions .btn,
.signin-alt .btn {
  /* 44px is the target-size floor (SC 2.5.8). These are the controls a stuck
     carrier is aiming at, so they are the last place to be stingy. */
  min-height: 44px;
  flex: 1 1 auto;
}
/* Both alternates are escape hatches rather than the main path - quieter than
   Continue, and still well clear of the contrast floor. */
.signin-alt .btn { color: var(--muted); }
.signin-alt .btn:hover { color: var(--text); }

@media (max-width: 380px) {
  /* One per row rather than two cramped ones: "Use a different account" does not
     fit beside anything at this width, and a truncated escape hatch is not an
     escape hatch. */
  .signin-alt .btn { flex: 1 1 100%; }
}

@media (forced-colors: active) {
  /* The tinted panel and the red border both collapse to system colours here,
     so without this the error would lose every visual channel but the glyph. */
  .signin-err { border: 1px solid CanvasText; border-left-width: 3px; }
  .err-mark { border: 1px solid CanvasText; forced-color-adjust: none; }
}
