/* ==========================================================================
   Thanks for Business — one stylesheet, no framework, one web font.
   Dark, dense, and built to be read on a phone in a driveway.
   ========================================================================== */

/* It used to say "no web fonts", and that was a real rule rather than a
   boast: a font is a render-blocking request on a van's phone, a third-party
   origin in the CSP if it comes from Google, and a layout shift when it
   arrives. Three of those four objections still hold and the rule still
   holds for body text, which is why body text has not changed — it is the
   system stack, it is already on the device, and it costs nothing.

   What changed is that the brand has a typeface and the wordmark is set in
   it. One face, Barlow Condensed 800 italic, for headings only:

   * **Self-hosted**, so `font-src 'self'` in app/security.py is unchanged
     and no CSP directive is loosened. Nothing is fetched from Google.
   * **One file, 22 KB**, Latin subset, woff2 only. There is no .woff
     fallback because every browser that can run this application supports
     woff2.
   * **`font-display: swap`**, so the heading renders immediately in the
     system font and is replaced when the file arrives. A heading that is
     invisible for three seconds on a bad connection is worse than a heading
     in the wrong font, and this page is read in a driveway.
   * **Headings only**, so a failed or slow font never touches the text
     somebody is actually reading. The fallback stack is named explicitly on
     the rule, so the swap is condensed-to-system rather than
     condensed-to-whatever.

   `app/static/brand/fonts/` also holds Barlow Condensed 700 and Barlow 700.
   Neither is declared here, because an undeclared face is never downloaded
   and a declared one nothing uses is dead weight in a file people read.
   They are in the repository because the logo files were built from them
   and the licence (OFL-LICENSE.txt) ships with them. */
@font-face {
  font-family: "Barlow Condensed";
  font-style: italic;
  font-weight: 800;
  font-display: swap;
  src: url("../brand/fonts/barlow-condensed-latin-800-italic.woff2") format("woff2");
}

/* Colour tokens — the Thanks for Business palette (app/static/brand/).
   Charcoal #1F2A33, Slate #334858, Orange #D3541F, Stone #E8E5DF, from
   Zach's brand sheet. The sheet's "Reverse / Dark Background" treatment is
   the one this product uses, so the four brand colours are joined by two
   more charcoals either side of #1F2A33 to give the three surfaces the
   application needs.

   Every ratio below is computed with app/contrast.py, not estimated, and the
   working is in ACCESSIBILITY.md. Two kinds of threshold apply and they are
   not the same number: body text needs 4.5:1 against the surface behind it
   (WCAG 1.4.3), and the *boundary* of anything you can operate needs 3:1
   (WCAG 1.4.11). That second one is why --line and --control-line are
   separate tokens.

   **Every token here meets or beats the ratio it had before the rebrand.**
   The thresholds did not move; the colours did. Where a number is lower than
   it used to be it is because --panel itself got lighter (#1E2127 to
   #1F2A33, which the brand sheet fixes), so --text on it is 11.62:1 where it
   was 12.93:1 — still two and a half times the 4.5:1 it has to clear. See
   DECISIONS.md D-126. */
