/* ===========================================================================
   Fonts — self-hosted, because there is no build step and no third-party CDN.
   ===========================================================================

   Variable files, one per family, and that is deliberate rather than a saving.
   The brief behind this theme names three static DM Sans weights (400/500/700),
   but this stylesheet sets weights *between* those: the body is 450 and labels
   550, both daylight decisions recorded in the engineering log. A browser
   holding only static faces snaps 450 down to 400 and 550–660 up to 700, so the
   screen would lose the body weight the sunlight work bought and gain bold
   everywhere else. The variable file draws every weight exactly and is smaller
   than the three statics it replaces (37KB against 43KB).

   Latin subset only, from @fontsource-variable (SIL OFL 1.1, licences beside
   the files). `swap`, so text is never invisible while a font loads: a rep on
   a slow connection reads the system face for a moment rather than nothing. */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 100 1000;
  font-display: swap;
  src: url('/fonts/dm-sans-latin-wght.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin-wght.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===========================================================================
   Design tokens — klinchit, light.
   ===========================================================================

   The palette is the klinchit.co website's: ink, emerald and lime on a warm
   paper ground, so a customer who signs up on the site lands in something that
   is visibly the same product. The source of truth for the website values is
   the marketing repo's `src/app/globals.css`; the mapping onto these tokens is
   `klinchit-marketing/docs/app-brand-alignment.md`.

   **Three greens, and each has exactly one job** — the discipline the previous
   palette's three blues carried, and the reason it survives the change of hue:

     --accent       #095c40  WORDS. Link text and small labels on white.
     --tone-accent  #0a6949  A SOLID BLOCK WITH A MARK ON IT — the KPI icon
                             plate and edge, the primary brand fill.
     --accent-fill  #0e8a5f  DECORATION WITH NO WORDS ON IT — chart areas,
                             progress arcs, the active-tab bar.

   They run light to dark in the order fill, tile, text: the more text a colour
   carries, the darker it has to be.

   **Four website values were darkened to clear this file's sunlight floors.**
   The website's text colours all pass WCAG AA; they do not all pass the floors
   `tests/contrast.test.ts` sets for a phone read outdoors, which are stricter
   than WCAG because veiling glare compresses every ratio. Each kept its hue
   and saturation and lost only lightness, the method this file has always
   used:

     --text-primary    #0b1a14 → #08120e   17.92 → 19.04 on white (floor 19)
     --text-secondary  #4d5a53 → #38413c    7.23 → 10.56          (floor 10.5)
     --text-muted      #56635c → #48534e    6.30 →  8.01          (floor 8)
     --accent          #0a6949 → #095c40    6.71 →  8.03          (floor 8)

   The eye cannot tell the first from the website's ink; the others read a
   shade firmer. --axis is darkened the same way for the 3:1 non-text floor
   (the website's #a3aba5 is 2.35:1).

   **Lime is never text on a light ground.** It fails contrast on white and on
   paper. It is an accent on ink, or a fill with ink words on it.

   **The KPI tiles stay inverted** — ink words on a tinted wash, the saturated
   tone kept for the icon plate and a 3px edge. That treatment was measured and
   then checked outdoors on a handset (engineering log, "The theme"), and the
   brief asks for values only. The brand tile's wash is lime-soft rather than
   the website's emerald-tint, because emerald-tint is indistinguishable from
   --good-wash: see --accent-wash below.

   Status colours (--good, --warning, --serious, --critical, --cold and their
   -text and -wash partners) do not move. Their meaning must not, and they sit
   comfortably on the warmer ground.

   Every existing token NAME is kept. `charts.js` and `ui.js` read --series-1,
   --grid, --axis, --good, --warning and --critical by name through
   getComputedStyle, so renaming one is a silent chart regression rather than a
   compile error. New tokens (--ink, --lime and the rest below) are additions.

   **Dark mode is gone**, by decision, and stays gone: light-on-dark collapses
   faster than dark-on-light in sunlight. The ink sidebar is a brand band, not
   a dark mode — it carries navigation, never data.

   `tests/contrast.test.ts` asserts every pair named above. If you change a
   value here, that test tells you what it cost. Do not re-measure by eye.
   =========================================================================== */
:root {
  color-scheme: light;

  /* How tall the mobile tab bar is, so `.main` can end above it.
     **Spelled once because it is read twice**, and the two drifting apart is
     the bug this replaced: `.main` reserved 56px for a bar that measures 63,
     so the last line of content sat behind it. `.tabbar` takes it as a
     `min-height` and `.main` as the bottom padding it must clear, which means
     a change to one is a change to both. */
  --tabbar-h: 64px;

  /* Grounds. Cards are white and lift off the plane with a shadow; the 1px
     border they used to carry is gone. */
  --plane: #f4f3ec;
  --surface-1: #ffffff;
  --surface-2: #ffffff;

  --text-primary: #08120e;
  --text-secondary: #38413c;
  --text-muted: #48534e;
  --grid: #e3e2d9;
  --axis: #8d9790;
  --border: rgba(11, 26, 20, 0.1);

  /* The three greens. See the header — each has one job. */
  --accent: #095c40;
  --tone-accent: #0a6949;
  --accent-fill: #0e8a5f;
  --accent-ink: #ffffff;
  /* The primary button: the website's ink pill. */
  --accent-strong: #0b1a14;

  --series-1: #0e8a5f;
  --series-2: #d9730d;
  /* The third chart series, for "achieved" beside potential and committed.
     Ink-2, so emerald can lead the charts as the brand colour. 15.6:1 on white
     and far darker than the other two (emerald 4.36, orange ~3.2), so the three
     are told apart by lightness as well as by hue: a reader who cannot separate
     green from orange can still separate three bars. */
  --series-3: #12281f;

  /* Each of these doubles as a solid tile ground with --accent-ink on it, and
     each was measured for that: good 5.37, serious 4.51, critical 5.57,
     cold 6.22, accent 6.89. */
  --good: #0b6642;
  --good-text: #0a5c3b;
  --warning: #eab308;
  --warning-text: #684c00;
  --serious: #924108;
  --serious-text: #853a07;
  --critical: #a42d26;
  --cold: #4e5a6b;

  /* Tinted grounds for pills, bands and selected states. Each is paired with
     the -text colour of the same name; the pairs are what was measured. */
  /* Lime-soft, not the website's emerald-tint, and that was a screenshot's
     finding rather than a preference. Emerald-tint (#e2f3ea) sits within 1.005:1
     of --good-wash (#e3f2ea), and the brand's emerald-dark within a shade of
     --good — so "Due today" (accent) and "Next 7 days" (good) became the same
     tile, and one colour was doing two jobs. Lime-soft is the website's own
     "due / active" ground, clears every floor here (ink 17.2:1, --accent 7.3:1)
     and reads as a different hue from status green at a glance. */
  --accent-wash: #eef9c4;
  --good-wash: #e3f2ea;
  --serious-wash: #fcf0e7;
  --warning-wash: #fcefb8;
  --critical-wash: #fbe7e5;
  --cold-wash: #eaeef3;

  /* Ink-tinted, like the website's card shadow, because a card's edge is a
     shadow rather than a border and a grey one reads as dirt on the paper. */
  --shadow-sm: 0 1px 1px rgba(11, 26, 20, 0.04), 0 1px 3px rgba(11, 26, 20, 0.05);
  --shadow: 0 1px 1px rgba(11, 26, 20, 0.04), 0 12px 32px -16px rgba(11, 26, 20, 0.18);
  --shadow-lg: 0 2px 4px rgba(11, 26, 20, 0.06), 0 24px 60px -20px rgba(11, 26, 20, 0.35);

  /* The website's own palette, for the places the app now wears it: the ink
     sidebar, the sign-in panel, the primary button, the focus ring. */
  --ink: #0b1a14;
  --ink-2: #12281f;
  /* Accent ON INK ONLY, or as a fill under ink text. Never lime text on a
     light ground: it fails contrast on white and on paper alike. */
  --lime: #d4f25a;
  --lime-soft: #eef9c4;
  --on-ink-muted: #a8b8af;
  --focus: #0e8a5f;
  /* Form-control boundaries: 3.36:1 on white, 3.02:1 on paper — the 3:1 a
     control's edge needs (WCAG 1.4.11). */
  --field-border: #878e88;

  --radius: 14px;
  --radius-lg: 22px;
  --radius-xl: 28px;
  --radius-pill: 999px;
  /* The type scale. Nineteen ad-hoc sizes from 9.5px to 27px collapsed to six
     tokens, and the floor raised — this app is read outdoors on a phone, and
     the old base of 14px with a 9.5px ring cap was set for a desk.

     **Size and weight are the fix for sunlight; colour is a bonus.** That is
     measurable and counter-intuitive: veiling glare adds a constant luminance
     to text and ground alike, so once text is near-black, making it blacker
     buys almost nothing (body text gains 1% at bright-sun k=0.4). What does
     help is more glyph area and thicker strokes, because the eye's contrast
     sensitivity peaks at low spatial frequencies — a large bold shape stays
     detectable at a contrast a small thin one disappears at.

     --fs-base at 16px also stops iOS Safari zooming the page when a field is
     focused, which it does for any input under 16px. Inputs are `font: inherit`,
     so the base fixes every form control at once. */
  --fs-xs: 12px;
  --fs-sm: 14px;
  --fs-base: 16px;
  --fs-lg: 18px;
  --fs-xl: 22px;
  --fs-stat: 28px;

  --font: 'DM Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}


* {
  box-sizing: border-box;
}

/* **`hidden` must actually hide, and without this it does not.** The `hidden`
   attribute is styled by the *user agent* sheet as `[hidden] { display: none }`,
   and an author rule beats the UA sheet whatever its specificity — so every
   author rule here that sets `display` on an element silently defeats it.
   `label.field { display: block }` is one, and it cost a shipped defect: the
   deal-value field set `.hidden = true` on a Cold lead and went on rendering,
   47px tall, while the closure date beside it disappeared correctly because
   that one is emptied rather than hidden. Nothing threw, and the property was
   set exactly as the code intended.

   `!important` is what makes the rule general. `label.field[hidden]` would fix
   this one field and leave the trap standing for the next element somebody
   hides, which is the "spelled twice" failure this codebase keeps paying for.
   `tests/hiddenAttribute.test.ts` turns red if this goes. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  font-family: var(--font);
  /* DM Sans is drawn at every weight from one variable file, so the 450 below
     is really 450 rather than a snap to the nearest static face. */
  -webkit-font-smoothing: antialiased;
  background: var(--plane);
  color: var(--text-primary);
  font-size: var(--fs-base);
  /* Not 400. Glare and a smudged screen eat thin strokes before they eat
     contrast, and it costs no layout space at all. */
  font-weight: 450;
  line-height: 1.5;
}

.boot {
  padding: 48px;
  color: var(--text-muted);
}

/* Headings: the website's tight tracking, lighter than its display type
   because the app has none. Weight 700, as the brief sets for headings. */
h1,
h2,
h3 {
  letter-spacing: -0.02em;
  font-weight: 700;
}

/* One focus ring for every control, the website's: emerald, 3px, offset.
   #0e8a5f is 4.36:1 on white and 4.1:1 on ink, so it holds on both grounds
   the app now has. Rules below that set their own offset keep it — a ring
   inside a scrolling table would otherwise be clipped — and take the colour
   and width from here. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

/* ---------- Login ---------- */
.login-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  /* The website's /signup page, which a customer sees seconds before this:
     paper ground, an ink panel of what they are getting, the white form card.
     On a phone the card stands alone — the panel is a desktop luxury, and a
     rep signing in at a shop counter wants the form, not the pitch. */
  background: var(--plane);
  gap: 24px;
}
@media (min-width: 900px) {
  .login-wrap {
    grid-template-columns: minmax(0, 420px) minmax(0, 440px);
    justify-content: center;
    align-items: stretch;
  }
}
.login-panel {
  display: none;
}
@media (min-width: 900px) {
  .login-panel {
    display: flex;
    flex-direction: column;
    gap: 20px;
    background: var(--ink);
    color: #ffffff;
    border-radius: var(--radius-xl);
    padding: 40px 36px;
  }
}
.login-panel .eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--lime);
}
.login-panel h2 {
  margin: 0;
  font-size: var(--fs-stat);
  line-height: 1.1;
  color: #ffffff;
}
.login-panel ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
}
.login-panel li {
  display: flex;
  gap: 12px;
  color: var(--on-ink-muted);
}
/* A lime disc with an ink tick: lime as a fill under ink, never as text. */
.login-panel .tick {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--lime);
  color: var(--ink);
}
.login-panel .legal {
  margin: auto 0 0;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--on-ink-muted);
}
.login-logo {
  display: block;
  width: 140px;
  height: auto;
  margin: 0 0 24px;
}
.login-card {
  width: 100%;
  max-width: 400px;
  background: var(--surface-1);
  /* No border: the shadow does the separating, as on the website's cards. */
  border-radius: var(--radius-xl);
  padding: 32px;
  box-shadow: var(--shadow-lg);
}
.login-card h1 {
  margin: 0 0 4px;
  font-size: var(--fs-xl);
}
.login-card .sub {
  color: var(--text-secondary);
  margin: 0 0 24px;
}
.demo-hint {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}
.demo-hint code {
  background: var(--plane);
  padding: 1px 5px;
  border-radius: 4px;
  border: 1px solid var(--border);
}
.demo-hint button {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  padding: 2px 0;
  font: inherit;
  text-align: left;
}

