/* Application styling — the visual identity from the design brief in the repository.
 *
 * Names deliberately absent: this file is fetched by the sign-in page, so anything written here is
 * readable by an unauthenticated visitor. The reasoning is worth keeping; the branding is not.
 *
 * EVERY COLOUR ROUTES THROUGH A CUSTOM PROPERTY. The handoff calls that required architecture rather
 * than a coat of paint, and it is what makes dark mode work without white tables or washed-out text.
 * Do not hardcode a colour in a component; add a token.
 *
 * Fonts are SELF-HOSTED, not fetched from Google. An application that phones a third party on every page
 * load hands that party its visitors' addresses, and this one is meant to keep working without depending
 * on anyone else's CDN.
 */

/* ---- fonts ------------------------------------------------------------------------------------- */
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 400; font-display: swap;
             src: url('/fonts/ibm-plex-sans-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 500; font-display: swap;
             src: url('/fonts/ibm-plex-sans-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 600; font-display: swap;
             src: url('/fonts/ibm-plex-sans-latin-600-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-style: normal; font-weight: 700; font-display: swap;
             src: url('/fonts/ibm-plex-sans-latin-700-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap;
             src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 500; font-display: swap;
             src: url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap;
             src: url('/fonts/ibm-plex-mono-latin-600-normal.woff2') format('woff2'); }

/* ---- tokens ------------------------------------------------------------------------------------ */
:root {
  --bg: #f4f6f8;            --surface: #ffffff;        --surface-2: #fafbfc;
  --border: #e7eaee;        --border-soft: #eef1f4;    --border-strong: #d3d8de;
  --track: #f1f3f5;         --chip: #eef0f2;
  --text: #16202e;          --text-body: #1f2937;      --text-secondary: #4b5563;
  --text-muted: #6b7480;    --text-label: #8a929d;     --text-dim: #97a0ab;    --text-faint: #aab2bc;
  --accent: #0f766e;        --accent-hover: #0b5d57;   --accent-text: #0f766e; --accent-tint: #e9f4f2;
  --ok: #15803d;            --ok-tint: #e8f5ec;
  --danger: #b91c1c;        --danger-tint: #fdeceb;
  --warn: #b45309;          --warn-tint: #fbf0e2;
  --indigo: #4f46e5;        --indigo-tint: #eef2ff;
  --switch-off: #cbd2d9;    --dot-off: #c4cbd3;        --toast-bg: #16202e;
  --sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* SECOND ACCENT and SIDEBAR CHROME. Added for the colourful theme, and defined here in terms of
     tokens light and dark ALREADY use, so both are unchanged by their existence — --accent-2 IS
     --accent until a theme says otherwise, and the sidebar is still a white panel. var() resolves at
     use, not at definition, so these follow whichever theme is stamped on <html> without needing a
     copy in the dark block. Components below now speak in these names, which is what lets one palette
     move the sidebar without a single rule being theme-aware. */
  --accent-2: var(--accent);              --accent-2-hover: var(--accent-hover);
  --accent-2-text: var(--accent-text);    --accent-2-tint: var(--accent-tint);
  --accent-2-on: #fff;                    /* the text colour that sits ON an --accent-2 fill */
  --figval: var(--text);                  /* the big numbers on the dashboard cards */
  --chart: var(--accent);                 /* bars, heat cells, progress fills */
  --chart-on: #fff;                       /* text that sits ON a full-strength chart fill */
  --sidebar-bg: var(--surface);           --sidebar-border: var(--border);
  --sidebar-border-soft: var(--border-soft);
  --sidebar-title: var(--text);           --sidebar-text: var(--text-secondary);
  --sidebar-muted: var(--text-muted);     --sidebar-label: var(--text-label);
  --sidebar-faint: var(--text-faint);     --sidebar-hover: var(--surface-2);
  --sidebar-pill: var(--surface-2);       --sidebar-active-bg: var(--accent-tint);
  --sidebar-active-text: var(--accent-text);
  --sidebar-active-bar: transparent;      /* the colourful theme marks the active row; others do not */

  /* WHICH SCHEME THE NATIVE CONTROLS FOLLOW. Without this the page declares `light dark` in a meta tag,
     which means the OPERATING SYSTEM decides, and the app's own theme has no say. On a machine set to
     dark that painted a light clock icon inside <input type="time"> — invisible against the white field
     of the light and colourful themes, visible only in dark. Same root cause as the near-black
     checkboxes, in a control we cannot redraw ourselves: the picker indicator belongs to the browser,
     so the only lever is telling it which scheme it is drawing into. Declared per theme below. */
  color-scheme: light;
}
/* theme.js stamps the RESOLVED theme on <html>, so "system" has already collapsed to one of these two
 * and no rule below ever has to reason about "auto". */
[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0d1117;            --surface: #161d27;        --surface-2: #1b232e;
  --border: #29323e;        --border-soft: #222b36;    --border-strong: #3a4450;
  --track: #232c38;         --chip: #29323e;
  --text: #eef2f7;          --text-body: #dfe5ec;      --text-secondary: #b0bac6;
  --text-muted: #8b95a2;    --text-label: #7a8490;     --text-dim: #8b95a2;    --text-faint: #5d6875;
  --accent: #0f766e;        --accent-hover: #148f84;   --accent-text: #2dd4bf; --accent-tint: #123832;
  --ok: #4ade80;            --ok-tint: #123524;
  --danger: #f87171;        --danger-tint: #3a1d1d;
  --warn: #fbbf24;          --warn-tint: #3a2c12;
  --indigo: #a5b4fc;        --indigo-tint: #232544;
  --switch-off: #3a4450;    --dot-off: #48525f;        --toast-bg: #0a0e14;
}

/* A THIRD PALETTE, not a third mode (Rick, 2026-08-24). Light and dark answer "how bright"; this one
 * answers "how much colour", so it is a peer of them rather than a variant — theme.js stamps it the
 * same way and nothing below has to know it exists.
 *
 * Blue and orange, and specifically THIS blue and orange, because they are near-exact complements
 * (hue 211 and 31) and because blue/orange is the pairing that survives red-green colour blindness.
 * That is not decoration here: the person who asked for it is colourblind.
 *
 * EVERY VALUE WAS CHECKED RATHER THAN EYEBALLED, and two would have shipped wrong if it had not been.
 *   - The first draft put --danger and --warn at IDENTICAL greyscale lightness (12.7 and 12.7). Red
 *     and amber are the one pair that must never converge, and to a reader who cannot use hue they
 *     would have been the same colour. The four status levels now sit at 4.6 / 8.7 / 12.7 / 16.0, so
 *     they separate on lightness alone.
 *   - White text on the bright orange is 2.75:1, nowhere near the 4.5:1 a button needs. Rather than
 *     dull the orange into brown, the orange fill carries DARK NAVY text (--accent-2-on, 5.42:1).
 *     That is why --accent-2-on exists as a token instead of #fff being assumed.
 * The checker also reports that brand blue sits 1.4 from --warn in greyscale. Left as is deliberately:
 * --accent is chrome (nav, fills, identity) and never stands in for a status, so no reader is asked to
 * tell those two apart. Statuses are compared against statuses.
 *
 * Bright orange is a FILL, never small text: --accent-2 for fills, --accent-2-text (deeper) for
 * anything read as words. The dashboard figures use the text variant for that reason.
 */
[data-theme="colour"] {
  color-scheme: light;   /* white cards and a tinted page: a LIGHT scheme, despite the navy chrome */
  --bg: #E9F1FA;            --surface: #FFFFFF;        --surface-2: #F4F9FE;
  --border: #C8DCF0;        --border-soft: #DEEAF7;    --border-strong: #9BC0E4;
  --track: #D7E7F6;         --chip: #DCEAF7;
  --text: #0A2942;          --text-body: #16344F;      --text-secondary: #33587A;
  --text-muted: #456B8A;    --text-label: #52738F;     --text-dim: #52738F;    --text-faint: #7897B3;
  --accent: #1668C4;        --accent-hover: #0F4E96;   --accent-text: #0C5AA6; --accent-tint: #DBEAFB;
  --accent-2: #E8820F;      --accent-2-hover: #F59A23; --accent-2-text: #A65308;
  --accent-2-tint: #FCEBD4; --accent-2-on: #0A2942;
  --figval: #A65308;
  /* DATA GETS ITS OWN BLUE, lighter than the chrome blue, so a bar chart stops reading as furniture.
     4.80:1 under navy digits, 14.7 of greyscale lightness clear of --accent and 4.4 clear of the
     orange. The obvious choice (the brand blue itself) is unusable here: it lands between the two
     text colours at 3.82:1 under navy and 3.90:1 under white, legible under neither. */
  --chart: #3D95F0;         --chart-on: #0A2942;
  --ok: #16803F;            --ok-tint: #E7F7EC;
  --danger: #A31D14;        --danger-tint: #FAE0DE;
  --warn: #8A5A00;          --warn-tint: #F7EBD3;
  --indigo: #4C1D95;        --indigo-tint: #E9E2F8;
  --switch-off: #A9C4DD;    --dot-off: #9DBAD6;        --toast-bg: #0A2942;
  --sidebar-bg: #0E3A5F;    --sidebar-border: #17507E; --sidebar-pill: #12456F;
  --sidebar-border-soft: #164A74;
  --sidebar-title: #FFFFFF; --sidebar-text: #C2DCF2;   --sidebar-muted: #9BBEDC;
  --sidebar-label: #9BBEDC; --sidebar-faint: #7FA6C8;  --sidebar-hover: #17507E;
  --sidebar-active-bg: #17507E; --sidebar-active-text: #FFFFFF;
  --sidebar-active-bar: #F59A23;
}

/* ---- base -------------------------------------------------------------------------------------- */
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--text-body); font: 400 13.5px/1.55 var(--sans); }
a { color: var(--accent-text); text-decoration: none; }
a:hover { text-decoration: underline; }
:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; border-radius: 4px; }
.mono, td.num { font-family: var(--mono); }

/* ---- shell ------------------------------------------------------------------------------------- */
.shell { display: flex; min-height: 100vh; }
.sidebar {
  width: 250px; flex: 0 0 250px; background: var(--sidebar-bg); border-right: 1px solid var(--sidebar-border);
  position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; padding: 16px 12px;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 4px 6px 14px; }
.brand-tile { width: 34px; height: 34px; border-radius: 9px; background: var(--accent); color: #fff;
              display: grid; place-items: center; flex: 0 0 34px; }
.brand-name { font: 700 16px/1.1 var(--sans); color: var(--sidebar-title); }
.brand-sub  { font: 500 11.5px/1.3 var(--sans); color: var(--sidebar-muted); }

.client-block { padding: 0 6px 14px; }
.client-label { font: 600 11px/1 var(--sans); letter-spacing: .4px; text-transform: uppercase;
                color: var(--sidebar-label); margin-bottom: 6px; }
.client-pill { display: flex; align-items: center; gap: 9px; background: var(--sidebar-pill);
               border: 1px solid var(--sidebar-border); border-radius: 9px; padding: 7px 9px; }
.client-initials { width: 30px; height: 30px; flex: 0 0 30px; border-radius: 8px; background: var(--accent-tint);
                   color: var(--accent-text); display: grid; place-items: center; font: 600 12px var(--sans); }
.client-name { font: 600 13.5px/1.2 var(--sans); color: var(--sidebar-title); }
.client-meta { font: 400 11.5px/1.3 var(--sans); color: var(--sidebar-muted); }