:root {
  /* The three surfaces. --panel is Charcoal exactly; --bg and --panel-2 are
     the same hue and saturation moved down and up in lightness (16.1% to
     12% and 20%), so the ramp is one colour rather than three guesses. The
     old ramp separated its surfaces by 1.10:1 and 1.07:1, which made
     --panel-2 almost invisible against --panel; this one is 1.14:1 and
     1.15:1, which is still subtle and actually distinguishable. */
  --bg:        #171F26;
  --panel:     #1F2A33;
  --panel-2:   #27343F;

  /* Decorative rules only: table row separators, panel edges, the divider
     under a panel head. 1.23:1 against --panel, which is deliberate — these
     are not component boundaries and 1.4.11 does not reach them. Raising
     them to 3:1 would turn a dense table into a wireframe. */
  --line:      #2A3946;
  --line-soft: #25323C;

  /* Slate, the brand's second dark, as the one structural edge that is meant
     to be seen: the top bar's lower border, a panel's outer edge, the bottom
     tab bar on a phone. 1.54:1 against --panel — heavier than --line and
     nothing like a control boundary, which is the point.

     Slate is *not* used for two things it might look right for, and both
     refusals are arithmetic rather than taste. It cannot be --control-line:
     at 1.34:1 against --panel-2 it misses 1.4.11's 3:1 by a mile. And it
     cannot be a text surface: --muted on it is 4.32:1, which fails 1.4.3, so
     a panel painted Slate would quietly break every secondary label on it. */
  --line-strong: #334858;

  /* The boundary of anything a person operates: an input, a select, a
     textarea, a button, the switch track. 3.20:1 against --panel-2,
     3.68:1 against --panel, 4.19:1 against --bg, so it clears 1.4.11 on
     every surface this application puts a control on. Now in the brand's
     own blue-grey family rather than the old neutral, and a shade lighter
     than before because the surfaces it sits on are lighter. */
  --control-line: #678399;
  --control-line-hover: #8BA0B1;

  /* The focus ring, and the one colour in this file a tenant cannot change.
     `--accent` is overridden per company by an inline style in base.html, it
     is validated as a hex string and nothing checks what it looks like, so a
     company that typed in their real brand navy used to get a focus ring at
     1.15:1 — invisible, on every control, for every keyboard user in that
     company. WCAG 1.4.11 asks 3:1 of a focus indicator and this is not a
     threshold to leave to whoever is holding the colour picker.
     11.62:1 against --panel, 13.25:1 against --bg, 10.13:1 against --panel-2.
     Stone, the brand's own light, so the ring belongs to the palette without
     belonging to the tenant. Same reasoning as the skip link below: a focus
     ring is a system affordance, not branding. See ACCESSIBILITY.md and
     app/contrast.py. */
  --focus-ring: #E8E5DF;

  /* Ink for anything sitting *on* the brand colour: the mark in the header,
     a primary button's label, the count inside an active tab. Black or
     white, chosen per tenant by `contrast.ink_for` and overridden by the one
     inline style in base.html; this literal is the right answer for the
     default accent — black on Orange is 5.06:1 and white on it is 4.15:1,
     which fails, so this is a case where the choice genuinely matters.

     It exists because of D-123. The brand colour is no longer used as a text
     colour anywhere — not for a link, a label, a status word or a heading —
     and the four places text still touches it — the mark in the header, the
     mark on the sign-in card, a primary button and an active tab's count —
     are places where the text is *on* it and the ink is therefore chosen
     rather than fixed. (The switch thumb is a fifth use of this token, as a
     fill rather than as ink, for the same reason: it sits on the accent.) Taking the
     better of black and white never falls below 4.58:1 on any colour in
     sRGB, which is the whole reason this is safe to hand to a colour picker.
     See app/contrast.py:ink_for for the arithmetic. */
  --accent-ink: #000000;

  /* The brand colour on hover: the same colour moved 12% toward the opposite
     of its ink, so the label can only become easier to read, never harder.
     Overridden per tenant by the same inline style as --accent. This literal
     is `contrast.hover_fill("#D3541F")`, and black on it is 6.00:1. */
  --accent-hover: #D8693A;

  --text:      #E8E5DF;   /* Stone. 11.62:1 on --panel */
  --muted:     #A7B0B7;   /*  6.64:1 on --panel, 5.78:1 on --panel-2 */
  /* Orange, the brand accent. No contrast threshold applies to it since
     D-123 — it is a fill, a border and a tint, never a glyph — and a tenant
     replaces it with anything they like. Worth knowing anyway: as text it
     would be 3.52:1 on --panel and would fail, which is exactly why D-123
     took it out of every text position before the rebrand rather than
     after. */
  --accent:    #D3541F;
  --accent-dim:#823312;

  /* Used as text far more than as fill: "is-good" on a money figure,
     "is-bad" on an overdue date, tag-won, tag-lost, the pause banner's own
     heading. So both clear 4.5:1 on the darkest and the lightest surface
     they land on. Re-derived for the new surfaces rather than carried over:
     the old pair cleared the bar against #1E2127, and #1F2A33 is lighter, so
     keeping them would have eaten the margin. Hues held so a page still
     reads as the same product, and --bad kept well clear of the accent's
     hue (1.50:1 between them) so an overdue date never reads as a button.
     The rgba() tints built from the old values are left alone on purpose: a
     tint is a fill, not text. */
  --good:      #55B394;   /*  5.75:1 on --panel, 5.01:1 on --panel-2 */
  --bad:       #DB8383;   /*  5.28:1 on --panel, 4.60:1 on --panel-2 */

  --radius: 3px;
  --gap: 16px;
  --topbar-h: 48px;   /* keep in step with .topslim min-height */
  --tap: 44px;        /* iOS minimum touch target */
  /* The PM module's fixed bottom bar. Here rather than beside its own rules
     so that every length a layout reserves space for is in one block. */
  --pm-tabbar-h: 64px;
  /* WCAG 2.2 2.5.8 Target Size (Minimum). --tap is what a phone wants;
     this is the floor a mouse-driven desktop control may not go below. */
  --target-min: 24px;

  /* Safe-area insets go through variables rather than being called inline, so
     there is a single place to change them and a test harness can override
     them to emulate a device headless Chrome cannot report. */
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --sar: env(safe-area-inset-right, 0px);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, "Noto Sans", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

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

html {
  -webkit-text-size-adjust: 100%;
  /* Anything scrolled into view — a focused input, an anchor — clears the
     sticky bar instead of landing underneath it. Includes the status-bar inset
     because the bar sits below it (see .topslim). */
  scroll-padding-top: calc(var(--topbar-h) + var(--sat) + 8px);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  /* Relative, not 15px, and that one character is the whole Larger text
     feature. Every other size in this sheet is already in `rem`, so they
     all followed the root when the toggle raised it; the body's own was
     the single absolute size left, which meant paragraphs, list items,
     flash messages and every line of explanatory copy stayed at 15px
     while the furniture around them grew. The control aimed at people who
     cannot read the default size left the reading at the default size.
     0.9375rem is 15px at the browser's own default. */
  font-size: 0.9375rem;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 { margin: 0 0 .35em; font-weight: 600; line-height: 1.2; }

/* The one place the brand typeface is used in the application, and the
   reason it is `h1` rather than a class: every page title in this product
   is an `h1`, so naming the element is the way to catch all of them without
   a class somebody forgets on the next screen. h2 and h3 stay on the system
   stack — a page set entirely in heavy condensed italic is a poster, not a
   tool.

   `--font` is repeated after the family rather than left to inherit, so the
   `font-display: swap` fallback is the system stack and not whatever the
   browser picks for an unknown condensed family. Letter-spacing goes back to
   0: Barlow Condensed is already narrow and the -0.01em that suited the
   system font closes it up too far. */
h1 {
  font-family: "Barlow Condensed", var(--font);
  font-style: italic;
  font-weight: 800;
  /* Larger than the 1.5rem it replaces because a condensed face at the same
     size reads smaller — this is the same optical size, not a bigger one. */
  font-size: 1.75rem;
  letter-spacing: 0;
  text-transform: uppercase;
  line-height: 1.1;
}
h2 { font-size: 1rem; }
h3 { font-size: .875rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
p  { margin: 0 0 .8em; }

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

.muted { color: var(--muted); }
.small { font-size: .8125rem; }
.is-good { color: var(--good); }
.is-bad  { color: var(--bad); }

/* Focus is always visible. This is a work tool; people tab through it.

   There is no `outline: none` anywhere in this file and there must never be
   one without a replacement indicator in the same rule — tests/
   test_accessibility.py fails the build on a bare one.

   The ring is --focus-ring and never --accent, because --accent is the
   tenant's and comes with no contrast guarantee. At 12.93:1 on --panel it
   clears WCAG 1.4.11's 3:1 by a wide margin on every surface and on every
   company's palette, including the ones nobody has chosen yet. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 2px;
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------------------------ skip link --
   The first thing in the tab order on every page. The signed-in nav is up to
   sixteen links, and without this a keyboard user tabbed through all of them
   on the way to the page body — on every single navigation, all day.

   It is in the document and focusable at all times (not display:none, which
   is not focusable), and only moves into view when it has focus. It lands
   below the sticky bar rather than under it, which is WCAG 2.2 2.4.11 Focus
   Not Obscured applied to the one control most likely to trip it.
   ------------------------------------------------------------------------ */
.skiplink {
  position: fixed;
  top: calc(var(--sat) + 8px);
  left: 8px;
  z-index: 100;
  /* Off-screen rather than hidden, so it keeps its place in the tab order. */
  transform: translateY(-200%);
  /* Deliberately NOT var(--accent).

     A tenant sets their own accent colour, it is validated as a hex string
     and nothing checks its contrast, and the demo company's is #2F7D6E —
     against which the dark ink this used to carry is 3.62:1, under the 4.5:1
     a short label needs. The skip link is a system affordance rather than
     branding, so it takes the panel surface and the body ink: 12.93:1 on
     every tenant's palette, including the ones nobody has chosen yet. The
     focus ring around it is still the accent, which only has to clear 3:1.
     See ACCESSIBILITY.md, "the tenant accent colour". */
  background: var(--panel);
  color: var(--text);                       /* 12.93:1 */
  border: 2px solid var(--control-line);
  font-size: .875rem; font-weight: 600;
  padding: 9px 13px;
  border-radius: var(--radius);
  text-decoration: none;
  /* --tap, not --target-min. 2.5.8 asks 24 and this is a keyboard affordance
     that a thumb will rarely find, but scripts/mobile_audit.py holds every
     control in this application to 44 on a phone and an exception here would
     be an exception somebody copies. */
  min-height: var(--tap);
  display: inline-flex; align-items: center;
}
.skiplink:focus {
  transform: translateY(0);
}


/* ============================================================ the nav -- */

/* Three shapes, one definition in app/nav.py: a fixed rail on a desktop, a
   fixed tab bar at the bottom of a phone, and the sheet behind its More
   button. All three are in the document on every page; this file decides
   which are visible, so nothing depends on a script having run.

   The page ships viewport-fit=cover and an Apple translucent status bar, so
   installed on a phone the layout extends under the notch and the home
   indicator. Every fixed edge below carries the matching inset, and the tab
   bar carries the bottom one twice over: once as padding so the labels clear
   the home indicator, and once in the page's own bottom padding so the last
   row of a list is not sitting underneath it. */

:root {
  --rail-w: 232px;
  --navtabs-h: 60px;
  --topslim-h: 48px;
}

/* ------------------------------------------------------------- shared -- */

.brand-mark {
  background: var(--accent); color: var(--accent-ink);
  font-weight: 700; font-size: .75rem; letter-spacing: .06em;
  padding: 3px 6px; border-radius: 2px;
}
.brand-logo { display: block; height: 22px; width: auto; }
.brand-text {
  font-weight: 600; font-size: .9375rem;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* One link, in the rail or in the sheet. A row rather than a pill: a rail of
   pills reads as a toolbar, and this is a list. */
.navlink {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px 8px 13px;
  min-height: var(--target-min);
  font-size: .875rem; text-decoration: none; color: var(--muted);
  /* The active state is a bar on this edge, so every row reserves the
     space and nothing shifts sideways when one lights up. */
  border-left: 3px solid transparent;
}
.navlink:hover { color: var(--text); background: var(--panel-2); }

/* The active item: an orange bar and white text, not a filled block.

   A tinted block behind the label was the old answer and it was the wrong
   one twice over. It put the brand colour behind text, which is a contrast
   question nobody can answer for a colour a customer typed into a picker;
   and at twelve per cent on Charcoal it read as a brown smear rather than
   as a selection. A left bar is a border, which has no text sitting on it,
   and the label simply goes to full Stone: 11.62:1 on the panel. See
   app/contrast.py and D-123. */
.navlink.is-active {
  color: var(--text);
  border-left-color: var(--accent);
  background: var(--panel-2);
  font-weight: 600;
}
.navlink-quiet { font-size: .8125rem; }
button.navlink {
  width: 100%; text-align: left; background: none; cursor: pointer;
  font-family: inherit; border-top: 0; border-right: 0; border-bottom: 0;
}
.nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* A count, in the brand colour with its own ink on it. `--accent-ink` is
   computed black or white by contrast.ink_for, so this stays readable
   whatever a company picks. Pushed to the far edge by the margin rather
   than by a spacer element. */
.nav-badge {
  margin-left: auto;
  background: var(--accent); color: var(--accent-ink);
  font-size: .6875rem; font-weight: 700; line-height: 1;
  padding: 3px 6px; border-radius: 9px;
  min-width: 18px; text-align: center;
}

/* The general-purpose spacer: pushes whatever follows it to the far end of
   a flex row. The navigation used to have a second copy of this, hidden at
   the phone breakpoint, and pages that borrowed the wrong one lost their
   button alignment on the width where tidiness matters most. There is one
   now, it is this, and the rail has no use for it: its foot is a block. */
.spacer { flex: 1; }

.navgroup { padding: 6px 0; border-bottom: 1px solid var(--line-soft); }
.navgroup:last-child { border-bottom: 0; }
.navgroup-head {
  margin: 6px 0 2px; padding: 0 13px;
  font-size: .6875rem; font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted);
}

/* The search box. In the rail on a desktop and in the slim bar on a phone,
   because the answer to "what do we have on this company" should not depend
   on remembering which list to open. */
.navsearch { display: flex; gap: 6px; padding: 10px 12px; }
.navsearch-input {
  flex: 1; min-width: 0;
  background: var(--panel-2); border: 1px solid var(--line);
  color: var(--text); border-radius: var(--radius);
  padding: 7px 9px; font-size: .875rem; font-family: inherit;
  min-height: var(--target-min);
}
.navsearch-input::placeholder { color: var(--muted); }
.navsearch-go {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: var(--radius); cursor: pointer;
  min-width: var(--target-min); min-height: var(--target-min);
  font-size: 1rem; line-height: 1;
}
.navsearch-go:hover { border-color: var(--control-line-hover); }
.navsearch-go { display: inline-flex; align-items: center; justify-content: center; }
.navsearch-svg {
  width: 15px; height: 15px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round;
}

/* --------------------------------------------------------- the rail -- */

.sidebar {
  position: fixed; inset: 0 auto 0 0; width: var(--rail-w); z-index: 30;
  display: none; flex-direction: column;
  background: var(--panel);
  border-right: 1px solid var(--line-strong);
  padding-top: var(--sat);
  padding-left: var(--sal);
}
.sidebar-brand {
  display: flex; align-items: center; gap: 9px;
  padding: 12px 13px; text-decoration: none; min-width: 0;
  border-bottom: 1px solid var(--line-soft);
}
/* `min-height: 0` is the load-bearing declaration here and it looks like
   noise, which is why it gets a comment.

   A flex item's `min-height` defaults to `auto`, which means "never
   smaller than my content". So `overflow-y: auto` on a flex child does
   nothing at all until you say otherwise: the box grows past the
   container instead of scrolling inside it. On a company with every
   feature switched on that cut the last five links off the bottom of the
   rail, with no scrollbar and no sign anything was missing. It is the
   same rule as the `min-width: 0` on the brand text, arriving on the
   other axis. */
.sidebar-scroll {
  flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;

  /* A shadow at the bottom edge, but only while there is more below.
     The `local` layers scroll with the content and the `scroll` layer
     does not, so the panel-coloured cover slides away from the shadow as
     you reach the end and the shadow disappears on its own. No script,
     which matters because the Content-Security-Policy forbids inline
     script and this has to work on the no-JavaScript path too.

     It earns its place: `min-height: 0` above fixed links being cut off
     with no scrollbar, and this fixes what replaced it. macOS hides
     overlay scrollbars until you actually scroll, so on a company with
     every feature on, six links sat below the fold with nothing on
     screen suggesting the list continued. */
  background:
    linear-gradient(to top, var(--panel), transparent)
      bottom / 100% 20px no-repeat local,
    linear-gradient(to top, rgba(0, 0, 0, 0.35), transparent)
      bottom / 100% 12px no-repeat scroll;
}
.sidebar-foot {
  /* `--line-strong`, not `--line-soft`. The soft line is #25323C on a
     #1F2A33 panel, which is a contrast ratio of about 1.1 to 1: present
     in the stylesheet and invisible on a screen. The account block is
     meant to be set apart from the links above it, and with no visible
     rule the user's own name read as the last entry under REPORTS. */
  border-top: 1px solid var(--line-strong);
  padding-bottom: var(--sab);
}

/* --------------------------------------------- the phone slim bar -- */

.topslim {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 10px;
  background: var(--panel); border-bottom: 1px solid var(--line-strong);
  padding-top: var(--sat);
  padding-left: max(10px, var(--sal)); padding-right: max(10px, var(--sar));
  min-height: var(--topslim-h);
}
.topslim-brand {
  display: flex; align-items: center; gap: 8px;
  text-decoration: none; min-width: 0; flex: 0 1 auto; padding: 6px 0;
}
.topslim .navsearch { flex: 1 1 auto; padding: 6px 0; min-width: 0; }

/* ----------------------------------------------------- the tab bar -- */

/* Five targets at the bottom of the screen, where a thumb is. Fixed rather
   than sticky: a tab bar that scrolls away is a tab bar somebody has to
   scroll back up to reach, and the whole point is that it is always there. */
.navtabs {
  position: fixed; inset: auto 0 0 0; z-index: 30;
  display: flex; align-items: stretch;
  background: var(--panel); border-top: 1px solid var(--line-strong);
  padding-bottom: var(--sab);
  padding-left: var(--sal); padding-right: var(--sar);
}
.navtab {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; padding: 7px 2px;
  min-height: var(--navtabs-h);
  text-decoration: none; color: var(--muted);
  font-size: .6875rem; font-family: inherit;
  background: none; border: 0; cursor: pointer;
  position: relative;
}
.navtab-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.navtab-svg { width: 22px; height: 22px; display: block; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
/* The same rule as the rail, turned ninety degrees: a bar on the edge the
   item meets, never a block behind the words. */
.navtab.is-active { color: var(--text); }
.navtab.is-active::before {
  content: ""; position: absolute; top: 0; left: 18%; right: 18%; height: 3px;
  background: var(--accent); border-radius: 0 0 2px 2px;
}
/* Pinned to the tab's own box rather than to a percentage of it. At
   320px a tab is 64px and a "99+" pill is about 32px, so `left: 52%`
   put a third of the badge over the next tab's icon. `max-width` plus
   the tab's own clipping keeps it inside whatever it is sitting on. */
.navtab { overflow: hidden; }
.navtab .nav-badge {
  position: absolute; top: 3px; right: 6px; left: auto; margin-left: 0;
  max-width: calc(100% - 12px);
  overflow: hidden; text-overflow: clip; white-space: nowrap;
}

/* Scan: the one thing somebody does standing up, holding a card, with one
   hand. Bigger, centred, and filled with the brand colour, which is the one
   place in this product a filled brand block is right: there is no text on
   it, only an icon in `--accent-ink`. */
.navtab-scan .navtab-icon {
  background: var(--accent); color: var(--accent-ink);
  width: 46px; height: 34px; margin-top: -10px;
  border-radius: 17px;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 12px -4px rgba(0,0,0,.6);
}
.navtab-scan .navtab-svg { width: 24px; height: 24px; }
.navtab-scan { color: var(--text); }

/* --------------------------------------------------- the More sheet -- */

.moresheet {
  position: fixed; inset: 0; z-index: 40;
  display: flex; flex-direction: column;
  background: var(--bg);
  padding-top: var(--sat);
}
.moresheet[hidden] { display: none; }
/* Open without a script: the button is also a link to #moresheet on a page
   whose script did not run, and this is what makes that work. */
.moresheet:target { display: flex; }
.moresheet-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 12px max(14px, var(--sal)) 12px max(14px, var(--sar));
  border-bottom: 1px solid var(--line);
}
.moresheet-title { margin: 0; font-weight: 600; }
.moresheet-scroll {
  flex: 1; overflow-y: auto; overscroll-behavior: contain;
  padding-left: var(--sal); padding-right: var(--sar);
  padding-bottom: calc(var(--navtabs-h) + var(--sab) + 12px);
}
.navgroup-account { border-top: 1px solid var(--line); margin-top: 10px; }

/* A preformatted preview of a whole email. `pre-wrap` rather than `pre`,
   so a long line wraps instead of scrolling sideways on a phone, and the
   blank line between body and signature survives. */
/* A detail beside a panel heading: an address, a count, a date. Allowed
   to wrap and to break, because an email address has no break
   opportunity in it and `justify-content: space-between` with no wrap
   gave the whole page horizontal scroll at 320px the moment one was put
   there. The same pair this stylesheet already uses on the queue rows. */
.panel-head { flex-wrap: wrap; }
.panel-head-detail { min-width: 0; overflow-wrap: anywhere; }

/* The day's picks, and the line that says who was left out.

   `--line-strong`, not `--line-soft`, and for the reason written beside
   `.sidebar-foot`: the soft token is #25323C on a #1F2A33 panel, a ratio
   of about 1.1 to 1, which is present in the stylesheet and absent from
   the screen. This rule carried that token under a comment claiming the
   skip line was "given a visible edge", which it was not. Two invisible
   dividers were introduced in one change and only one of them was
   noticed.

   1.54 to 1 is below the 3 to 1 that `contrast.COMPONENT_MINIMUM` holds
   the tenant accent to, and that is deliberate rather than an oversight.
   WCAG 1.4.11 governs boundaries a person needs in order to identify a
   control; this is a hairline between a list and a sentence, and drawing
   it at 3 to 1 puts a bar across the middle of a quiet panel. It was
   checked the way the original fault was found, by looking at it. */
.today-skipped {
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--line-strong);
}

/* The panel itself needs no rules of its own; `.panel` does the work. The
   class is kept as a hook for the tests and for whatever this grows into,
   and said so here rather than leaving a reader to grep for it. */
.today-panel { }

.signature-preview {
  margin: 0;
  padding: 12px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: inherit;
  font-size: .875rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The page behind a full-height sheet must not scroll under it. Set by
   appnav.js, so with no script the sheet opens on :target and the page
   behind it scrolls, which is survivable and still readable. */
html.sheet-open, html.sheet-open body { overflow: hidden; }

/* ------------------------------------------------- the platform bar -- */

.platformbar {
  display: flex; align-items: center; gap: 2px; flex-wrap: wrap;
  background: var(--panel); border-bottom: 1px solid var(--line-strong);
  padding: 4px max(10px, var(--sar)) 4px max(10px, var(--sal));
}
.platformbar .navlink { border-left: 0; border-bottom: 3px solid transparent; }
.platformbar .navlink.is-active { border-left: 0; border-bottom-color: var(--accent); }

/* --------------------------------------------------- which is shown -- */

/* Phone and small tablet: slim bar at the top, tabs at the bottom, no
   rail. The rail's base rule is already `display: none`, so there is
   nothing to write here; the breakpoint that matters is the desktop one
   below, which turns it on. */

/* Desktop: the rail, and none of the phone furniture.

   `.platformbar` is in this list because without it a platform
   administrator got both navigations at once: the fixed rail down the
   side and the horizontal bar across the top, with Console, Switchboard
   and Audit in each and two Sign out links. Two exposed landmarks both
   called the main navigation is also the kind of thing a screen reader
   user has to work out for themselves. */
@media (min-width: 1024px) {
  .sidebar { display: flex; }
  .topslim, .navtabs, .moresheet, .platformbar { display: none; }
  .moresheet:target { display: none; }
  /* `body.has-rail`, not `body`.
     
     This reserves the width of the fixed sidebar, so it belongs only to
     pages that have one. On bare `body` it applied to every page that
     loads this stylesheet, and the public site loads it: /welcome,
     /pricing, /login and everything else were shoved 232 pixels right
     on any screen wider than 1024, with a horizontal scrollbar. It was
     invisible in testing because the signed-in pages it was written for
     are the ones anybody looks at on a laptop.

     `base.html` sets the class from `{% if nav %}`, which is the same
     condition that includes the rail, so there is one decision rather
     than two that can disagree.

     `--sal` is not added here. `box-sizing: border-box` means the
     rail's own `width: var(--rail-w)` already contains its
     `padding-left`, so adding the inset again would indent by it
     twice. */
  body.has-rail { padding-left: var(--rail-w); }
}

/* A platform administrator has no tab bar, so the page must not reserve
   sixty pixels of empty space at the foot of every screen for one.
   `.platformbar` is a bar across the *top*; the `{% else %}` branch in
   `_nav.html` that renders `.navtabs` is the one they do not get.

   The class is on `<body>`, set from `nav.platform` in base.html. It
   said that before and was not true: nothing set it, so this rule had
   never once matched and every platform screen below 1024px carried
   sixty pixels of dead space at the bottom. Found while scoping the
   rail padding, which was the same mistake in the other direction. */
body.no-tabs .page {
  padding-bottom: calc(var(--gap) + var(--sab));
}

/* Larger text, set by the toggle and remembered per browser. Not a zoom:
   the browser's own zoom already exists and is better. This is the one
   control the people who need it actually find, because it is in the
   navigation rather than in a browser menu.

   It works by raising the root size, which every other size in this
   sheet is relative to. That is why `body` must not carry an absolute
   font size: see the note on it above. */
html.text-large { font-size: 118%; }

/* ----------------------------------------------------------------- page -- */
/* ----------------------------------------------------------------- page -- */

.page {
  max-width: 1400px; margin: 0 auto;
  padding: var(--gap);
  /* Keep content clear of the notch in landscape and the home indicator. */
  padding-left: max(var(--gap), var(--sal));
  padding-right: max(var(--gap), var(--sar));
  /* Room for the fixed tab bar as well as the home indicator, or the last
     row of every list on the product sits underneath it and cannot be
     tapped. The rail is a left padding on `body` and needs nothing here. */
  padding-bottom: calc(var(--gap) + var(--sab) + var(--navtabs-h));
}

@media (min-width: 1024px) {
  /* No tab bar at this width, so the page gets its ordinary foot back. */
  .page { padding-bottom: calc(var(--gap) + var(--sab)); }
}

.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--gap); margin-bottom: var(--gap); flex-wrap: wrap;
}
.page-head p { margin: 0; }
.page-head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.backlink {
  font-size: .8125rem; color: var(--muted); text-decoration: none;
  /* Same reason as .nav-user: it sits on a line of its own above the
     page heading, so it is a standalone target. */
  display: inline-flex; align-items: center; min-height: var(--target-min);
}
.backlink:hover { color: var(--text); }

.count-badge {
  font-size: .8125rem; font-weight: 500; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line);
  padding: 1px 7px; border-radius: 2px; vertical-align: middle;
}
.pipeline-chip {
  font-size: .8125rem; color: var(--muted);
  border: 1px solid var(--line); padding: 6px 10px; border-radius: var(--radius);
}