/* The show/hide toggle beside a password box. Sits inside the field rather
   than beside it, so the row keeps the same footprint as every other input. */
.password-row {
  display: flex;
  align-items: stretch;
  gap: 8px;
}
.password-row input {
  flex: 1;
  min-width: 0;
}
.reveal-btn {
  flex: 0 0 auto;
  /* A secondary pill beside the field, so it reads as the field's own control
     rather than a grey box from an older screen. */
  background: var(--surface-2);
  border: 1px solid rgba(11, 26, 20, 0.15);
  border-radius: var(--radius-pill);
  color: var(--text-secondary);
  font: inherit;
  font-weight: 500;
  font-size: var(--fs-sm);
  padding: 0 12px;
  cursor: pointer;
}
.reveal-btn:hover {
  color: var(--text-primary);
  border-color: rgba(11, 26, 20, 0.4);
}

/* The alternate action under a sign-in or sign-up card: the research is
   explicit that conflating "sign in" and "sign up" is how people create
   duplicate accounts and try to sign into ones that do not exist, so the two
   stay visually and conceptually distinct. */
.auth-alt {
  margin: 20px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  text-align: center;
  color: var(--text-secondary);
  font-weight: 500;
  font-size: var(--fs-sm);
}

/* The derived workspace address, shown back rather than demanded. */
/* "A workspace called this already exists. Is it yours?" — under the company name on sign-up. A
   plain notice with a way on, not an error: a different company may honestly share a name. */