nav.side { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
nav.side a { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 9px;
             font: 500 14px var(--sans); color: var(--sidebar-text); }
nav.side a:hover { background: var(--sidebar-hover); text-decoration: none; }
/* The active row is marked by a BAR as well as a fill. In light and dark the bar is transparent and
   nothing changes; the colourful theme turns it orange, which gives "where am I" a second signal that
   does not depend on telling one blue from another. */
nav.side a.active { font-weight: 600; color: var(--sidebar-active-text); background: var(--sidebar-active-bg);
                    box-shadow: inset 3px 0 0 var(--sidebar-active-bar); }
nav.side svg { flex: 0 0 18px; }
/* A not-yet-built entry says "soon" in WORDS. A greyed-out row on its own is not a message, and this
 * portal's rule is that nothing is conveyed by colour or dimness alone. */
nav.side span.soon { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 9px;
                     font: 500 14px var(--sans); color: var(--sidebar-faint); cursor: default; }
nav.side span.soon em { margin-left: auto; font: 600 10px var(--sans); font-style: normal;
                        letter-spacing: .4px; text-transform: uppercase; }

.user-block { margin-top: auto; border-top: 1px solid var(--sidebar-border-soft); padding: 12px 6px 2px;
              display: flex; align-items: center; gap: 9px; }
.user-avatar { width: 32px; height: 32px; flex: 0 0 32px; border-radius: 999px; background: var(--accent);
               color: #fff; display: grid; place-items: center; font: 600 12px var(--sans); }
.user-name { font: 600 13px/1.2 var(--sans); color: var(--sidebar-title); }
.user-role { font: 400 11.5px/1.3 var(--sans); color: var(--sidebar-muted); text-transform: capitalize; }
.signout { margin-left: auto; color: var(--sidebar-muted); display: grid; place-items: center; padding: 6px;
           border-radius: 7px; background: none; border: 0; cursor: pointer; }
.signout:hover { color: var(--sidebar-title); background: var(--sidebar-hover); }

.main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.topbar { position: sticky; top: 0; z-index: 5; background: var(--surface);
          border-bottom: 1px solid var(--border); padding: 13px 30px; display: flex;
          align-items: center; gap: 12px; }
.topbar h1 { margin: 0; font: 700 19px/1.25 var(--sans); letter-spacing: -.2px; color: var(--text); }
.topbar .sub { font: 400 12.5px var(--sans); color: var(--text-muted); }
.client-tag { font: 600 12px var(--sans); color: var(--accent-text); background: var(--accent-tint);
              border-radius: 6px; padding: 3px 8px; white-space: nowrap; }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; }
/* The client belongs ON the title line. As a sibling of the title block the flex row centred it between
   the heading and the subtitle, where it read as attached to neither. */
.titleline { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.lastcall { display: inline-flex; align-items: center; gap: 7px; font: 400 12.5px var(--sans);
            color: var(--text-muted); white-space: nowrap; }
.lastcall strong { font-weight: 600; color: var(--text); }
/* The dot is decoration. The words beside it carry the state — colour is never the only signal. */
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: 0 0 auto; }
.dot-idle { background: var(--text-faint); }
/* The Alerts states. Named after the LEVEL rather than the colour, so a level can be recoloured without
   the markup lying about what it means. dot-ok is the default fill and is listed for completeness. */
.dot-ok   { background: var(--ok); }
.dot-warn { background: var(--danger); }
.dot-info { background: var(--indigo); }

/* Alerts list. A state column, then the alert. The state column is a fixed width so the titles line up
   into a readable left edge instead of stepping in and out with the length of each word. */
.alertlist  { margin-top: 16px; border-top: 1px solid var(--border); }
.alertrow   { display: grid; grid-template-columns: 132px 1fr; gap: 14px;
              padding: 14px 2px; border-bottom: 1px solid var(--border); align-items: start; }
.alertstate { display: flex; align-items: center; gap: 8px; padding-top: 2px; }
/* THE WORD, NOT THE COLOUR. Every row states its level in text beside the dot, so the page reads
   identically with colour ignored entirely — the same rule the status messages follow. */
.alertword  { font: 600 12px var(--sans); color: var(--text-secondary); white-space: nowrap; }
.alerttitle { font: 600 14.5px var(--sans); color: var(--text); }
.alertdetail{ margin-top: 3px; font: 400 13px/1.6 var(--sans); color: var(--text-muted); }
@media (max-width: 700px) { .alertrow { grid-template-columns: 1fr; gap: 4px; } }
.nowrap { white-space: nowrap; }
.muted { color: var(--text-faint); }
/* Holidays: the list and the add-form side by side, as in the design. Collapses to one column when
   there is not room for two, because a date field squeezed into a third of a phone is unusable. */
.holgrid { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 14px; align-items: start; }
.sidecard { position: sticky; top: 86px; }
@media (max-width: 1080px) { .holgrid { grid-template-columns: 1fr; } .sidecard { position: static; } }
.owner { color: var(--text-muted); font-size: 12.5px; }
.actions .danger { color: var(--danger); }
/* Apply-to: two cards rather than bare radios — the choice changes how many records are written, which
   is worth making hard to misread. Worded without naming the industry: this file is served to the
   SIGN-IN page, which must disclose nothing about the product to anyone unauthenticated. */
.applyto { margin-bottom: 14px; }
.radiocard { display: flex; align-items: center; gap: 9px; padding: 9px 11px; margin-top: 6px;
             border: 1px solid var(--border-strong); border-radius: 9px; cursor: pointer; }
.radiocard:has(input:checked) { border-color: var(--accent-text); background: var(--accent-tint); }
.radiocard span { display: flex; flex-direction: column; }
.radiocard strong { font: 600 13px var(--sans); color: var(--text); }
.radiocard em { font: 400 11.5px var(--sans); color: var(--text-muted); font-style: normal; }
.phcount { text-align: center; min-width: 58px; }
.phcount-n { font: 700 21px var(--sans); color: var(--text); line-height: 1.1; }
.lunchpair { display: flex; align-items: center; gap: 8px; }
.lunchpair input { flex: 1 1 0; min-width: 0; }
.opt { font-weight: 400; color: var(--text-faint); text-transform: none; letter-spacing: 0; }
/* A disabled field has to LOOK disabled wherever it is. This only covered .fieldset, so ticking
   "Closed all day" greyed nothing on the holiday form and left four live-looking boxes. */
input:disabled { opacity: .4; cursor: not-allowed; }
/* The special-messages grid: sites down, message slots across. (Worded without naming the industry —
   this stylesheet is served to the SIGN-IN page, which discloses nothing to anyone unauthenticated.
   Three separate comments have tripped that check now; assume every word here is public.) */
.grid th.slotcol, .grid td.slotcol { text-align: center; width: 104px; white-space: nowrap; }
.grid th.slotcol em { display: block; font: 400 10.5px var(--sans); font-style: normal; color: var(--text-faint); }
.phname { font: 600 14px var(--sans); color: var(--text); }
/* BLOCK-LEVEL flex, not inline-flex, so no baseline is involved at all.
   inline-flex sits on the text baseline, and vertical-align:middle only moves it to the parent's baseline
   plus half an x-height — close to the cell's centre but never exactly it, which is the last pixel or two
   of offset that survived making both controls the same height. A block-level flex box inside a table
   cell is centred by the cell's own vertical-align:middle, which IS exact. */
.cellpair { display: flex; align-items: center; justify-content: center; gap: 9px; line-height: 1; }
.slotcol { vertical-align: middle; }
/* A DRAWN checkbox, not the browser's. The native control is a small blue system square that ignores the
   palette entirely and sits on its own baseline — the single biggest reason this grid looked unfinished
   next to the design. appearance:none is what makes any of the styling below apply at all.
   The tick is a SHAPE, not a colour change: the box fills teal AND grows a check, so the state is
   readable without relying on hue.

   SCOPED TO EVERY CHECKBOX, not just this grid (2026-08-24). It was written for .cellpair and stayed
   there, so Hours, Holidays, Accounts and Outbound went on rendering the OS control — and since the
   app then declared color-scheme: light dark, a machine in dark mode painted them near-black on a
   white card. (That declaration is now per-theme, a few dozen lines up, which fixes the same problem
   for the controls we cannot redraw.) Rick found it on the Hours page, where "Closed" and "24 hours" decide what every caller hears
   and the tick therefore has to be unambiguous. The narrow scope also meant the "readable without
   relying on hue" promise directly above only held on one page out of five.

   This is the one change here that DOES alter light and dark: those four pages get the drawn control
   in every theme, which is what the rule always intended. */
input[type="checkbox"] { appearance: none; -webkit-appearance: none;
    width: 22px; height: 22px; margin: 0; display: block; cursor: pointer;
    background: var(--surface); border: 1.5px solid var(--switch-off); border-radius: 6px; }
input[type="checkbox"]:hover { border-color: var(--accent-text); }
input[type="checkbox"]:checked { background: var(--accent); border-color: transparent; }
input[type="checkbox"]:checked::after {
    content: ""; display: block; width: 5px; height: 10px; margin: 1px auto 0;
    border: solid #fff; border-width: 0 2.5px 2.5px 0; transform: rotate(45deg); }
input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
/* appearance:none here too. Without it the browser paints its NATIVE button — a grey rounded box that
   ignores `background` outright and carries its own padding, which dropped the control below the
   checkbox and made it look disabled. */
.playbtn { appearance: none; -webkit-appearance: none;
           display: grid; place-items: center; width: 22px; height: 22px; padding: 0; line-height: 0;
           color: var(--accent-text); background: transparent; border: 0; border-radius: 6px; cursor: pointer; }
/* 20px box so the triangle inside lands at ~15px — the size the design's 15px icon FONT renders at. */
.playbtn svg { display: block; width: 20px; height: 20px; }
/* The client-wide play is a 34px TINTED disc with an accent-coloured glyph — not a solid disc with a
   white one, which is what I had guessed. Taken from the design rather than eyeballed. */
.playbtn.disc { width: 34px; height: 34px; border-radius: 50%;
                color: var(--accent-text); background: var(--accent-tint); }
.playbtn.disc:hover { background: var(--accent-tint); filter: brightness(1.15); }
.playbtn.disc svg { width: 19px; height: 19px; margin-left: 2px; }
.playbtn:hover { background: var(--chip); }
.playbtn.playing { background: var(--accent-tint); }
.playbtn.failed { color: var(--danger); }

/* The client-wide card: a badge, a switch, and a word saying which way it is set. */
/* Room to breathe under the save row. The button sitting hard against the card edge made the card look
   cropped rather than finished. */
.globalbadge { display: grid; place-items: center; width: 42px; height: 42px; flex: 0 0 auto;
               border-radius: 12px; color: var(--accent-text); background: var(--accent-tint); }
.globalbadge svg { display: block; }
.globalswitch { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.switch { position: relative; display: block; line-height: 0; }
.switch input { position: absolute; inset: 0; opacity: 0; margin: 0; width: 100%; height: 100%; cursor: pointer; }
.switch-track { display: block; width: 44px; height: 24px; border-radius: 999px;
                background: var(--switch-off); transition: background .15s ease; }
.switch-knob { display: block; width: 18px; height: 18px; margin: 3px; border-radius: 50%;
               background: #fff; transition: transform .15s ease; }
.switch input:checked + .switch-track { background: var(--accent); }
.switch input:checked + .switch-track .switch-knob { transform: translateX(20px); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--accent-text); outline-offset: 2px; }
/* The WORD, beside the switch. A knob's position is a real non-colour cue, but a word is unambiguous and
   costs one span — the same rule every other status on this site follows. */
.switch-state { font: 600 12px var(--sans); color: var(--text-secondary); min-width: 22px; }
.globalplay { display: flex; align-items: center; gap: 9px; margin-top: 10px; }
.globalplay .hint { margin: 0; }
/* margin: 0 IS THE ALIGNMENT FIX, and it is worth knowing why. The bare `label` rule further down is for
   FORM FIELDS and gives every label `margin: 14px 0 5px`. This label is not a field caption, it is a
   wrapper around the checkbox — but it inherited that margin all the same, so its MARGIN box was
   14 + 22 + 5 = 41px tall. align-items: center centres the margin box, not the border box, so the flex
   line became 41px, the play button centred on it exactly, and the checkbox landed 4.5px lower. The two
   controls were both 22px, both centred, and still visibly out of line.
   Measured in a headless browser rather than reasoned about, after the same class of bug had already
   cost four attempts elsewhere. Do not "fix" this with a nudge on .playbtn; the margin is the cause. */
.cellbox { display: inline-grid; place-items: center; cursor: pointer; margin: 0; }
.cellbox input { width: 17px; height: 17px; }
.cellbox.big input { width: 22px; height: 22px; }
.globalcard { border-color: var(--accent-text); }
.globalhead { display: flex; align-items: center; gap: 16px; }
.globaltext { flex: 1 1 auto; min-width: 0; }
.globalhead .hint { margin: 4px 0 0; max-width: 62ch; }
.lunchcell { display: flex; align-items: center; gap: 7px; }
.tablenote { margin: 14px 0 0; }
/* margin:0 overrides the global label rule, which adds 14px above every label and was spacing the
   checkboxes apart down the cell. */
.daycheck { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
            margin: 0; font: 500 13px var(--sans); color: var(--text-body); }
.daycheck input { margin: 0; }
/* ---- the per-site configuration report --------------------------------------------------------------
   CLASS NAMES ARE PREFIXED cfg-. The first version reused .frow and .fname, which the Upgrades page has
   owned for 800 lines — same specificity, later definition wins, so the three-column feature row
   silently became Upgrades' two-column one and shipped looking broken.

   THE SECOND VERSION WAS DENSE AND STILL UNREADABLE, which is the more interesting failure. Pairs were
   laid out as label-left / value-right with the value pushed to the far edge, so "NCPDP" and "9999999"
   sat 200px apart and the eye had to travel an empty gap to join them. Every pair then carried a bottom
   border, so four columns of eight pairs drew a grid of rules across the card. Density was fine; the
   READING was the problem.

   So: the value sits DIRECTLY UNDER its label, and the rules are gone. A stacked pair has no gap to
   cross and needs no line to group it — proximity does that work, and whitespace separates the pairs
   from each other. The label is small and quiet, the value is the thing you came to read.

   FEATURES ARE SPLIT BY STATE, not listed alphabetically with nine identical "Off"s down the right.
   What is ON, and what is on but not working, is the answer to the question; what is off is a footnote
   and reads as one. Every feature still carries its own mark and its own word.

   A feature's state is carried THREE ways at once — a distinct glyph, a word, and only then a colour.
   Drop the colour and the page still reads. */

/* Stacked label/value. No borders: proximity groups them, whitespace separates them. */
/* A FIXED COLUMN COUNT, not auto-fit. auto-fit packed eight pairs as seven across and one orphan on a
   row of its own, which reads as a mistake. Four columns means eight pairs are two tidy rows, and the
   count steps down with the window instead of being decided by whatever width is left over. */
.cfg-pairs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px 26px; margin: 0; }
@media (min-width: 700px)  { .cfg-pairs { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .cfg-pairs { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.cfg-pair { min-width: 0; }
.cfg-pair dt { font: 600 9.5px/1.4 var(--sans); letter-spacing: .55px; text-transform: uppercase;
               color: var(--text-label); }
/* SANS, not mono. Nothing stacks under anything here, so there is no column to align and mono only
   makes "America/New_York" and a voice name look like machine output. The LISTS keep mono, where the
   values genuinely do line up under each other. */
.cfg-pair dd { margin: 1px 0 0; font: 500 13.5px/1.45 var(--sans); color: var(--text);
               overflow-wrap: anywhere; }
/* A long list (weekly hours, transfer numbers) reads better as rows than as a wrapping grid. */
.cfg-list { display: grid; grid-template-columns: auto 1fr; gap: 3px 14px; margin: 0; align-items: baseline; }
.cfg-list dt { font: 500 12.5px var(--sans); color: var(--text-secondary); white-space: nowrap; }
.cfg-list dd { margin: 0; font: 500 12.5px var(--mono); color: var(--text); text-align: right; }

/* Features that are ON or need attention. One line each; the state follows the name, it is not flung
   to the far margin. */
.cfg-feat { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; padding: 5px 0; }
.cfg-mark { display: inline-grid; place-items: center; width: 15px; height: 15px; flex: 0 0 15px;
            position: relative; top: 2px; }
.cfg-mark svg { width: 14px; height: 14px; }
.cfg-on { color: var(--ok); } .cfg-partial { color: var(--warn); } .cfg-off { color: var(--text-faint); }
.cfg-name  { font: 600 13px var(--sans); color: var(--text); }
.cfg-state { font: 500 12.5px var(--sans); color: var(--text-secondary); }
.cfg-note  { flex: 1 0 100%; margin: 0 0 0 23px; font: 400 12px/1.5 var(--sans); color: var(--text-muted); }
/* What is switched off is a footnote, and reads as one: small, quiet, and wrapped into a paragraph
   rather than nine rows each saying the same word down the right-hand edge. */
.cfg-offlist { margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--border-soft);
               font: 400 12.5px/2 var(--sans); color: var(--text-muted); }
.cfg-offlist b { font: 600 9.5px var(--sans); letter-spacing: .55px; text-transform: uppercase;
                 color: var(--text-label); margin-right: 4px; }
.cfg-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; margin: 0 5px 5px 0;
            border: 1px solid var(--border); border-radius: 999px; background: var(--surface-2);
            font: 500 12px var(--sans); color: var(--text-secondary); white-space: nowrap; }
.cfg-chip svg { width: 9px; height: 9px; color: var(--text-faint); flex: 0 0 9px; }

.cfg-blocker { display: grid; grid-template-columns: 18px 1fr; gap: 11px; align-items: start;
               padding: 10px 13px; border-radius: 8px; background: var(--danger-tint);
               border-left: 3px solid var(--danger); margin-bottom: 7px; }
.cfg-blocker .cfg-mark { color: var(--danger); top: 1px; }
.cfg-blocker b { display: block; font: 600 13px var(--sans); color: var(--text); }
.cfg-blocker span { font: 400 12px/1.5 var(--sans); color: var(--text-body); }
.cfg-clear { display: flex; align-items: baseline; gap: 9px; margin: 14px 0 0; padding-top: 12px;
             border-top: 1px solid var(--border-soft);
             font: 400 12.5px var(--sans); color: var(--text-secondary); }
.cfg-clear .cfg-mark { color: var(--ok); }

.cfg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
            gap: 14px; align-items: start; }
.cfg-grid .card { margin: 0; padding: 15px 17px; }
.cfg-grid .cardtop { margin-bottom: 11px; }
/* Section headings are the map of the page, so they are set apart from the body text rather than
   being another line of 14px bold among many. */
.cfg-grid h2, .cfg-card h2 { font: 600 10.5px var(--sans); letter-spacing: .7px; text-transform: uppercase;
                             color: var(--text-secondary); }
.cfg-card { padding: 16px 18px; }
.cfg-card .cardtop { margin-bottom: 13px; }
.cfg-card > h3 { margin: 0 0 14px; font: 700 16px var(--sans); color: var(--text); letter-spacing: -.2px; }
.cfg-edit { margin-left: auto; font: 500 12px var(--sans); text-transform: none; letter-spacing: 0; }

/* The filing copy: one flat grid, setting and value, so two printouts line up row for row. Deliberately
   NOT the card layout above — that one is built to be scanned on a screen, this one to be read in order
   and compared against another copy months later. */
.cfgsheet { width: 100%; border-collapse: collapse; }
.cfgsheet td { padding: 4px 8px; font: 400 12px var(--sans); color: var(--text-body);
               border-bottom: 1px solid var(--border-soft); vertical-align: top; }
.cfgsheet td:first-child { width: 34%; color: var(--text-secondary); font-weight: 500; }
.cfgsheet td:last-child { font-family: var(--mono); font-size: 11.5px; color: var(--text); }
.cfgsheet .cfgsect th { text-align: left; padding: 14px 8px 4px; font: 600 10px var(--sans);
                        letter-spacing: .7px; text-transform: uppercase; color: var(--text-label);
                        border-bottom: 1px solid var(--border-strong); }
/* A section heading stranded at the foot of a sheet names rows that are on the next one. */
@media print { .cfgsheet .cfgsect th { break-after: avoid; page-break-after: avoid; } }

.cfg-checklist { margin: 6px 0 0; padding-left: 20px; font: 400 12.5px/1.7 var(--sans);
                 color: var(--text-body); }
.cfg-checklist li { margin-bottom: 2px; }

/* ---- dashboard ------------------------------------------------------------------------------------ */
.cardtop { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 12px; }
.cardtop h2 { margin: 0; }

/* WHEN THESE NUMBERS WERE LAST TRUE. The card above this one refreshes every three seconds; everything
   below it refreshes every five minutes, and without a line saying so the two are indistinguishable —
   a live header sat above figures that had stopped counting an hour earlier. The countdown is as much
   of the point as the refresh: a page that updates silently cannot be told apart from one that has
   stopped. Words and numbers only, no colour carrying meaning.
   A BAND, in the same tone as a table's header row (Rick, 2026-08-06). It sat as bare text between two
   cards, which read as something left on the page rather than part of it — the tone and the border are
   what make it deliberate, and they are the ones this file already uses for a header row rather than a
   fifth invention. */
/* WRAPS RATHER THAN CRUSHES. The band carries three things now (which window, how old, and a button),
   and at a narrow width space-between would squeeze the window range into a column of single words. */
.freshline { display: flex; align-items: center; justify-content: space-between; gap: 6px 12px;
             flex-wrap: wrap;
             background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px;
             padding: 8px 14px; margin-bottom: 12px; }
.freshline .hint { font-size: 12px; }
/* The control and the timestamp are one group, so space-between divides the row into what it says
   (which window) and how current it is (with the button that changes that), rather than stranding a
   button in the middle. Wraps as a unit, so the button never separates from its own label. */
.freshright { display: inline-flex; align-items: center; gap: 10px; }
/* THE LAST TWO MINUTES, and not a second before. A permanent countdown to being signed out is ambient
   anxiety for the twenty-eight minutes it does not matter, and people learn to ignore exactly the
   warning you need them to read. Silence until it matters, then a strip that says how long and offers
   the one thing worth doing about it.
   Across the top rather than in the figures band: this is about the person, not the data. */
.sessionwarn { position: fixed; top: 0; left: 0; right: 0; z-index: 50;
               display: flex; align-items: center; justify-content: center; gap: 14px;
               background: var(--warn-tint); color: var(--text); border-bottom: 1px solid var(--border);
               font: 500 13px var(--sans); padding: 9px 16px; }
/* FIXED, so appearing does not shove the page down under whoever is mid-edit — which on the hours editor
   would move the field they are typing into at the exact moment they are being warned about losing it. */
/* Colour is never the only signal — the sentence says what is happening and the clock counts down. */
.refreshbtn { font: 500 12px var(--sans); color: var(--text-secondary); background: var(--surface);
              border: 1px solid var(--border); border-radius: 8px; padding: 5px 10px; cursor: pointer; }
.refreshbtn:hover { color: var(--accent-text); border-color: var(--accent-text); }
.livetitle { display: flex; align-items: center; gap: 8px; font: 600 14px var(--sans); color: var(--text); }
.livegrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
.livegrid > [hidden] { display: none; }
@media (max-width: 1000px) { .livegrid { grid-template-columns: 1fr; } }
.cardtools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.livetabs { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border);
            border-radius: 9px; padding: 3px; gap: 2px; }
.livetab { appearance: none; -webkit-appearance: none; border: 0; border-radius: 7px; cursor: pointer;
           padding: 5px 10px; font: 600 12px var(--sans); color: var(--text-secondary);
           background: transparent; }
.livetab[aria-pressed="true"] { background: var(--accent); color: #fff; }
.tabn { opacity: .75; font-variant-numeric: tabular-nums; }
/* A RULE, NOT A FILL, and the difference is the shape of the thing being labelled.
   The refresh banner and a table's header row earn var(--surface-2) because both are full-width strips
   with content spread across them. These are three short labels in 450px columns: filled, they read as
   disabled input fields rather than headers, and the 22px grid gap breaks them into separate blobs
   instead of the continuous strip that makes a header row work. Rendered both before choosing (Rick,
   2026-08-06). A hairline separates "what this column is" from "what is in it" and spends nothing.
   Borrowing the tint for a shape it does not suit is how a design vocabulary stops meaning anything. */
.livehead { font: 600 10.5px var(--sans); letter-spacing: .6px; text-transform: uppercase;
            color: var(--text-label); border-bottom: 1px solid var(--border);
            padding-bottom: 6px; margin-bottom: 10px; }
/* --ok, NOT --accent-text, and the difference is visible across the room (Rick, 2026-08-07). The design
   sets this count in var(--ok) (#4ade80); this had it in var(--accent-text) (#2dd4bf), a teal that sits
   much closer to the card behind it. Both tokens exist in both stylesheets with identical values, so
   comparing the TOKEN TABLES said they matched and proved nothing — what differs is which token gets
   applied, and that is the only comparison worth making. */
.livecount { color: var(--ok); }
/* The one cell in the breakdown that is a VERDICT rather than a count, marked the way the design marks
   it — var(--ok) at 600, everything else in the row plain. Never applied to the no-data dash: a dash in
   success green claims a result where nothing was measured. The number stays beside it, always; colour
   is a second encoding here, never the only one. */
.selfserve { color: var(--ok); font-weight: 600; }
/* A FIXED height, so the card cannot resize under the reader. Six rows plus the "and N more" line; the
   min-height matters as much as the max, or an empty panel collapses and the page jumps the other way. */
/* Fixed, so the card cannot resize under the reader — but sized for FOUR rows rather than six. Six left a
   wall of empty space whenever nothing was happening, which is most of the time. */
/* Sized to hold exactly what is rendered: three rows plus the "and N more" line. It previously rendered
   FOUR and clipped the fourth — and the overflow note with it — so the panel silently under-reported. */
.liverows { min-height: 186px; max-height: 186px; overflow: hidden; }
.livemore { padding: 9px 0 0; font: 500 12px var(--sans); color: var(--text-muted); }
.liverow { display: flex; align-items: center; justify-content: space-between; gap: 12px;
           padding: 9px 0; border-bottom: 1px solid var(--border-soft); }
.liverow:last-child { border-bottom: 0; }
/* THE STEP AS A SHAPE, beside the words rather than instead of them. Three rows of similar-length
   sentences in one weight are read a word at a time; a glyph resolves from across the shop, which is the
   distance this card is actually read from. The disc is the same tinted-square treatment the sidebar and
   the client tile already use, so it reads as part of the vocabulary rather than as a new kind of badge.
   min-width:0 on the text column, or a long site name pushes the timer off the row. */
.liveleft { display: flex; align-items: center; gap: 10px; min-width: 0; }
.livetext { min-width: 0; }
.liveicon { width: 30px; height: 30px; flex: 0 0 30px; border-radius: 9px; display: grid;
            place-items: center; color: var(--accent-text); background: var(--accent-tint); }
/* THE TINT SAYS WHAT KIND OF STEP IT IS, using the design's own three states and its own tokens. It is
   the THIRD encoding of that fact, never the first: the row already carries its words and its glyph, and
   both survive if the tint does not arrive. That ordering is the rule here generally and it earns its
   keep on this card, which is scanned rather than read.
   st-step is the default and needs no rule — it is what .liveicon already is. */
.liveicon.st-good  { color: var(--ok);         background: var(--ok-tint); }
.liveicon.st-warn  { color: var(--warn);       background: var(--warn-tint); }
.liveicon.st-text  { color: var(--indigo);     background: var(--indigo-tint); }
.liveicon.st-muted { color: var(--text-muted); background: var(--track); }
.livestep { font: 500 13.5px var(--sans); color: var(--text); }
.livewho { font-size: 12px; color: var(--text-muted); }
.livetime { font-size: 13px; color: var(--text-secondary); }

/* Tiles: SIX ACROSS, as the design has them (repeat(6, 1fr), twice, for its twelve). auto-fit was here
   instead, which was fine under a 1180px ceiling but wrong once the page went full width — it keeps
   adding columns as the window grows, so twelve tiles reflowed to 8+4 and the two rows stopped lining
   up. A fixed count is the thing that makes them a grid rather than a wrap.
   THE LADDER IS ALL EVEN NUMBERS, and that is exactly why 3 is no longer on it. The first version had
   every step dividing TWELVE, which was true until the count became thirteen and then sixteen inside one
   afternoon. The steps have to survive a list that keeps growing rather than divide one particular
   number, and even steps leave a clean last row for every even count — a three-across step orphaned a
   single tile the moment there were sixteen. The regression pack counts the tiles on the page and fails
   if any step leaves one alone, which is what will say so out loud next time the list changes.
   minmax(0, 1fr) rather than 1fr because a grid item's default min-width is auto: one long value would
   otherwise widen its column and knock the rest out of alignment. */
.ktiles { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px;
          margin-bottom: 16px; }
@media (max-width: 1200px) { .ktiles { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 700px)  { .ktiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ktile { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
.klabel { font: 600 10px var(--sans); letter-spacing: .6px; text-transform: uppercase; color: var(--text-label); }
.kvalue { font: 700 24px var(--sans); color: var(--figval); margin: 6px 0 4px; letter-spacing: -.5px; }
/* SEVERAL FIGURES IN THE SAME TILE, in the same slot the single number normally occupies. Language is
   three shares that only mean anything beside each other, so they share one tile rather than taking
   three — and keeping the label/value structure means it still reads as a tile rather than as a second
   kind of card. Smaller and unsquashed: at 24px three lines would tower over the rest of the row. */
/* NAME LEFT, FIGURE RIGHT, and the figures line up into a column you can read straight down. Six of
   these cards ended up on the page, every line reading "Label - value" as one run-on string, so the
   only part anybody was looking for had to be found by reading past the words each time (Rick,
   2026-08-07). The hyphen went with the change: alignment separates the two now, and a separator that
   has stopped separating is noise.

   THE RULE IS ON THE COLUMN BOUNDARY, NOT THE MIDDLE OF THE CARD. A line down the geometric centre cuts
   through the longer names at the 4-across breakpoint and through nearly all of them at 2-across. The
   figure column is `auto`, so its edge sits just left of the widest figure in THAT card and the rule
   follows it — the same effect, and it cannot collide with the words.

   row-gap is 0 ON PURPOSE: the border-left on each figure cell is what draws the rule, and any gap
   between rows would break it into dashes. Spacing comes from line-height instead.

   tabular-nums so the digits sit in the same place row to row. Without it 1 and 7 are different widths
   and a right-aligned column still looks ragged. */
.kvalue-rows { display: grid; grid-template-columns: 1fr auto; row-gap: 0; align-items: baseline;
               font: 600 14px/1.6 var(--sans); letter-spacing: 0; }
.krow-k { color: var(--text-secondary); font-weight: 500; padding-right: 10px; }
.krow-v { text-align: right; font-variant-numeric: tabular-nums; color: var(--text);
          border-left: 1px solid var(--border); padding-left: 10px; }
/* A SLOT HELD OPEN, said in words. A dashed edge reads as "not a figure" without relying on colour, and
   the label says what it is — the sidebar's unbuilt entries carry "soon" for the same reason. These are
   scaffolding: they come out before this page is shown to a customer. */
.knote { font-size: 11.5px; color: var(--text-muted); }
/* The ARROW says which way it moved. Colour agrees with it but is never the only signal. */
.kdelta { display: inline-block; font: 600 11.5px var(--sans); padding: 2px 7px; border-radius: 999px; }
/* good/bad/flat, not up/down: the colour states whether the movement is WELCOME, which is a different
   question from which way it went. The arrow already carries the direction. A metric with no good
   direction (call volume, unique callers) gets no colour rather than an accidental verdict. */
.kdelta.good { color: var(--ok); background: var(--ok-tint); }
.kdelta.bad  { color: var(--danger); background: var(--danger-tint); }
.kdelta.flat { color: var(--text-muted); background: var(--surface-2); }

/* Bar charts in CSS. Two dozen bars do not justify a charting library, and the design's is fetched from
   a CDN the CSP forbids. */
.chartrow { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 1000px) { .chartrow { grid-template-columns: 1fr; } }
/* A TRACK OF DEFINITE HEIGHT, and the fill sized inside it. A percentage against ONE explicit pixel
   value has nothing left to be ambiguous about. */
.barchart { display: flex; align-items: flex-end; gap: 10px; margin-top: 6px; }
.bar { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; }
.barval { font: 600 11px var(--mono); color: var(--text-muted); height: 14px; }
.bartrack { width: 100%; height: 120px; display: flex; align-items: flex-end; }
.barfill { width: 100%; background: var(--chart); border-radius: 6px 6px 0 0; min-height: 3px; }
/* THE SECOND CHART IS QUIETER THAN THE FIRST, so the pair does not read as one wide chart cut in half.
   The design does this with var(--accent-tint), and that token is right in dark and useless in light: it
   resolves to a near-white mint on a white surface, and the bars all but vanish. Opacity gets the same
   INTENT from one rule in both themes — same hue, visibly stepped back, whatever it is sitting on.
   It also separates the way that survives every kind of colour vision, because what changes is
   LIGHTNESS. Two hues at equal lightness is the one distinction that is not safe to rely on, and it is
   the obvious thing to reach for here. */
.barfill-alt { opacity: .45; }

/* HORIZONTAL BARS, for comparing a handful of named things rather than a series over time. The Volume
   tab needs to answer "which channel is doing the most work" at a glance, and the vertical .bar above is
   wrong for it: five long labels do not fit under five columns, and the values span an order of magnitude.

   WIDTH BY CLASS, NOT BY STYLE ATTRIBUTE, for the reason written above the .bh ramp — style-src 'self'
   discards inline styles, which is what made every bar on this site render flat until somebody noticed.

   THE NUMBERS ARE ALWAYS PRINTED BESIDE THE BAR. The bar is the comparison; the figure is the fact. That
   is not decoration: a reader who cannot separate the fill from its track still has every value, and no
   meaning on this page is ever carried by colour alone. */
.vbars { display: grid; gap: 9px; margin-top: 8px; }
.vbrow { display: grid; grid-template-columns: 170px 1fr 62px 108px; align-items: center; gap: 12px; }
.vblab { font: 500 13px var(--sans); color: var(--text-body); }
.vbtrack { display: block; height: 13px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
.vbfill { display: block; height: 100%; background: var(--chart); border-radius: 4px; min-width: 3px; }
.vbnum, .vbmoney { font: 600 13px var(--mono); font-variant-numeric: tabular-nums; text-align: right;
                   white-space: nowrap; }
.vbnum { color: var(--text-muted); }
.vbmoney { color: var(--figval); }
/* The track is the first thing to go when there is no room for it: the label and both figures still say
   everything, and a 40px bar compares nothing. */
@media (max-width: 780px) {
  .vbrow { grid-template-columns: 1fr 56px 100px; }
  .vbtrack { display: none; }
}
.bw0 { width: 0%; } .bw2 { width: 2%; } .bw4 { width: 4%; } .bw6 { width: 6%; } .bw8 { width: 8%; } .bw10 { width: 10%; } .bw12 { width: 12%; } .bw14 { width: 14%; } .bw16 { width: 16%; } .bw18 { width: 18%; } .bw20 { width: 20%; } .bw22 { width: 22%; } .bw24 { width: 24%; } .bw26 { width: 26%; } .bw28 { width: 28%; } .bw30 { width: 30%; } .bw32 { width: 32%; } .bw34 { width: 34%; } .bw36 { width: 36%; } .bw38 { width: 38%; } .bw40 { width: 40%; } .bw42 { width: 42%; } .bw44 { width: 44%; } .bw46 { width: 46%; } .bw48 { width: 48%; } .bw50 { width: 50%; } .bw52 { width: 52%; } .bw54 { width: 54%; } .bw56 { width: 56%; } .bw58 { width: 58%; } .bw60 { width: 60%; } .bw62 { width: 62%; } .bw64 { width: 64%; } .bw66 { width: 66%; } .bw68 { width: 68%; } .bw70 { width: 70%; } .bw72 { width: 72%; } .bw74 { width: 74%; } .bw76 { width: 76%; } .bw78 { width: 78%; } .bw80 { width: 80%; } .bw82 { width: 82%; } .bw84 { width: 84%; } .bw86 { width: 86%; } .bw88 { width: 88%; } .bw90 { width: 90%; } .bw92 { width: 92%; } .bw94 { width: 94%; } .bw96 { width: 96%; } .bw98 { width: 98%; } .bw100 { width: 100%; }

/* ---- call map (trial page) ---------------------------------------------------------------------
   A FIVE-STEP RAMP OF ONE HUE, varied by fill-opacity rather than by five separate colours. It
   separates by LIGHTNESS, which is the distinction that survives every kind of colour vision — a ramp
   built from different hues is the obvious thing to reach for and the one that fails. One rule also
   means both themes get a correct ramp from the same code, because the token underneath it flips.
   The steps are classes, not inline styles: style-src 'self' discards style attributes, which is what
   made every bar chart render flat until it was found. */
.mapwrap { margin: 4px 0 10px; }
.us-state-map { width: 100%; height: auto; display: block; }
.us-state { fill: var(--track); stroke: var(--surface); stroke-width: 1; }
/* THE MAP AND ITS OWN LEGEND MUST BE THE SAME COLOUR. .swatch moved to --chart and these did not, so
   the ramp under the map was explaining it in a blue the map never used. Caught by Rick's screenshot,
   not by the checker, which is why .us-state is now one of the covered selectors. */
.us-state.sh1 { fill: var(--chart); fill-opacity: .28; }
.us-state.sh2 { fill: var(--chart); fill-opacity: .46; }
.us-state.sh3 { fill: var(--chart); fill-opacity: .64; }
.us-state.sh4 { fill: var(--chart); fill-opacity: .82; }
.us-state.sh5 { fill: var(--chart); fill-opacity: 1; }
/* The halo is what makes a 4px dot findable on a busy shape; the core is what makes it a dot. Colour
   names the direction, and the <title> on each says the same thing in words. */
.livedot circle { fill: var(--ok); }
.livedot-halo { fill-opacity: .22; }
.livedot-inbound circle { fill: var(--accent-text); }
.livedot-outbound circle { fill: var(--ok); }
.livedot-text circle { fill: var(--indigo); }
/* THE HALO PULSES; THE CORE DOES NOT. The core is the position — it is what the projection check pins
   and what a reader points at — so it stays put and stays solid. Only the halo moves, which is the
   same division of labour the two circles already had.
   2.6s matches the design prototype's ring (us-call-map.js), but it is NOT the same idea and the
   difference is why the mark differs: that one flashed once per ARRIVING call, an instant. Ours marks a
   call IN FLIGHT, a duration — so this repeats for as long as the call lasts and stops when it ends.
   A dot that pulsed once would say a call happened, which is what the shading already says.
   Keyframes, not a style attribute: style-src 'self' governs inline style ATTRIBUTES, and the animation
   living in the stylesheet is what keeps it CSP-legal. This is the portal's first animation. */
@keyframes livedot-pulse {
  0%   { transform: scale(.9);  fill-opacity: .30; }
  70%  { transform: scale(2.1); fill-opacity: 0; }
  100% { transform: scale(2.1); fill-opacity: 0; }
}
.livedot-halo { transform-box: fill-box; transform-origin: center;
                animation: livedot-pulse 2.6s ease-out infinite; }
/* Motion is not the only thing saying a call is live: the halo keeps its resting opacity, the core is
   unchanged, and the <title> says it in words. So switching the animation off costs nothing. */
@media (prefers-reduced-motion: reduce) { .livedot-halo { animation: none; } }
.maplegend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-top: 2px; }
.rampwrap { display: inline-flex; align-items: center; gap: 4px; }
.swatch { width: 22px; height: 10px; border-radius: 2px; background: var(--chart); display: inline-block; }
.swatch.sh1 { opacity: .28; } .swatch.sh2 { opacity: .46; } .swatch.sh3 { opacity: .64; }
.swatch.sh4 { opacity: .82; } .swatch.sh5 { opacity: 1; }
.dotkey { display: inline-flex; align-items: center; gap: 6px; font: 500 12px var(--sans);
          color: var(--text-secondary); }
.dotkey span[class^="livedot-"] { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dotkey .livedot-inbound { background: var(--accent-text); }
.dotkey .livedot-outbound { background: var(--ok); }
.dotkey .livedot-text { background: var(--indigo); }
.barlab { font: 500 11px var(--sans); color: var(--text-muted); }

/* THE HEIGHT COMES FROM A CLASS BECAUSE A style ATTRIBUTE WOULD BE BLOCKED. Our own CSP says
   style-src 'self', and that directive governs inline style ATTRIBUTES, not only <style> blocks and
   third-party stylesheets. So `<div class="barfill" style="height:33px">` was served correctly, parsed
   correctly, and then refused by the browser, leaving min-height's 3px — correct numbers above flat
   lines, no matter what the flex rules said. Four rewrites of this chart were chasing a CSS bug that
   was never in the CSS.
   The fix keeps the CSP strict: a fixed vocabulary of heights in this file, and PHP picks one. Two
   percent of a 120px track is 2.4px, which is below what anyone can see, and it is exact where it
   matters — the tallest bar is always .bh100 and an empty one is always .bh0.
   Adding 'unsafe-inline' here instead would have re-opened style injection across the whole portal to
   spare fifty lines of generated CSS. */
.bh0   { height:   0%; } .bh2   { height:   2%; } .bh4   { height:   4%; } .bh6   { height:   6%; } .bh8   { height:   8%; } .bh10  { height:  10%; }
.bh12  { height:  12%; } .bh14  { height:  14%; } .bh16  { height:  16%; } .bh18  { height:  18%; } .bh20  { height:  20%; } .bh22  { height:  22%; }
.bh24  { height:  24%; } .bh26  { height:  26%; } .bh28  { height:  28%; } .bh30  { height:  30%; } .bh32  { height:  32%; } .bh34  { height:  34%; }
.bh36  { height:  36%; } .bh38  { height:  38%; } .bh40  { height:  40%; } .bh42  { height:  42%; } .bh44  { height:  44%; } .bh46  { height:  46%; }
.bh48  { height:  48%; } .bh50  { height:  50%; } .bh52  { height:  52%; } .bh54  { height:  54%; } .bh56  { height:  56%; } .bh58  { height:  58%; }
.bh60  { height:  60%; } .bh62  { height:  62%; } .bh64  { height:  64%; } .bh66  { height:  66%; } .bh68  { height:  68%; } .bh70  { height:  70%; }
.bh72  { height:  72%; } .bh74  { height:  74%; } .bh76  { height:  76%; } .bh78  { height:  78%; } .bh80  { height:  80%; } .bh82  { height:  82%; }
.bh84  { height:  84%; } .bh86  { height:  86%; } .bh88  { height:  88%; } .bh90  { height:  90%; } .bh92  { height:  92%; } .bh94  { height:  94%; }
.bh96  { height:  96%; } .bh98  { height:  98%; } .bh100 { height: 100%; }

/* Server-rendered "start hidden". Script that later swaps it uses element.style, which is CSSOM and not
   subject to style-src, and an element's own style property outranks this class when it does. */
.is-hidden { display: none; }
/* One-off spacing that used to ride on a style attribute, for the same CSP reason as the bars above. */
.spaced { margin-top: 14px; }

/* The pinned live panel in the sidebar. */
.livepin { margin: 10px 12px; padding: 10px 12px; background: var(--surface-2);
           border: 1px solid var(--border); border-radius: 10px; }
.pinhead { display: flex; align-items: center; gap: 7px; font: 600 11px var(--sans);
           color: var(--text-secondary); margin-bottom: 8px; }
.pinrow { padding: 6px 0; border-top: 1px solid var(--border-soft); }
.pinmore { padding: 7px 0 0; font: 500 11px var(--sans); color: var(--text-muted); }
.pinstep { font: 500 12px var(--sans); color: var(--text); }
.pinwho { font: 400 11px var(--mono); color: var(--text-muted); }
.pagefoot { margin-top: auto; padding: 18px 30px 22px; text-align: center;
            font-size: 12px; color: var(--text-faint); }
.faq { border-bottom: 1px solid var(--border); padding: 10px 0; }
.faq summary { font: 600 14px var(--sans); color: var(--text); cursor: pointer; }
.faq p { margin: 8px 0 2px; color: var(--text-muted); font-size: 13.5px; line-height: 1.55; max-width: 70ch; }
.tools { display: flex; align-items: center; gap: 2px; }
.tool { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px;
        color: var(--text-muted); }
a.tool:hover { color: var(--accent-text); background: var(--chip); text-decoration: none; }
/* Not built yet, and saying so. The sidebar already marks Messages and Reports this way; a control that
   looks live and does nothing is worse than one that admits it. */
.tool.off { color: var(--text-faint); cursor: not-allowed; }
@media (max-width: 900px) { .lastcall { display: none; } }
/* FULL WIDTH, deliberately. This carried max-width: 1180px on the theory that a wide monitor made the
   breakdown table a long line to read across. In practice it left a band of dead space down the right of
   every page, and the design does not do that either — its <main> is flex:1 with `padding: 26px 30px`
   and no ceiling at all (Rick, 2026-08-06, comparing the two side by side). If a table becomes hard to
   scan on some very wide monitor, cap THE TABLE, not the page.
   NB: this file is served to signed-out visitors, so nothing in it — comments included — may name the
   customer or the trade. The pack checks; the first draft of this comment failed it. */
.content { padding: 26px 30px 40px; }

/* ---- theme control ----------------------------------------------------------------------------- */
.seg { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border);
       border-radius: 9px; padding: 2px; gap: 2px; }
.seg button { font: 500 12px var(--sans); color: var(--text-secondary); background: none;
              border: 1px solid transparent; border-radius: 7px; padding: 5px 9px; cursor: pointer;
              display: flex; align-items: center; gap: 5px; }
.seg button[aria-pressed="true"] { color: var(--accent-text); background: var(--accent-tint);
                                   border-color: var(--accent-text); font-weight: 600; }

/* ---- cards, tables, forms ---------------------------------------------------------------------- */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
        padding: 20px 22px; margin-bottom: 16px; }
.card h1 { margin: 0 0 4px; font: 700 18px var(--sans); color: var(--text); }
.card h2 { margin: 0 0 10px; font: 600 15px var(--sans); color: var(--text); }
.hint { color: var(--text-muted); font-size: 12.5px; }
/* A .hint used as a PARAGRAPH rather than as a label needs room to be read as one. Without this the
   Support card's two triage paragraphs ran together into a single grey block, which is the opposite of
   what a paragraph that says "call us" and one that says "do not call us" should do. */
.hint-block { margin: 14px 2px 0; line-height: 1.6; }

/* TABLES ARE BANDED, and that is most of why the design reads more easily than a flat grid: the header
   sits on its own tone so the eye can find where the data starts, and rows run edge to edge so the
   separators are continuous rather than floating inside the card's padding (Rick, 2026-08-05).
   The negative margins are what produce the full bleed; the first and last cells give the padding back
   so text still lines up with the card's own heading. */
table { width: calc(100% + 44px); margin: 14px -22px 0; border-collapse: collapse; }
/* THE BLEED MOVES TO THE WRAPPER when there is one. A table already 44px wider than its container will
   always overflow an overflow-x:auto parent by exactly that much, which is why a scrollbar appeared under
   a table that fits perfectly well. The wrapper carries the negative margins; the table inside is plain. */
.tablewrap { margin: 14px -22px 0; overflow-x: auto; }
.cardband + .tablewrap { margin-top: 0; }
.tablewrap > table { width: 100%; margin: 0; }
thead th { background: var(--surface-2); }
/* Values taken from the design file rather than guessed at: 11px / 600 / .4px on --surface-2. */
th { text-align: left; font: 600 11px var(--sans); letter-spacing: .4px; text-transform: uppercase;
     color: var(--text-label); padding: 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
td { padding: 13px 10px; border-bottom: 1px solid var(--border-soft); font-size: 13.5px;
     color: var(--text-body); }
th:first-child, td:first-child { padding-left: 24px; }
th:last-child, td:last-child  { padding-right: 24px; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--surface-2); }

/* The band under a card's content: the save button and the sentence that explains it. Full bleed and
   toned for the same reason the header is — it closes the card off instead of letting the button float
   against the bottom edge. align-items:center is what puts the sentence on the button's centre line. */
.cardfoot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
            margin: 0 -22px -20px; padding: 16px 22px; border-top: 1px solid var(--border-soft);
            background: var(--surface-2); border-radius: 0 0 11px 11px; }
.cardfoot .hint { margin: 0; }
/* The global submit rule adds margin-top:18px, and :not(.link) makes it (0,2,1) — MORE specific than the
   .cardfoot override I first wrote at (0,1,1), so the margin survived and align-items:center dutifully
   centred the button's MARGIN box, sitting the button 9px low. Matching the pseudo-class wins it back. */
.cardfoot button[type="submit"]:not(.link) { margin-top: 0; }
/* The quick-set row is NOT tinted — a rule beneath it and nothing else, straight from the design.
   Three tones stacked (quick set, header, footer) was one more than the eye needs; the design tints only
   the two that bracket the data (Rick, 2026-08-05). */
.cardband { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
            margin: 14px -22px 0; padding: 14px 22px;
            border-bottom: 1px solid var(--border-soft); }
td.num { font-family: var(--mono); font-size: 13px; }

label { display: block; margin: 14px 0 5px; font: 600 12px var(--sans); color: var(--text-secondary); }
input[type="text"], input[type="email"], input[type="password"], input[type="date"], input[type="time"] {
  width: 100%; padding: 10px 11px; font: 400 14px var(--sans); color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 8px;
}
input[type="time"], input[type="date"] { font-family: var(--mono); font-size: 13px; }
/* TEXTAREA AND SELECT WERE NEVER STYLED, because until Support no page had either — every form in the
   portal was text, time, date and checkboxes. Left alone they render at browser defaults: a 20x2
   textarea with the submit button jammed beside it, and a native select that ignores the dark theme. */
textarea {
  width: 100%; padding: 10px 11px; font: 400 14px/1.55 var(--sans); color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 8px;
  resize: vertical; min-height: 132px;
}
select {
  width: 100%; padding: 10px 11px; font: 400 14px var(--sans); color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: 8px;
  /* appearance:none, then draw the caret ourselves — the native control paints its own grey box and
     ignores `background`, the same fact .playbtn records a few hundred lines above. */
  appearance: none; -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 18px) 51%, calc(100% - 13px) 51%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 34px;
}
textarea:focus, select:focus { border-color: var(--accent-text); }
input:focus { border-color: var(--accent-text); }
button[type="submit"]:not(.link), button.btn-primary {
  margin-top: 18px; padding: 11px 18px; min-height: 44px; font: 600 14px var(--sans);
  color: var(--accent-2-on); background: var(--accent-2); border: 0; border-radius: 8px; cursor: pointer;
}
button[type="submit"]:not(.link):hover, button.btn-primary:hover { background: var(--accent-2-hover); }

/* Status messages lead with a bold word, so they read correctly with colour ignored entirely. */
.error  { background: var(--danger-tint); color: var(--danger); border-left: 3px solid var(--danger);
          padding: 11px 13px; border-radius: 7px; font-size: 13px; }
.notice { background: var(--indigo-tint); color: var(--indigo); border-left: 3px solid var(--indigo);
          padding: 11px 13px; border-radius: 7px; font-size: 13px; }
.error ul { margin: 6px 0 0; padding-left: 18px; }

/* ---- hours + holidays -------------------------------------------------------------------------- */
/* The DAY column keeps its readable weight; the header row above it stays a header. */
table.hours tbody th { text-transform: none; font: 600 13.5px var(--sans); color: var(--text); width: 9rem;
                 vertical-align: middle; letter-spacing: 0; border-bottom: 1px solid var(--border-soft); }
/* Every cell used to hold the WHOLE day, so it was a flex row. Now each field has its own cell, and
   leaving that rule in made all five cells full-width flex containers with 100%-wide inputs inside —
   which stacked every box vertically and broke the page outright. Only the lunch cell holds two things. */
table.hours td { vertical-align: middle; }
table.hours input[type="time"] { width: auto; min-width: 7.2rem; }
.pair { display: inline-flex; align-items: center; gap: 6px; }
.pair input { width: auto; min-width: 7.5rem; }
/* "Closed" is a WORD in a pill, never an absence of ink. */
.closed-tag { display: inline-block; font: 600 10.5px var(--sans); letter-spacing: .3px;
              text-transform: uppercase; color: var(--text-muted); background: var(--chip);
              border-radius: 999px; padding: 2px 8px; }
.spoken { background: var(--surface-2); border-left: 3px solid var(--accent-text); padding: 12px 14px;
          border-radius: 8px; white-space: pre-wrap; font-size: 13.5px; color: var(--text-body);
          margin: 0 0 12px; }
pre.spoken { font-family: var(--mono); font-size: 12.5px; }
details { margin-top: 12px; }
summary { cursor: pointer; color: var(--text-muted); font-size: 12.5px; }
.actions { white-space: nowrap; }
form.inline { display: inline; margin: 0; }
button.link { background: none; border: 0; color: var(--danger); font: 600 13px var(--sans);
              padding: 0; margin: 0; min-height: 0; cursor: pointer; }
button.link:hover { text-decoration: underline; }
.collide { margin-top: 4px; color: var(--warn); }
.sub-hours { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.sub-hours p { width: 100%; margin: 0; }
label.check { display: flex; align-items: center; gap: 9px; font: 500 13.5px var(--sans);
              color: var(--text-body); margin: 12px 0 0; }
label.check input { margin: 0; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 7px 20px; margin: 6px 0 0; }
dt { color: var(--text-muted); font-size: 12.5px; }
dd { margin: 0; font-size: 13.5px; }

/* ---- sign-in ----------------------------------------------------------------------------------- */
/* Deliberately theme-INDEPENDENT: dark field, white card, in light mode and dark alike. The legacy
 * screen's shape, but OUR dark — the same #0d1117 the design system already uses for a dark background
 * (Rick, 2026-08-04), not the legacy navy. Fixing the card light means the wordmark needs one file
 * rather than a light and a dark variant. */
.signin-wrap { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: #0d1117; }
.signin { width: 100%; max-width: 380px; }
.signin-mark { display: flex; justify-content: center; padding: 4px 0 26px; }
.signin-mark img { max-width: 230px; height: auto; }
.signin .card { background: #fff; border: 0; border-radius: 12px; padding: 30px 34px 26px;
                box-shadow: 0 16px 44px rgba(0,0,0,.45); margin: 0; }
.signin .card h2 { color: #16202e; text-align: center; margin: 0 0 6px; }
/* Chrome paints autofilled inputs with its own grey-blue background and ignores `background`. An inset
 * box-shadow the full height of the field is the only thing that covers it, and -webkit-text-fill-color
 * the only way to set the text on top. Without this the first thing anyone sees of the product is two
 * grey smudges where the fields should be. */
.signin input:-webkit-autofill,
.signin input:-webkit-autofill:hover,
.signin input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 40px #fff inset;
  box-shadow: 0 0 0 40px #fff inset;
  -webkit-text-fill-color: #16202e;
  caret-color: #16202e;
  transition: background-color 9999s ease-in-out 0s;   /* outlast Chrome's own fade-in */
}
.signin label { color: #4b5563; }
.signin input { background: #fff; border-color: #cbd2d9; color: #16202e; }
.signin input:focus { border-color: #0f766e; }
.signin button[type="submit"] { width: 100%; }
.signin .forgot { margin: 16px 0 0; text-align: center; font-size: 12.5px; }
.signin .forgot a { color: #0b5d57; }
.signin .error, .signin .notice { margin-bottom: 14px; }

@media (max-width: 760px) {
  .shell { flex-direction: column; }
  .sidebar { width: auto; flex: none; height: auto; position: static; flex-direction: row;
             align-items: center; gap: 14px; padding: 10px 14px; overflow-x: auto; }
  .client-block, .user-block { display: none; }
  nav.side { flex-direction: row; margin: 0; }
  .content, .topbar { padding-left: 16px; padding-right: 16px; }
}

/* ---- page furniture ----------------------------------------------------------------------------- */
/* The idle-timeout notice used to float loose under the card, belonging to nothing. It is a footnote
 * about the session, so it sits with the session: quiet, at the end of the content, set apart. */
.footnote { margin: 22px 2px 0; color: var(--text-faint); font-size: 12px; }
/* CENTRED, and only this one. Left-aligned at the foot of a card that is mostly empty most of the time,
   the live panel's promise read as boilerplate somebody forgot to delete. Centred it reads as a caption
   for the card it belongs to. The other two footnotes sit directly under the content they qualify and
   are correctly left-aligned; this one qualifies the whole card. */
.footnote.livenote { text-align: center; }
/* An empty table should say WHY it is empty. A blank row reads as a bug; a sentence reads as a state. */
td.empty { color: var(--text-muted); font-style: normal; padding: 22px 10px; text-align: center; }

/* ---- hours: the location index ------------------------------------------------------------------- */
.phlist { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.phcard { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; background: var(--surface-2);
          border: 1px solid var(--border); border-radius: 11px; padding: 14px 16px; }
.phcard-main { flex: 1 1 auto; min-width: 0; }
.phcard-name { font: 600 14.5px var(--sans); color: var(--text); display: flex; align-items: center;
               gap: 8px; flex-wrap: wrap; }
.phcard-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.phcard-today { text-align: right; }
.phcard-label { font: 600 10px var(--sans); letter-spacing: .4px; text-transform: uppercase;
                color: var(--text-label); }
.phcard-hours { font-size: 14px; color: var(--text); letter-spacing: -.2px; }
/* Every status carries a WORD. The colour is reinforcement, never the message. */
.pill { display: inline-block; font: 600 10.5px var(--sans); letter-spacing: .3px; text-transform: uppercase;
        border-radius: 999px; padding: 2px 9px; white-space: nowrap; }
.pill-open { color: var(--ok); background: var(--ok-tint); }
.pill-shut { color: var(--text-muted); background: var(--chip); }
.pill-warn { color: var(--warn); background: var(--warn-tint); }
/* Added for the suppression outcome panel (finding 3.7). The panel never relies on colour alone — every
   row also carries a symbol column and the word itself — so this is emphasis, not information. */
.pill-bad  { color: var(--danger); background: var(--danger-tint); }
/* Day chips: the letter is the label, the fill is decoration, and the title says it in full. */
.daystrip { display: flex; gap: 4px; padding-bottom: 8px; }
.daychip { width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center;
           font: 600 11px var(--mono); color: var(--text-faint); background: var(--chip);
           border: 1px solid transparent; }
.daychip.on { color: var(--accent-text); background: var(--accent-tint); border-color: var(--accent-text); }
/* Today is marked with SHAPE AND WEIGHT, never a third colour. The fill already carries open vs closed;
   layering a second hue on top of that says nothing to anyone who does not separate the two hues, and the
   ring plus the dot are legible whether or not the chip is filled. */
.daychip.today { position: relative; border-color: var(--text); border-width: 2px; font-weight: 800; }
.daychip.today::after { content: ""; position: absolute; left: 50%; bottom: -7px; width: 4px; height: 4px;
                        margin-left: -2px; border-radius: 50%; background: var(--text); }
/* The disclosure control and the panel it reveals. The panel takes a whole flex line of its own, so it
   opens UNDER the card's row rather than squeezing it. */
.btn-say { display: grid; place-items: center; width: 34px; height: 34px; flex: 0 0 auto;
           color: var(--text-muted); background: var(--surface); border: 1px solid var(--border-strong);
           border-radius: 8px; cursor: pointer; }
.btn-say:hover { color: var(--accent-text); border-color: var(--accent-text); }
.btn-say svg { width: 16px; height: 16px; transition: transform .15s ease; }
.btn-say[aria-expanded="true"] svg { transform: rotate(180deg); }
.phcard-say { flex: 1 0 100%; border-top: 1px solid var(--border); margin-top: 4px; padding-top: 12px; }
.saylab { font: 600 10px var(--sans); letter-spacing: .4px; text-transform: uppercase;
          color: var(--text-muted); margin-bottom: 4px; }
.phcard-say .spoken { margin: 0 0 12px; }
.phcard-say pre.spoken { white-space: pre-wrap; }
/* Visible to a screen reader, not to the eye — the caret alone does not say which card it belongs to.
   Worded without naming the industry on purpose: this file is loaded by the SIGN-IN page, which must not
   disclose what the product is to anyone who has not authenticated. A stylesheet comment is still a
   comment the whole internet can read. */
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
      clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* The editor's own header: which store this schedule belongs to, on the page that changes it. */
.edhead { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
          margin-bottom: 10px; }
.edname { font: 600 16px var(--sans); color: var(--text); }
.backlink { margin: 0 0 12px; font-size: 13px; }
.qslab { font: 600 10.5px var(--sans); letter-spacing: .6px; text-transform: uppercase;
         color: var(--text-label); margin-right: 4px; }
.btn-chip { font: 600 12px var(--sans); color: var(--text); background: var(--surface);
            border: 1px solid var(--border-strong); border-radius: 999px; padding: 6px 12px;
            cursor: pointer; white-space: nowrap; }
.btn-chip:hover { color: var(--accent-text); border-color: var(--accent-text); }
.btn-ghost { font: 600 13px var(--sans); color: var(--text); background: var(--surface);
             border: 1px solid var(--border-strong); border-radius: 8px; padding: 8px 14px;
             white-space: nowrap; }
.btn-ghost:hover { border-color: var(--accent-text); color: var(--accent-text); text-decoration: none; }

/* ---- hours: the editor -------------------------------------------------------------------------- */
.daycheck { display: inline-flex; align-items: center; gap: 6px; margin: 0 14px 0 0; font: 500 12.5px var(--sans);
            color: var(--text-secondary); white-space: nowrap; }
.daycheck input { margin: 0; }
.fieldset { display: inline-flex; align-items: center; gap: 6px; margin-right: 16px; }
.fieldlab { font: 600 10.5px var(--sans); letter-spacing: .4px; text-transform: uppercase;
            color: var(--text-label); }
.fieldset input { width: auto; min-width: 7rem; }
.fieldset input:disabled { opacity: .4; cursor: not-allowed; }
.dash { color: var(--text-faint); }
.custom-day { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
h3.subhead { font: 600 12px var(--sans); letter-spacing: .4px; text-transform: uppercase;
             color: var(--text-label); margin: 16px 0 6px; }

@media (max-width: 900px) {
  .phcard { flex-wrap: wrap; }
  .phcard-today, .daystrip { order: 3; }
  /* Real columns now, so a narrow screen SCROLLS the table rather than stacking it into a column of
     unlabelled boxes — the labels are in the header row, and stacking separates them from their fields. */
  table.hours tbody th { width: auto; }
}

/* ---- Reports (P5) ------------------------------------------------------------------------------
   None of the dashboard's polling machinery is here on purpose: a page you are reading and exporting
   should not move under you. */
/* ONE ROW, AND THE SELECTORS HAVE TO FIGHT FOR IT. This bar is built from the same elements the editors
   use, and those carry page-form defaults meant for a STACKED form: `label` is display:block with 14px
   of top margin, `input[type="date"]` is width:100%, and `button[type="submit"]:not(.link)` adds
   margin-top:18px and a 44px floor. Every one of those out-specifies a bare class, so the first version
   of this toolbar rendered as four stacked rows with a low, oversized Apply.
   The rules below are written to WIN rather than to tie: each carries the container class plus whatever
   the page rule matched on. Tying and leaning on source order would work today and break the first time
   somebody appends a rule underneath. */
.reptools { display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
            background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
            padding: 10px 14px; margin-bottom: 12px; }
.reptools label.replab { display: inline; margin: 0; font: 500 12.5px var(--sans);
                         color: var(--text-muted); }
.reptools input.repdate[type="date"], .reptools select.repsel {
            width: auto; margin: 0; padding: 6px 10px; min-height: 32px; font: 500 13px var(--mono);
            color: var(--text); background: var(--surface-2);
            border: 1px solid var(--border-strong); border-radius: 8px; }
.reptools input.repdate[type="date"] { min-width: 9.5rem; }
.reptools select.repsel { font-family: var(--sans); min-width: 10rem; }
.reptools button.btn-primary[type="submit"] { margin: 0 0 0 4px; padding: 7px 16px; min-height: 32px;
            font: 600 13px var(--sans); color: var(--accent-2-on); background: var(--accent-2);
            border: 1px solid var(--accent-2); border-radius: 8px; cursor: pointer; }
.reptools button.btn-primary[type="submit"]:hover { background: var(--accent-2-hover); }
/* The exports sit hard right as ONE group, away from the controls that change what they would export.
   The margin belongs to the group, not to each button: on the button class it pushed every ghost button
   away from whatever preceded it, which spread two of them across the whole bar. */
.reptools .repexports { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }

/* TABS ARE LINKS, NOT SCRIPT. Each is a real URL carrying the range and the site filter, so a report can
   be bookmarked, mailed to a colleague, and opened in a second tab beside another one. A JS tab strip
   would have made every one of those impossible for no gain.
   NOTE, and it is why this comment reads "site" rather than naming the industry: this stylesheet is
   loaded by the SIGNED-OUT page, so its comments are part of the sign-in surface that spec §1b keeps
   free of company, industry and product hints. A regression check reads the CSS for exactly that, and
   caught the first draft of this block. Keep vocabulary here generic. */
.reptabs { display: flex; flex-wrap: wrap; gap: 4px; background: var(--surface-2);
            border: 1px solid var(--border); border-radius: 10px; padding: 3px; margin-bottom: 16px; }
.reptabs a { font: 500 13px var(--sans); color: var(--text-secondary); text-decoration: none;
              border: 1px solid transparent; border-radius: 7px; padding: 7px 13px; }
.reptabs a:hover { color: var(--text); }
.reptabs a.active { color: var(--accent-text); background: var(--accent-tint);
                     border-color: var(--accent-text); font-weight: 600; }

/* A horizontal bar, for breakdowns where the LABEL is the thing being compared and needs room to be
   read. The vertical bar chart is for series where the label is a date and the shape is the point. */
.lrow { display: grid; grid-template-columns: minmax(9rem, 14rem) 1fr auto; align-items: center;
         gap: 12px; padding: 5px 0; }
.llabel { font: 500 13px var(--sans); color: var(--text-body); }
.ltrack { background: var(--track); border-radius: 999px; height: 10px; overflow: hidden; }
.lfill { background: var(--chart); height: 100%; border-radius: 999px; }
.lval { font-size: 12.5px; color: var(--text-secondary); white-space: nowrap; }

/* Definition rows: a LABEL and the fact beside it, for pages that state things rather than chart
   them. NOT .lrow — that is the bar-chart row above, whose value cell is nowrap and vertically
   centred because it holds a short figure. Borrowing it for prose put multi-line hints into a cell
   built for a number: the hints inherited nowrap and ran off the card, and centring floated each
   label to the middle of however tall its value got (the misalignment reported 2026-09-02). Here
   values wrap, and the label sits level with the first line of its value. */
.defrow { display: grid; grid-template-columns: minmax(9rem, 12rem) 1fr; gap: 12px;
          padding: 9px 0; align-items: start; border-top: 1px solid var(--border); }
.cardtop + .defrow { border-top: 0; padding-top: 2px; }
.deflabel { font: 500 13px var(--sans); color: var(--text-body); }
.defval { font-size: 13px; color: var(--text-body); min-width: 0; overflow-wrap: anywhere; }
.defval .hint { margin-top: 2px; }
/* Widths from a class for the same reason heights are: style-src 'self' throws away style attributes. */
.bw0   { width:   0%; } .bw2   { width:   2%; } .bw4   { width:   4%; } .bw6   { width:   6%; } .bw8   { width:   8%; } .bw10  { width:  10%; }
.bw12  { width:  12%; } .bw14  { width:  14%; } .bw16  { width:  16%; } .bw18  { width:  18%; } .bw20  { width:  20%; } .bw22  { width:  22%; }
.bw24  { width:  24%; } .bw26  { width:  26%; } .bw28  { width:  28%; } .bw30  { width:  30%; } .bw32  { width:  32%; } .bw34  { width:  34%; }
.bw36  { width:  36%; } .bw38  { width:  38%; } .bw40  { width:  40%; } .bw42  { width:  42%; } .bw44  { width:  44%; } .bw46  { width:  46%; }
.bw48  { width:  48%; } .bw50  { width:  50%; } .bw52  { width:  52%; } .bw54  { width:  54%; } .bw56  { width:  56%; } .bw58  { width:  58%; }
.bw60  { width:  60%; } .bw62  { width:  62%; } .bw64  { width:  64%; } .bw66  { width:  66%; } .bw68  { width:  68%; } .bw70  { width:  70%; }
.bw72  { width:  72%; } .bw74  { width:  74%; } .bw76  { width:  76%; } .bw78  { width:  78%; } .bw80  { width:  80%; } .bw82  { width:  82%; }
.bw84  { width:  84%; } .bw86  { width:  86%; } .bw88  { width:  88%; } .bw90  { width:  90%; } .bw92  { width:  92%; } .bw94  { width:  94%; }
.bw96  { width:  96%; } .bw98  { width:  98%; } .bw100 { width: 100%; }

/* THE HEATMAP SEPARATES BY LIGHTNESS OF ONE TOKEN AND ALWAYS CARRIES ITS NUMBER. A grid encoded only in
   colour tells a colour-blind reader nothing, and this one is meant to be read by whoever does the rota.
   The tint is a ::before behind the digits rather than the cell's own background, so the figure stays at
   full contrast as the cell gets darker instead of dimming with it. */
.heatwrap { overflow-x: auto; margin: 4px -22px 0; padding: 0 22px; }
/* THE SAME SPECIFICITY TRAP THE RANGE BAR FELL INTO, and here it showed as ONE column out of fourteen
   with its figure sitting left of its own tint. The page's table rules end with
     th:first-child, td:first-child { padding-left: 24px }
     th:last-child,  td:last-child  { padding-right: 24px }
   because a table inside a card should breathe at its edges. Both are (0,1,1) and beat `.heat`'s
   (0,1,0), so the LAST cell of every row was 24px wider than the other thirteen while its digit stayed
   centred in the 40px content box. The ::before tint is inset:0 and therefore covers the padding, so the
   tint grew and the number did not move with it. Nothing looked broken; one column looked off.
   Fixed twice over, deliberately. The rules below carry .heatgrid so they WIN rather than tie, and the
   grid is table-layout:fixed with named header cells, so every column is the same width by construction
   instead of by whatever the automatic algorithm decides each column's content deserves. */
.heatgrid { width: auto; margin: 0; border-collapse: separate; border-spacing: 3px;
            table-layout: fixed; }
.heatgrid thead th, .heatgrid tbody th { background: none; border: 0; font: 600 10.5px var(--sans);
            letter-spacing: .3px; text-transform: uppercase; color: var(--text-muted);
            padding: 2px 6px; text-align: center; }
.heatgrid th.heatday { text-align: right; padding: 2px 8px 2px 0; }
.heatgrid th.heatcorner { width: 3.4rem; }
.heatgrid th.heathour { width: 44px; }
.heatgrid td.heat { position: relative; border: 0; padding: 0; width: 44px; height: 30px;
            text-align: center; vertical-align: middle; }
/* The row hover from the page tables paints BEHIND the tint and makes a quiet row look tinted. */
.heatgrid tbody tr:hover td.heat { background: none; }
.heat::before { content: ""; position: absolute; inset: 0; border-radius: 6px;
                 background: var(--chart); opacity: 0; }
.heat.hb0::before { background: var(--track); opacity: 1; }
.heat.hb1::before { opacity: .20; } .heat.hb2::before { opacity: .38; }
.heat.hb3::before { opacity: .56; } .heat.hb4::before { opacity: .76; }
.heat.hb5::before { opacity: 1; }
.heatn { position: relative; font: 600 12px var(--mono, var(--sans)); color: var(--text); }
.heat.hb4 .heatn, .heat.hb5 .heatn { color: var(--chart-on); }
.swatch.hb1 { opacity: .20; } .swatch.hb2 { opacity: .38; } .swatch.hb3 { opacity: .56; }
.swatch.hb4 { opacity: .76; } .swatch.hb5 { opacity: 1; }
/* A disabled resolution says why on hover rather than vanishing: a reader can see that City exists and is
   switched off, which is the honest state of it. */
.geoseg button[disabled] { opacity: .45; cursor: not-allowed; }
/* The Epic environment filter is the same control made of LINKS, because each choice is a real URL that
   can be bookmarked and shared — the same reason the tabs are anchors. .seg only styled buttons. */
.geoseg a { font: 500 12px var(--sans); color: var(--text-secondary); text-decoration: none;
            border: 1px solid transparent; border-radius: 7px; padding: 5px 9px;
            display: flex; align-items: center; gap: 5px; }
.geoseg a:hover { color: var(--text); }
.geoseg a.active { color: var(--accent-text); background: var(--accent-tint);
                   border-color: var(--accent-text); font-weight: 600; }

/* ---- Reports: bands and figure grids --------------------------------------------------
   TILES ARE THE DASHBOARD'S IDIOM AND THEY STAY THERE (Rick, 2026-08-10). A dashboard is read standing
   up in a few seconds and one big number per box is exactly right for that. A report is read sitting
   down, with the intent to export it, and tiles fight that job: each is an island with its own label, so
   figures in the same unit cannot be scanned down a column, and ten of them cost the height that a
   report needs for the figures a dashboard does not carry.
   The deciding argument is the CSV. Every figure on this page has an export twin, so a two-column grid
   of measure and value means the screen and the file are the same rows in the same order. A tile has no
   CSV shape at all. */
.repband { display: flex; align-items: center; justify-content: space-between; gap: 12px;
           background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--accent);
           border-radius: 8px; padding: 9px 14px; margin: 18px 0 10px;
           font: 600 11.5px var(--sans); letter-spacing: .5px; text-transform: uppercase;
           color: var(--text-secondary); }
.repband .hint { text-transform: none; letter-spacing: 0; font-weight: 500; }
/* Two columns where there is room, one where there is not. The VALUE column stays right-aligned in both,
   because a column of figures that do not line up is the thing this replaced tiles to fix. */
.figgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
           gap: 0 32px; margin-top: 2px; }
/* ONE COLUMN, WHEN THE ROWS ARE A SEQUENCE RATHER THAN A SET. auto-fit is right for Summary, where each
   measure stands alone and four across a wide screen saves scrolling. It is wrong wherever the rows read
   DOWN — a headline figure, the share inside it, then what was left over — because auto-fit scatters that
   sequence across four columns and the reader has to reassemble it. Every value then lands in one
   right-aligned column, which is the whole point: figures that line up can be compared, and figures that
   do not, cannot. */
.figgrid.stack { grid-template-columns: 1fr; }
/* EVERY ROW KEEPS ITS RULE, including the visually last ones. `:last-child` removes the border from the
   DOM-last item, which in a responsive multi-column grid is only ever the last cell of the RIGHTMOST
   column — so one figure lost its divider and the one beside it kept it. There is no selector for "last
   visual row" when the column count is decided by the viewport, so the honest answer is to treat all
   rows alike and let the card's own padding do the separating. */
.figrow { display: grid; grid-template-columns: 1fr auto; column-gap: 16px; align-items: baseline;
          padding: 8px 0; border-bottom: 1px solid var(--border-soft); }
.figlabel { font: 500 13px var(--sans); color: var(--text-body); }
/* tabular-nums so 1,284 and 612 align on the comma rather than drifting by glyph width. */
.figval { font: 600 15px var(--mono); color: var(--figval); font-variant-numeric: tabular-nums;
          white-space: nowrap; }
.figval.muted { color: var(--text-muted); font-weight: 500; }
/* Not-live is a caution rather than a neutral reading: every figure beside it came from a simulator or a
   test system, and the page should say so in a way a skim-reader cannot miss. */
.figval.warn { color: var(--warn); }
/* The note sits UNDER the label rather than beside the value, so it can be any length without pushing
   the figures out of their column. */
/* A SENTENCE, NOT A ROW. The Volume tab states its figures in prose rather than a label/value grid,
   because its numbers only mean anything with their basis attached: "$850 across 240 prescriptions, what
   the patient owed on a fill already on the shelf" does not survive being cut down to a right-aligned
   number under a two-word label. .figrow above is still right for the tabs that report one measure per
   line; this is for the ones that cannot.
   Its own name, checked against the whole file first: .frow and .fname were already owned by a page 800
   lines below when they were reused, and the new three-column row silently became that page's
   two-column one. */
.figline { margin: 10px 2px; line-height: 1.65; color: var(--text-body); font-size: 13.5px; }
.figline strong { font: 600 15px var(--mono); font-variant-numeric: tabular-nums; color: var(--figval);
                  white-space: nowrap; }
.figline .hint { display: block; margin-top: 2px; }

.fignote { grid-column: 1 / -1; font: 400 11.5px var(--sans); color: var(--text-muted);
           margin: -3px 0 2px; }

/* AFTER THE RULES THEY OVERRIDE, and portal_style_test is why this is not merely tidiness: it reads the
   FIRST .figval declaration it finds to check the colour token is wired, and these sat above the base
   rule — so the check reported "found: font-size: 17px" and failed a stylesheet that was correct. A test
   that reads the first match wants the base rule first, and so does anyone reading the file. */
/* THE HEADLINE ROW OF A GROUP, and the row that belongs inside it. Weight and size carry the hierarchy
   rather than colour, so it survives every kind of colour vision — and the indent on .sub says "part of
   the row above" in a way no palette can. */
.figrow.lead .figlabel { font-weight: 600; }
.figrow.lead .figval   { font-size: 17px; }
.figrow.sub  .figlabel { padding-left: 16px; color: var(--text-muted); font-weight: 400; }
.figrow.sub  .figval   { color: var(--text-muted); font-weight: 500; }
/* A column heading for the value side, so a reader is told what the numbers ARE before reading four of
   them. Borderless: it labels the column, it is not a row of data. */
.figrow.fighead { border-bottom: none; padding-bottom: 2px; }
.figrow.fighead .figlabel,
.figrow.fighead .figval { font: 600 10.5px var(--sans); letter-spacing: .06em; text-transform: uppercase;
                          color: var(--text-faint); }

/* ---- the printable report -----------------------------------------------------------------------
   A SEPARATE DOCUMENT, NOT THE PAGE WITH BITS HIDDEN. Hiding the sidebar, topbar, toolbar, tab strip and
   footer with display:none would mean every element added later is printed by default and has to be
   remembered — which is exactly how a PDF button comes to emit the whole screen. This body only ever
   contains the report, so nothing can leak in by being forgotten.
   It is styled for SCREEN too, because the print view is a real URL a reader can land on and read before
   the dialog opens, and a flash of unstyled report is worse than a slow one. */
.printdoc { background: var(--surface); }
/* The wrapper carries no styling of its own: the global `table` rule bleeds 44px wide with negative
   margins for card tables, and every one of those would apply here. Reset to a plain container. */
.printsheet { width: 100%; margin: 0; border-collapse: collapse; }
.printsheet > thead > tr > td, .printsheet > tbody > tr > td { padding: 0; border: 0; background: none; }
.printsheet > tbody > tr:hover > td { background: none; }
.printwrap { max-width: 62rem; margin: 0 auto; padding: 24px 28px 40px; }
.printhead { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
             border-bottom: 2px solid var(--accent); padding-bottom: 12px; margin-bottom: 6px; }
.printtitle { font: 700 19px var(--sans); color: var(--text); }
.printclient { font: 500 13px var(--sans); color: var(--text-secondary); margin-top: 2px; }
.printmeta { text-align: right; font: 500 12.5px var(--sans); color: var(--text-secondary);
             line-height: 1.55; }
.printgen { color: var(--text-muted); font-size: 11.5px; }
.printfoot { margin-top: 22px; padding-top: 10px; border-top: 1px solid var(--border);
             font: 400 11.5px var(--sans); color: var(--text-muted); }

@media print {
  /* WITHOUT THIS THE CHARTS PRINT BLANK. Every bar, every heat cell and every ramp swatch on this page is
     a BACKGROUND, and browsers drop backgrounds when printing unless told not to. The figures would
     survive and the shapes beside them would vanish, which is worse than printing neither. */
  * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  @page { margin: 14mm 12mm; }
  html, body { background: #fff; }
  .printwrap { max-width: none; margin: 0; padding: 0; }
  /* The header repeats because it is a <thead>, which every browser has repeated across page breaks
     since long before paged-media CSS existed. See Boot::printPage for why the two obvious answers
     (position: running, position: fixed) are both wrong here. */
  .printsheet > thead { display: table-header-group; }
  .printhead { margin-bottom: 10px; }
  /* A card broken across a page boundary loses the relationship between its label and its figures, so
     they are kept whole where they fit; a card taller than a page still breaks, which is correct. */
  .card, .figgrid, .lrow, .barchart, .heatwrap { break-inside: avoid; page-break-inside: avoid; }
  /* …except a card built around a long table, which would otherwise be shoved whole onto a fresh page
     and could outgrow one entirely. Those break, and their header row repeats. Progressive: a browser
     without :has() simply keeps the current behaviour rather than breaking. */
  .card:has(> .rtable), .card:has(> table) { break-inside: auto; page-break-inside: auto; }
  /* A band is a heading. Stranded at the foot of a sheet it names something on the next one. */
  .repband { break-after: avoid; page-break-after: avoid; }
  thead { display: table-header-group; }
  tr { break-inside: avoid; page-break-inside: avoid; }
  /* Belt and braces: anything interactive that ever reaches this document has no meaning on paper. */
  .reptools, .reptabs, .sidebar, .topbar, .pagefoot, .sessionwarn, button, .btn-ghost { display: none; }
}


/* ---- Upgrades ---------------------------------------------------------------------------------
   The only marketing surface in the portal. Cards rather than a table because each item has to carry a
   paragraph of explanation, and a table of paragraphs is unreadable. auto-fit so it is three across on
   a desk, one on a counter tablet, without a breakpoint per layout. */
.ugrid  { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
          gap: 14px; margin-top: 16px; }
.ucard  { display: flex; flex-direction: column; padding: 16px 17px;
          background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
.uhead  { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.uhead h3 { margin: 0; font: 600 15px var(--sans); color: var(--text); }
.ublurb { margin: 8px 0 0; font: 500 13.5px/1.55 var(--sans); color: var(--text-secondary); }
.udetail{ margin: 8px 0 0; font: 400 13px/1.6 var(--sans); color: var(--text-muted); }
/* margin-top:auto pins the button to the bottom so a row of cards has its actions on one line,
   however different the paragraphs above them turn out to be. */
.uform  { margin-top: auto; padding-top: 14px; }
.ubtn   { width: 100%; padding: 9px 14px; min-height: 40px; font: 600 13px var(--sans);
          color: var(--accent-text); background: transparent; border: 1px solid var(--border-strong);
          border-radius: 8px; cursor: pointer; }
.ubtn:hover { border-color: var(--accent-text); background: var(--accent-tint); }
/* THE STATE IS A WORD WITH A DOT, never a colour alone — the rule the whole portal follows. */
.ustate { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
          font: 600 11.5px var(--sans); color: var(--text-muted);
          padding: 3px 8px; border-radius: 999px; border: 1px solid var(--border); }
.ustate-included { color: var(--ok); border-color: var(--ok); }
.ustate-soon     { color: var(--indigo); border-color: var(--indigo); }

/* The free group's button: same shape as the paid one, quieter, because it is asking for something that
   costs nothing and should not shout as loudly as a purchase. */
.uform-inline { margin: 0; }
.ubtn-quiet   { width: auto; padding: 7px 12px; min-height: 34px; font-size: 12.5px;
                color: var(--text-secondary); }
.ubtn-quiet:hover { color: var(--accent-text); }

/* Feature rows (Upgrades). NOT .lrow, which is a three-column label/value/action grid — putting a name
   AND a sentence into its 14rem label column crushed the text and left the wide middle column empty,
   so the section read as unfinished beside the cards above it. Here the description gets the room and
   the action sits hard right, with a max-width so the line length stays readable on a wide monitor. */
.frow  { display: grid; grid-template-columns: 1fr auto; gap: 18px; align-items: center;
         padding: 13px 2px; border-bottom: 1px solid var(--border); }
.frow:last-child { border-bottom: 0; }
.fname { font: 600 13.5px var(--sans); color: var(--text); }
.fdesc { margin-top: 2px; max-width: 78ch; font: 400 12.5px/1.55 var(--sans); color: var(--text-muted); }
@media (max-width: 620px) { .frow { grid-template-columns: 1fr; gap: 8px; } }

/* Partially enabled is neither owned nor unowned — it is a gap, and it reads as one.
   (Worded without naming the industry: this stylesheet is served to the SIGN-IN page, which discloses
   nothing to anyone unauthenticated. That is now four comments to trip the check — assume every word
   in this file is public.) */
.ustate-some { color: var(--danger); border-color: var(--danger); }