/* ---------------------------------------------------------------- panel -- */

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--gap);
  margin-bottom: var(--gap);
}
.panel-flush { padding: 0; overflow: hidden; }
.panel.narrow, .form.narrow { max-width: 620px; }
.panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 12px;
  padding-bottom: 10px; border-bottom: 1px solid var(--line-soft);
}
.panel-head h2 { margin: 0; }
/* An empty state is a page, not an absence. Every one of them says what to
   do next and offers the control that does it, so the actions need a row of
   their own that wraps on a phone. See tests/test_empty_states.py. */
.empty { text-align: center; padding: 40px var(--gap); }
.empty h2 { font-size: 1rem; }
.empty p { max-width: 54ch; margin-left: auto; margin-right: auto; }
.empty-actions {
  display: flex; flex-wrap: wrap; gap: 8px;
  justify-content: center; margin-top: 14px;
}
.empty-actions form { margin: 0; }
@media (max-width: 460px) {
  /* Stacked full-width buttons, the same shape the page head takes. */
  .empty-actions { flex-direction: column; align-items: stretch; }
}
.error-panel { max-width: 520px; margin: 60px auto; text-align: center; }
/* Was 3rem, which was sized for the word "404". The error pages say what
   happened in a sentence now, and a sentence at 48px wraps to four lines and
   reads as shouting. */
.error-panel h1 { font-size: 1.875rem; color: var(--text); margin-bottom: .5em; }
.error-panel p { max-width: 46ch; margin-left: auto; margin-right: auto; }

.split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); align-items: start; }
/* A grid item's `min-width` defaults to `auto`, meaning "never narrower
   than my content", which is the same trap as a flex item and bites the
   same way: at 320px the contact page's columns had already collapsed to
   one, and a panel inside still measured 320 against a 296 box, giving
   the whole page twelve pixels of horizontal scroll. Nothing overflowed
   its own box, so it was invisible to an audit that checks each element
   against the viewport; only the document width showed it. */
.split > * { min-width: 0; }
.stack > *:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------------- stats -- */

.stat-grid {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: var(--gap); margin-bottom: var(--gap);
}
.stat {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
}
.stat-label {
  font-size: .75rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin-bottom: 4px;
}
.stat-value { font-size: 1.75rem; font-weight: 600; line-height: 1.1; letter-spacing: -0.02em; }

/* --------------------------------------------------------------- tables -- */

/* Two table classes, on purpose, and the difference is the phone.
   `table.data` folds into labelled cards below 860px and therefore needs a
   data-label on every cell. `table.table` keeps its columns and scrolls
   inside .table-scroll — which is the honest answer for the console's
   thirteen columns, where hiding one means the figure you came for is the
   one that is gone.

   They share this type and border treatment because they were visibly
   different products before: `.table` had no rules at all, so the console,
   the support log and the support view rendered as browser-default tables —
   serif-ish metrics, no header fill, no row separators — inside panels
   styled by this sheet. */
table.data, table.table { width: 100%; border-collapse: collapse; font-size: .875rem; }
table.data th, table.table th {
  text-align: left; font-size: .6875rem; text-transform: uppercase;
  letter-spacing: .07em; color: var(--muted); font-weight: 600;
  padding: 10px 12px; border-bottom: 1px solid var(--line);
  background: var(--panel-2); white-space: nowrap;
}
table.data td, table.table td {
  padding: 10px 12px; border-bottom: 1px solid var(--line-soft); vertical-align: top;
}
/* A row header carries the row's identity, so it is left-aligned body text
   rather than an uppercase column label. */
table.table tbody th[scope="row"], table.data tbody th[scope="row"] {
  text-transform: none; letter-spacing: 0; font-size: .875rem;
  color: var(--text); background: none; white-space: normal;
  border-bottom: 1px solid var(--line-soft); vertical-align: top;
}
table.data tbody tr:last-child td, table.table tbody tr:last-child td,
table.data tbody tr:last-child th, table.table tbody tr:last-child th { border-bottom: 0; }
table.data tbody tr:hover, table.table tbody tr:hover { background: var(--panel-2); }
table.data tr.is-inactive, table.table tr.is-inactive { opacity: .55; }

.rowlink { font-weight: 600; text-decoration: none; }
.rowlink:hover { color: var(--text); }
.sub { display: block; font-size: .75rem; color: var(--muted); }
.row-actions { white-space: nowrap; text-align: right; }

/* ---------------------------------------------------------- record cards --
   Purpose-built, not a table folded up. The generic table-to-stack transform
   printed a label above every field, which put one contact on a phone screen
   and made a 38-row list 38 screens long. Here the card face carries only what
   a rep scans for — who, where they work, how cold they are — with no field
   labels at all, and the rest sits behind a native <details>. Roughly five
   records per 390px screen.
   ------------------------------------------------------------------------ */

.cards {
  list-style: none; margin: 0 0 var(--gap); padding: 0;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: 10px;
  row-gap: 1px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line-soft);
}
.card:last-child { border-bottom: 0; }

.card-title {
  grid-row: 1; grid-column: 1;
  font-size: 1.0625rem; font-weight: 600; line-height: 1.25;
  text-decoration: none;
  overflow-wrap: anywhere;
}
.card-title:hover { color: var(--text); }
.card-status { grid-row: 1; grid-column: 2; justify-self: end; align-self: center; }

.card-sub {
  grid-row: 2; grid-column: 1 / -1;
  margin: 0; font-size: .875rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.card-more { grid-row: 4; grid-column: 1; min-width: 0; }
.card-more > summary {
  display: flex; align-items: center; gap: 8px;
  list-style: none; cursor: pointer;
  min-height: var(--tap);      /* the disclosure is the card's second target */
  margin-left: -4px; padding: 0 4px;
  border-radius: var(--radius);
}
.card-more > summary::-webkit-details-marker { display: none; }
.card-more > summary:hover { background: var(--panel-2); }
.card-meta { font-size: .8125rem; color: var(--muted); }
.card-chevron {
  width: 0; height: 0; flex: none;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--muted);
  transition: transform .12s linear;
}
.card-more[open] > summary .card-chevron { transform: rotate(180deg); }

.card-detail {
  display: grid;
  /* minmax(0,1fr), not 1fr: a plain 1fr track has an automatic minimum of its
     content's min-content width, so a long email address pushes the column
     past the card instead of wrapping inside it. */
  grid-template-columns: auto minmax(0, 1fr);
  gap: 3px 12px; margin: 2px 0 8px; font-size: .8125rem;
}
.card-detail dt { color: var(--muted); white-space: nowrap; }
.card-detail dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* start, not center: row 3 grows to hold the expanded disclosure, and a
   centred button drifts down the card as it opens. Aligning to the top keeps
   Edit level with the summary line whether the card is open or shut. */