.known-workspace {
  margin: -4px 0 16px;
  padding: 12px 14px;
  background: var(--plane);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.known-workspace p {
  margin: 0 0 10px;
}
.known-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* The two answers on the sign-up question, stacked and full width on every screen. */
.gate-choices {
  display: grid;
  gap: 10px;
}
.slug-preview {
  margin: -4px 0 16px;
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}
.slug-preview code {
  background: var(--plane);
  padding: 1px 5px;
  border-radius: 4px;
  border: 1px solid var(--border);
  color: var(--text-primary);
  /* One box, never two. A long address split across a line break renders as
     two separate bordered chips and reads as two addresses. */
  white-space: nowrap;
  display: inline-block;
}

/* The verdict under a contact field. Quiet by default: a check that could not
   run says nothing, because the rep did nothing wrong and the save will work. */
.verify-note {
  margin: -8px 0 12px;
  font-weight: 500;
  font-size: var(--fs-sm);
  min-height: 1em;
  color: var(--text-secondary);
}
.verify-ok {
  color: var(--good-text, #157347);
  font-weight: 600;
}
.verify-bad {
  color: var(--danger-text, #b02a37);
  font-weight: 600;
}

/* ---------- Shell ---------- */
.shell {
  display: grid;
  grid-template-columns: 232px 1fr;
  min-height: 100vh;
}
/* **Ink, like the website's footer and dark bands.** It is a brand band that
   carries navigation and never data, so the sunlight argument against dark
   grounds does not reach it: nothing a rep reads outdoors is drawn here, and
   its labels clear 8.9:1 (--on-ink-muted) and 18:1 (white) regardless. */
.sidebar {
  background: var(--ink);
  color: var(--on-ink-muted);
  padding: 20px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
.brand {
  display: flex;
  align-items: center;
  /* Clear space around the lock-up, as the brand guide asks. */
  padding: 8px 10px 22px;
}
.brand img {
  display: block;
  width: 132px;
  height: auto;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  color: var(--on-ink-muted);
  text-decoration: none;
  font-weight: 500;
  border: none;
  background: none;
  font: inherit;
  cursor: pointer;
  width: 100%;
  text-align: left;
}
.nav-item:hover {
  background: rgba(255, 255, 255, 0.06);
  color: #ffffff;
}
/* The active destination is the strongest affordance in the shell: white on a
   faint lift, with a 3px lime bar — the website's own "you are here" mark, and
   lime on ink, the only ground it is allowed on. */
.nav-item.active {
  background: rgba(255, 255, 255, 0.06);
  color: #ffffff;
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--lime);
}
.nav-item.active .ico {
  opacity: 1;
}
.nav-item .ico {
  /* A flex box around an <svg>, not a text cell: `text-align` and `font-size`
     do nothing to an SVG child, which is what the glyphs used to be sized by. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: none;
  opacity: 0.85;
}
.nav-badge {
  margin-left: auto;
  background: var(--critical);
  color: #fff;
  border-radius: 99px;
  font-size: var(--fs-xs);
  padding: 0 6px;
  font-weight: 600;
}
.sidebar-foot {
  margin-top: auto;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding-top: 12px;
}
.who {
  padding: 4px 10px 10px;
}
.who .name {
  font-weight: 600;
  color: #ffffff;
}
.who .role {
  color: var(--on-ink-muted);
  font-weight: 500;
  font-size: var(--fs-sm);
}
/* Your own name is the way into your own account, so it has to read as a
   control rather than as a caption — and it has to answer the keyboard, since
   it is the only route to the Profile screen. */
.who-link {
  display: block;
  border-radius: var(--radius);
  text-decoration: none;
  color: inherit;
}
.who-link:hover {
  background: rgba(255, 255, 255, 0.06);
}
.who-link:focus-visible {
  outline-offset: 2px;
}

/* Call and WhatsApp on one list row. The gap is what stops a thumb aimed at the
   number landing on WhatsApp — 8px plus the links' own padding clears the 44px
   target guidance without making the row taller. */
.contact-links {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.call-symbol {
  /* No text to give it width, so the tap target comes from padding. */
  padding: 2px 6px;
  font-size: var(--fs-base);
  justify-content: center;
  line-height: 1;
}

/* The lens inside the search box. Absolute rather than a flex sibling so the
   input keeps its own full width and the allow-listed input[type=search] rule
   still applies to it unchanged. */
.search-field {
  position: relative;
  display: block;
}
.search-field .search-ico {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  color: var(--text-muted);
  pointer-events: none;
}
.search-field input[type='search'] {
  padding-left: 32px;
}

/* The lead row at 390px.
   The title line carries a firm name, a tier pill, sometimes an "Unassigned"
   pill, a contact name and a flag; the meta line carries products, city, an
   owner and two contact links. Both wrap, and without a minimum the firm name
   was being squeezed to a couple of characters by whatever followed it. */
.row-name {
  min-width: 0;
  overflow-wrap: anywhere;
}
.row-meta > span:first-child {
  min-width: 0;
  overflow-wrap: anywhere;
}
.row-meta .contact-links {
  /* Never wrapped away from each other, and never split from the number. */
  margin-left: auto;
  flex: none;
}

/* ---------- The lead row ----------
   Three lines, each a left half and a right half, at every width. The client
   asked for exactly this shape on a phone, and it is the right shape on a
   desktop too — so there is one layout rather than a table that rearranges
   itself below 720px into something nobody looked at.

   `min-width: 0` on the left half is what makes the truncation happen there
   rather than pushing the right half off the row: a flex item defaults to
   min-width:auto and refuses to shrink below its content, which is the same
   rule that .main already carries for the same reason. */
.lead-lines {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.lead-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
/* The left half truncates; the right half never does.
   That asymmetry is the whole layout. The right side carries the short,
   decisive things — a tier, a value, a number to ring — and each is useless
   half-shown. The left side carries names, which stay recognisable from their
   first few words and are one tap from being read in full.
   `min-width: 0` is what lets it shrink at all: a flex item defaults to
   min-width:auto and refuses to go below its content, which is the same rule
   `.main` already carries. Without it the left half pushes the right half off
   the row instead of clipping itself. */
.lead-l {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lead-l > * + * {
  margin-left: 8px;
}
.lead-r {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  white-space: nowrap;
}
.lead-line:nth-child(2) .lead-l,
.lead-line:nth-child(3) {
  font-weight: 500;
  font-size: var(--fs-sm);
}
.row-where {
  color: var(--text-muted);
}
/* Conditional, so it does not count against the three. */
.lead-note {
  font-weight: 500;
  font-size: var(--fs-sm);
  margin-top: 2px;
}

.main {
  padding: 24px 28px 64px;
  max-width: 1400px;
  width: 100%;
  /* A grid item defaults to min-width:auto, so a wide table would push the
     track past the viewport and defeat .table-scroll's overflow-x. */
  min-width: 0;
}
.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.page-head h1 {
  margin: 0;
  font-size: var(--fs-xl);
}
.page-head p {
  margin: 4px 0 0;
  color: var(--text-secondary);
  max-width: 68ch;
}

/* ---------- Cards / tiles ---------- */
.card {
  background: var(--surface-1);
  /* A shadow rather than a border. The card is white and the plane behind it is
     only a shade darker, so a 1px line reads as a seam; the lift is what
     separates them. */
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 18px;
}
.card + .card {
  margin-top: 16px;
}
.card h2 {
  margin: 0 0 2px;
  font-size: var(--fs-base);
}
.card .card-sub {
  margin: 0 0 16px;
  color: var(--text-secondary);
  font-weight: 500;
  font-size: var(--fs-sm);
}

/* What a Reports tab is about, one line above its cards. */
.report-focus {
  margin: 0 0 12px;
  font-weight: 600;
  color: var(--text-primary);
}

/* A collapsed "how to read this" note. One line high until somebody wants it,
   which is what keeps a screen's explanation off the first screenful of a
   phone without throwing the explanation away. See `explainer()` in ui.js. */
.explainer {
  margin: 0 0 14px;
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}
.explainer > summary {
  cursor: pointer;
  color: var(--accent);
  /* A 44px tap target without a 44px box: the row is 20px of text with the
     rest as padding, so a thumb hits it and the layout does not pay for it. */
  padding: 4px 0;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
}
.explainer > summary::-webkit-details-marker {
  display: none;
}
/* Drawn rather than a glyph, so it rotates and needs no font support. */
.explainer > summary::before {
  content: '';
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.15s;
}
.explainer[open] > summary::before {
  transform: rotate(90deg);
}
.explainer > summary:hover {
  text-decoration: underline;
}

/* A whole card that folds away — the "Coming up" follow-ups, which run a month
   ahead and must not push the overdue work off a phone screen.

   Deliberately not `.explainer`: that one is an accent-coloured line of prose
   inviting somebody to read a rationale, and this is a section heading that
   happens to open. It borrows only the drawn marker, which rotates and needs no
   font support. The 44px floor is the sunlight pass's, and the negative margin
   is how the row gets it without the card growing. */
.card-collapsible > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 0;
  margin: -12px 0;
  min-height: 44px;
}
.card-collapsible > summary::-webkit-details-marker {
  display: none;
}
.card-collapsible > summary::before {
  content: '';
  flex: none;
  width: 0;
  height: 0;
  border-left: 6px solid var(--muted);
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  transition: transform 0.15s;
}
.card-collapsible[open] > summary::before {
  transform: rotate(90deg);
}
/* The heading keeps its own type; what it must not keep is the margin that
   would push it away from the marker it now sits beside. */
.card-collapsible > summary > h2 {
  margin: 0;
}

/* What was said on the last three contacts, above the box for what was said on
   this one. Inside a modal form, so it is deliberately quiet: no card, no rule,
   just indented lines that read as a margin note rather than as another
   section competing with the fields around them. */
.note-history {
  margin: -4px 0 12px;
  padding-left: 10px;
  border-left: 2px solid var(--border);
}
.note-line {
  margin: 0 0 6px;
  font-size: var(--fs-sm);
  /* 500 is the floor for anything at this size or below — the sunlight pass's
     rule, and it costs no layout space. */
  font-weight: 500;
  line-height: 1.45;
}
.note-history .explainer {
  margin-top: 2px;
}
.explainer-body {
  padding: 6px 0 2px;
  line-height: 1.5;
}
.explainer-body p {
  margin: 0 0 8px;
}
.explainer-body p:last-child {
  margin-bottom: 0;
}
.grid {
  display: grid;
  gap: 16px;
}
.tiles {
  grid-template-columns: repeat(auto-fit, minmax(184px, 1fr));
}
.cols-2 {
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
}
.tile {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
}
.tile .label {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.tile .value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-stat);
  font-weight: 700;
  margin-top: 6px;
  letter-spacing: -0.01em;
}
.tile .foot {
  color: var(--text-muted);
  font-weight: 500;
  font-size: var(--fs-sm);
  margin-top: 4px;
}

/* ---------- Tables ---------- */
.table-scroll {
  overflow-x: auto;
}
table {
  border-collapse: collapse;
  width: 100%;
  font-weight: 500;
  font-size: var(--fs-sm);
}
th {
  text-align: left;
  font-weight: 600;
  color: var(--text-secondary);
  padding: 8px 10px;
  border-bottom: 1px solid var(--axis);
  white-space: nowrap;
}
td {
  padding: 9px 10px;
  border-bottom: 1px solid var(--grid);
  vertical-align: top;
}
tbody tr:hover td {
  background: var(--plane);
}
/* Figures in the website's mono, lined up by digit — the figures, not their
   column heading, which stays in the body face like every other heading. */
td.num {
  font-family: var(--font-mono);
}
td.num,
th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.empty {
  padding: 28px;
  text-align: center;
  color: var(--text-muted);
}

/* ---------- The two-line lead row ----------
   The whole row is the control on Leads and Funnel: no Open button, and the
   only thing inside it that is separately clickable is the tel: link. So the
   row has to *look* actionable and has to take focus from a keyboard, or it is
   worse than the button it replaced. */
tr.row-link {
  cursor: pointer;
}
tr.row-link:focus-visible {
  /* On the row rather than a cell: a table row cannot carry a border-radius
     reliably across engines, and an outline on the row is what a screen reader
     user is actually focused on. */
  outline: 3px solid var(--focus);
  outline-offset: -2px;
}
.row-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.row-name {
  font-weight: 600;
}
.row-person {
  color: var(--text-secondary);
}
.row-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin-top: 3px;
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-muted);
}
.row-meta.muted {
  color: var(--text-muted);
}

/* ---------- Tags ----------
   Slot names, never hex from the user: a free colour picker puts the legibility
   of the screen in the hands of whoever last opened the dialog, and a tag set to
   near-white is invisible in one theme and fine in the other.

   Each slot sets one hue and the pill mixes it against the *theme's* surface and
   text tokens, so light and dark are one definition rather than two lists that
   drift. Mixing toward `--surface-1` is what makes the dark tint dark. */
.tag-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.6;
  white-space: nowrap;
  background: color-mix(in srgb, var(--tag-hue) 16%, var(--surface-1));
  color: color-mix(in srgb, var(--tag-hue) 65%, var(--text-primary));
  border: 1px solid color-mix(in srgb, var(--tag-hue) 30%, var(--surface-1));
}
.tag-slate  { --tag-hue: #64748b; }
.tag-blue   { --tag-hue: #2f81d6; }
.tag-green  { --tag-hue: #1f9d5a; }
.tag-amber  { --tag-hue: #d08a1a; }
.tag-red    { --tag-hue: #d8524a; }
.tag-violet { --tag-hue: #7c53c9; }

/* A row of pills under a name, wrapping rather than pushing the cell wide. */
.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 4px;
}

/* A tag the Super Admin has retired. Still shown where a firm carries one — an
   unrelated save must not silently strip it — but visibly out of use. */
.tag-pill.retired {
  opacity: 0.55;
  text-decoration: line-through;
}

/* The chip row in the tag editor: the same pill, pressable, and plainly empty
   when it is not applied rather than merely a shade lighter. */
button.tag-pill {
  cursor: pointer;
  font-family: inherit;
}
button.tag-pill[aria-pressed='false'] {
  background: none;
  color: var(--text-muted);
  border-color: var(--border);
}
button.tag-pill:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 1px;
}

/* ---------- Pills & flags ---------- */
/* A pill now carries its state in a tinted ground as well as in its dot, so a
   tier is readable before the word is. The dot stays: colour alone is not a
   distinction everyone can see, and at 12px the word does the real work.
   The markup is unchanged — same classes, same `.dot` child. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: var(--radius-pill);
  padding: 2px 10px;
  font-size: var(--fs-sm);
  font-weight: 600;
  border: 1px solid transparent;
  white-space: nowrap;
  background: var(--cold-wash);
  color: var(--cold);
}
.pill .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.pill-hot {
  background: var(--critical-wash);
  color: var(--critical);
}
.pill-warm {
  background: var(--warning-wash);
  color: var(--warning-text);
}
.pill-cold {
  background: var(--cold-wash);
  color: var(--cold);
}
.pill-closed {
  background: var(--good-wash);
  color: var(--good-text);
}
/* No per-tier dot colours: `.pill .dot` takes currentColor, which is the pill's
   own text colour and therefore always legible on its own wash. A saturated dot
   was fine on the old outline pill and vanishes on a tinted one — a yellow dot
   on a yellow ground most of all. */
.pill-muted {
  background: var(--cold-wash);
  color: var(--text-secondary);
}
.flag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  font-size: var(--fs-sm);
}
.flag-yellow {
  color: var(--text-primary);
}
.flag-yellow .marker {
  color: var(--warning-text);
}
.flag-red .marker {
  color: var(--critical);
}
.flag-red {
  color: var(--critical);
}
.muted {
  color: var(--text-muted);
}
.small {
  font-weight: 500;
  font-size: var(--fs-sm);
}

/* ---------- Forms ---------- */
label.field {
  display: block;
  margin-bottom: 14px;
}
label.field > span.lbl {
  display: block;
  font-weight: 550;
  margin-bottom: 5px;
  font-size: var(--fs-sm);
}
label.field .hint {
  color: var(--text-muted);
  font-weight: 500;
  font-size: var(--fs-sm);
}
input[type='text'],
input[type='email'],
input[type='password'],
input[type='number'],
input[type='date'],
input[type='search'],
/* `tel` was missing, so the two most-used boxes on the lead form — the mobile
   and the WhatsApp number — rendered as bare browser inputs: no width, no
   padding, no border, visibly unlike every field around them. An allow-list of
   input types fails this way silently, one type at a time. */
input[type='tel'],
select,
textarea {
  width: 100%;
  /* 44px like every other tap target, and `font: inherit` now means 16px —
     which is the size below which iOS Safari zooms the page on focus. */
  min-height: 48px;
  padding: 11px 14px;
  border-radius: 16px;
  /* --field-border, the website's: 3.36:1 on white and 3.02:1 on paper, the
     3:1 a control's boundary needs. --axis is structure for charts. */
  border: 1px solid var(--field-border);
  background: var(--surface-2);
  color: var(--text-primary);
  font: inherit;
}
/* The website's focus: the edge turns emerald-dark and a soft ring appears.
   The edge is what carries it (6.7:1); the ring is the halo around it. */
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--tone-accent);
  box-shadow: 0 0 0 4px rgba(14, 138, 95, 0.15);
}
textarea {
  min-height: 72px;
  resize: vertical;
}
/* The whole row is the target, not the 16px box. A checkbox is the smallest
   thing anybody is asked to hit, and "Same as mobile number" on the lead form
   is ticked on nearly every lead. */
.check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  margin-bottom: 8px;
  font-weight: 500;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.check input {
  width: 22px;
  height: 22px;
  flex: none;
  accent-color: var(--accent);
}
/* A confirmation that belongs to the field above it, not to the form. */
.inline-check {
  margin: 6px 0 0;
  color: var(--text-secondary);
  font-weight: 500;
  font-size: var(--fs-sm);
}
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0 16px;
}
/* The two sides of the business card, plus whatever the scan has to say. */
.card-scan {
  display: block;
  margin: 0 0 14px;
}
.card-scan > .lbl {
  display: block;
  margin-bottom: 2px;
}
/* Its own line under the file button. Beside it, "Attached ✓" breaks between
   the word and the tick on a phone, which reads as a stray mark. */
.card-scan .file-status {
  display: block;
  margin-top: 4px;
}
/**
 * A box the card filled in, rather than the rep.
 *
 * A left edge and a wash, not a border swap: the field must read as "check
 * this" without reading as "this is wrong", which is what a red or amber
 * outline would say. It clears itself the moment the rep types in the box —
 * once they have looked at it, where the value came from stops mattering.
 */
.from-card {
  background: var(--accent-wash);
  border-left: 3px solid var(--accent);
}
fieldset {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  margin: 0 0 16px;
}
legend {
  font-weight: 650;
  padding: 0 6px;
  font-size: var(--fs-sm);
}

button {
  font: inherit;
}
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  /* 44px, which is what an unsteady thumb needs. It was ~34px: 8px of padding
     on 14px text, set for a mouse. The design case here is a rep standing in a
     market holding a phone in one hand. */
  min-height: 44px;
  padding: 10px 20px;
  /* The website's pill, and its secondary button: white, a faint ink edge. */
  border-radius: var(--radius-pill);
  border: 1px solid rgba(11, 26, 20, 0.15);
  background: var(--surface-2);
  color: var(--text-primary);
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.btn:hover:not(:disabled) {
  border-color: rgba(11, 26, 20, 0.4);
}
.btn:active:not(:disabled) {
  transform: scale(0.98);
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* The website's primary: an ink pill, white bold words, 48px. White on ink is
   17.9:1, so the fill can be as dark as the brand wants it. */
.btn-primary {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  color: var(--accent-ink);
  font-weight: 700;
  min-height: 48px;
}
.btn-primary:hover:not(:disabled) {
  background: var(--ink-2);
  border-color: var(--ink-2);
}
.btn-sm {
  padding: 4px 10px;
  font-weight: 500;
  font-size: var(--fs-sm);
}
.btn-danger {
  color: var(--critical);
  border-color: var(--critical);
  background: transparent;
}
.btn-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
}
/* Two actions held apart, one at each end of the row.

   **The reason changed and the rule did not**, which is worth saying rather
   than leaving the old sentence to be read as still true. This began as
   emphasis — a secondary at the left edge and the primary pushed right, so the
   eye landed on the thing most people came to do. The client then asked for
   both to be primary, so the split is now *placement* rather than emphasis:
   Log a lead left and Record a follow-up right, the same on both screens that
   carry the pair.

   `margin-left: auto` on the last child rather than `justify-content:
   space-between`, because the row wraps — and a wrapped `space-between` row
   strands one button in the middle of a line of its own. That half is
   unchanged and is why this is a rule at all. */
.btn-row-split > *:last-child {
  margin-left: auto;
}
/* Text button that reads as a link — used for the auth screens' secondary
   actions, where a full button would compete with the primary one. */
/* A button that looks like a link is still a button. "Send the link again" was
   29px tall, which is the same thumb problem as the phone number — and the
   negative margin is the same answer, so an inline link-button does not add
   15px to the sentence it sits in. */
.link-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: -10px;
  margin-bottom: -10px;
  background: none;
  border: none;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  padding: 4px;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link-btn:hover {
  filter: brightness(1.15);
}

.filters {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: flex-end;
  margin-bottom: 16px;
}
.filters label.field {
  margin-bottom: 0;
  min-width: 150px;
}

/* ---------- Banners ---------- */
.banner {
  border-radius: var(--radius);
  padding: 11px 14px;
  margin-bottom: 16px;
  display: flex;
  gap: 10px;
  align-items: flex-start;
  border: 1px solid var(--border);
  background: var(--surface-1);
  font-weight: 500;
  font-size: var(--fs-sm);
}
.banner .marker {
  flex: none;
  font-weight: 700;
}
.banner-warn .marker {
  color: var(--warning-text);
}
.banner-error .marker {
  color: var(--critical);
}
.banner-error {
  border-color: var(--critical);
}
.banner-info .marker {
  color: var(--series-1);
}
.banner-good .marker {
  color: var(--good);
}

/* ---------- Modal ---------- */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(11, 11, 11, 0.45);
  display: grid;
  place-items: start center;
  padding: 32px 16px;
  overflow-y: auto;
  z-index: 40;
}
.modal {
  background: var(--surface-1);
  border-radius: 14px;
  border: 1px solid var(--border);
  width: 100%;
  max-width: 720px;
  box-shadow: var(--shadow);
}
.modal-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 18px 20px;
  border-bottom: 1px solid var(--border);
}
.modal-head h2 {
  margin: 0;
  font-size: var(--fs-base);
}
.modal-body {
  padding: 20px;
}
.modal-foot {
  padding: 14px 20px;
  border-top: 1px solid var(--border);
  display: flex;
  /* A footer that cannot wrap overflows instead, and takes the modal with it.
     A flex item defaults to `min-width: auto`, so the lead detail's five
     buttons refused to shrink below their longest word, needed 505px inside a
     390px screen, and dragged the card sideways until the contact's name,
     email address and PIN code were clipped off the left edge — on the screen
     a rep opens most, on the device this product is built for. Wrapping makes
     that overflow impossible rather than merely unlikely, whatever a caller
     puts in the footer. */
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 10px;
}
.icon-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-secondary);
  font-size: var(--fs-lg);
  line-height: 1;
  padding: 4px 8px;
  border-radius: 6px;
}
.icon-btn:hover {
  background: var(--plane);
}

/* ---------- Toasts ---------- */
#toast-host {
  position: fixed;
  right: 18px;
  bottom: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 60;
}
.toast {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  padding: 10px 14px;
  box-shadow: var(--shadow);
  max-width: 380px;
  font-weight: 500;
  font-size: var(--fs-sm);
}
.toast-error {
  border-left-color: var(--critical);
}
.toast-success {
  border-left-color: var(--good);
}

/* ---------- Charts ---------- */
.chart {
  width: 100%;
}
/* The plot is measured to size the SVG in real pixels, so it must be able to
   shrink — hence min-width:0 rather than relying on the default auto. */
.chart-plot {
  width: 100%;
  min-width: 0;
}
.chart svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.legend {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}
.legend .key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.legend .swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
}
/* Month cells in the commitment-vs-achievement grid */
.months {
  display: flex;
  gap: 2px;
}
.mcell {
  width: 15px;
  height: 22px;
  border-radius: 3px;
  background: var(--grid);
}
.mcell-met {
  background: var(--good);
}
.mcell-missed {
  background: var(--critical);
}
/* Before the person's first commitment month: nothing was owed, so it is drawn
   as an outline rather than as the grey of a month nobody filed. */
.mcell-prejoin {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--grid);
}

.line-item-row {
  display: grid;
  grid-template-columns: 1fr 96px 120px 36px;
  gap: 10px;
  align-items: center;
  margin-bottom: 8px;
}
.line-item-row .rev {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

.notif {
  display: flex;
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--grid);
}
.notif.unread {
  font-weight: 500;
}
.notif .when {
  color: var(--text-muted);
  font-weight: 500;
  font-size: var(--fs-sm);
  white-space: nowrap;
}

/* ---------- Mobile app bar, tab bar & drawer ---------- */
/* Hidden on desktop; the sidebar is permanent there. */
.topbar,
.drawer-scrim,
.menu-btn,
.tabbar {
  display: none;
}

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

  .topbar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    background: var(--surface-1);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 25;
  }
  .topbar-title {
    font-weight: 650;
    font-size: var(--fs-base);
  }
  .topbar-alerts {
    margin-left: auto;
    color: var(--critical);
    font-weight: 650;
    text-decoration: none;
    padding: 8px;
  }

  .menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: 1px solid var(--axis);
    border-radius: 9px;
    background: var(--surface-2);
    cursor: pointer;
    flex: none;
  }
  /* Three-bar glyph drawn in CSS — no icon font to fail to load. */
  .menu-ico,
  .menu-ico::before,
  .menu-ico::after {
    display: block;
    width: 16px;
    height: 2px;
    border-radius: 2px;
    background: var(--text-primary);
    content: '';
  }
  .menu-ico {
    position: relative;
  }
  .menu-ico::before {
    position: absolute;
    top: -5px;
  }
  .menu-ico::after {
    position: absolute;
    top: 5px;
  }

  /* The sidebar becomes an off-canvas drawer. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 264px;
    max-width: 82vw;
    height: 100%;
    z-index: 45;
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    box-shadow: var(--shadow);
  }
  .shell.drawer-open .sidebar {
    transform: translateX(0);
  }
  .drawer-scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(11, 11, 11, 0.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
    z-index: 40;
  }
  .shell.drawer-open .drawer-scrim {
    opacity: 1;
    pointer-events: auto;
  }

  /* --- Tab bar ---------------------------------------------------------
     Three destinations plus the drawer, fixed to the bottom where a thumb
     reaches. Below the drawer and scrim in the stack (z-index 45/40) on
     purpose: with the drawer open the bar must not float above the scrim,
     or it would offer taps that look live and are not. */
  .tabbar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    background: var(--surface-2);
    border-top: 1px solid var(--border);
    padding: 6px 4px;
    /* Clears the home indicator on a notched phone; 0 everywhere else. */
    padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
    /* The height `.main` reserves for it. Nothing set one before, so the bar
       measured whatever its contents came to and the padding below was a guess
       against it — 56px reserved for a bar that came to 63.

       `border-box`, so the property means the bar's *total* height rather than
       its content box: with the default the 6px padding and the 1px border sit
       on top and the number stops describing what `.main` has to clear. The
       inset is added rather than absorbed, because on a notched phone the bar
       genuinely is that much taller. */
    box-sizing: border-box;
    min-height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  }
  .tab {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 4px 2px;
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--text-muted);
    text-decoration: none;
    cursor: pointer;
    /* A label that wraps would shift the row's height; ellipsis instead. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .tab.active {
    color: var(--text-primary);
  }
  .tab.active .tab-ico {
    color: var(--accent-fill);
  }
  .tab-ico {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 22px;
    line-height: 1;
    position: relative;
  }
  .tab-dot {
    position: absolute;
    top: -2px;
    right: -6px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--critical);
    border: 1.5px solid var(--surface-2);
  }
  .tab:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: -2px;
    border-radius: 8px;
  }

  .nav-item {
    padding: 11px 10px;
  }

  /* **The bar is fixed, so the last card would sit under it** — and for a while
     it did. There were two `.main` rules here: this one, and an earlier
     `padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px))` four rules
     above it. The shorthand below wins on cascade order, so the inset term was
     dead and the rule that carried it was unreachable.

     It matters because `.tabbar` is about 58px *plus* `env(safe-area-inset-bottom)`
     — 34px on a notched iPhone — so the last line of content sat behind the bar
     on exactly the devices the inset exists for. Nothing said so: the page
     still scrolled, it just stopped a little short.

     One rule now, and the term is in the shorthand rather than in a sibling
     that something else can override. */
  .main {
    padding: 16px 14px calc(var(--tabbar-h) + 8px + env(safe-area-inset-bottom, 0px));
  }
  /* The sticky topbar already names the screen, from the same nav label the
     h1 repeats — so on a phone this is the title printed twice, ~50px of the
     first screenful spent saying nothing new. Clipped rather than
     `display: none`: a screen reader still gets the page's only h1, and the
     topbar's copy is decoration that scrolls with nothing. */
  .page-head h1 {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* Primary actions go full width — they are thumb targets, not asides. */
  .page-head .btn-row {
    width: 100%;
  }
  .page-head .btn-row .btn {
    flex: 1 1 auto;
    justify-content: center;
    min-height: 44px;
  }

  .filters {
    gap: 12px;
  }
  .filters label.field {
    min-width: 0;
    flex: 1 1 100%;
  }

  .card {
    padding: 14px;
  }
  .tiles {
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  }
  .tile .value {
    font-size: var(--fs-xl);
  }

  /* Modals become full-screen sheets — forms need the room. */
  /* `start stretch`, not `stretch`: align to the top, stretch across.
     Stretching *vertically* gives the card a definite height equal to the
     viewport, which `min-height` cannot then grow — so a form taller than the
     screen rendered its fields outside the card's own background, over the
     page behind it. The lead form is exactly that tall on a phone. */
  .modal-backdrop {
    padding: 0;
    place-items: start stretch;
  }
  .modal {
    max-width: none;
    min-height: 100vh;
    border-radius: 0;
    display: flex;
    flex-direction: column;
  }
  /* `1 1 auto`, not `1`. The shorthand `flex: 1` means `flex-basis: 0`, so the
     body contributes nothing to the card's height and a long form overflows it
     — the same failure from the other direction. `auto` keeps the content as
     the basis while still filling the screen when the form is short. */
  .modal-body {
    flex: 1 1 auto;
  }
  .modal-head {
    position: sticky;
    top: 0;
    background: var(--surface-1);
    z-index: 2;
  }
  /* Tighter than the desktop spacing, and the 20px of side padding is what
     buys the row. A Manager's lead carries five actions — Edit, Record a
     follow-up, Confirm closed value, Transfer and Close — and at 393px they
     needed three rows, which is 187px of sticky footer, 22% of the screen
     spent on buttons. Measured in Chromium: trimming the footer's own padding
     and gap saved 4px and changed nothing, trimming the buttons' saved nothing
     at all, and the two together crossed the wrap threshold and folded it to
     two rows at 129px — 15%. Neither half is worth keeping without the other,
     which is why they are written here as one decision.

     The Super Admin never saw this: their fifth action is "Delete lead", short
     enough to fit in two rows all along. A screen has to be verified as the
     role that reads it. */
  .modal-foot {
    position: sticky;
    bottom: 0;
    background: var(--surface-1);
    padding-left: 12px;
    padding-right: 12px;
    gap: 8px;
  }
  /* `1 1 auto`, not `1`. The shorthand `flex: 1` means `flex-basis: 0`, so
     every button claims an equal share of the row however long its label —
     which squeezed "Record a follow-up" to 89px and wrapped it into a
     three-line stack 118px tall, while "Edit" was pushed off the screen
     entirely. `auto` starts each button at its own label width, so a label
     keeps one line and the row wraps when the next button will not fit.
     `min-height` stays at 44px: that is the touch target, and this breakpoint
     is a phone. */
  .modal-foot .btn {
    flex: 1 1 auto;
    justify-content: center;
    min-height: 44px;
    padding-left: 12px;
    padding-right: 12px;
  }

  /* Commitment line items stack instead of squeezing into four columns. */
  .line-item-row {
    grid-template-columns: 1fr auto;
    gap: 8px 10px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--grid);
  }
  .line-item-row > *:first-child {
    grid-column: 1 / -1;
  }
  .line-item-row .rev {
    text-align: left;
  }

  #toast-host {
    right: 12px;
    left: 12px;
    bottom: 12px;
  }
  .toast {
    max-width: none;
  }
}