.card-edit { grid-row: 4; grid-column: 2; align-self: start; justify-self: end; }

/* ----------------------------------------------------------------- tags -- */

.tag {
  display: inline-block; font-size: .6875rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em;
  padding: 2px 6px; border-radius: 2px;
  border: 1px solid var(--line); color: var(--muted); background: var(--panel-2);
  white-space: nowrap;
}
.tag-won, .tag-complete, .tag-active { color: var(--good); border-color: rgba(61,139,111,.5); }
.tag-lost, .tag-dormant, .tag-inactive, .tag-overdue { color: var(--bad); border-color: rgba(168,67,67,.5); }
/* In-flight states. Neutral ink and a neutral edge: --good and --bad are
   spoken for by won and lost, and the brand colour is not available to carry
   meaning any more (D-123). The word in the tag is what says which state it
   is, which was always true — the colour was never doing that work alone. */
.tag-engaged, .tag-in_progress, .tag-scheduled { color: var(--text); border-color: var(--control-line); }
.tag-plan-vip { color: var(--text); border-color: var(--control-line); }
/* A feature the plan does not include. Deliberately quiet rather than red:
   it is not an error, it is a thing this customer has not bought. */
.tag-locked { color: var(--muted); border-style: dashed; }

/* A tag is normally a label *on* something and a label has no target size to
   meet. The setup wizard is the one place a tag is itself the link — the six
   step chips across the top of every wizard page — and those measured 22px
   high in the browser, under the 24 WCAG 2.2 2.5.8 asks of a target. Sized
   on `a.tag` rather than in the wizard's own rules so the next page that
   makes a tag clickable inherits the floor instead of re-discovering it. */
a.tag {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
}

/* ------------------------------------------------------- lists and feed -- */

.queue, .feed, .bullets, .togglelist { list-style: none; margin: 0; padding: 0; }

.queue-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 0; border-bottom: 1px solid var(--line-soft); font-size: .875rem;
}
.queue-row:last-child { border-bottom: 0; }
.queue-name { font-weight: 600; text-decoration: none; }
a.queue-name:hover { color: var(--text); }
.queue-meta { color: var(--muted); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-touch { font-size: .8125rem; color: var(--muted); white-space: nowrap; }
/* Company names are the first thing to get squeezed out when four items share
   one line, so give the ellipsis something to work with before it truncates. */
.queue-meta { min-width: 6ch; }

/* Grid, not flex. The three parts of a feed row need to reflow onto two lines
   on a narrow screen, and a flex row cannot do that without wrapping rules
   that fight the "date never wraps" requirement. Grid lets the date move to
   its own line by changing one template, with no risk of it landing on top of
   the summary. */
.feed-row {
  display: grid;
  grid-template-columns: 74px minmax(0, 1fr) auto;
  gap: 2px 10px;
  align-items: baseline;
  padding: 8px 0; border-bottom: 1px solid var(--line-soft); font-size: .875rem;
}
.feed-row:last-child { border-bottom: 0; }
.feed-type {
  grid-column: 1; font-size: .6875rem; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted); font-weight: 600;
}
.feed-agent_run { color: var(--text); font-weight: 700; }
/* min-width:0 lets the summary shrink; overflow-wrap keeps a long unbroken
   token (a URL, an address) from pushing the column wider than the panel. */
.feed-summary { grid-column: 2; min-width: 0; overflow-wrap: anywhere; }
.feed-when { grid-column: 3; font-size: .75rem; white-space: nowrap; }

.bullets li { padding: 6px 0 6px 18px; position: relative; }
.bullets li::before {
  content: ""; position: absolute; left: 2px; top: 14px;
  width: 5px; height: 5px; background: var(--accent);
}

.togglelist li {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line-soft);
}
.togglelist li:last-child { border-bottom: 0; }

.deflist { display: grid; grid-template-columns: 130px 1fr; gap: 6px 12px; margin: 0; font-size: .875rem; }
.deflist dt { color: var(--muted); font-size: .8125rem; }
.deflist dd { margin: 0; }
/* The weekly report's figures are links — "11" linking to the eleven contacts
   it counts — and a two-character link is a 13x17 target. The touch block
   further down grows these to 44px, but it is `(pointer: coarse),
   (max-width: 860px)`, so a mouse on a 1280px screen got 13x17 and WCAG 2.2
   2.5.8 asks 24 of both of us. Floored here, unconditionally; the touch rule
   still raises it to 44 where a finger is involved. */
.deflist dd a {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--target-min); min-width: var(--target-min);
}

.notes { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.notes p { white-space: pre-wrap; font-size: .875rem; }
.stub-lede { font-size: 1rem; color: var(--muted); max-width: 62ch; }
.legend { max-width: 68ch; }

/* ---------------------------------------------------------------- forms -- */

label { display: block; font-size: .75rem; text-transform: uppercase;
        letter-spacing: .06em; color: var(--muted); margin-bottom: 4px; }

/* --control-line, not --line: the edge of a text box is the only thing that
   says a text box is there, so WCAG 1.4.11 asks 3:1 of it. See the token. */
input[type=text], input[type=email], input[type=password], input[type=search],
input[type=date], input[type=number], input[type=tel], input[type=file],
input[type=url], input:not([type]), select, textarea {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--control-line); border-radius: var(--radius);
  padding: 8px 10px; font: inherit; font-size: .875rem;
}
input:hover, select:hover, textarea:hover { border-color: var(--control-line-hover); }
/* A second indicator under the ring: the field's own border takes the accent
   too. Redundant on purpose — if a tenant's accent is dark this border
   disappears and the --focus-ring outline above is still there, which is why
   the outline and not the border is the one 1.4.11 is satisfied by. */
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline-offset: 0; }

/* Checkbox and radio: 24×24 is WCAG 2.2 2.5.8, and the browser default is
   about 13px. Sized here rather than per page because the bulk-select boxes
   on the prospecting table and the day boxes in the setup wizard were the
   two that were genuinely hard to hit with a mouse. accent-color keeps the
   native control — a hand-built one loses the indeterminate state and the
   platform's own high-contrast handling. */
input[type=checkbox], input[type=radio] {
  width: var(--target-min); height: var(--target-min);
  min-width: var(--target-min); min-height: var(--target-min);
  margin: 0; accent-color: var(--accent); cursor: pointer;
}

/* The branding step's colour picker. It had no rule at all, so it rendered
   at whatever the platform's default swatch size is — about 50x24 on one
   browser and 64x30 on another, which is the only control in the app whose
   size came from the operating system. */
input[type=color] {
  width: 56px; height: 34px; padding: 2px; cursor: pointer;
  background: var(--bg); border: 1px solid var(--control-line);
  border-radius: var(--radius);
}
textarea { resize: vertical; min-height: 80px; }
select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
                    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 28px;
}

.field { margin: 0 0 14px; }
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 0 var(--gap); }
.form-actions { display: flex; align-items: center; gap: 10px; margin-top: 6px; flex-wrap: wrap; }
.form-actions-spacer { flex: 1; }
.danger-form { margin-top: -4px; }
.fieldset {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px var(--gap); margin: 0 0 var(--gap);
}
.fieldset legend {
  font-size: .75rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); padding: 0 6px;
}

.filterbar {
  display: flex; gap: 8px; margin-bottom: var(--gap); flex-wrap: wrap;
  align-items: center;
}
.filterbar input[type=search] { flex: 2 1 220px; width: auto; }
.filterbar select { flex: 1 1 150px; width: auto; }

.inline-form {
  display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap;
  padding-bottom: 14px; border-bottom: 1px solid var(--line-soft);
}
.inline-form select { flex: 0 0 120px; width: auto; }
.inline-form input { flex: 1 1 160px; width: auto; }

/* -------------------------------------------------------------- buttons -- */

/* .btn's own fill is --panel-2 on --panel, which is 1.06:1 — invisible. The
   border is the whole affordance, so it is --control-line and not --line.
   min-height is the 2.5.8 floor; .btn-sm at 5px padding and 12px type came
   out at 26px, which passed, but only by accident of the font metrics. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: .8125rem; font-weight: 600;
  padding: 8px 13px; border-radius: var(--radius); cursor: pointer;
  min-height: var(--target-min);
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--control-line); text-decoration: none; white-space: nowrap;
}
.btn:hover { border-color: var(--control-line-hover); background: #292D35; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
/* Away from the ink, never toward it.
   This was `#F08A25`, a lighter copy of the *default* orange, and it did not
   follow the tenant — so a navy-branded company's button turned orange on
   hover while its label stayed the white chosen for navy: 2.51:1, on the one
   control the customer is actually pressing. `--accent-hover` is computed by
   `contrast.hover_fill`, which mixes the brand colour toward the opposite of
   its own ink, so the hover state's contrast is always *better* than the
   resting state's and the floor in `ink_for` holds for both. */
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-quiet { background: transparent; color: var(--muted); }
.btn-quiet:hover { color: var(--text); }
.btn-danger { background: transparent; color: var(--bad); border-color: var(--bad); }
.btn-danger:hover { background: rgba(168,67,67,.12); border-color: var(--control-line-hover); }
.btn-sm { padding: 5px 9px; font-size: .75rem; }
.btn-block { width: 100%; margin-top: 6px; }

/* A form that is being submitted.
   Not `disabled`: a disabled submit button is left out of the form data, so
   the outreach bulk bar — three buttons sharing name="action" with different
   values — would post with no action at all and the whole bulk approve would
   silently do nothing. aria-disabled tells assistive technology, the class
   stops the pointer, and app.js refuses the second submit event. */
.btn[aria-disabled="true"], .btn.is-pending {
  opacity: .7; cursor: progress; pointer-events: none;
}

/* -------------------------------------------------------- switch toggle -- */

.switch { display: inline-flex; align-items: center; margin: 0; cursor: pointer; }
.switch input {
  position: absolute; opacity: 0; margin: 0; cursor: pointer;
  /* Overrides the 24px floor on a checkbox: this one is invisible and the
     visible track plus its label carry the target. */
  width: 36px; height: 20px; min-width: 0; min-height: 0;
}
/* --control-line on the track, not --line. An off switch is the state you
   have to be able to see from across the switchboard, and at --line (1.25:1
   against the row behind it) the pill had no edge at all — the only thing
   distinguishing "off" from "no switch here" was a 14px grey dot. */