/* ---------- Tables become cards on small screens ---------- */
/* Every table goes through ui.js `table()`, which stamps each cell with a
   data-label, so this transformation applies app-wide without per-view work. */
@media (max-width: 720px) {
  .table-scroll table,
  .table-scroll tbody,
  .table-scroll tr,
  .table-scroll td {
    display: block;
    width: 100%;
  }
  .table-scroll thead {
    /* Visually hidden, still announced to screen readers. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  /* Each row is its own tile: white, lifted off the card behind it by a
     shadow rather than boxed in by a hairline. That is the same treatment
     `.card` gets one level up — a border reads as a seam between two things and
     a shadow reads as one thing in front of another, which is what a list of
     leads on a plane actually is. The gap does as much work as the shadow. */
  .table-scroll tr {
    border: none;
    border-radius: var(--radius-lg);
    padding: 12px 14px;
    margin-bottom: 12px;
    background: var(--surface-2);
    box-shadow: var(--shadow-sm);
  }
  /* A row that opens something says so when a thumb is on it. */
  .table-scroll tr.row-link:active {
    box-shadow: var(--shadow);
  }
  .table-scroll tbody tr:hover td {
    background: none;
  }
  .table-scroll td {
    border-bottom: none;
    padding: 4px 0;
    display: flex;
    gap: 12px;
    align-items: baseline;
    justify-content: space-between;
  }
  .table-scroll td.num {
    text-align: left;
  }
  .table-scroll td[data-label]::before {
    content: attr(data-label);
    flex: none;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-weight: 550;
  }
  /* A cell whose content is a block rather than a value — the trailing-12-month
     strip is a sparkline, twelve month cells and a date range. The flex row
     above lays every child out beside the label, which for three children is
     528px of content in a 310px card: the last months are clipped off the
     right of the screen and cannot be reached. Stacked instead, with the label
     above, and the cells spread to the card's width rather than holding the
     rigid 15px that is right on a desktop and leaves a ragged gap here.
     Scoped to the media query so the wide layout is untouched. */
  .table-scroll td.cell-block {
    display: block;
  }
  .table-scroll td.cell-block[data-label]::before {
    display: block;
    margin-bottom: 6px;
  }
  .table-scroll td.cell-block .mcell {
    flex: 1 1 0;
    width: auto;
  }
  /* The first cell is the card's heading, so it keeps no label and full width. */
  .table-scroll td:first-child {
    display: block;
    padding-top: 0;
    padding-bottom: 8px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--grid);
    font-size: var(--fs-base);
  }
  /* A lead row is not a heading followed by labelled fields — the first cell
     *is* the whole card. So the rule that separates a heading from what
     follows it lands under the last line with nothing beneath it, which reads
     as a card that failed to finish rendering. */
  .table-scroll td.lead-cell {
    border-bottom: none;
    padding-bottom: 0;
    margin-bottom: 0;
  }
  .table-scroll td:first-child::before {
    display: none;
  }
  /* The actions strip is a second `tr`, so the card rule above would draw it
     as a separate tile floating under its owner. Joined instead: the data row
     loses its bottom rounding and margin, the strip loses its top rounding and
     its shadow, and together they read as one card. */
  .table-scroll tr.row-actions {
    margin-top: -12px;
    padding-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    box-shadow: var(--shadow-sm);
    clip-path: inset(0 -8px -8px -8px);
  }
  .table-scroll tr:has(+ tr.row-actions) {
    margin-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }
  .table-scroll tr.row-actions > td {
    padding-bottom: 0;
  }
  .table-scroll tr.row-actions .btn-row {
    gap: 8px;
  }
  /* The action-cell rule below stretches buttons to share the width, which is
     right for a single "Open" but wrong for a strip of six: it forces "Works
     deals" onto two lines. Here they take their natural width and wrap. */
  .table-scroll tr.row-actions .btn-row .btn {
    flex: none;
    width: auto;
  }
  /* An action cell (empty header) spans the card and stretches its button. */
  .table-scroll td:not([data-label]):last-child {
    padding-top: 10px;
  }
  .table-scroll td:not([data-label]):last-child .btn {
    width: 100%;
    justify-content: center;
    min-height: 44px;
  }
  .table-scroll td:not([data-label]):last-child .btn-row .btn {
    width: auto;
    flex: 1;
  }
}

@media (max-width: 420px) {
  /* The same inset as the 860px block: this narrows the side padding and must
     not quietly drop the bottom term while doing it. */
  .main {
    padding: 14px 12px calc(var(--tabbar-h) + 8px + env(safe-area-inset-bottom, 0px));
  }
  .grid,
  .cols-2 {
    grid-template-columns: 1fr;
  }
  .form-grid {
    grid-template-columns: 1fr;
  }
}

/* --- Funnel ---------------------------------------------------------------
   Bar width is proportional to open value, so the shape narrows the way a
   funnel does. Colour reuses the deal-potential tokens the pills already use —
   Hot and Warm mean the same thing on every screen — and is never the only
   signal: each stage carries its pill, count and value as text. */
.funnel-stage {
  margin-top: 14px;
}
.funnel-stage:first-of-type {
  margin-top: 8px;
}
.funnel-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}
.funnel-value {
  margin-left: auto;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.funnel-track {
  background: var(--surface-sunken, rgba(127, 127, 127, 0.12));
  border-radius: 6px;
  overflow: hidden;
}
.funnel-bar {
  height: 14px;
  border-radius: 6px;
  transition: width 240ms ease;
}
.funnel-hot {
  background: var(--critical);
}
.funnel-warm {
  background: var(--warning);
}
@media (prefers-reduced-motion: reduce) {
  .funnel-bar {
    transition: none;
  }
}

/* Tappable phone number in the follow-up list. Sized to a comfortable touch
   target on a phone, where this link is the whole point of the screen. */
/* **44px of tap target without 44px of layout.** A `tel:` link is the single
   most-tapped thing in this app — the lead row exists to make dialling easy —
   and it was a 16px-tall anchor: fine for a mouse, wrong for a thumb in a
   market. The negative margin hands the extra height back to the row, so line
   three stays the height it was while the target is what a hand needs.

   Note there is deliberately only one `.call-link` rule. A second one added
   earlier in the file loses to this one on `display`, which is how a CSS fix
   ships doing nothing at all. */
.call-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: -12px;
  margin-bottom: -12px;
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
  padding: 3px 0;
  white-space: nowrap;
}
.call-link:hover {
  text-decoration: underline;
}
.call-link.small {
  font-size: var(--fs-sm);
  font-weight: 500;
}

/* ===========================================================================
   Smart Sales Edge — components added by the reskin
   ===========================================================================

   Everything below is additive. No existing rule was rewritten to accommodate
   it, and nothing here is required for a screen to work: a view that does not
   use `.ring` renders exactly as it did before. That is deliberate, so the
   markup can move across one view at a time rather than in one commit.
   =========================================================================== */

/* --- Attainment ring ------------------------------------------------------
   One number a rep can read at arm's length, with its components beside it.
   Drawn as two SVG circles with a stroke-dasharray rather than a chart, since
   there is no axis and nothing to scale — see `ring()` in ui.js. */
.scoreblock {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
}
.ring {
  position: relative;
  flex: none;
  width: 118px;
  height: 118px;
}
.ring svg {
  display: block;
  /* Start the arc at twelve o'clock; SVG circles start at three. */
  transform: rotate(-90deg);
}
.ring .ring-center {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
  /* **The padding is what keeps a caption inside the arc.** `place-content:
     center` sizes the track to its content and centres it, which constrains
     nothing — so a two-word caption ("OF COMMITTED") rendered straight over the
     stroke and out the sides. The ring is 118px with a 9px stroke, leaving an
     inner circle about 91px across; 15px a side holds the text to 88px and
     wraps it instead. Reported from the live workspace. */
  padding: 0 15px;
}
.ring .ring-pct {
  font-size: var(--fs-stat);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.ring .ring-cap {
  margin-top: 3px;
  font-size: var(--fs-xs);
  font-weight: 600;
  /* 0.06em rather than 0.09em: at 12px uppercase the wider tracking pushed
     COMMITTED past the 88px the padding above allows, so it wrapped a second
     time onto a line of its own letters. */
  letter-spacing: 0.06em;
  line-height: 1.15;
  text-transform: uppercase;
  color: var(--text-muted);
}
.ring-legend {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  flex: 1;
}
.ring-legend .leg-k {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}
.ring-legend .leg-swatch {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
}
.ring-legend .leg-v {
  font-size: var(--fs-base);
  font-weight: 650;
  letter-spacing: -0.022em;
  font-variant-numeric: tabular-nums;
}
.ring-legend .leg-v small {
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: 0;
  color: var(--text-muted);
  /* Never split "37 leads" across two lines: the count and its noun are one
     phrase, and a legend that wraps mid-phrase reads as two facts. */
  white-space: nowrap;
}

/* --- Movement chip on a tile ---------------------------------------------
   The tile's third line. `.up` and `.down` are about direction, not virtue:
   an increase in overdue follow-ups uses `.down` because it is bad news, so
   the caller picks the tone rather than the arithmetic picking it. */
.chip {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: var(--cold-wash);
  color: var(--cold);
}
.chip-up {
  background: var(--good-wash);
  color: var(--good-text);
}
.chip-down {
  background: var(--critical-wash);
  color: var(--critical);
}
.chip-flag {
  background: var(--warning-wash);
  color: var(--warning-text);
}

/* --- Worklist state stripe ------------------------------------------------
   The follow-up screen carries state in a stripe as well as in a date, so what
   needs attention reads before anything is read.

   The stripe is a border on the <tr>, which covers both layouts: with
   `border-collapse: collapse` it renders in the table, and below 720px the row
   becomes a block card and the same edge becomes the card's stripe.

   `.table-scroll` is in the selector for specificity, not for scoping. The
   collapsed-card rule is `.table-scroll tr { border: 1px solid var(--border) }`
   — a shorthand at (0,1,1) that resets all four edges. A bare `.row-overdue`
   is (0,1,0) and loses to it, so the stripe rendered on desktop and silently
   vanished on a phone, which is the one place the screen is actually used. */
/* A full-width strip of actions belonging to the row above it.
   The Team screen's six controls were a seventh *column*, about 200px wide on
   a desk screen, so they stacked one per line and each person stood ~300px
   tall. Given the table's width they now sit on one line.
   `td` keeps its own top border off so the pair reads as one person rather
   than as a row and an orphan beneath it. */
.table-scroll tr.row-actions > td {
  border-top: none;
  padding-top: 0;
  padding-bottom: 14px;
}
.table-scroll tr.row-actions:hover > td {
  background: none;
}
/* The separating line belongs under the *pair*, not between them. */
.table-scroll tbody tr:not(.row-actions) > td {
  border-bottom: none;
}

.table-scroll tr.row-overdue {
  border-left: 3px solid var(--critical);
}
.table-scroll tr.row-due {
  border-left: 3px solid var(--accent);
}

/* --- Segmented period control --------------------------------------------
   Real buttons, so it is keyboard-reachable and announces its state. Anything
   that looks pressable here is pressable. */
.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  /* **Scrolls rather than pushing the page wide.** Reports has five tabs, and
     five pills do not fit 390px — which is not a reason to shorten their names,
     because "Quarterly" is what the tab is. The strip takes its own scrollbar,
     the rule wide content follows everywhere here, so the *page* never scrolls
     sideways. `max-width` is what makes an inline-flex box scroll at all. */
  max-width: 100%;
  overflow-x: auto;
  /* No scrollbar drawn: on a pill strip it reads as a broken border. */
  scrollbar-width: none;
  /* The track is the plane, not a surface. The selected segment below is
     --surface-2 (white) and would be invisible on a white track — the two
     tokens now hold the same value, so only the *roles* keep them apart. */
  background: var(--plane);
  border-radius: var(--radius-pill);
}
.segmented::-webkit-scrollbar {
  display: none;
}
.segmented button {
  appearance: none;
  /* A pill must not be squeezed narrower than its own word. */
  flex: none;
  border: 0;
  cursor: pointer;
  padding: 5px 14px;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-secondary);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.segmented button[aria-pressed='true'] {
  background: var(--surface-2);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}
.segmented button:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* --- Filter chips ---------------------------------------------------------
   Selected uses ink rather than the accent. The accent already means
   "achieved" on the ring and "due today" on the worklist; a third meaning on a
   filter would make the colour say nothing. */
.filter-chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.filter-chips button {
  appearance: none;
  cursor: pointer;
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-secondary);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.filter-chips button[aria-pressed='true'] {
  background: var(--text-primary);
  color: var(--surface-2);
  border-color: var(--text-primary);
}
.filter-chips button:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* --- Monospace for things that are literally codes ------------------------
   Pincodes, lead references and server timestamps. Not for money: at a large
   size a proportional face with tabular figures is more legible, and money is
   already aligned by `font-variant-numeric`. */
.code {
  font-family: var(--font-mono);
  font-size: 0.92em;
  letter-spacing: -0.01em;
  color: var(--text-muted);
}

/* Type-ahead district picker (territory creation).
   A list rather than a <select>: there are ~750 districts in the national
   directory, and a native dropdown that long is unusable on a phone — which is
   where this app is used. Results are capped at eight in the view, so this box
   never grows tall enough to need its own scrollbar on a small screen. */
.typeahead-results:not(:empty) {
  margin-top: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
}
.typeahead-item {
  display: block;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: none;
  color: var(--text-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.typeahead-item:last-child {
  border-bottom: 0;
}
.typeahead-item:hover,
.typeahead-item:focus-visible {
  background: var(--accent-wash);
}
/* The chosen-district chips are buttons that remove themselves, so they need to
   read as pressable rather than as the read-only status chips elsewhere. */
.filter-chips .chip {
  border: 1px solid var(--border);
  cursor: pointer;
}

/* The support-view bar. Fixed and full-bleed on purpose: an operator looking at
   somebody else's account must not be able to scroll the reminder away, and the
   colour is the critical one because acting on the wrong account is the mistake
   it exists to prevent. */
/* **A newer build is available.** Informational, never critical: nothing has
   gone wrong, and a red bar over a screen that is working teaches people to
   ignore red bars. Same geometry as the impersonation bar above it — both are
   the same kind of thing, a standing fact about this session rather than a
   toast — and both can be on screen at once, which stacks correctly because
   each is prepended to the root in turn. */
.stale-build-bar {
  position: sticky;
  top: 0;
  z-index: 59;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 14px;
  background: var(--accent-wash);
  /* `--accent`, not `--accent-ink`: that one is white, for a *solid* accent
     ground. This is the pale wash, so it takes the word colour of the same
     name — the pairing `tests/contrast.test.ts` already measures. */
  color: var(--accent);
  border-bottom: 1px solid var(--accent);
  font-weight: 500;
  font-size: var(--fs-sm);
}

.impersonation-bar {
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 8px 14px;
  background: var(--critical-wash);
  color: var(--critical);
  border-bottom: 1px solid var(--critical);
  font-weight: 500;
  font-size: var(--fs-sm);
}

/* ---------- The kit's building blocks --------------------------------------
   Tones name a semantic, never a colour. Each ground below was measured with
   --accent-ink on it; see the token header for the ratios and for why the
   kit's own lighter blue is not among them. */
/* **Inverted: dark text on a pale tint, not white on a saturated fill.** This
   is the single biggest legibility win on the screen and it was measured rather
   than preferred. White on the kit's blue is 4.51:1 indoors and 2.29:1 under
   bright sun; near-black on the same hue's wash is 17.11:1 and 2.95:1 — better
   at every light level, and by a factor of nearly four indoors.

   It is the same physics that makes dark mode the wrong answer outdoors:
   reflected light is added to both layers, so a dark ground rises toward grey
   while light text stays put, and light-on-dark collapses faster than
   dark-on-light.

   The colour coding survives in the wash and in the icon plate, which keeps the
   saturated tone. A tile is still read as Overdue or Due today at a glance; it
   simply no longer spends its contrast budget on a coloured background. */
.kpi-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 14px 14px 16px;
  border-radius: var(--radius-lg);
  color: var(--text-primary);
  background: var(--accent-wash);
  box-shadow: inset 3px 0 0 var(--tone-accent);
  text-decoration: none;
  min-width: 0;
  min-height: 44px;
}
/* The brand tile: an ink plate with a lime glyph (lime on ink, its one
   permitted ground, 14.2:1) and an ink edge. Emerald would have matched --good
   exactly — see --accent-wash. The status tiles keep white glyphs on their
   own tones. */
.kpi-tile .kpi-ico { background: var(--ink); color: var(--lime); }
.kpi-tile:not(.kpi-good):not(.kpi-serious):not(.kpi-critical):not(.kpi-cold) {
  box-shadow: inset 3px 0 0 var(--ink);
}
.kpi-good { background: var(--good-wash); box-shadow: inset 3px 0 0 var(--good); }
.kpi-good .kpi-ico { background: var(--good); color: var(--accent-ink); }
.kpi-serious { background: var(--serious-wash); box-shadow: inset 3px 0 0 var(--serious); }
.kpi-serious .kpi-ico { background: var(--serious); color: var(--accent-ink); }
.kpi-critical { background: var(--critical-wash); box-shadow: inset 3px 0 0 var(--critical); }
.kpi-critical .kpi-ico { background: var(--critical); color: var(--accent-ink); }
.kpi-cold { background: var(--cold-wash); box-shadow: inset 3px 0 0 var(--cold); }
.kpi-cold .kpi-ico { background: var(--cold); color: var(--accent-ink); }
.kpi-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-bottom: 8px;
  border-radius: 10px;
  /* The plate now carries the saturated tone with a white glyph on it, which is
     where the tile's colour identity moved when the ground went pale. An icon
     is a mark rather than a word, so it can afford a ratio a number cannot. */
  color: var(--accent-ink);
}
.kpi-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-stat);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.kpi-label {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-secondary);
}
.kpi-foot {
  font-weight: 500;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  margin-top: 2px;
}
.kpi-link:hover { filter: brightness(1.06); }
.kpi-link:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* Stays two or three across at 390px — the point of it. `.tiles` is the
   one-card-per-figure grid and collapses to a column; this does not. */