.switch-track {
  display: block; width: 36px; height: 20px; border-radius: 10px;
  background: #33373F; border: 1px solid var(--control-line); position: relative;
  transition: background-color .12s linear, border-color .12s linear;
}
.switch-thumb {
  position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%; background: var(--muted);
  transition: transform .12s linear, background-color .12s linear;
}
.switch input:checked + .switch-track { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(16px); background: var(--accent-ink); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.switch input:disabled + .switch-track { opacity: .5; cursor: wait; }
.switch-track.is-error { border-color: var(--bad); }

.cell-toggle { text-align: center; }
.col-feature { text-align: center; }
.col-feature span { display: inline-block; max-width: 82px; white-space: normal; line-height: 1.25; }

/* ----------------------------------------------------------- danger zone --
   Destructive actions live at the bottom of a record, visually quieter than
   the primary action and separated from it by the whole page. */
.danger-zone {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap;
  border: 1px solid var(--line);
  border-left: 3px solid rgba(168, 67, 67, .55);
  border-radius: var(--radius);
  padding: 12px var(--gap);
  margin-bottom: var(--gap);
  background: var(--panel);
}
.danger-zone h2 { font-size: .875rem; margin-bottom: 2px; }
.danger-zone p { margin: 0; max-width: 52ch; }

/* ------------------------------------------------------- flashes/toasts -- */

.flashes { margin-bottom: var(--gap); }
.flash {
  border: 1px solid var(--line); border-left: 3px solid var(--muted);
  background: var(--panel); padding: 9px 12px; margin-bottom: 8px;
  font-size: .875rem; border-radius: var(--radius);
}
.flash-ok    { border-left-color: var(--good); }
.flash-error { border-left-color: var(--bad); }
.flash-warn  { border-left-color: var(--accent); }

.toast-host {
  position: fixed; z-index: 60;
  bottom: calc(16px + var(--sab));
  right: calc(16px + var(--sar));
  display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
  pointer-events: none;
}
.toast {
  background: var(--panel); border: 1px solid var(--line);
  border-left: 3px solid var(--good);
  padding: 9px 13px; border-radius: var(--radius); font-size: .8125rem;
  max-width: 320px;
}
.toast-error { border-left-color: var(--bad); }

/* ----------------------------------------------------------------- auth -- */

.body-auth { display: flex; min-height: 100vh; }
.auth-wrap { margin: auto; padding: var(--gap); width: 100%; max-width: 380px; }
.auth-card { background: var(--panel); border: 1px solid var(--line);
             border-radius: var(--radius); padding: 26px 22px; }
/* The lockup on the three signed-out cards. Replaces `.auth-mark`, which
   was the letters TFB in an orange square — the same monogram the real logo
   contains, drawn in CSS because there was no logo file. There is one now.

   Width-capped rather than height-capped: the card is 380px wide at most
   and the logo should fill a comfortable share of it rather than being a
   fixed small height on a surface with room. */
.auth-logo {
  display: block;
  width: 100%;
  max-width: 230px;
  height: auto;
  margin: 0 0 18px;
}
.auth-card h1 { font-size: 1.5rem; }
.auth-card .field, .auth-card label { text-align: left; }
.auth-card input { margin-bottom: 12px; }

/* ------------------------------------------------------------ credbox -- */

.credbox {
  border: 1px solid var(--accent-dim); border-left: 3px solid var(--accent);
  background: rgba(224,122,24,.06); border-radius: var(--radius);
  padding: 14px; margin: var(--gap) 0;
}
.credbox h2 { font-size: .8125rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.cred { display: flex; gap: 10px; align-items: baseline; margin-bottom: 6px; flex-wrap: wrap; }
.cred-label { font-size: .75rem; color: var(--muted); min-width: 150px; }
.cred code, code {
  font-family: var(--mono); font-size: .8125rem;
  background: var(--bg); border: 1px solid var(--line);
  padding: 3px 7px; border-radius: 2px; user-select: all;
}

/* ------------------------------------------------------------- account -- */

/* Secondary link under an auth card's button: "forgot your password", "back to
   sign in". Centred to match the block button above it. */
.auth-alt { margin: 14px 0 0; text-align: center; font-size: .8125rem; }
.auth-alt a { color: var(--muted); }
.auth-alt a:hover { color: var(--text); }

.field-hint { display: block; margin-top: 4px; }

/* A reset link is long and has no spaces, so it has to be told it may break
   mid-string or it widens the card past the viewport. */
.cred-wrap { overflow-wrap: anywhere; word-break: break-all; }

/* ---------------------------------------------------------------- team -- */

/* The per-user controls live inside the card's <details>, so three forms sit
   in a row that must wrap on a phone rather than overflow. */
.card-controls {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin: 4px 0 6px;
}
.card-controls form { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.card-controls select { width: auto; flex: 0 1 120px; margin: 0; }
.card-controls-note { grid-column: 1 / -1; margin: 2px 0 6px; }

/* A card list already inside a .panel does not need its own frame. */
.cards-flush { margin: 0; border: 0; background: none; border-radius: 0; }

/* A switched-off account stays legible but reads as inactive, the same way an
   inactive tenant row does on the switchboard. */
.card.is-inactive .card-title, .card.is-inactive .card-sub { opacity: .55; }

/* --------------------------------------------------------- prospecting -- */

/* Three tiers that have to be scannable as a ranking, in neutrals.
   Tier 1 was the brand colour, which made the ladder obvious and is no
   longer available (D-123). Ink and edge now fall together — bright with a
   visible edge, bright with a faint one, muted and dashed — so the column
   still reads top to bottom at a glance and the digit is not the only cue.
   The zones page sells the tier as the cadence; a ranking nobody can scan
   is that page's whole argument gone. */
.tag-tier1 { color: var(--text); border-color: var(--control-line); font-weight: 700; }
.tag-tier2 { color: var(--text); border-color: var(--line); }
.tag-tier3 { color: var(--muted); border-color: var(--line); border-style: dashed; }

.scrapetable td { vertical-align: top; }
.cell-fit select { width: auto; min-width: 110px; margin: 0; }
.cell-fit.is-error select { outline: 2px solid var(--bad); }

/* -------------------------------------------------------------- pause -- */

/* Loud on purpose. While this is up it is the most important fact on the
   screen, so it sits above the header's content, spans the full width, and
   does not scroll away with the page body. */
.pausebar {
  background: rgba(168, 67, 67, .16);
  border-bottom: 2px solid var(--bad);
  color: var(--text);
  padding: 10px var(--gap);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: .875rem;
}
.pausebar strong { color: var(--bad); }
.pausebar-who { color: var(--muted); font-size: .8125rem; flex: 1; min-width: 200px; }

/* --------------------------------------------------------------- tabs -- */

.tabbar {
  display: flex; gap: 2px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--gap);
}
.tab {
  padding: 8px 12px; text-decoration: none; color: var(--muted);
  border-bottom: 2px solid transparent; display: inline-flex;
  align-items: center; gap: 6px; min-height: var(--tap);
}
.tab:hover { color: var(--text); }
/* The underline is the brand, the label is not.
   A two-pixel rule in an unchecked colour cannot be the only mark of the
   active tab — for a dark-branded company it is about 1.1:1 against the
   panel, which is no mark at all. So the label also goes from --muted to
   --text and takes weight, and the markup carries `aria-current="page"`.
   Three cues, two of which do not depend on the tenant's colour. */
.tab.is-active { color: var(--text); font-weight: 600; border-bottom-color: var(--accent); }
.tab-count {
  font-size: .6875rem; background: var(--panel-2); color: var(--muted);
  border-radius: 10px; padding: 1px 7px; font-weight: 600;
}
/* 4.12:1 before: --accent on an 18% accent tint of --panel, at 11px, so
   it needed 4.5:1 and did not have it. Filled instead, and since D-123 the
   ink on that fill is chosen per tenant rather than being a fixed #16181C —
   so the pill is readable on any brand colour, and a solid pill reads more
   plainly as the live tab than tinted text did. */
.tab.is-active .tab-count { background: var(--accent); color: var(--accent-ink); }

/* ------------------------------------------------------------ outreach -- */

.bulkbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; margin-bottom: 8px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* The words beside a checkbox or a radio are the choice itself, not the
   name of a field above one, so they are body text. The global `label` rule
   makes every label small uppercase grey — which is right over an input and
   wrong beside a tick box, where it turned "Select all on this page" and the
   seven weekday names in the setup wizard into column headings. */
.bulk-all, .togglelist label, .switch, .draft-pick {
  text-transform: none; letter-spacing: 0;
  font-size: .875rem; color: var(--text); margin-bottom: 0;
}
.bulk-all { display: inline-flex; align-items: center; gap: 8px; min-height: var(--tap); }
.bulk-all input { width: var(--target-min); height: var(--target-min); }

.draft { grid-template-columns: auto minmax(0, 1fr) auto; }
.draft .card-title { grid-column: 2; }
.draft .card-status { grid-column: 3; }
.draft .card-sub { grid-column: 2 / -1; }
.draft-pick { grid-row: 1; grid-column: 1; display: flex; align-items: center;
              min-height: var(--tap); min-width: var(--tap); }
.draft-pick input { width: var(--target-min); height: var(--target-min); }

/* Why this draft exists. The approver needs it before they need the text. */
.draft-why {
  grid-column: 2 / -1; margin: 2px 0 4px;
  /* Weight rather than colour. The approver needs this before they need the
     draft, and at --text with no weight it read as the first line of the
     draft rather than as a note about it. */
  font-size: .8125rem; color: var(--text); font-weight: 600;
}
.draft-warn { grid-column: 2 / -1; margin: 0 0 4px; display: flex; gap: 4px; flex-wrap: wrap; }
.draft-body { grid-column: 2 / -1; }

.draft-text {
  font-family: var(--font); font-size: .875rem; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
  background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px; margin: 6px 0;
}

.draftactions { background: none; border: 0; }
.draft-actionrow {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding: 8px 12px; border-bottom: 1px solid var(--line-soft);
}
.draft-actionrow form { display: inline; }
.editwrap > summary { list-style: none; cursor: pointer; }
.editwrap > summary::-webkit-details-marker { display: none; }
.editwrap .form { margin-top: 8px; width: 100%; }
/* Open, it takes a row of its own.
   `.draft-actionrow` is a wrapping flex row, so a closed <details> is a flex
   item the width of its "Edit" summary — about 185px. The form inside is
   `width: 100%` of *that*, which put the subject field, a twelve-row body
   textarea and the save button into a 185px column pinned to the right-hand
   edge of a 1248px row, with the button hanging 37px outside it. Found at
   1280px in a browser; nothing in the suite opens this editor and measures
   it. `flex-basis: 100%` makes the open editor wrap onto its own line and
   fill the card; the summary keeps its own width so the Edit control does
   not become a full-width button. */
.editwrap[open] { flex: 1 1 100%; }
.editwrap[open] > summary { width: fit-content; }

.snooze-all { margin: var(--gap) 0; text-align: center; }

@media (max-width: 600px) {
  .draft-actionrow { gap: 6px; }
  .draft-actionrow .btn { flex: 1 1 auto; }
}

/* ------------------------------------------------------------- paging -- */

.sortbar { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.sortbar select { width: auto; min-width: 180px; margin: 0; }

.pager { margin: var(--gap) 0; text-align: center; }
.pager .btn { margin-bottom: 6px; }
.pager-count { margin: 0; }

@media (max-width: 600px) {
  .sortbar { justify-content: stretch; }
  .sortbar select { width: 100%; }
}

/* -------------------------------------------------------------- import -- */

/* One row per CSV column: the header we found, an arrow, and the field it
   maps to. Grid rather than flex so the arrows line up down the page however
   long the header names are. */
.maplist { list-style: none; margin: 0 0 var(--gap); padding: 0; }
.maprow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: 8px; align-items: center;
  padding: 5px 0; border-bottom: 1px solid var(--line-soft);
}
.maprow:last-child { border-bottom: 0; }
.mapfrom {
  font-family: var(--mono); font-size: .8125rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.maparrow { color: var(--muted); }
.maprow select { width: 100%; margin: 0; }

@media (max-width: 600px) {
  /* The arrow stops earning its column on a phone; stack instead. */
  .maprow { grid-template-columns: minmax(0, 1fr); gap: 2px; padding: 8px 0; }
  .maparrow { display: none; }
}

/* ----------------------------------------------------------- audit log -- */

/* Wider action column than the activity feed: "password.reset_link_issued" is
   the longest name and clipping it would hide which action it was. */
.auditfeed .feed-row { grid-template-columns: 170px minmax(0, 1fr) auto; }
.audit-action {
  font-family: var(--mono); font-size: .6875rem; color: var(--muted);
  text-transform: none; letter-spacing: 0;
  overflow-wrap: anywhere;
}
.audit-actor { font-weight: 600; }
.audit-payload { display: block; font-family: var(--mono); overflow-wrap: anywhere; }

/* ---------------------------------------------------------- responsive -- */

@media (max-width: 1100px) {
  .col-feature span { max-width: 60px; font-size: .625rem; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; }

  /* The top bar, the hamburger and the drawer that used to live here are
     gone: below 1024px the navigation is now the slim bar at the top and
     the fixed tab bar at the bottom, and both are declared with the rest
     of the nav rather than as a phone exception to a desktop row. See
     "the nav" above. */

  /* Tables become stacked cards. Sideways scrolling on a phone is a
     non-starter for a tool used one-handed in a truck. */
  table.data, table.data tbody, table.data tr, table.data td { display: block; width: 100%; }
  table.data thead { display: none; }
  table.data tr {
    border-bottom: 1px solid var(--line);
    padding: 10px 12px;
  }
  table.data tbody tr:last-child { border-bottom: 0; }
  table.data td {
    border: 0; padding: 3px 0;
    display: flex; align-items: baseline; justify-content: space-between;
    /* wrap is required, not cosmetic: .sub below sets flex-basis:100% to claim
       its own line, and without wrapping it would sit on the same line and
       overflow — the same defect the activity feed had. Row gap stays 0 so a
       wrapped secondary line sits tight under its value. */
    flex-wrap: wrap; column-gap: 12px; row-gap: 0;
  }
  table.data td::before {
    content: attr(data-label);
    flex: 0 0 42%;
    font-size: .6875rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); font-weight: 600;
  }
  table.data td:empty, table.data td[data-label=""]::before { display: none; }
  .row-actions { text-align: left; justify-content: flex-start; }
  .cell-toggle, .col-feature { text-align: left; }
  /* A secondary line (a job title, a tenant slug) belongs under its value, not
     beside it competing for the same few characters of width. */
  table.data td .sub { flex-basis: 100%; text-align: right; }
  /* Let the value itself shrink and wrap normally rather than forcing the row
     wider than the card. */
  table.data td > * { min-width: 0; }

  .field-grid { grid-template-columns: 1fr; }

  /* Date drops to its own line under the summary instead of sharing one. */
  .feed-row { grid-template-columns: 62px minmax(0, 1fr); }
  .feed-when { grid-column: 2; }

  /* The audit log's action names are long enough that a fixed first column
     leaves nothing for the actor. Stack all three instead. */
  .auditfeed .feed-row { grid-template-columns: minmax(0, 1fr); }
  .auditfeed .feed-type,
  .auditfeed .feed-summary,
  .auditfeed .feed-when { grid-column: 1; }

  /* Four items on one queue line leaves the company name a few pixels wide.
     Two lines: name and status on top, company and recency underneath. */
  .queue-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 2px 10px;
    align-items: center;
  }
  .queue-name  { grid-column: 1; grid-row: 1; }
  .queue-row .tag { grid-column: 2; grid-row: 1; justify-self: end; }
  .queue-meta  { grid-column: 1; grid-row: 2; min-width: 0; }
  .queue-touch { grid-column: 2; grid-row: 2; justify-self: end; }
}

/* --- Touch sizing -------------------------------------------------------
   Applied by pointer type rather than width: a small tablet with a mouse does
   not need 44px rows, and a large phone does. --tap is the iOS minimum. */
@media (pointer: coarse), (max-width: 860px) {
  .btn { min-height: var(--tap); display: inline-flex; align-items: center; justify-content: center; }
  .btn-sm { min-height: var(--tap); padding: 5px 12px; }
  /* Every navigation target on a touch screen. The tabs are already 60px
     tall by their own rule; these two are rows in the rail and the sheet,
     which are 24px by the WCAG 2.2 2.5.8 floor and should be a thumb's
     worth where a thumb is what is being used. */
  .navlink, .navtab { min-height: var(--tap); }
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
  select, textarea { min-height: var(--tap); }
  /* Checkbox and radio are deliberately NOT grown to 44px here. They are
     24x24 at every width, which is what WCAG 2.2 2.5.8 asks (AA); the 44px
     of 2.5.5 is AAA, and where this application can reach it — the bulk
     select, the draft pick, the switch — it does so by wrapping the control
     in a 44px label rather than by inflating the box itself. A 44px native
     checkbox beside 13px type does not look like this product. */
  /* Inline links grow their hit area vertically rather than gaining borders or
     backgrounds, so the page looks the same and only the target changes.
     inline-flex + min-height hits exactly 44px regardless of font metrics. */
  .rowlink, .queue-name, .backlink, .brand, .nav-user, .nav-signout,
  .deflist dd a, table.data td a, .feed-summary a,
  .card-detail a, .card-controls a, .card-controls-note a {
    display: inline-flex; align-items: center; min-height: var(--tap);
  }
  /* The card title is the record's primary target, so it takes the full width
     of its grid cell rather than only the width of the name. */
  .card-title {
    display: flex; align-items: center; min-height: var(--tap);
  }
  /* The switch keeps its 36x20 look; the label around it carries the target,
     and the (invisible) checkbox is stretched to fill it so a tap anywhere
     inside registers even where the label's own hit testing does not reach. */
  .switch { min-height: var(--tap); min-width: var(--tap); justify-content: center; }
  .switch input { top: 0; left: 0; width: 100%; height: 100%; }
}

/* --- Phone cards --------------------------------------------------------
   Below 600px the label-beside-value split leaves the value about half a
   column, which wraps names like "Solveig Fitzwilliam" mid-word. Stack the
   label above a full-width value instead. */
@media (max-width: 600px) {
  table.data td {
    display: block;
    padding: 5px 0;
  }
  table.data td::before {
    display: block;
    flex: none;
    margin-bottom: 1px;
  }
  table.data td .sub { text-align: left; }
  table.data tr { padding: 12px; }
  /* Values that were right-aligned by the two-column layout go back to the
     left edge now that they own the full width. */
  table.data td, table.data td .rowlink { text-align: left; }
}

@media (max-width: 460px) {
  :root { --gap: 12px; }
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
  .stat-value { font-size: 1.375rem; }
  h1 { font-size: 1.5rem; }
  .page-head-actions { width: 100%; }
  .page-head-actions .btn { flex: 1; text-align: center; }
  .deflist { grid-template-columns: 1fr; gap: 0 0; }
  .deflist dt { margin-top: 8px; }
  .toast-host { left: 12px; right: 12px; align-items: stretch; }
  .toast { max-width: none; }
}

/* Motion is decoration here, never information. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------- owner console -------- */

/* States the console reports. Colour carries meaning here, so each one is
   also a word in the markup — a reader who cannot tell the orange from the
   red still gets "Trial expired" and "Every run failing" in text. */
.tag-ok, .tag-paying, .tag-connected { color: var(--good); border-color: rgba(61,139,111,.5); }
.tag-failing, .tag-expired, .tag-revoked, .tag-unpaid, .tag-canceled {
  color: var(--bad); border-color: rgba(168,67,67,.5);
}
.tag-warning, .tag-trialing, .tag-stale, .tag-error {
  color: var(--text); border-color: var(--control-line);
}
/* "Never run" and "no billing record" are not faults — they are a company
   that has not got there yet, and red would send somebody to fix nothing. */
.tag-never, .tag-none, .tag-disconnected { color: var(--muted); border-style: dashed; }

/* The one tile on the console worth looking at.
   `--bad`, not the brand colour: this is a fixed semantic token at 4.88:1 on
   --panel, it is the same red the failing rows already use, and D-123 is
   about the *brand* colour rather than about colour. The border carries it
   too, because the first version of this fix set only the text colour, which
   resolved to the value the tile already had — a rule that did nothing on
   the one tile that needed to shout. */
.stat-warn { border-color: var(--bad); }
.stat-warn .stat-value { color: var(--bad); }

/* The console's table is wider than a phone and there is no useful way to
   fold thirteen columns. Scrolling it is honest; hiding columns would mean
   the figure you came for is the one that is gone. */
/* `min-width: 0` and `max-width: 100%` as well as the overflow. Without
   them the wrapper is sized by its content like any other block whose
   parent does not constrain it, grows to the table's width and takes the
   page sideways with it, and the scroll bar it was given never appears.
   The nine-column per-person table on the weekly report is the widest
   thing in the product and is what found this. */
.table-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  min-width: 0; max-width: 100%;
}
.table-scroll table { min-width: 900px; }