.stat-grid {
  display: grid;
  /* 130px, not 96px: at 390px a card is ~340px wide, and 96px fits three
     columns — which leaves a four-figure grid as three and an orphan. This
     gives two, so four cells read as a block. Money values (₹32L) and labels
     ("Revenue potential") both fit a 170px cell without wrapping. */
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.stat-cell {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 12px 14px;
  min-width: 0;
  /* Hairlines drawn inward, so the grid needs no gap and no outer double edge. */
  box-shadow: inset 1px 0 0 var(--border), inset 0 1px 0 var(--border);
}
/* A figure that opens the rows it counted. Reset to look like its neighbours,
   then underlined on the value so it reads as something to press. */
button.stat-cell {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.stat-link .stat-value {
  text-decoration: underline;
  text-decoration-color: var(--border-strong, var(--text-muted));
  text-underline-offset: 4px;
}
button.stat-cell:hover,
button.stat-cell:focus-visible {
  background: var(--accent-wash);
}
button.stat-cell:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.stat-label {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--text-secondary);
}
.stat-value {
  font-family: var(--font-mono);
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
.stat-foot {
  font-weight: 500;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
/* A daily activity read against its target. Below it is washed and toned so a
   rep sees the shortfall before the form asks them why; at or above it only
   the figure turns green, because a met target is not news. */
.stat-cell.stat-short {
  background: var(--warning-wash);
}
.stat-cell.stat-short .stat-value {
  color: var(--warning-text);
}
.stat-cell.stat-met .stat-value {
  color: var(--good-text);
}
/* The roster's twin of `stat-short`: a figure under that day's target. */
td.target-short {
  color: var(--warning-text);
  font-weight: 650;
}

.list-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 2px;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
.list-row + .list-row { border-top: 1px solid var(--border); }
.list-row-link { cursor: pointer; }
.list-row-link:hover { background: var(--plane); }
.list-row-link:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: -2px;
  border-radius: var(--radius);
}
.lrow-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 11px;
}
.lrow-ico-accent { background: var(--accent-wash); color: var(--accent); }
.lrow-ico-good { background: var(--good-wash); color: var(--good-text); }
.lrow-ico-serious { background: var(--serious-wash); color: var(--serious-text); }
.lrow-ico-warning { background: var(--warning-wash); color: var(--warning-text); }
.lrow-ico-critical { background: var(--critical-wash); color: var(--critical); }
.lrow-ico-cold { background: var(--cold-wash); color: var(--cold); }
.lrow-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1;
}
.lrow-head {
  font-weight: 600;
  font-size: var(--fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lrow-sub {
  font-weight: 500;
  font-size: var(--fs-sm);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.lrow-value {
  font-weight: 640;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  flex: none;
  margin-left: auto;
}

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 18px 0 8px;
}
.section-head h3 {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 650;
}

.donut {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.spark {
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
}

/* Three tiles across, and they stay across at 390px. Stacking them would put
   the one thing on this screen a rep must act on today three screens tall. */
.kpi-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
/* Four tiles are two rows of two on a phone, never three-and-a-lonely-one.
   `repeat(auto-fit, minmax(…))` was the tempting spelling and gives exactly
   that at 390px, where four tiles do not fit across and the fourth drops to a
   row of its own beside two empty cells. */
.kpi-row-4 { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 720px) {
  .kpi-row-4 { grid-template-columns: repeat(4, 1fr); }
}
/* `.scoreblock` is the whole card; this is the same layout for a donut that
   sits under a heading inside one. */
.scoreblock-body {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}
/* Whatever sits beside the ring takes the rest of the row and shrinks rather
   than pushing it off — the `.main` `min-width: 0` rule again, one level in. The
   200px floor is what makes it wrap under the ring on a phone instead of
   squeezing a four-cell grid into 120px. */
.scoreblock-body > .stat-grid {
  flex: 1;
  min-width: 200px;
}

/* An alert carries a sentence and an action, so it is a two-column block —
   icon, then everything else stacked — rather than the three fixed columns of
   a `.list-row`. */
.alert-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 2px;
}
.alert-row + .alert-row {
  border-top: 1px solid var(--border);
}
.alert-row.unread {
  /* A tint rather than a dot: the whole row is the unread thing. */
  background: var(--accent-wash);
  border-radius: var(--radius);
  border-top-color: transparent;
  padding-left: 10px;
  padding-right: 10px;
}
.alert-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  min-width: 0;
  flex: 1;
}
.alert-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 8px;
  width: 100%;
  font-weight: 500;
  font-size: var(--fs-sm);
}
.alert-head .when {
  margin-left: auto;
  color: var(--text-muted);
  font-weight: 500;
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.alert-msg {
  margin: 0;
  color: var(--text-secondary);
  font-weight: 500;
  font-size: var(--fs-sm);
  line-height: 1.45;
}

/* ---------------------------------------------------------------------------
   How to use this app
   ---------------------------------------------------------------------------
   One page per role. The numbered plate is a drawn ring rather than a filled
   circle, because a background colour is the first thing a browser drops when
   "background graphics" is off — and a plate that vanishes takes the sequence
   with it, which is the whole structure of the page.
*/
.help-steps {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* The location notice. A plain list rather than the numbered plates above it:
   those are a sequence somebody follows, and this is five separate facts in no
   order. It prints with the rest of the page, which is the point — a notice
   about what is collected is exactly the thing somebody wants on paper. */
.help-notice {
  margin: 14px 0 0;
  padding-left: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  line-height: 1.55;
}

.help-step {
  display: flex;
  gap: 14px;
  align-items: flex-start;
}

.help-plate {
  flex: 0 0 auto;
  color: var(--accent);
}

/* The `.main` rule again: without it a long step body pushes the row past the
   viewport instead of wrapping inside it. */
.help-body {
  min-width: 0;
}

.help-body p {
  margin: 4px 0 0;
  color: var(--text-secondary);
  line-height: 1.5;
}

.help-step-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
}

/* The screen the step happens on, named by its nav label so a reader can match
   it against the sidebar. */
.help-where {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--accent);
  background: var(--accent-wash);
  border-radius: 999px;
  padding: 2px 9px;
}

/* ---------------------------------------------------------------------------
   Print
   ---------------------------------------------------------------------------
   The client asked for a printable handout "later". Written now because it is
   four rules while the markup is fresh, and retrofitting it means rediscovering
   which parts of the chrome are chrome.

   Everything that is navigation, action or state goes: it is meaningless on
   paper and it is what makes a printed app screen look like a mistake. What is
   left is the sheet.
*/
@media print {
  .sidebar,
  .topbar,
  .tabbar,
  .drawer,
  .scrim,
  .toast-stack,
  .modal-backdrop,
  .btn-row,
  .banner {
    display: none !important;
  }

  body,
  .main,
  .card {
    background: #fff !important;
    box-shadow: none !important;
    margin: 0 !important;
    padding: 0 !important;
    color: #000;
  }

  .help-sheet {
    border: 0 !important;
  }

  /* A step split across a page break reads as two half-instructions. */
  .help-step {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  .help-where {
    background: transparent !important;
    border: 1px solid #666;
    color: #000 !important;
  }

  .help-print-note {
    display: none;
  }
}

/* ---------------------------------------------------------------------------
   The read-only product catalogue
   ---------------------------------------------------------------------------
   The lead row's asymmetry, applied to a product: the left half is a name and
   truncates cleanly, the right half is a price and never does. Without
   `min-width: 0` the name pushes the price off a 390px screen instead of
   clipping itself — the `.main` rule again.
*/
.product-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 10px 0;
  border-top: 1px solid var(--line);
}

.product-row:first-of-type {
  border-top: 0;
}

.product-main {
  min-width: 0;
  flex: 1 1 auto;
}

.product-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.product-price {
  flex: 0 0 auto;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ===========================================================================
   The Dashboard, action first
   The client's reference designs: a slim strip of figures, a panel of big
   coloured cards that are each one real firm with its own buttons, and a
   column of the day's other work. Scoped under `.dash` so no other screen
   moves. Every ground/text pair below is one `tests/contrast.test.ts` already
   measured — the washes with their own -text colour, and white on the ink.
   =========================================================================== */
.dash .card + .card,
.dash .dash-grid + .card {
  margin-top: 16px;
}

/* The client's revenue note. Small and quiet: it qualifies a figure, it is not
   the figure, and on a phone it must wrap rather than widen the card. */
.revenue-note {
  margin: 8px 0 0;
  line-height: 1.45;
}
/* Under the row, not in it — see `headlineStrip`. */
.dash-strip-note {
  margin: -4px 0 16px;
}
.dash-strip-note > .revenue-note {
  margin: 0;
}

.dash-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}
/* The other figures: a wrapping row whose tiles take their width from `--strip-basis`, which
   `balanceStrip` sets from how many fit across — so every line is full and the last line's tiles
   stretch (flex-grow) rather than leaving a gap. Before it first runs, or without it, the basis is
   the minimum and the row wraps as a plain one. `--strip-min` is read by the script as well, so the
   figure is written once. */
.dash-strip-rows {
  display: flex;
  flex-wrap: wrap;
  --strip-min: 150px;
}
.dash-strip-rows > .dash-stat {
  flex: 1 1 var(--strip-basis, var(--strip-min));
  min-width: 0;
}
/* Exactly three across at every width, so the day's three targets never wrap
   onto a second line. The tiles shrink their padding and type on a phone so a
   "WhatsApp today" label and a "target met" chip still fit a ~110px column. */
.dash-strip-three {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 560px) {
  .dash-strip-three {
    gap: 8px;
  }
  .dash-strip-three .dash-stat {
    padding: 10px 10px;
  }
  .dash-strip-three .dash-stat-label {
    font-size: var(--fs-xs);
    font-weight: 500;
  }
  .dash-strip-three .dash-stat-value {
    font-size: var(--fs-lg);
  }
  .dash-strip-three .dash-chip {
    padding: 2px 8px;
    font-size: var(--fs-xs);
    font-weight: 600;
  }
}
.dash-stat {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px;
  background: var(--surface-1);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
/* Every figure opens its detail, so the tile is the link. */
.dash-stat-link {
  color: inherit;
  text-decoration: none;
}
.dash-stat-link:hover {
  box-shadow: var(--shadow-md, var(--shadow-sm));
  outline: 1px solid var(--border);
}
.dash-stat-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.dash-stat-label {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.dash-stat-value {
  font-size: var(--fs-xl);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.dash-chip {
  align-self: flex-start;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
  font-weight: 600;
}
.dash-chip-met {
  background: var(--good-wash);
  color: var(--good-text);
}
.dash-chip-short {
  background: var(--serious-wash);
  color: var(--serious-text);
}
.dash-chip-plain {
  background: var(--accent-wash);
  color: var(--accent);
}

/* Act now beside Today's list on a desk; one column on a phone. */
.dash-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}
@media (min-width: 1100px) {
  .dash-grid {
    grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
    /* **Equal height, not top-aligned.** Act now and the column beside it are never the same
       height, and a top-aligned grid leaves a hole under the shorter one — the client's third
       "wastage" screenshot. Stretching makes the two cards one block; what is left over is
       absorbed inside Act now's cards (below) and the team card, where it is padding and not a
       gap in the page. */
    align-items: stretch;
  }
}
.dash .dash-grid > .card {
  margin-top: 0;
}
/* Today's list and the team, one under the other in the column beside Act now. */
.dash-stack {
  display: grid;
  gap: 16px;
  /* The list is as tall as its rows; the team card takes whatever height the row gives. */
  grid-template-rows: auto 1fr;
  min-width: 0;
}
/* Act now's cards grow to fill the card they sit in, buttons pinned to the bottom of each
   (`.act-actions` already has `margin-top: auto`), so a taller neighbour makes taller cards
   rather than a blank band. */
.dash-act {
  display: flex;
  flex-direction: column;
}
.dash-act .act-grid {
  flex: 1;
  grid-auto-rows: 1fr;
}
.dash .dash-stack > .card {
  margin-top: 0;
}
/* The side column is narrow: one compact row per person — dot, name, and the issue on the right —
   so six people are six short rows and the column is no taller than the cards it sits beside. */
.dash-stack .team-strip {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
}
.dash-stack .team-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 44px;
  padding: 6px 12px;
}
.dash-stack .team-dot {
  flex: none;
}
.dash-stack .team-name {
  flex: none;
}
.dash-stack .team-sub {
  flex: 1;
  min-width: 0;
  text-align: right;
  overflow-wrap: anywhere;
}
/* One block with something in it takes the whole width — an empty one steps
   aside rather than holding half the screen open to say nothing. */
@media (min-width: 1100px) {
  .dash-grid.dash-grid-one {
    grid-template-columns: minmax(0, 1fr);
  }
}
.dash-caught-up {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  color: var(--text-secondary);
  font-weight: 500;
}
.dash-caught-up .lrow-ico {
  width: 28px;
  height: 28px;
}
/* A full-width Today's list: its rows in columns rather than one long strip.
   Each row keeps its own rule, since a row's neighbour is now beside it too. */
@media (min-width: 900px) {
  .dash-panel-wide .dash-rows {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    column-gap: 24px;
  }
  .dash-panel-wide .dash-rows > .list-row {
    border-top: 1px solid var(--border);
  }
}
/* The side column is narrow; a row there wraps rather than cutting off the
   thing it asks somebody to do. */
.dash-panel .lrow-head,
.dash-panel .lrow-sub {
  white-space: normal;
}

.act-grid {
  display: grid;
  /* auto-fit, not auto-fill: one card is the full width and two are half each. auto-fill kept a
     track for every card that *could* fit, so a lone card sat in a corner of a wide white box. */
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}
.act-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  padding: 14px 16px;
  border-radius: var(--radius-lg);
  cursor: pointer;
  background: var(--accent-wash);
  color: var(--text-primary);
}
.act-card:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
.act-tag {
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--accent);
}
.act-firm {
  font-size: var(--fs-lg);
  font-weight: 700;
  overflow-wrap: anywhere;
}
.act-sub,
.act-value {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.act-value {
  font-weight: 600;
  color: var(--text-primary);
}
.act-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: auto;
  padding-top: 8px;
}
.act-actions .call-link {
  margin: 0;
  min-width: 44px;
  justify-content: center;
  background: var(--surface-1);
  border-radius: var(--radius-pill);
}
.act-actions .btn {
  background: var(--surface-1);
  /* A link drawn as a button: the underline belongs to the text-link style, not here. */
  text-decoration: none;
}
.act-overdue {
  background: var(--serious-wash);
}
.act-overdue .act-tag {
  color: var(--serious-text);
}
.act-first {
  background: var(--warning-wash);
}
.act-first .act-tag {
  color: var(--warning-text);
}
/* A cheque due: the palette's "serious" — money with a date on it, not a wrong. */
.act-cheque {
  background: var(--serious-wash);
}
.act-cheque .act-tag {
  color: var(--serious-text);
}
.act-confirm {
  background: var(--text-primary);
  color: var(--accent-ink);
}
.act-confirm .act-tag,
.act-confirm .act-sub,
.act-confirm .act-value {
  color: var(--accent-ink);
}
.act-confirm .btn {
  color: var(--text-primary);
}

.dash-more {
  margin: 12px 0 0;
  font-weight: 600;
}
.dash-footer {
  margin: 16px 0;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
}

.team-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* ---------- The map tab (Dashboard) ------------------------------------------
   Sits where the feed sits, the same height, so the card does not change size when a tab is
   chosen. Leaflet's own stylesheet does the map; this does the pane around it. */
.field-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 8px;
}
.field-tab {
  padding: 6px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-1);
  color: var(--text-secondary);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.field-tab.selected {
  border-color: var(--accent);
  background: var(--accent-wash);
  color: var(--accent);
}
.vmap {
  /* With the tabs, legend and caption this pane is about as tall as a four-person list beside it. */
  height: 360px;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  font: inherit;
}
@media (max-width: 899px) {
  .vmap {
    height: 320px;
  }
}
.vmap .leaflet-container,
.vmap.leaflet-container {
  font: inherit;
}
.vmap-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin-top: 8px;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.vmap-key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.vmap-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.vmap-caption {
  margin: 6px 0 0;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.vmap-pop {
  display: grid;
  gap: 4px;
  min-width: 150px;
  font-size: var(--fs-sm);
  font-weight: 500;
}
.vmap-pop .btn {
  margin-top: 4px;
}

/* ---------- What changed since yesterday (Dashboard) ------------------------
   Up to four lists side by side — won, moved up, slipped, lost — drawn only when they have
   something in them and filling the row, so two lists are two wide columns and not two narrow
   ones beside a blank. A coloured rule on top says which; the words say it too. */
.dash-changes {
  margin-bottom: 16px;
}
.chg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  margin-top: 8px;
}
.chg-col {
  min-width: 0;
  padding-top: 10px;
  border-top: 3px solid var(--cold);
}
.chg-won {
  border-top-color: var(--good);
}
.chg-up {
  border-top-color: var(--accent);
}
.chg-slipped {
  border-top-color: var(--warning);
}
.chg-lost {
  border-top-color: var(--serious);
}
.chg-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.chg-ico {
  display: inline-flex;
  color: var(--text-secondary);
}
.chg-count {
  margin-left: auto;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-secondary);
}
.chg-rows {
  display: grid;
  gap: 2px;
}
.chg-row {
  display: grid;
  gap: 2px;
  width: 100%;
  padding: 8px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-primary);
  text-align: left;
  font: inherit;
  cursor: pointer;
}
.chg-row:hover {
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border);
}
.chg-firm {
  font-weight: 600;
  overflow-wrap: anywhere;
}
.chg-meta,
.chg-reason {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.chg-reason {
  font-style: italic;
}
.chg-more {
  display: inline-block;
  margin-top: 6px;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.chg-none {
  margin: 8px 0 0;
  color: var(--text-secondary);
  font-weight: 500;
}

/* ---------- In the field today (Dashboard) --------------------------------
   Two columns on a desktop — a row per person, the newest contacts beside them — and
   one on a phone, people first. Status is a dot plus words, never colour alone. */
.field-live {
  margin-bottom: 16px;
}
.field-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
}
.field-head h2 {
  margin: 0;
}
.field-flag {
  color: var(--serious-text);
  font-weight: 600;
}
.live {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  background: var(--good-wash);
  color: var(--good-text);
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
}
.live-stale {
  background: var(--cold-wash);
  color: var(--text-secondary);
}
.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--good);
  animation: live-pulse 2s ease-in-out infinite;
}
.live-stale .live-dot {
  background: var(--cold);
  animation: none;
}
@keyframes live-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.45;
    transform: scale(0.8);
  }
}
@media (prefers-reduced-motion: reduce) {
  .live-dot {
    animation: none;
  }
}
.field-grid {
  display: grid;
  gap: 16px;
  margin-top: 12px;
}
@media (min-width: 900px) {
  .field-grid {
    grid-template-columns: minmax(280px, 0.9fr) minmax(0, 1.4fr);
  }
}
.field-people {
  display: grid;
  gap: 8px;
  align-content: start;
  /* Rows keep their own height when the list is capped and scrolls — a grid otherwise
     squeezes its tracks to fit, and a row's third line is cut off. */
  grid-auto-rows: max-content;
}
.field-scroll {
  /* About five people; the sixth is a scroll away. */
  max-height: 440px;
  overflow-y: auto;
  padding-right: 4px;
}
@media (max-width: 899px) {
  .field-scroll {
    max-height: 440px;
  }
}
.fr {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 10px;
  row-gap: 2px;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  color: var(--text-primary);
  text-align: left;
  font: inherit;
  cursor: pointer;
}
.fr:hover,
.fr.selected {
  border-color: var(--accent);
}
.fr.selected {
  background: var(--accent-wash);
}
.fr-dot {
  grid-row: span 2;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--cold);
}
.fr-active .fr-dot {
  background: var(--good);
}
.fr-quiet {
  border-color: var(--serious);
}
.fr-quiet .fr-dot {
  background: var(--serious);
}
.fr-name {
  font-weight: 600;
}
.fr-counts {
  grid-column: 2;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.fr-line {
  grid-column: 2;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.fr-quiet .fr-line {
  color: var(--serious-text);
  font-weight: 600;
}
.field-feed {
  min-width: 0;
}
.ff-list {
  display: grid;
  gap: 2px;
  grid-auto-rows: max-content;
  /* A busy day carries forty contacts; the card should not become the page. */
  max-height: 440px;
  overflow-y: auto;
  padding-right: 4px;
}
@media (max-width: 899px) {
  .ff-list {
    max-height: 420px;
  }
}
.ff {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: start;
  width: 100%;
  padding: 10px 8px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-primary);
  text-align: left;
  font: inherit;
  cursor: pointer;
}
.ff:hover {
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--border);
}
.ff-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--cold-wash);
  color: var(--cold);
}
.ff-good .ff-ico {
  background: var(--good-wash);
  color: var(--good-text);
}
.ff-body {
  display: grid;
  gap: 4px;
  min-width: 0;
}
.ff-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
.ff-note {
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.ff-time {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
  white-space: nowrap;
}
.ff.fresh {
  animation: ff-arrive 8s ease-out;
}
@keyframes ff-arrive {
  0% {
    background: var(--accent-wash);
    box-shadow: 0 0 0 1px var(--accent);
  }
  100% {
    background: transparent;
    box-shadow: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .ff.fresh {
    animation: none;
    background: var(--accent-wash);
  }
}
.field-narrow {
  margin-bottom: 8px;
  padding: 6px 12px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  background: var(--accent-wash);
  color: var(--accent);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.field-empty,
.field-foot {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.field-foot {
  margin: 12px 0 0;
}
.team-chip {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 8px;
  align-items: center;
  min-height: 44px;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  color: var(--text-primary);
  text-decoration: none;
}
.team-chip:hover {
  border-color: var(--accent);
}
.team-dot {
  grid-row: span 2;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--good);
}
.team-critical .team-dot {
  background: var(--critical);
}
.team-serious .team-dot {
  background: var(--serious);
}
.team-cold .team-dot {
  background: var(--cold);
}
.team-name {
  font-weight: 600;
}
.team-sub {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-secondary);
}