/* A narrow table that wants the scrolling and not the 900px floor. The
   two-column "busiest zip codes" list is a heading and a number, and
   inheriting a nine-column minimum gave it a horizontal scrollbar on
   every screen narrower than a desktop panel. */
.table-scroll-narrow table { min-width: 0; }

.plainlist { list-style: none; margin: 0; padding: 0; }
.plainlist > li { padding: 8px 0; border-bottom: 1px solid var(--line); }
.plainlist > li:last-child { border-bottom: 0; }

/* A read-only block of text meant to be selected and copied. Monospace
   because every use of it so far is markdown or a command. */
.codebox {
  width: 100%; font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: .8125rem; line-height: 1.5;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 3px; padding: 10px;
  resize: vertical;
}

/* Support view banner. Same loudness as the pause bar and the same reason:
   while it is up it is the most important fact on the screen, because
   somebody is reading a customer's account. */
.banner {
  padding: 10px var(--gap); margin-bottom: var(--gap);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: .875rem; border-radius: 3px;
}
.banner-warning {
  background: rgba(224, 122, 24, .14);
  border: 1px solid rgba(224, 122, 24, .5);
}
.banner form { margin: 0; }

/* ------------------------------------------------- brand-following tints --
   Three decorative fills that were hard-coded `rgba(224,122,24,…)` — the
   default accent, frozen — so on a navy-branded page they painted orange.
   D-123 says the brand colour is for decorative accents, and a tint is one.

   `@supports` and not the two-declaration trick. A declaration containing
   `var()` parses fine everywhere, so a browser without `color-mix` accepts
   it and *discards* the literal above it, then fails at computed-value time
   and falls back to the property's initial value — `transparent` — rather
   than to the previous declaration. The first version of this did exactly
   that and the comment claimed otherwise. `@supports` asks the question the
   fallback was trying to ask, and the plain rules above stay in force for
   anything that answers no.

   `color-mix(… , transparent)` interpolates premultiplied, so the result is
   the accent at that alpha rather than a colour mixed with black.
   ------------------------------------------------------------------------ */
@supports (background: color-mix(in srgb, red 50%, transparent)) {
  .credbox { background: color-mix(in srgb, var(--accent) 6%, transparent); }
  .banner-warning {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  }
}

/* The "open a support view" disclosure. Collapsed by default so the reason
   field is a deliberate step rather than something to tab past. */
.support-start > summary { cursor: pointer; display: inline-block; }
.support-start[open] > summary { margin-bottom: 8px; }
.support-start form { margin: 0; max-width: 320px; }
.support-start label { display: block; font-size: .75rem; color: var(--muted); margin-bottom: 4px; }
.support-start input { width: 100%; margin-bottom: 6px; }

@media (max-width: 720px) {
  .table-scroll table { min-width: 760px; }
  .table-scroll-narrow table { min-width: 0; }
}

/* --------------------------------------------------------- help centre -- */

.helpsearch { display: flex; gap: 8px; margin-bottom: var(--gap); flex-wrap: wrap; }
.helpsearch input { flex: 1; min-width: 200px; margin: 0; }

.helplist { list-style: none; margin: 0; padding: 0; }
.helplist > li { padding: 10px 0; border-bottom: 1px solid var(--line); }
.helplist > li:last-child { border-bottom: 0; }
.helplist a { font-weight: 600; display: block; }
.helplist .muted { font-size: .875rem; }

/* Readable measure. A help article at full desktop width is about 140
   characters a line, which is roughly twice what anybody reads comfortably. */
.helpbody { max-width: 68ch; }
.helpbody p { margin: 0 0 1em; line-height: 1.65; }
.helpbody h2 { margin: 1.5em 0 .5em; font-size: 1.0625rem; }
.helpbody ol.steps { margin: 0 0 1em 1.25em; padding: 0; }
.helpbody ol.steps > li { margin-bottom: .5em; line-height: 1.6; }
.helpbody .bullets { margin: 0 0 1em 1.25em; }
.helpbody .bullets > li { margin-bottom: .35em; line-height: 1.6; }

/* The thing in an article most worth not missing. A left bar rather than a
   colour alone, so it still reads as set apart in greyscale. */
.helpnote {
  border-left: 3px solid var(--accent);
  padding: 8px 0 8px 12px;
  background: var(--panel-2);
  margin: 0 0 1em;
}

/* ------------------------------------------------------------- billing -- */

/* Two plan cards side by side on a desktop, stacked on a phone. `auto-fit`
   with a minimum rather than a media query, because the number of plans is
   a product decision and this should not need editing when there are three. */
.plan-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--gap);
}
.plan-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px;
  background: var(--panel-2);
}
/* The plan they are on. A border rather than a fill: the brand colour is a
   decorative accent here (D-123) and the words "Your plan" are what
   actually says it, for anybody who cannot tell the two borders apart. */
.plan-card.is-current { border-color: var(--accent); }
.plan-price { font-size: 1.75rem; font-weight: 600; margin: 4px 0; letter-spacing: -0.02em; }
.plan-card .bullets { margin: 10px 0; }
.plan-card .bullets li { padding: 3px 0 3px 18px; font-size: .875rem; }
.plan-card form { margin-top: 10px; }

/* ---------------------------------------------------- business cards -- */

/* The camera entry on the contacts page. One button and a line saying
   what it does. The shutter, the "add the back" and the "Read it" that
   used to live here are gone: the file input is in `_scanentry.html`
   now, on every page, and this is a label for it. */
.cardbar {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: var(--gap);
}

/* A `<label>` styled as a button, bound to the one file input. That is
   what lets `capture="environment"` open the camera on a tap while the
   control still looks like the rest of the product: a bare file input
   cannot be styled and renders as "Choose file" on every platform. */
.cardbar-shutter {
  cursor: pointer;
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
}
.cardbar-hint { margin: 8px 0 0; }

/* The global camera entry, and the strip that reports on it.

   The form itself has no box of its own: its only visible parts are a
   `.sr-only` input and, while something is happening, the strip. */
/* No `display: contents`. It was there to keep the form out of the
   layout, which it does not need: every child is either `.sr-only`
   (absolutely positioned), a `<noscript>` that is empty while scripts
   run, or a fixed strip. An empty block takes no height anyway, and
   `display: contents` on a form has a history of dropping it out of
   the accessibility tree in some screen reader and browser pairs,
   which is a real cost for no gain. */
.scanentry { margin: 0; }

/* Every camera entry point. The file input lives inside the label, so
   the focusable thing and the visible thing are the same thing: the
   ring lands where the eye already is.
   
   Before this the input was one shared element far away in the
   document, which meant a `<label for=>` with no tab stop and no Enter
   or Space handling. Scan worked with a thumb and not with a keyboard,
   on every page, in a product whose stylesheet carries a note three
   hundred lines up about never removing a focus outline. */
.scanentry-label { cursor: pointer; }
.scanentry-label:focus-within {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.scanentry-go { margin-top: 8px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* Fixed above the tab bar, because the person is looking at whatever
   page they pressed Scan from and the answer has to find them there.
   Above `.navtabs` rather than over it: two pinned bars on top of each
   other is the bug `scripts/block_walk.py` now fails on. */
.scanentry-strip {
  position: fixed;
  left: 0; right: 0;
  bottom: calc(var(--navtabs-h) + var(--sab));
  z-index: 35;
  background: var(--panel);
  border-top: 1px solid var(--line);
  padding: 8px 14px calc(8px + 0px);
}
/* The project management screens carry a second fixed bar of their own,
   between the content and `.navtabs`, so the strip has to clear both.
   The walk cannot see this one: the strip has no box until a scan is
   running, and a hidden element cannot overlap anything. */
.pm-screen .scanentry-strip {
  bottom: calc(var(--pm-tabbar-h) + var(--navtabs-h) + var(--sab));
}
@media (min-width: 1024px) {
  /* No tab bar up here, so the strip sits on the floor. */
  .scanentry-strip,
  .pm-screen .scanentry-strip { bottom: 0; }
}
.scanentry-progress {
  height: 2px;
  background: var(--line);
  border-radius: 1px;
  overflow: hidden;
}
.scanentry-bar {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
}
@media (prefers-reduced-motion: no-preference) {
  .scanentry-bar { transition: transform .2s ease; }
}
.scanentry-status { margin: 6px 0 0; min-height: 1.2em; }

/* The screen a scan lands on.

   Two actions, both of them large, because this is the screen somebody
   uses standing up with a stack of cards in one hand. `btn-big` is here
   and not in the button section because this is the only place that
   wants it: a 56px control is wrong everywhere a form has several. */
.btn-big {
  min-height: 56px;
  font-size: 1.0625rem;
  padding-left: 20px; padding-right: 20px;
}
.savedcard { text-align: center; padding: 18px 14px; }
.savedcard-name { font-size: 1.25rem; font-weight: 600; margin: 0 0 4px; }
.savedcard-sub { margin: 2px 0; }
.savedcard-actions {
  display: flex; flex-wrap: wrap; gap: 10px;
  margin-top: var(--gap);
}
.savedcard-actions > * { flex: 1 1 14rem; justify-content: center; }
.savedcard-again { cursor: pointer; display: inline-flex; align-items: center; }
.savedcard-note { margin-top: 12px; }
.savedcard-open { margin-top: var(--gap); }

/* Row actions on a contact in the list. Their own row under the
   disclosure, for the same reason `.card-tags` has one: `.card` places
   its children by name and two things sharing `grid-row: 4` would be
   drawn on top of each other. */
.card-rowacts {
  /* Row 5, not 4. `.card-more` is row 4 column 1 and `.card-edit` is
     row 4 column 2, which is how those two coexisted; a full-width row
     4 lands on top of the disclosure. Caught by the walk's overlap
     check on the first run after it was written, at all three widths. */
  grid-row: 5; grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 6px;
}
.card-tophone { display: inline-flex; align-items: center; gap: 6px; }
.card-phoneicon {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

/* The review screen's optional second photograph. */
.cardback-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.cardback-shutter {
  cursor: pointer;
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
}
.cardback-preview {
  max-width: 120px;
  max-height: 80px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  object-fit: cover;
}

/* The review screen. One column on a phone, photograph first so it can
   be compared against the fields with a thumb. */
.card-review-grid { display: grid; gap: var(--gap); grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .card-review-grid { grid-template-columns: 0.8fr 1.2fr; align-items: start; }
  /* Sticky, so the photograph stays beside the fields while somebody
     scrolls down a long form on a laptop. */
  .card-review-shot { position: sticky; top: var(--gap); }
}
.card-shot {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  margin-bottom: 10px;
}
.panel-note { border-left: 3px solid var(--control-line); }
.panel-warn { border-left: 3px solid var(--accent); }

/* ------------------------------------------- the pause form's date -- */

/* The optional end date sits beside Pause everything on a computer and
   wraps above it on a phone, where a date input and a danger button will
   not share 390 pixels without one of them shrinking under the 24px
   target size WCAG 2.2 asks for (2.5.8). Wrapping rather than shrinking. */
.pauseform {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
}
.pauseform-until {
  display: flex; align-items: center; gap: 6px;
}
.pauseform-until label {
  color: var(--muted); font-size: 0.9rem;
}
.pauseform-until input[type="date"] {
  width: auto; min-height: 36px; margin: 0;
}

/* --------------------------------------------- the automation panel -- */

/* Whether the agents are running. Green most days, and deliberately not
   hidden when all is well: the failure it exists for is an agent going
   quiet, which looks exactly like a slow week, so a panel that only
   appears when something is wrong is one nobody learns to read.

   The state is carried on the panel as a left edge rather than as a fill,
   because D-123 keeps the brand colour off text and a tinted panel behind
   body copy is how that rule gets broken by accident. */
.autohealth { border-left: 3px solid var(--line-strong); }
.autohealth-ok { border-left-color: var(--good); }
.autohealth-stale, .autohealth-failing { border-left-color: var(--bad); }

.autolist { list-style: none; margin: 0; padding: 0; }
.autorow {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 0; border-top: 1px solid var(--line-strong);
}
.autorow:first-child { border-top: 0; }
/* `min-width: 0` so a long failure message wraps instead of pushing the
   row sideways; the same pair this stylesheet uses on every queue row. */
.autorow-name { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.autorow .muted { min-width: 0; overflow-wrap: anywhere; }

/* ------------------------------------------ one contact's money -- */

/* Two facts side by side, never added together: what they paid us and
   what they sent us. The job line is deliberately quieter than the
   payment line, because an estimate is not money. */
.ledger-row {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 8px 0; border-top: 1px solid var(--line-strong);
}
.ledger-row:first-child { border-top: 0; }
.ledger-what { font-weight: 600; }
.ledger-amount { font-variant-numeric: tabular-nums; }
.ledger-job .ledger-amount { color: var(--muted); }
/* `min-width: 0` so a long reference wraps rather than pushing the date
   off the row; the same pair this stylesheet uses on every queue line. */
.ledger-detail { min-width: 0; overflow-wrap: anywhere; }

/* ------------------------------------------------ the money tile -- */

/* Collected this year, and whether this paid for itself. The figure is
   the one number on the dashboard an owner looks for, so it is sized like
   the stat tiles rather than like body copy. */
.moneytile-figure {
  font-size: 1.75rem; font-weight: 700; margin: 4px 0 10px;
  font-variant-numeric: tabular-nums;
}
.moneytile-progress {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 10px;
}
/* A native `meter`, so the numbers reach a screen reader without a second
   hidden copy to drift from them. Firefox and Safari style the bar and
   the value separately, hence both selectors; the fallback where neither
   applies is the browser's own bar, which is legible anyway. */
.moneytile-progress meter {
  width: 140px; height: 10px; flex: 0 0 auto;
}
.moneytile-progress meter::-webkit-meter-bar {
  background: var(--panel-2); border: 1px solid var(--line-strong);
  border-radius: 999px;
}
.moneytile-progress meter::-webkit-meter-optimum-value {
  background: var(--good); border-radius: 999px;
}
.moneytile-progress meter::-moz-meter-bar { background: var(--good); }
.moneytile-roi { margin: 0; }

/* A checkbox among the dropdowns on a filter bar. Baseline-aligned with
   the selects beside it rather than centred, because a label whose text
   sits half a line above the dropdown next to it reads as a heading. */
.filterbar-check {
  display: flex; align-items: center; gap: 6px;
  color: var(--muted); font-size: .875rem; white-space: nowrap;
}
.filterbar-check input { margin: 0; }
.ledger-remove { margin: 0; }

/* ------------------------------------------- the territory map -- */

/* The map is a 640 by 400 image, and on a 390px phone it has to come
   down to fit rather than push the page sideways. `width` and `height`
   stay on the element so the browser reserves the right box before the
   image arrives and the panel does not jump; `height: auto` is what keeps
   the aspect ratio once the width is overridden. */
.territory-map img { max-width: 100%; height: auto; display: block; }
.territory-map { margin: 0 0 10px; }

/* A table wide enough that a phone has to scroll it sideways. The scroll
   is on the wrapper rather than the page, so the rest of the report stays
   put, and `tabindex` makes it reachable by keyboard: a scroll region
   nobody can focus is a scroll region a keyboard user cannot read. */


/* The orphan row. Set apart because it is not a person, and quieter
   because nobody is being judged by it. */
.is-team th, .is-team td { color: var(--muted); font-style: italic; }

/* A recorded visit position on the contact timeline. Quiet, because it is
   a footnote on the activity rather than the activity. */
.feed-where { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ------------------------------------------ tags, views, bulk -- */

/* Saved views and the way into the tag list. Wraps on a phone rather
   than scrolling, because these are shortcuts and a shortcut you have to
   scroll to is not one. */
.viewbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-bottom: var(--gap);
}
.viewchip {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--panel-2); border: 1px solid var(--line-strong);
  border-radius: 999px; padding: 2px 4px 2px 10px; font-size: .875rem;
}
.viewchip a { text-decoration: none; }
.viewchip form { margin: 0; display: flex; }

/* A control that is only a glyph. Sized to the 24px minimum WCAG 2.2
   asks for (2.5.8) rather than to its own text, which is one character. */
.btn-bare {
  background: none; border: 0; color: var(--muted); cursor: pointer;
  min-width: var(--target-min); min-height: var(--target-min);
  line-height: 1; padding: 0;
  border-radius: 999px;
}
.btn-bare:hover { color: var(--ink); }

/* One action on many contacts.

   Shut by default, because on a phone the open bar is four stacked
   controls and that is four rows of furniture in front of the first
   contact for something nobody does on most visits. A `<details>` costs
   one line when shut and needs no script.

   **The per-row checkboxes are hidden with it**, by the sibling selector
   below rather than by a script: a column of empty boxes down a list
   nobody is selecting from is the clutter this is avoiding. The
   `<details>` precedes the list inside the same form, which is what
   makes `~` reach them. */
.bulkwrap { margin: 0 0 6px; }
.bulkwrap > summary {
  cursor: pointer; color: var(--muted); font-size: .875rem;
  padding: 6px 0; min-height: 24px;
}
/* `.pickbar`, not `.bulkbar`. That name already belongs to the outreach
   queue's action bar and is used by three other pages; re-declaring it
   here won on every property this rule names, so Outreach, Prospecting
   and the admin switchboard quietly lost their horizontal padding and
   gained an asymmetric border. A test greps the *first* `.bulkbar` rule,
   so it stayed green over a change it no longer described. */
.pickbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 8px 0; margin-top: 6px;
  border-top: 1px solid var(--line-strong);
}
.bulkform { margin: 0; }
/* The boxes and the action bar appear and disappear together. */
.bulkwrap:not([open]) ~ ul .card-pick,
.bulkwrap:not([open]) ~ .pickbar { display: none; }

/* A leading control on a card, the way the outreach queue already does
   it: a column of its own, with the title and the status shifted along.

   The first version put the checkbox at `grid-row: 1; grid-column: 1` --
   the title's own cell -- and tried to move it with `float: left`, which
   does nothing to a grid item. Two items in one cell overlap, the title
   comes second in the DOM and paints over the box, and below 860px the
   title is a 44px block link filling the cell. So on the one device this
   is for, tapping the checkbox opened the contact instead. The card list
   has had the right pattern since the drafts queue; it was not followed.

   The third column only exists while the disclosure is open, so a list
   nobody is selecting from keeps the two-column grid it always had. Done
   with the same sibling selector that hides the boxes rather than with a
   class, because a class would need a script and this has to work
   without one. */
.bulkwrap[open] ~ ul .card { grid-template-columns: auto minmax(0, 1fr) auto; }
.bulkwrap[open] ~ ul .card-title { grid-column: 2; }
.bulkwrap[open] ~ ul .card-status { grid-column: 3; }
.bulkwrap[open] ~ ul .card-sub { grid-column: 2 / -1; }
.bulkwrap[open] ~ ul .card-more { grid-column: 2; }
.bulkwrap[open] ~ ul .card-edit { grid-column: 3; }
.card-pick {
  grid-row: 1; grid-column: 1;
  display: flex; align-items: center;
  min-height: var(--tap); min-width: var(--tap);
}

/* Tags and the target badge on a contact card.

   **Placed on row 2**, not left to auto-placement. Unplaced grid items
   go after every placed one, two per row, so the badge and the chips
   appeared underneath the disclosure and the Edit button in a zig-zag --
   the exact trap the comment on `.card-pick` above describes.

   `--muted` and not `--accent-ink`. `--accent-ink` is ink for things
   sitting **on** the brand colour, black or white per tenant; used as
   text on a dark panel it measured 1.65 to 1 for the default accent,
   which is a badge nobody can read. D-123 and `app/contrast.py` exist to
   stop exactly this, and the guard had a hole: the contrast tests forbid
   `--accent` as a text colour and require `--accent-ink` where the
   background is `--accent`, and nothing asserted the converse. */
/* Tags and the target badge, on a row of their own between the summary
   line and the disclosure.

   Three placements were tried. Row 2 put them on top of the summary line,
   which already spans the full width there. Unplaced, they auto-placed
   below the Edit button, two per row, in a zig-zag. Inside the summary
   line they were clipped: that line is `white-space: nowrap` with an
   ellipsis by design, so a pill pushed past the end vanished off the side
   of a phone, which is how a 150px chip came to be drawn at x=624 in a
   390px viewport.

   The row below them is empty on every card that has no tags, and an
   implicit grid row with nothing in it has no height, so nothing else
   moves. */
.card-tags {
  grid-row: 3; grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  min-width: 0;
}
/* **A pill cannot be wider than the row it is on.**
   `.tag` is `white-space: nowrap`, and `flex-wrap` wraps between pills
   and never inside one, so a single long name ran straight off the side:
   a 36-character tag measured 310px with its right edge at 335 in a
   320px viewport, clipped by `.cards` and invisible. A tag may be sixty
   characters -- the box on the contact page says so -- so this is
   reachable through the product's own form, which is the kind that
   actually happens.

   Truncated with an ellipsis rather than wrapped, because a pill that
   becomes three lines stops reading as a pill, and the full name is one
   tap away on the tag's own filtered list. */
.card-tags .tag {
  max-width: 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis;
}
.bulkwrap[open] ~ ul .card-tags { grid-column: 2 / -1; }
.tag-label { text-decoration: none; }
.tag-target { color: var(--muted); border-color: var(--line-strong); }
/* The tag cell on a contact: chips that wrap, with the add box after them. */
.tagcell { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* The chip's own link, which is not an `a.tag` and so misses the target
   size floor that rule applies on the card list. */
.tagcell .tag-label a {
  display: inline-flex; align-items: center;
  min-height: var(--target-min); text-decoration: none;
}
.tagcell .tag-label {
  display: inline-flex; align-items: center; gap: 2px;
}
.tagcell form { margin: 0; }

/* A URL or an identifier in running text. Long, unbreakable and exactly
   the kind of string that pushes a page sideways on a phone: an inline
   address in the setup help put eight pixels of horizontal scroll on the
   page at 320px, which is the width `scripts/mobile_audit.py` calls the
   one anything that will overflow a phone overflows at. */
.breakable {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9em; overflow-wrap: anywhere; word-break: break-all;
}

/* ------------------------------------------- reconstruction PM module -- */

/* Five screens for somebody standing in a half-built house holding a phone
   in one hand. Three things drive every rule below, and they are the spec's:
   the primary action sits where a thumb reaches, nothing needs two hands or
   two typed fields, and it has to be readable in direct sun.
   
   Everything reuses the existing `.card`, `.btn`, `.tag` and `.panel`
   components rather than restyling them. A module with its own button is a
   module whose buttons drift from the rest of the product, and the contrast
   and touch-target sweeps in tests/test_contrast.py and
   tests/test_mobile_layout.py already hold those components to the right
   numbers — joining them is cheaper than being audited separately. */

/* Room at the bottom for **both** fixed bars, so the last card is never
   under either. A padding on the screen rather than a margin on the bar,
   because the bar is fixed and a margin on a fixed element reserves
   nothing.

   Both, because this module was written against a product whose phone
   navigation was a bar across the top. Block 1 replaced that with
   `.navtabs`, fixed to the bottom, and nothing here knew. Merged as it
   stood, a project manager on a phone opened any of these five screens
   and the module's own bar (z-index 40, opaque) painted over Dashboard,
   Contacts, Scan, Jobs and the More sheet (z-index 30, opaque). There was
   no way back to the rest of the product except the browser's Back
   button. The module is part of the product, not a kiosk. */
.pm-screen main {
  padding-bottom: calc(
    var(--pm-tabbar-h) + var(--navtabs-h) + var(--sab) + 16px
  );
}

.pm-tabbar {
  position: fixed;
  left: 0; right: 0;
  /* Above the product's tab bar, not on top of it. Same sum the rest of
     the app reserves with. */
  bottom: calc(var(--navtabs-h) + var(--sab));
  z-index: 40;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 4px;
  min-height: var(--pm-tabbar-h);
  /* No `--sab` here any more. This bar no longer touches the bottom of
     the screen: `.navtabs` is below it and already clears the home
     indicator, so adding the inset again would reserve it twice. */
  padding: 6px 8px;
  background: var(--panel);
  border-top: 1px solid var(--line);
}

.pm-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* 48px, not the 44px iOS minimum, because the spec asks for 48 and
     because this is the one control used with wet or dusty fingers. */
  min-height: 48px;
  padding: 0 12px;
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
}
.pm-tab.is-active { background: var(--panel-2); }
.pm-tab:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* The centre button. Bigger than the tabs either side of it, because it is
   the one action the whole screen exists for and because a thumb reaching
   the middle of the bottom edge is the easiest motion on a phone. */
.pm-tab-primary {
  min-height: 52px;
  padding: 0 22px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 1.0625rem;
}

.pm-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 11px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: .75rem;
  font-weight: 700;
}

.pm-stack { margin: 0 0 var(--gap); }
.pm-stack h2 {
  display: flex; align-items: center; gap: 8px;
  font-size: 1.0625rem; margin: 0 0 6px;
}

.pm-pane {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 12px;
  margin: 0 0 var(--gap);
}
.pm-pane h2 { font-size: 1.0625rem; margin: 0 0 8px; }

/* A card whose thing is late or stale. A left edge rather than a fill: the
   fill would have to clear 4.5:1 against the text on it for every tenant
   accent, and a 3px edge carries the same signal without becoming a
   background colour. The words on the card say it too — D-123's rule, that
   colour is never the only carrier. */
.pm-card.is-late { border-left: 3px solid var(--bad); padding-left: 9px; }
/* A PM card stacks its own sub-lines instead of sharing one.

   `.card` is a grid and `.card-sub` is placed on `grid-row: 2` **by
   name**, which is fine for a contact card that has one. Every card in
   this module has two or three: a task has a project line and a blocker
   reason and the note it came from; a suggestion has an assignee line and
   the quote it was drawn from; a scope change has a requester line and a
   price. All of them were drawn in the same cell, on top of each other.

   Nothing caught it. It is not overflow, the markup is valid, the browser
   logs nothing and the tests passed: the elements are exactly where the
   stylesheet put them. `scripts/block_walk.py` compares sibling rectangles
   now, which is the check that would have.

   `auto` rather than a second named row, because a scope change can have
   three sub-lines and the next screen could have four. Auto-placement
   takes rows 2, 3, 4 in source order and never needs this rule edited
   again. Nothing else on a PM card is placed below row 1, so there is
   nothing for them to run into. */
.pm-card .card-sub { grid-row: auto; }

/* The quote keeps its whole self.

   `.card-sub` is one line with an ellipsis, which is right for an address
   and wrong for this: it is the sentence of the PM's own note that the
   model says it worked from, and a quote clipped at one line is exactly
   how you would fail to notice the model quoting something that is not in
   the note. */
.pm-card-why {
  white-space: normal; overflow: visible; text-overflow: clip;
  overflow-wrap: anywhere;
}

.pm-card-actions {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 8px;
}
.pm-card-actions form { margin: 0; }

/* The note, read-only, on the review screen. Monospace-adjacent line height
   and a quote mark, so it reads as "this is what you said" and not as an
   editable field somebody is being invited to fix. */
.pm-raw-note {
  margin: 0;
  padding: 10px 12px;
  border-left: 3px solid var(--control-line);
  background: var(--panel-2);
  border-radius: 0 var(--radius) var(--radius) 0;
  white-space: pre-wrap;
  line-height: 1.5;
}
.pm-source { font-size: .8125rem; color: var(--muted); margin: 6px 0 0; }

.pm-draft-text {
  margin: 0 0 8px;
  padding: 10px 12px;
  background: var(--panel-2);
  border-radius: var(--radius);
  white-space: pre-wrap;
  font: inherit;
  line-height: 1.5;
}

/* The Missing info box and the "waiting for you" list.
   
   Deliberately not `--bad`. Nothing has gone wrong: the model could not fill
   something in, which is the correct outcome when a note does not say. An
   error colour here would teach PMs that a well-handled gap is a fault, and
   they would stop reading the box. `--control-line` is the "this is a thing
   you operate" edge, which is what this is — and the heading says
   "Missing information" in words, which is what actually carries it. */
.pm-missing { border-left: 3px solid var(--control-line); }
.pm-missing ul { margin: 0; padding-left: 20px; }
.pm-missing li { padding: 3px 0; }

.pm-demo-banner {
  margin: 0 0 var(--gap);
  padding: 10px 12px;
  border: 1px dashed var(--control-line);
  border-radius: var(--radius);
  background: var(--panel-2);
  font-size: .875rem;
}

.pm-capture label { display: block; font-weight: 600; margin-bottom: 4px; }
.pm-capture textarea {
  width: 100%;
  min-height: 9.5rem;
  /* 1.0625rem rather than the form default: this is the one field in the
     product somebody types a paragraph into on a phone, and 16px or more is
     also what stops iOS zooming the viewport on focus. */
  font-size: 1.0625rem;
  line-height: 1.5;
}
/* The Save button is full width and pinned above the tab bar on a phone,
   because "tap Save" has to be possible without looking. */
.pm-save { width: 100%; min-height: 52px; font-size: 1.0625rem; }

.pm-sent-form, .pm-phase-form, .pm-task-form {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px;
  margin-top: 10px;
}
.pm-task-form input[type="text"] { flex: 1 1 14rem; }
.pm-sent-form label, .pm-phase-form label, .pm-task-form label {
  flex: 1 1 100%; font-weight: 600;
}

/* The copy fallback's textarea. Off-screen until the clipboard API refuses,
   then shown — selecting something invisible and telling somebody to press
   copy is not a fallback. Not `display: none`, because a hidden element
   cannot be selected. */
.pm-copy-source {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
}
.pm-copy-source.is-shown {
  position: static;
  width: 100%; height: auto; min-height: 6rem;
  margin-top: 8px;
}

.pm-timeline { list-style: none; margin: 0; padding: 0; }
.pm-timeline-item {
  padding: 10px 0;
  border-bottom: 1px solid var(--line-soft);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
}
.pm-timeline-item:last-child { border-bottom: 0; }
.pm-timeline-body {
  flex: 1 1 100%;
  margin: 4px 0 0;
  white-space: pre-wrap;
  color: var(--text);
}

/* Wide screens get the note beside the drafts, which is the spec's "Left
   (or top on phone): the original note". One media query, at the width where
   two columns stop being cramped, and the phone layout is the default rather
   than the override. */
/* 1024px, which is where `.navtabs` disappears and the sidebar appears.
   It used to be 56rem (896px), chosen before this module knew `.navtabs`
   existed, and the mismatch had its own bug: between 896 and 1023 the PM
   bar went static while the product's bar was still fixed, so the
   "+ Update" primary action sat underneath it. A module's breakpoint is
   not its own business when it shares the bottom of the screen. */
@media (min-width: 1024px) {
  .pm-screen .pm-note {
    position: sticky;
    top: calc(var(--topbar-h) + 12px);
  }
  .pm-screen main {
    /* Neither bar is fixed on a pointer device, so the reserved space
       goes with them. */
    padding-bottom: var(--gap);
  }
  .pm-tabbar {
    position: static;
    padding-bottom: 6px;
    border-top: 0;
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--gap);
  }
}
