/* =========================================================================
   Wizebix CRM - mobile-first design system
   Built thumb-first: bottom navigation, 44px+ tap targets, safe-area aware.
   ========================================================================= */

:root {
  --brand: #4f46e5;
  --brand-strong: #4338ca;
  --brand-soft: #eef2ff;
  --brand-contrast: #ffffff;

  --bg: #f4f5fb;
  --surface: #ffffff;
  --surface-2: #fafbff;
  --surface-sunken: #f1f3f9;
  --text: #0f172a;
  --text-muted: #64748b;
  --text-faint: #94a3b8;
  --border: #e5e8f0;
  --border-strong: #d3d8e6;

  --success: #16a34a;
  --success-soft: #dcfce7;
  --warning: #d97706;
  --warning-soft: #fef3c7;
  --danger: #dc2626;
  --danger-soft: #fee2e2;
  --info: #0284c7;
  --info-soft: #e0f2fe;

  --s-new: #64748b;
  --s-contacted: #0ea5e9;
  --s-qualified: #8b5cf6;
  --s-proposal: #f59e0b;
  --s-negotiation: #f97316;
  --s-won: #16a34a;
  --s-lost: #ef4444;

  --radius-sm: 11px;
  --radius: 16px;
  --radius-lg: 22px;
  /* Surfaces are separated by a hairline border, not a drop shadow. Shadow is
     kept for the few things that genuinely float: the sheet, the FAB, the one
     primary button on a screen. Everything else sitting on its own soft cloud
     is what makes an interface look a decade old. */
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
  --shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
  --shadow-lg: 0 16px 40px rgba(15, 23, 42, 0.14);
  --shadow-brand: 0 2px 10px color-mix(in srgb, var(--brand) 30%, transparent);

  --nav-h: 70px;
  /* The bar floats clear of the screen edge, so anything positioned above it
     has to clear the gap and the home indicator as well as the bar itself.
     --nav-space is that total; nothing should add the three up by hand. */
  --nav-inset: 10px;
  --nav-space: calc(var(--nav-h) + var(--nav-inset) + var(--safe-b));
  --header-h: 58px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-num: ui-rounded, 'SF Pro Rounded', var(--font);
  /* A high-contrast serif for the sign-in screen's two headings, and nowhere
     else. Built from faces the machine already has rather than a webfont: this
     is a PWA that has to open on a phone with no signal, and a sign-in screen
     waiting on a font request is a sign-in screen that looks broken. */
  --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Baskerville,
                  'Playfair Display', Georgia, 'Times New Roman', serif;

  /* Type scale.
     This grew to 22 different sizes and 11 different weights, which is what
     "13.5px semibold-620" next to "14px semibold-650" looks like once there are
     forty screens of it: not a design, just forty separate decisions. These are
     the sizes and weights the app has. Anything not on this list is a bug.
     The steps are close together on purpose - it is a dense data app, and body
     text, an emphasised value and a card title genuinely do different jobs. */
  --fs-3xs: 10px;   /* badge digits, the nav label on a 320px phone */
  --fs-2xs: 11px;   /* uppercase micro-labels, pills, timestamps */
  --fs-xs: 12px;    /* captions, meta lines, help text */
  --fs-sm: 13px;    /* secondary body: chips, table cells, small buttons */
  --fs-md: 14px;    /* body */
  --fs-lg: 15px;    /* emphasised body, names, list values */
  --fs-xl: 17px;    /* card and sheet titles */
  --fs-2xl: 19px;   /* page title */
  --fs-3xl: 24px;   /* KPI figures */
  --fs-4xl: 30px;   /* the one hero number on a lead */
  /* Two display sizes, and they belong to the sign-in screen alone.
     Nothing inside the app is allowed to shout - it is a dense data tool and a
     42px heading over a lead would be absurd. The door is a different room:
     one page, seen once a day, where the only jobs are saying which app this
     is and giving somebody something to look at while they type. */
  --fs-display: 42px;   /* "Welcome Back" */
  --fs-display-lg: 64px; /* the quote on the artwork */
  /* Not a scale step: anything below 16px makes iOS Safari zoom the page when
     the field takes focus, and it never zooms back out. */
  --fs-input: 16px;

  /* Four weights. 500/550/560 all meant "slightly bolder than body", and
     580/600/620/640/650/680 all meant "a heading" - they are one each now. */
  --fw-body: 400;
  --fw-medium: 530;
  --fw-semibold: 620;
  --fw-bold: 700;
}

/**
 * Dark: neutral, not navy.
 *
 * These were the slate family - #0b1020, #94a3b8 - which carry a blue cast that
 * reads as a colour choice rather than as an absence of light. Every grey here
 * is a true neutral: red, green and blue within a point or two of each other,
 * so nothing tints. The accents that remain coloured are the ones carrying
 * meaning - brand, won, overdue, and a person's avatar hue.
 */
/* Native controls follow the app's theme, not the operating system's.
 *
 * A checkbox, a date picker and a scrollbar are drawn by the browser, and with
 * no color-scheme declared it draws them for the OS preference - so someone on
 * a dark Mac reading the light theme got black checkboxes on a white table.
 * The app has its own switch; this is how the browser is told about it. */
:root { color-scheme: light; }

:root[data-theme='dark'] {
  color-scheme: dark;
  --brand: #818cf8;
  --brand-strong: #a5b4fc;
  --brand-soft: #1f1f27;
  --brand-contrast: #0a0a0b;

  --bg: #0a0a0b;
  --surface: #171718;
  --surface-2: #1f1f21;
  --surface-sunken: #101011;
  --text: #ededee;
  --text-muted: #a1a1a4;
  --text-faint: #6e6e72;
  --border: #2a2a2c;
  --border-strong: #3b3b3e;

  --success-soft: #052e1a;
  --warning-soft: #3b2606;
  --danger-soft: #3f1212;
  --info-soft: #062b41;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.6);
}

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

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-lg);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}

body { overflow-x: hidden; }

button, input, select, textarea { font: inherit; color: inherit; }
a { color: var(--brand); text-decoration: none; }
h1, h2, h3, h4 { margin: 0; font-weight: var(--fw-semibold); letter-spacing: -0.015em; }
p { margin: 0; }

.icon { width: 20px; height: 20px; flex: none; stroke-width: 1.9; }

/* Visible to screen readers, invisible on screen. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.icon-sm { width: 16px; height: 16px; }
.icon-lg { width: 24px; height: 24px; }

/* ------------------------------- app shell ------------------------------ */

#app { min-height: 100%; display: flex; flex-direction: column; }

/* Base layout at every width - the desktop block below only adds the sidebar.
   Without this the shell cannot constrain a full-height child on mobile. */
.shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.app-header {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 10px;
  min-height: var(--header-h);
  padding: calc(var(--safe-t) + 8px) 12px 8px;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--border);
}
/* A lead called "Orchid Event Managers" wrapped the title onto a second line
   and grew the header by 20px on every screen it was open. One line, truncated;
   the full name is on the card underneath. */
.app-header__title { flex: 1; min-width: 0; }
.app-header__title h1 {
  font-size: var(--fs-2xl); line-height: 1.25; letter-spacing: -0.02em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-header__title .sub {
  font-size: var(--fs-xs); color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-header__actions { display: flex; align-items: center; gap: 5px; }

/* Header actions get the same soft rounded container as the nav pills. Bare
   glyphs floating at the top of the screen with filled pills at the bottom read
   as two different apps stitched together; a resting surface also says these
   are buttons rather than decoration. Squircle rather than circle, to match. */
.app-header .btn--icon {
  border-radius: 12px;
  background: var(--surface-sunken);
  color: var(--text-muted);
  transition: background 0.18s ease, color 0.18s ease, transform 0.15s ease;
}
.app-header .btn--icon:hover {
  background: color-mix(in srgb, var(--brand) 13%, transparent);
  color: var(--brand);
}
.app-header .btn--icon:active { transform: scale(0.92); }
:root[data-theme='dark'] .app-header .btn--icon:hover {
  background: color-mix(in srgb, var(--brand) 24%, transparent);
}

/* The avatar is the same size and shape family, so it sits in the row rather
   than beside it. */
.app-header__profile .avatar { border-radius: 12px; }

/* A lead detail carries back, edit, delete, the bell and the avatar. On a
   375px phone that is more than half the bar before the title gets a pixel, so
   the controls tighten up and give the name the room instead. */
@media (max-width: 480px) {
  .app-header { gap: 6px; }
  .app-header .btn--icon { min-height: 38px; width: 38px; }
  .app-header__title h1 { font-size: var(--fs-xl); }
}

/* Header actions the tab bar already provides. 899px is the sidebar
   breakpoint, so this hides exactly when the centre Add button is on screen. */
@media (max-width: 899px) {
  .app-header__actions .is-desktop-only { display: none; }
}

/* The bell and the chat button belong to the app rather than to a screen, so
   they sit outside #header-actions and never change as you navigate. One rule
   for both: they are the same object - a header button that can be carrying a
   number - and the badge below is positioned against it. */
.app-header__bell, .app-header__chat { flex: none; position: relative; }
/* Where you already are, said the way the avatar beside it says it. */
.app-header__chat[aria-current='page'] { color: var(--brand); background: var(--brand-soft); }
.app-header__badge {
  position: absolute; top: 2px; right: 2px;
  min-width: 17px; height: 17px; padding: 0 4px;
  display: grid; place-items: center;
  border-radius: 9px; border: 2px solid var(--surface-sunken);
  background: var(--danger); color: #fff;
  font-size: var(--fs-3xs); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  line-height: 1; pointer-events: none;
}
.app-header__badge[hidden] { display: none; }

/* "Me" lives here rather than in the tab bar - always reachable, and it doubles
   as a reminder of which account you are signed in as. */
.app-header__profile {
  flex: none; padding: 0; margin-left: 2px;
  background: none; border: 0; border-radius: 50%; cursor: pointer;
}
.app-header__profile .avatar { transition: transform 0.15s, box-shadow 0.15s; }
.app-header__profile:active .avatar { transform: scale(0.92); }
.app-header__profile[aria-current='page'] .avatar {
  background: var(--brand); color: var(--brand-contrast);
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--brand);
}
/* The circle a person is drawn as.
 *
 * The base has to come before its own modifiers. .avatar, .avatar--sm and
 * .avatar--xs all weigh exactly the same, so whichever is written last wins -
 * and this rule spent a long time four hundred lines further down, in the
 * middle of the leaderboard section, quietly overriding both. Every "small"
 * avatar in the app was rendering at the full 38px: the 26px spacer that keeps
 * board cards the same height matched nothing, and the 30px face row on the
 * dashboard was being overflowed by the faces in it. */
.avatar {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: 50%;
  background: var(--brand-soft); color: var(--brand);
  font-size: var(--fs-sm); font-weight: var(--fw-bold);
}

/* Avatar initials are sized to their circle rather than to the type scale:
   they are a graphic, and 11px in a 32px disc looks like a mistake. */
.avatar--lg { width: 60px; height: 60px; font-size: 21px; }
.avatar--sm { width: 32px; height: 32px; font-size: 12px; }
.avatar--xs { width: 26px; height: 26px; font-size: var(--fs-3xs); }

/* One hue per person, picked from their name. The tint carries the colour and
   the initials repeat it, so it reads as somebody rather than as a status. */
.avatar { --avatar-hue: var(--brand); }
.avatar--t0 { --avatar-hue: #6366f1; }
.avatar--t1 { --avatar-hue: #0ea5e9; }
.avatar--t2 { --avatar-hue: #8b5cf6; }
.avatar--t3 { --avatar-hue: #d97706; }
.avatar--t4 { --avatar-hue: #0d9488; }
.avatar--t5 { --avatar-hue: #e11d48; }
.avatar[class*='avatar--t'] {
  background: color-mix(in srgb, var(--avatar-hue) 15%, transparent);
  color: var(--avatar-hue);
}
:root[data-theme='dark'] .avatar[class*='avatar--t'] {
  background: color-mix(in srgb, var(--avatar-hue) 26%, transparent);
  color: color-mix(in srgb, var(--avatar-hue) 60%, #fff);
}

.main {
  flex: 1;
  padding: 14px 14px calc(var(--nav-space) + 84px);
  max-width: 1180px; width: 100%; margin: 0 auto;
}

/* --------------------------- bottom navigation -------------------------- */

/* A bar welded to the bottom edge is the default every app shipped in 2016.
   Floating it clear of the edge gives it a shape of its own, lets the page
   scroll visibly underneath the blur, and reads as a control rather than as
   the end of the screen. */
.tabbar {
  position: fixed; z-index: 50;
  left: var(--nav-inset); right: var(--nav-inset);
  bottom: calc(var(--safe-b) + var(--nav-inset));
  display: flex; align-items: stretch;
  height: var(--nav-h); padding: 0 4px;
  border-radius: 20px;
  border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  backdrop-filter: saturate(190%) blur(20px);
  -webkit-backdrop-filter: saturate(190%) blur(20px);
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.13), 0 2px 6px rgba(15, 23, 42, 0.05);
}
/* A one-pixel highlight along the top edge. It is what stops a translucent
   panel looking like a flat grey rectangle laid over the page. */
.tabbar::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(to bottom, color-mix(in srgb, #fff 55%, transparent), transparent 42%);
}
:root[data-theme='dark'] .tabbar {
  background: color-mix(in srgb, var(--surface) 86%, transparent);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.02);
}
:root[data-theme='dark'] .tabbar::before {
  background: linear-gradient(to bottom, color-mix(in srgb, #fff 9%, transparent), transparent 42%);
}

.tabbar__item {
  position: relative; z-index: 1;
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0; background: none; border: 0; cursor: pointer;
  color: var(--text-muted);
  -webkit-tap-highlight-color: transparent;
  transition: color 0.2s ease;
}

/* The selected tab is a filled block behind its icon and its name, not a faint
   tint behind the icon alone. Five grey icons spread across a wide bar with one
   of them slightly bluer is a lot of nothing; a solid shape says where you are
   from across a desk.
   Deliberately the same width as every other tab. An expanding pill that
   reveals the label on selection is the fashionable version, but "Accounts"
   needs 117px of pill and a 375px phone with five slots has 98px to give -
   the label came out clipped. This keeps every label legible at every width. */
.tabbar__pill {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; width: 100%; max-width: 66px; padding: 7px 3px 6px;
  border-radius: 15px; background: transparent;
  transition: background 0.26s ease, transform 0.2s ease;
}
.tabbar__pill .icon { flex: none; }
.tabbar__item:active .tabbar__pill { transform: scale(0.92); }

.tabbar__label {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); letter-spacing: 0.005em;
}

/* Tinted, not filled. A solid brand block sitting next to the solid brand Add
   button gave the bar two identical lumps of indigo an inch apart, and neither
   one said which was the button. The selected tab is a state and the Add button
   is an action, so only one of them is allowed to be solid: the tab gets the
   colour at a fraction of its strength, and keeps the shape and the weight. */
.tabbar__item[aria-current='page'] { color: var(--brand); }
.tabbar__item[aria-current='page'] .tabbar__pill {
  background: color-mix(in srgb, var(--brand) 13%, transparent);
}
.tabbar__item[aria-current='page'] .tabbar__label { font-weight: var(--fw-semibold); }
.tabbar__item[aria-current='page'] .icon { stroke-width: 2.4; }
/* The bar is dark and near-transparent here, so the same 13% barely registers. */
:root[data-theme='dark'] .tabbar__item[aria-current='page'] .tabbar__pill {
  background: color-mix(in srgb, var(--brand) 24%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .tabbar__item, .tabbar__pill { transition: none; }
}

/* The centre "Add" tab carries no label on purpose. Its button is taller than
   an icon, so a label underneath sat lower than every other one - and a plus in
   a filled circle needs no caption. The text stays for screen readers. */
.tabbar__item--primary { flex: 0 0 62px; justify-content: center; }
.tabbar__plus {
  display: grid; place-items: center;
  width: 46px; height: 46px;
  /* No lift. It used to need one to line up with the icons either side, which
     sat above their labels; now that a tab is a block containing both, the
     circle lines up with those blocks by simply being centred. */
  margin-bottom: 0;
  border-radius: 50%; color: var(--brand-contrast);
  background: linear-gradient(150deg, var(--brand), var(--brand-strong));
  /* Three layers doing three jobs: a coloured glow that lifts it off the bar,
     a tight dark shadow that gives it an edge, and an inner highlight along
     the top so it reads as a raised object rather than a flat disc. */
  box-shadow:
    0 6px 18px color-mix(in srgb, var(--brand) 45%, transparent),
    0 1px 3px rgba(15, 23, 42, 0.22),
    inset 0 1px 0 color-mix(in srgb, #fff 30%, transparent);
  transition: transform 0.22s cubic-bezier(0.34, 1.5, 0.64, 1), box-shadow 0.2s ease;
}
.tabbar__plus .icon { stroke-width: 2.4; }
.tabbar__item--primary:hover .tabbar__plus { transform: translateY(-1px); }
.tabbar__item--primary:active .tabbar__plus {
  transform: scale(0.9);
  box-shadow: 0 2px 8px color-mix(in srgb, var(--brand) 40%, transparent), 0 1px 2px rgba(15, 23, 42, 0.2);
}

/* A count on the More button.
 *
 * Chat lives behind More on a phone, so without this an unread message is
 * invisible until somebody happens to open the drawer. Positioned against the
 * pill rather than the button, so it sits on the icon and not out in the
 * padding. */
.tabbar__badge {
  position: absolute; top: -2px; right: 50%; margin-right: -18px;
  min-width: 15px; height: 15px; padding: 0 3px;
  display: grid; place-items: center;
  border-radius: 8px; border: 2px solid var(--surface);
  background: var(--danger); color: #fff;
  font-size: var(--fs-3xs); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  line-height: 1; pointer-events: none;
}
.tabbar__badge[hidden] { display: none; }

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

/*
 * Everywhere the thumb bar could not fit.
 *
 * The bar holds three destinations and the Add button; the app has eleven
 * places to go. The other seven used to be sidebar entries, which on a phone
 * means they did not exist at all - you could not reach Chat, or Rewards, or
 * your own account, without a tablet. This is where they live.
 *
 * A sheet rather than a menu, because it has to be dismissible by tapping away
 * from it: a phone has no Escape key within reach, and the scrim is what makes
 * "tap anywhere else" a thing you can do.
 */
.moresheet { position: fixed; inset: 0; z-index: 60; }
.moresheet[hidden] { display: none; }

.moresheet__scrim {
  position: absolute; inset: 0;
  background: color-mix(in srgb, #0b1020 52%, transparent);
  opacity: 0; transition: opacity 0.24s ease;
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
body.more-open .moresheet__scrim { opacity: 1; }

/* Sits directly on top of the bar and shares its shape, so the two read as one
   object opening rather than a card that happened to appear above it. */
.moresheet__panel {
  position: absolute; left: var(--nav-inset); right: var(--nav-inset);
  bottom: calc(var(--nav-space) + 8px);
  padding: 16px 12px;
  border-radius: 22px;
  border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(190%) blur(22px);
  -webkit-backdrop-filter: saturate(190%) blur(22px);
  box-shadow: 0 18px 44px rgba(15, 23, 42, 0.22), 0 2px 6px rgba(15, 23, 42, 0.06);
  transform: translateY(14px) scale(0.97);
  transform-origin: bottom center;
  opacity: 0;
  transition: transform 0.26s cubic-bezier(0.2, 0.9, 0.25, 1), opacity 0.2s ease;
}
body.more-open .moresheet__panel { transform: translateY(0) scale(1); opacity: 1; }

:root[data-theme='dark'] .moresheet__panel {
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.03);
}

.moresheet__grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
}
@media (max-width: 359px) { .moresheet__grid { grid-template-columns: repeat(3, 1fr); } }

/* Each tile arrives a beat after the one before it. Small enough that nobody
   reads it as an animation - it is what makes the drawer feel like it opened
   rather than cut. */
.moretile {
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 12px 4px 10px;
  border: 0; border-radius: 16px; background: none; cursor: pointer;
  color: var(--text); font-family: inherit;
  -webkit-tap-highlight-color: transparent;
  opacity: 0; transform: translateY(8px);
  transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.2, 0.9, 0.25, 1),
              background 0.15s ease;
  /* Capped, or twelve tiles means the last one does not begin until 240ms and
     the drawer feels slow to a person who only wanted Sign out. Eight steps of
     16ms is a ripple; the rest arrive with the eighth. */
  transition-delay: calc(min(var(--i), 8) * 16ms);
}
body.more-open .moretile { opacity: 1; transform: translateY(0); }
.moretile:hover { background: var(--surface-sunken); }
.moretile:active { transform: scale(0.95); }

.moretile__icon {
  position: relative;
  display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: 15px;
  background: var(--surface-sunken);
  color: var(--text-muted);
}
.moretile[aria-current='page'] .moretile__icon { background: var(--brand); color: var(--brand-contrast); }
.moretile[aria-current='page'] .moretile__label { color: var(--brand); font-weight: var(--fw-semibold); }

.moretile__label {
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  color: var(--text-muted); text-align: center; line-height: 1.2;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.moretile__badge {
  position: absolute; top: -3px; right: -3px;
  min-width: 17px; height: 17px; padding: 0 4px;
  display: grid; place-items: center;
  border-radius: 9px; border: 2px solid var(--surface);
  background: var(--danger); color: #fff;
  font-size: var(--fs-3xs); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  line-height: 1;
}
.moretile__badge[hidden] { display: none; }

/* Somebody who has asked not to be moved gets the same drawer, instantly. */
@media (prefers-reduced-motion: reduce) {
  .moresheet__scrim, .moresheet__panel, .moretile { transition-duration: 0.01ms; }
  .moretile { transition-delay: 0ms; }
}

/* -------------------------------- desktop ------------------------------- */

@media (min-width: 900px) {
  #app { flex-direction: row; }
  /* Both go: the sidebar has every one of these places written out in full,
     so a drawer holding the overflow of a bar that is not on screen would be a
     drawer that cannot be opened and must not be reachable by keyboard. */
  .tabbar, .moresheet { display: none; }
  .main { padding: 22px 26px 90px; }
  .app-header { padding-left: 26px; padding-right: 26px; }
}
@media (max-width: 899px) { .sidebar { display: none; } }

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

/* Quiet by default, loud only where it matters.
   A secondary button is a hairline outline and nothing else - no heavy border,
   no shadow, no near-bold text - so the one primary action on a screen is the
   only thing that reads as a button at a glance. Tap targets stay at 44px:
   minimal means less ink, not smaller things to hit. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 44px; padding: 0 15px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-size: var(--fs-md); font-weight: var(--fw-semibold); letter-spacing: -0.005em; cursor: pointer;
  transition: transform 0.08s ease, background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.btn:hover { background: var(--surface-sunken); }
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn:disabled:active { transform: none; }
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.btn--primary {
  background: var(--brand); border-color: transparent; color: var(--brand-contrast);
  font-weight: var(--fw-semibold); box-shadow: var(--shadow-brand);
}
.btn--primary:hover { background: var(--brand-strong); }
.btn--danger { background: var(--danger); border-color: transparent; color: #fff; font-weight: var(--fw-semibold); }
.btn--danger:hover { background: var(--danger); opacity: 0.9; }

.btn--ghost { background: transparent; border-color: transparent; color: var(--text-muted); }
.btn--ghost:hover { background: var(--surface-sunken); color: var(--text); }

.btn--block { width: 100%; }
.btn--sm { min-height: 36px; padding: 0 12px; font-size: var(--fs-sm); border-radius: 9px; gap: 6px; }
.btn--icon {
  min-height: 40px; width: 40px; padding: 0; border-radius: 50%;
  border-color: transparent; background: transparent; color: var(--text-muted);
}
.btn--icon:hover { background: var(--surface-sunken); color: var(--text); }

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

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.card__head {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px 0;
}
.card__head h2 { font-size: var(--fs-lg); flex: 1; }
.card__head .hint { font-size: var(--fs-xs); color: var(--text-muted); font-weight: var(--fw-medium); }
.card__body { padding: 14px 16px; }

.section-title {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  margin: 24px 2px 10px; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-faint);
}
.section-title .grow { flex: 1; min-width: 0; }
.section-title .icon { flex: none; }
/* The trailing hint explains the heading; on a phone there is not room for
   both, and the heading is the half that has to survive. */
.section-title__hint {
  flex: none; max-width: 52%;
  font-weight: var(--fw-medium); text-transform: none; letter-spacing: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 400px) { .section-title__hint { display: none; } }

.stack { display: flex; flex-direction: column; gap: 12px; }
.row { display: flex; align-items: center; gap: 10px; }
.grow { flex: 1; min-width: 0; }
.muted { color: var(--text-muted); }
.faint { color: var(--text-faint); }
.tiny { font-size: var(--fs-xs); }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num { font-family: var(--font-num); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }

/* The one big number on a screen - a lead's value, a project's total. It was
   being written inline as "font-size:30px;font-weight:750;letter-spacing:-.03em"
   in one view and 28px/750 in another, which is how two screens showing the
   same kind of thing end up two points apart. */
.figure { font-size: var(--fs-4xl); font-weight: var(--fw-bold); letter-spacing: -0.03em; }
.figure--sm { font-size: var(--fs-xl); font-weight: var(--fw-bold); letter-spacing: -0.02em; }

/* --------------------------------- KPIs --------------------------------- */

.kpi-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
@media (min-width: 700px) { .kpi-grid { grid-template-columns: repeat(4, 1fr); } }

.kpi {
  padding: 11px 13px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius);
}
.kpi__label {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--text-faint);
}
/* Was --fs-3xl. Four of these stacked two-up on a phone read as a poster
   rather than a dashboard - still the biggest thing on the card, just no
   longer shouting over the eight sections underneath it. */
.kpi__value { margin-top: 3px; font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -0.03em; }
.kpi__meta { margin-top: 1px; font-size: var(--fs-xs); color: var(--text-muted); }

/* A KPI that answers a question when tapped. Being a real <button> gives it
   keyboard focus and Enter/Space for free; these rules only undo the browser
   defaults a button brings with it and add the affordance back. */
.kpi--link {
  width: 100%; text-align: left; font: inherit; color: inherit;
  cursor: pointer; -webkit-appearance: none; appearance: none;
  transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.kpi--link:hover { border-color: var(--border-strong); background: var(--surface-2); }
.kpi--link:active { transform: scale(0.985); }
.kpi--link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The accent card is brand-coloured, so a mid-tone border reads as a smudge. */
.kpi--accent.kpi--link:hover { border-color: color-mix(in srgb, #fff 45%, transparent); }
.kpi--accent {
  background: linear-gradient(150deg, var(--brand), var(--brand-strong));
  border-color: transparent; color: var(--brand-contrast); box-shadow: var(--shadow-brand);
}
.kpi--accent.kpi--link:hover { background: linear-gradient(150deg, var(--brand-strong), var(--brand-strong)); }
.kpi--accent .kpi__label, .kpi--accent .kpi__meta {
  color: color-mix(in srgb, var(--brand-contrast) 78%, transparent);
}

/* -------------------------------- badges -------------------------------- */

.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 999px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); white-space: nowrap;
  background: var(--surface-sunken); color: var(--text-muted);
}
.pill__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.pill--status { background: color-mix(in srgb, var(--pill-color) 14%, transparent); color: var(--pill-color); }
:root[data-theme='dark'] .pill--status { background: color-mix(in srgb, var(--pill-color) 22%, transparent); }
.pill--success { background: var(--success-soft); color: var(--success); }
.pill--danger { background: var(--danger-soft); color: var(--danger); }
.pill--warning { background: var(--warning-soft); color: var(--warning); }
.pill--info { background: var(--info-soft); color: var(--info); }

/* ------------------------------- lead list ------------------------------ */

/* The count and the sort control share one line and neither may push the other
   off it: the count shrinks and truncates, the button keeps its width because a
   half-visible label is worse than a shorter one. */
.list-summary {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin: 2px 2px 10px; min-width: 0;
}
.list-summary__count { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-summary .btn { flex: none; margin-right: -6px; }

.lead-card {
  display: block; width: 100%; text-align: left;
  padding: 13px 15px; margin: 0;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--pill-color, var(--border));
  border-radius: var(--radius);
  cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease;
}
.lead-card:hover { background: var(--surface-2); border-color: var(--border-strong); }
.lead-card:active { background: var(--surface-sunken); transform: scale(0.995); }
.lead-card__top { display: flex; align-items: flex-start; gap: 10px; }
.lead-card__name { font-size: var(--fs-lg); font-weight: var(--fw-semibold); letter-spacing: -0.015em; }
.lead-card__sub { font-size: var(--fs-xs); color: var(--text-muted); margin-top: 1px; }
.lead-card__value { font-size: var(--fs-xl); font-weight: var(--fw-bold); text-align: right; white-space: nowrap; }
.lead-card__foot {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 11px;
}
/* Pinned to the end of the row, so it is never the thing that wraps onto a
   line of its own when the card is carrying a lot. */
.lead-card__who { flex: none; display: inline-flex; }

/* -------------------------------- funnel -------------------------------- */

/* Label above the bar, never inside it - a 3%-wide bar cannot hold text. */
.funnel { display: flex; flex-direction: column; }
/* Rows are buttons that open the leads behind the number. */
.funnel__row {
  display: block; width: 100%; margin: 0; padding: 4px 6px;
  background: none; border: 0; border-radius: var(--radius-sm);
  color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: background 0.15s;
}
.funnel__row:hover:not(:disabled) { background: var(--surface-sunken); }
.funnel__row:active:not(:disabled) { transform: scale(0.995); }
.funnel__row:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.funnel__row:disabled { cursor: default; opacity: 0.75; }
.funnel__go { color: var(--text-faint); opacity: 0; transition: opacity 0.15s; }
.funnel__row:hover .funnel__go, .funnel__row:focus-visible .funnel__go { opacity: 1; }
/* Touch has no hover, so keep the affordance permanently visible there. */
@media (hover: none) { .funnel__go { opacity: 0.5; } }

.funnel__head {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px;
}
.funnel__label {
  display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0;
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.funnel__label .pill__dot { width: 8px; height: 8px; flex: none; }
.funnel__numbers { display: flex; align-items: baseline; gap: 8px; font-size: var(--fs-xs); white-space: nowrap; }
.funnel__numbers b { font-size: var(--fs-md); font-weight: var(--fw-bold); }
.funnel__track {
  height: 8px; border-radius: 4px; overflow: hidden;
  background: var(--surface-sunken);
}
.funnel__fill {
  height: 100%; border-radius: 4px; min-width: 3px;
  transition: width 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.funnel__drop {
  display: flex; align-items: center; gap: 5px;
  padding: 1px 0 1px 6px; font-size: var(--fs-2xs); color: var(--text-faint); font-weight: var(--fw-semibold);
}
.funnel__drop .icon { flex: none; }

/* --------------------------------- bars --------------------------------- */

.bar-row { display: grid; grid-template-columns: 1fr auto; gap: 3px 10px; padding: 5px 0; }
.bar-row + .bar-row { border-top: 1px solid var(--border); }
.bar-row__label { font-size: var(--fs-sm); font-weight: var(--fw-medium); }
.bar-row__value { font-size: var(--fs-sm); font-weight: var(--fw-semibold); text-align: right; white-space: nowrap; }
.bar-row__track {
  grid-column: 1 / -1; display: block; height: 6px; border-radius: 3px;
  background: var(--surface-sunken); overflow: hidden;
}
.bar-row__fill { display: block; height: 100%; border-radius: 3px; transition: width 0.4s ease; }

/* ------------------------------ leaderboard ----------------------------- */

.rep-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0; cursor: pointer; width: 100%;
  background: none; border: 0; border-top: 1px solid var(--border); text-align: left;
}
.rep-row:first-child { border-top: 0; }

/* display:block is explicit so these work as <span> or <div>: an inline
   element ignores height/width and the fill would silently vanish. */
.progress {
  display: block; height: 6px; margin-top: 6px;
  border-radius: 3px; background: var(--surface-sunken); overflow: hidden;
}
.progress__fill {
  display: block; height: 100%; border-radius: 3px;
  background: var(--brand); transition: width 0.4s ease;
}

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

/**
 * `hidden` means hidden.
 *
 * The browser's own rule is `[hidden] { display: none }`, which any class that
 * sets display outranks - so `.field { display: flex }` quietly won and a field
 * marked hidden stayed on screen. Every element that sets display has to lose
 * to the attribute, which is what the !important is for: this is not a
 * preference being forced, it is the meaning of the attribute being restored.
 */
[hidden] { display: none !important; }

.field { display: flex; flex-direction: column; gap: 6px; }
.field > label { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-muted); }
.field .req { color: var(--danger); }
.field .help { font-size: var(--fs-xs); color: var(--text-faint); }

.input, .select, .textarea {
  width: 100%; min-height: 48px; padding: 12px 14px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-size: var(--fs-input);
  appearance: none; outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }
.textarea { min-height: 96px; resize: vertical; line-height: 1.5; }
.input:focus, .select:focus, .textarea:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 14%, transparent);
}
.select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center; padding-right: 38px;
}

/* Date fields.
   `appearance: none` above removes the browser's calendar icon but leaves the
   picker indicator stretched across the entire field - so a date input worked
   on click yet looked exactly like a plain text box, with nothing to say a
   calendar was one tap away. Give the indicator a real size and our own icon. */
.input[type='date'] { cursor: pointer; }
.input[type='date']::-webkit-calendar-picker-indicator {
  width: 22px; height: 22px;
  margin: 0 0 0 8px; padding: 0;
  cursor: pointer; opacity: 0.6;
  background-repeat: no-repeat; background-position: center; background-size: 20px 20px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E");
  transition: opacity 0.15s;
}
.input[type='date']::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.input[type='date']:focus::-webkit-calendar-picker-indicator { opacity: 1; }
/* The stroke is baked into the data URL, so it cannot follow a token - it is
   the one colour in the dark palette that has to be repeated by hand. Keep it
   equal to --text-faint, or every date field wears the old palette. */
:root[data-theme='dark'] .input[type='date']::-webkit-calendar-picker-indicator {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236e6e72' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='18' x='3' y='4' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E");
}
/* An empty date reads as "dd/mm/yyyy" in grey; keep that muted, not black. */
.input[type='date']:not(:focus):invalid { color: var(--text-faint); }
.input-group { display: flex; }
.input-group .select { width: auto; min-width: 92px; border-radius: var(--radius-sm) 0 0 var(--radius-sm); border-right: 0; }
.input-group .input { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; min-width: 0; }

/* The other way round: a unit after the number, not a currency before it.
   The base rules assume a leading select and put the rounded corners and the
   missing border on the wrong sides for this. */
.input-group--suffix .input { border-radius: var(--radius-sm) 0 0 var(--radius-sm); flex: 1; }
.input-group--suffix .select {
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  border-left: 0; border-right: 1px solid var(--border);
  min-width: 88px;
}
.form-grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 640px) { .form-grid--2 { grid-template-columns: 1fr 1fr; } .form-grid .span-2 { grid-column: 1 / -1; } }

/* A filter row that runs off the edge of a phone needs to look like it does.
   The mask fades the last few pixels out, which reads as "there is more" without
   a scrollbar, an arrow, or anything that takes up room. It stops as soon as the
   row fits, because a fade over nothing looks like a rendering fault. */
/* ------------------------------- tabs ------------------------------------ */

/* A row of filters that are one-of-N, drawn as tabs rather than as pills.
 *
 * They were pills, and ten of them in a row is ten competing shapes - the
 * selected one shouted and the other nine still asked to be read. A tab strip
 * has one job: say which of these you are looking at. The underline does that
 * with no border, no fill and no radius, and the rule underneath ties the
 * whole set together so it reads as one control instead of ten.
 *
 * The markup did not change. Every one of these rows already marked its
 * current option with aria-pressed, which is the right thing to have been
 * saying all along, so the conversion is entirely here.
 */
.chips {
  display: flex; gap: 2px; overflow-x: auto;
  padding: 0; margin-bottom: 14px;
  border-bottom: 1px solid var(--border);
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  /* Fades the right edge while there is more to scroll to, and only then. */
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
}
.chips::-webkit-scrollbar { display: none; }
@supports (animation-timeline: scroll(x)) {
  /* Once scrolled to the end there is nothing more to hint at, so drop it. */
  .chips { animation: chips-fade linear; animation-timeline: scroll(x); }
  @keyframes chips-fade {
    to {
      mask-image: linear-gradient(to right, #000 100%, #000 100%);
      -webkit-mask-image: linear-gradient(to right, #000 100%, #000 100%);
    }
  }
}

.chip {
  position: relative; flex: none; scroll-snap-align: start;
  padding: 11px 16px; border: 0; border-radius: 0; background: none;
  color: var(--text-muted); cursor: pointer; white-space: nowrap;
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  transition: color 0.15s ease;
}
.chip:hover { color: var(--text); }
.chip:focus-visible {
  outline: 2px solid var(--brand); outline-offset: -2px; border-radius: var(--radius-sm);
}

.chip[aria-pressed='true'] { color: var(--text); font-weight: var(--fw-bold); }

/* The underline sits on the rule rather than above it, so the strip reads as
   one edge with a mark on it instead of two parallel lines. */
.chip[aria-pressed='true']::after {
  content: ''; position: absolute; left: 12px; right: 12px; bottom: -1px;
  height: 3px; border-radius: 3px 3px 0 0; background: var(--brand);
}

/* The funnel slice: not a tab.
 *
 * A tab is one of a set you switch between; this is a filter that got applied
 * and can be taken off again, which is a different thing and has to look like
 * one. It keeps the stage's own colour, stays a token with an x on it, and
 * sits at the head of the strip where the applied filter belongs. */
.chip--reached {
  display: inline-flex; align-items: center; gap: 6px; align-self: center;
  margin-right: 8px; padding: 5px 10px 5px 11px; border-radius: 999px;
  background: color-mix(in srgb, var(--pill-color) 16%, transparent);
  color: var(--pill-color); font-weight: var(--fw-semibold);
}
.chip--reached::after { content: none; }
.chip--reached[aria-pressed='true'] {
  background: var(--pill-color); color: #fff;
}
.chip--reached .icon { flex: none; opacity: 0.9; }

.status-picker { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }

/* Three options in a two-column grid leaves the third stranded on a row of its
   own, reading as a different question. The company picker sets its own count
   so Wizebix, Webcastle and Both sit level - and it goes on doing that if a
   third company ever arrives, by naming the columns from the DOM. */
.status-picker--3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 420px) { .status-picker--3 { grid-template-columns: repeat(2, 1fr); } }
.status-option {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 13px; border-radius: var(--radius-sm); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text-muted); font-size: var(--fs-sm); font-weight: var(--fw-medium); text-align: left;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.status-option .icon { flex: none; opacity: 0.55; }
.status-option:hover { border-color: var(--border-strong); color: var(--text); }
/* A 10% tint read as "unavailable" rather than "chosen", and the chosen option
   has to be the loudest thing in the group or the question looks unanswered. */
.status-option[aria-pressed='true'] {
  border-color: var(--pill-color);
  background: color-mix(in srgb, var(--pill-color) 12%, transparent);
  color: var(--pill-color); font-weight: var(--fw-semibold);
  box-shadow: inset 0 0 0 1px var(--pill-color);
}
.status-option[aria-pressed='true'] .icon { opacity: 1; }
:root[data-theme='dark'] .status-option[aria-pressed='true'] {
  background: color-mix(in srgb, var(--pill-color) 20%, transparent);
}

.searchbar { position: relative; display: flex; align-items: center; }
.searchbar .icon { position: absolute; left: 12px; color: var(--text-faint); pointer-events: none; }
.searchbar .input { padding-left: 40px; min-height: 44px; }

/* An explanation with an action beside it. Side by side there is room for both
   on a laptop; on a phone the button stole enough width to squeeze the sentence
   into a four-line column, so below 560px it drops underneath instead. */
.notice { border-left: 3px solid var(--warning); }
.notice__body { display: flex; align-items: flex-start; gap: 10px; }
.notice__body > .icon { flex: none; color: var(--warning); }
.notice__action { flex: none; }
@media (max-width: 560px) {
  .notice__body { flex-wrap: wrap; }
  .notice__action { width: 100%; justify-content: center; margin-top: 4px; }
}

/* ------------------------------- finance -------------------------------- */

.money-bar {
  display: block; height: 9px; border-radius: 5px; overflow: hidden;
  background: var(--surface-sunken); border: 1px solid var(--border);
}
.money-bar__fill {
  display: block; height: 100%; border-radius: 5px;
  background: linear-gradient(90deg, var(--s-won), color-mix(in srgb, var(--s-won) 72%, #000));
  transition: width 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.money-dot { width: 9px; height: 9px; border-radius: 3px; flex: none; }

.pay-row {
  padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--pill-color, var(--border));
  border-radius: var(--radius-sm);
}
.pay-row--tappable { cursor: pointer; }
.pay-row--tappable:active { background: var(--surface-2); }
.pay-row__top { display: flex; align-items: flex-start; gap: 10px; }
.pay-row__foot {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 10px;
}

/* Currency symbol welded to the amount field. */
.input-group__prefix {
  display: flex; align-items: center; flex: none;
  padding: 0 12px; min-height: 48px;
  background: var(--surface-sunken); color: var(--text-muted);
  border: 1px solid var(--border); border-right: 0;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  /* Welded to a 16px field, so it matches that rather than the scale - a "₹" a
     point smaller than the number beside it reads as misaligned. */
  font-size: var(--fs-input); font-weight: var(--fw-semibold);
}
.input-group__prefix + .input { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }

/* Update stage and Share, side by side under the figure. Sharing used to be two
   taps into an overflow menu, which is two taps more than a rep will spend on
   "what do you make of this one". The primary action still takes the room it
   needs; Share sits beside it at its natural width. */

/* ------------------------- custom split builder -------------------------- */

.split-row {
  padding: 11px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.split-row + .split-row { margin-top: 8px; }
.split-row__head { display: flex; align-items: center; gap: 8px; }
.split-row__head .input { min-height: 42px; font-weight: var(--fw-semibold); }
.split-row__head .btn--icon { flex: none; color: var(--text-faint); }
.split-row__head .btn--icon:hover { color: var(--danger); background: var(--danger-soft); }

/* Share and amount stay side by side even on the narrowest phone - seeing both
   at once is the whole point of the two-way calculation. */
.split-row__fields { display: grid; grid-template-columns: 92px 1fr; gap: 10px; margin-top: 9px; }
.split-row__fields .input { min-height: 44px; }
.split-row__fields label { font-size: var(--fs-2xs); }

.split-total {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 12px; padding: 11px 13px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: var(--fs-sm); color: var(--text-muted);
}
.split-total b { color: var(--text); }
.split-total--exact { background: var(--success-soft); border-color: transparent; }
.split-total--exact b { color: var(--success); }
.split-total--under { background: var(--warning-soft); border-color: transparent; }
.split-total--under b { color: var(--warning); }
.split-total--over { background: var(--danger-soft); border-color: transparent; }
.split-total--over b { color: var(--danger); }

/* Six tabs on a narrow phone: the idle pills tighten and the selected one
   claims a little less extra, so its label still fits without squeezing the
   icons either side to nothing. */
/* A small phone. The longest label is the first to truncate, so the padding
   comes down until it fits whole - a nav item reading "Dashbo..." is worse
   than one with less air around it.
   
   That longest label used to be "Accounts" at 52px, which 62 minus padding
   held comfortably. It is "Dashboard" now, at 60px, and it did not. */
@media (max-width: 400px) {
  .tabbar__pill { max-width: 68px; padding: 6px 1px 5px; }
  .tabbar__label { font-size: var(--fs-2xs); }
  .tabbar__item--primary { flex-basis: 56px; }
}
@media (max-width: 340px) {
  .tabbar { padding: 0 2px; }
  .tabbar__pill { max-width: 56px; padding: 6px 1px 5px; gap: 2px; }
  .tabbar__label { font-size: var(--fs-3xs); letter-spacing: -0.01em; }
  .tabbar__item--primary { flex-basis: 50px; }
  .tabbar__plus { width: 42px; height: 42px; }
}

/* ---------------------------- pipeline board ---------------------------- */

/* The toolbar holds a segmented control and sometimes a search box. Four tabs
   ("Overview / Money in / Recurring / Money out") measure 447px, which on a
   375px phone put the last one 100px past the edge with nothing to scroll -
   Money out was not merely cut off, it was unreachable. The row scrolls now,
   with the same fade the filter chips use to say so. */
.board-toolbar {
  display: flex; align-items: center; gap: 10px; margin-bottom: 10px;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
  /* Vertical padding keeps the selected tab's shadow from being clipped by the
     scroll container, which would shave a pixel off its bottom edge. */
  padding: 2px 0;
}
.board-toolbar::-webkit-scrollbar { display: none; }
.board-toolbar .searchbar { min-width: 0; }
/* A search box takes the leftover room, so only the tabs need the fade. */
.board-toolbar:not(:has(.searchbar)) {
  mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
}
@supports (animation-timeline: scroll(x)) {
  .board-toolbar:not(:has(.searchbar)) { animation: chips-fade linear; animation-timeline: scroll(x); }
}
/* Nothing overflows once there is room, so nothing should be faded. */
@media (min-width: 620px) {
  .board-toolbar:not(:has(.searchbar)) { mask-image: none; -webkit-mask-image: none; animation: none; }
}
/* Only when a search box is sharing the row does the toggle drop to icons -
   otherwise there is room for labels, and a bare icon pair is a guessing game. */
@media (max-width: 560px) {
  .board-toolbar:has(.searchbar) .segmented__item span { display: none; }
  .board-toolbar:has(.searchbar) .segmented__item { padding: 8px 13px; }
}

.segmented {
  display: inline-flex; flex: none; padding: 3px; gap: 2px;
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: 11px;
}
.segmented__item {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border: 0; border-radius: 9px; cursor: pointer;
  background: none; color: var(--text-muted);
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); white-space: nowrap;
}
.segmented__item {
  transition: background 0.15s ease, color 0.15s ease;
}
.segmented__item:hover { color: var(--text); }
.segmented__item[aria-selected='true'] {
  background: var(--surface); color: var(--brand);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}
/* Two halves of a sheet rather than a control sitting in a toolbar. Sized off
   the sheet so the pair read as the top of the panel - an inline-flex segment
   floating left of a lot of white space reads as a filter somebody applied. */
.segmented--fill { display: flex; width: 100%; }
.segmented--fill .segmented__item { flex: 1; justify-content: center; }

/* While the board is up the page itself must not scroll - each column scrolls
   on its own, otherwise the whole layout moves under a dragged card. */
body.board-mode { overflow: hidden; }
body.board-mode #app { height: 100dvh; overflow: hidden; }
body.board-mode .shell { min-height: 0; }
body.board-mode .main {
  display: flex; flex-direction: column; min-height: 0; overflow: hidden;
  padding-bottom: calc(var(--nav-space) + 6px);
}
body.board-mode #board-host { flex: 1; min-height: 0; display: flex; flex-direction: column; }
@media (min-width: 900px) { body.board-mode .main { padding-bottom: 14px; } }

/* A filter can empty whole columns, so the active ones are always on screen. */
.filter-summary {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 10px;
}
.filter-summary__scroll {
  display: flex; align-items: center; gap: 7px;
  flex: 1; min-width: 0; overflow-x: auto;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  /* Fades the last few pixels so an overflowing chip reads as "scroll me"
     rather than as a clipped, broken element. Invisible when nothing overflows. */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
}
.filter-summary__scroll::-webkit-scrollbar { display: none; }
.filter-summary__clear { flex: none; }
.filter-summary__count {
  flex: none;
  font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--text-muted);
  padding-right: 2px; white-space: nowrap;
}
.filter-summary__chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  padding: 5px 9px; border-radius: 999px; cursor: pointer;
  background: var(--brand-soft); border: 1px solid transparent; color: var(--brand);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); white-space: nowrap;
}
.filter-summary__chip:hover { border-color: var(--brand); }
.filter-summary__chip .icon { flex: none; opacity: 0.75; }
.filter-summary__chip span { overflow: hidden; text-overflow: ellipsis; max-width: 46vw; }

/* Dot on the filter button so a filtered board is never mistaken for an empty one. */
.btn--badged { position: relative; }
.btn--badged::after {
  content: ''; position: absolute; top: 6px; right: 6px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--brand); border: 2px solid var(--surface);
}

/* Nothing should select or long-press-highlight while a card is in hand. */
body.is-dragging { user-select: none; -webkit-user-select: none; cursor: grabbing; }
body.is-dragging * { cursor: grabbing !important; }

.board {
  flex: 1; min-height: 0;
  display: flex; gap: 10px; align-items: stretch;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 4px;
}
.board::-webkit-scrollbar { height: 8px; }
.board::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
.board--dragging { scroll-snap-type: none; }

.board__column {
  /* Leaves a wide enough sliver of the next column that a card can be dropped
     on it without sitting inside the auto-scroll edge zone. */
  flex: 0 0 min(72vw, 296px);
  display: flex; flex-direction: column; min-height: 0;
  scroll-snap-align: start;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: background 0.15s, border-color 0.15s;
}
@media (min-width: 900px) { .board__column { flex: 0 0 288px; } }

.board__column.is-drop-target {
  border-color: var(--pill-color);
  background: color-mix(in srgb, var(--pill-color) 9%, var(--surface-sunken));
  box-shadow: inset 0 0 0 1px var(--pill-color);
}

.board__head {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 12px 9px;
  border-bottom: 1px solid var(--border);
}

/* The header opens that stage as a list. Deliberately understated - the cards
   below are the point of this screen, and a loud button above each column
   would compete with them. */
.board__head--link { cursor: pointer; border-radius: var(--radius-sm); transition: background 0.14s; }
.board__head--link:hover { background: color-mix(in srgb, var(--pill-color, var(--brand)) 10%, transparent); }
.board__head--link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.board__head-title {
  display: flex; align-items: center; gap: 7px; flex: 1; min-width: 0;
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* The column already declares its colour once, as --pill-color; the dot takes
   it from there rather than from currentColor, which is the header's near-black
   text and made every column's marker identical. */
.board__head-title .pill__dot {
  width: 8px; height: 8px; flex: none;
  background: var(--pill-color, currentColor);
}
.board__count {
  flex: none; padding: 1px 7px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--text-muted);
}
.board__total { font-size: var(--fs-xs); color: var(--text-muted); font-weight: var(--fw-semibold); white-space: nowrap; }

.board__cards {
  flex: 1; min-height: 0; overflow-y: auto;
  /* Setting only overflow-y makes the other axis compute to `auto`, quietly
     turning every column into a horizontal scroll container with no range -
     which can absorb a sideways swipe instead of passing it to the board.
     `clip` states the intent without creating a scroll container at all;
     `hidden` is the fallback for browsers that do not know it. */
  overflow-x: hidden;
  overflow-x: clip;
  display: flex; flex-direction: column; gap: 8px;
  padding: 10px; scrollbar-width: thin;
}

.board__card {
  padding: 11px 12px; cursor: grab;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--pill-color);
  border-radius: var(--radius-sm);
  transition: border-color 0.15s ease, background 0.15s ease;
  /* Both axes must stay with the browser: the column scrolls vertically and
     the board scrolls horizontally, and a card sits inside both. Restricting
     this to pan-y silently killed sideways scrolling anywhere over a card -
     the only place the board would still scroll was the column header.
     Nothing is needed here for dragging: the board that used to own these
     styles has been replaced by the shared React one, which handles its own
     touch behaviour. These rules remain for the cards the vanilla board still
     renders elsewhere. */
  touch-action: pan-x pan-y;
}
.board__card:hover { border-color: var(--border-strong); background: var(--surface-2); }
.board__card:active { border-color: var(--border-strong); background: var(--surface-sunken); }
/* Someone else's card: still openable, just not draggable. A grab cursor here
   would promise something the server would refuse. */
.board__card--locked { cursor: pointer; border-left-style: dotted; }
.board__card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.board__card-top { display: flex; align-items: baseline; gap: 8px; }
.board__card-name {
  flex: 1; min-width: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold); letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.board__card-value { font-size: var(--fs-sm); font-weight: var(--fw-bold); white-space: nowrap; }
/* The sub-line is always present, even with nothing in it: a card that skips
   it stands a row shorter than the one below, and a column that does not line
   up reads as a bug rather than as sparse data. */
/* line-height and min-height are the same number on purpose: an empty div
   takes the min-height, a filled one takes its line box, and anything less
   than an exact match leaves the sparse cards a couple of pixels short. */
.board__card-sub { line-height: 1.4; min-height: 1.4em; }

/**
 * The foot: facts on the left, the person on the right, on one line.
 *
 * It used to wrap, which meant a busy card pushed its assignee onto a second
 * row - the avatar moved, and that card became taller than its neighbours. The
 * metas now share whatever width is left and clip; the person is outside them
 * and never moves.
 */
.board__card-foot {
  display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: nowrap;
}
.board__card-metas {
  display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; overflow: hidden;
  /* A soft edge where it runs out of room, so a clipped pill reads as "there
     is more" rather than as a rendering fault. Lands on empty space - and so
     is invisible - whenever the row fits. */
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 14px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 14px), transparent);
}
.board__card-metas > * { flex: none; }
.board__card-who {
  flex: 0 1 auto; min-width: 0; margin-left: auto;
  display: inline-flex; align-items: center; gap: 6px;
}
/* The rep's name beside their face. Capped so a long one cannot take the whole
   footer, and truncated rather than wrapped - the card's height is what keeps a
   column readable. */
.board__card-rep {
  max-width: 72px;
  font-size: var(--fs-2xs); font-weight: var(--fw-medium); color: var(--text-faint);
}
/* A rep who is not there yet. Reserving the slot is what makes every card the
   same height; drawing a ring rather than nothing means the gap says something
   - this one is unassigned - instead of looking like a rendering slip. */
.avatar--empty {
  border: 1px dashed var(--border-strong); background: none;
}
.board__card-who--spacer { width: 26px; height: 26px; }
.board__card-foot .pill { font-size: var(--fs-2xs); padding: 2px 7px; }

/* Facts about the card, not states of it: no badge, no fill, just the figure
   and its icon. Only something that needs acting on gets to be a pill. */
.board__meta {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fs-2xs); font-weight: var(--fw-medium);
  color: var(--text-faint); white-space: nowrap;
}
.board__meta .icon { flex: none; opacity: 0.8; }

/* The card left behind while its ghost is being dragged. */
.board__card--origin {
  opacity: 0.34; border-style: dashed; box-shadow: none;
}

/* The ghost follows the pointer. pointer-events:none is load-bearing: without
   it elementFromPoint would only ever return the ghost, never a column. */
.board__card--ghost {
  position: fixed; top: 0; left: 0; z-index: 300;
  margin: 0; pointer-events: none; cursor: grabbing;
  box-shadow: var(--shadow-lg);
  opacity: 0.96;
  will-change: transform;
}

.board__empty {
  display: grid; place-items: center; min-height: 76px; padding: 12px;
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius-sm);
  color: var(--text-faint); font-size: var(--fs-xs); font-weight: var(--fw-semibold); text-align: center;
}

.board__hint {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 4px 0; text-align: center;
}
.board__hint .icon { flex: none; }

/* --------------------- the delivery board, ClickUp-style ------------------ */
/*
 * The projects board is the one React island in this app: react-kanban-kit,
 * wrapped by client/projects-board.jsx. Two things about it shape everything
 * below.
 *
 * First, the library injects its own stylesheet from JavaScript at import time
 * - so it lands in <head> after this file and wins every tie. Overrides here
 * are therefore written one class heavier than the rule they replace (.ckb in
 * front, usually) rather than trusting source order. That is the only reason
 * these selectors look overweight.
 *
 * Second, the card markup is still cardMarkup() in views/projects.js, so the
 * .board__card rules further up still do the work inside a card. What changes
 * here is only the shell around it, and only on this board - the leads
 * pipeline shares those classes and is deliberately left alone.
 */

.ckb-host { flex: 1; min-height: 0; display: flex; }

.ckb.rkk-board {
  flex: 1; min-height: 0;
  gap: 12px; padding-bottom: 6px;
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch; scrollbar-width: thin;
}
.ckb.rkk-board::-webkit-scrollbar { height: 8px; }
.ckb.rkk-board::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }

/* Each column is wrapped in a bare div of the library's own, which sits between
   the flex board and the column - so these, not the columns, are the board's
   flex items. Without flex:none they shrink to fit instead of letting the board
   scroll, and the last one - the "new status" button - is squeezed to nothing. */
.ckb.rkk-board > div { flex: none; height: 100%; display: flex; }

/* --------------------------------- columns -------------------------------- */

/* The library pins every column to exactly 264px, both min and max. A phone
   wants most of the screen and a desktop wants something steadier, and a
   collapsed column wants 46px, so all three have to be taken back. */
.ckb .ckcol.rkk-column-outer {
  min-width: 0; max-width: none;
  width: 82vw; max-width: 300px; height: 100%;
  transition: width 0.16s ease;
}
@media (min-width: 700px) { .ckb .ckcol.rkk-column-outer { width: 290px; } }

.ckb .ckcol.rkk-column-outer.ckcol--shut { width: 46px; max-width: 46px; }

/* Tinted from the page's own text colour rather than a fixed grey, so the
   column reads as a slight recess in both themes without a second palette. */
.ckb .ckcol .rkk-column {
  height: 100%; padding: 8px; gap: 0;
  border-radius: 12px;
  background: color-mix(in srgb, var(--text) 4%, transparent);
}
.ckb .ckcol .rkk-column-wrapper { gap: 0; }
.ckb .ckcol .rkk-column-content-list { padding: 1px; scrollbar-width: thin; }
.ckb .ckcol .rkk-column-content-list::-webkit-scrollbar { width: 6px; }
.ckb .ckcol .rkk-column-content-list::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 3px;
}

/* A folded column keeps only its header, turned on its side. */
.ckb .ckcol--shut .rkk-column-content { display: none; }
.ckb .ckcol--shut .rkk-column-wrapper > div:first-child { flex: 1; min-height: 0; display: flex; }

/* ------------------------------ column header ----------------------------- */

/* This whole strip is the column's drag handle - the library hangs the handle
   off whatever renderColumnHeader returns. */
.ckcol__head {
  display: flex; align-items: center; gap: 7px;
  padding: 3px 3px 9px; cursor: grab;
}
.ckcol__head:active { cursor: grabbing; }

.ckcol__grip {
  flex: none; display: grid; place-items: center;
  width: 20px; height: 20px; padding: 0;
  border: 0; background: none; border-radius: 5px;
  color: var(--text-faint); cursor: pointer;
}
.ckcol__grip:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
.ckcol__grip:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.ckcol__pill {
  flex: 0 1 auto; min-width: 0;
  padding: 4px 9px; border-radius: 6px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.055em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: color-mix(in srgb, var(--col) 16%, transparent);
  /* Mixed towards the page's text colour rather than towards a fixed dark, so
     an amber column is readable on white and that same amber is readable on
     black. A solid pill in the column colour - which is what ClickUp draws -
     puts white on #f59e0b at 2.1:1, and half these columns would be unreadable. */
  color: color-mix(in srgb, var(--col) 80%, var(--text));
}

.ckcol__count {
  flex: none; font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  color: var(--text-faint); font-variant-numeric: tabular-nums;
}

.ckcol__acts { flex: none; margin-left: auto; display: flex; gap: 2px; }
.ckcol__act {
  display: grid; place-items: center;
  width: 24px; height: 24px; padding: 0;
  border: 0; background: none; border-radius: 6px;
  color: var(--text-muted); cursor: pointer;
}
.ckcol__act:hover { background: color-mix(in srgb, var(--text) 9%, transparent); color: var(--text); }
.ckcol__act:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* Kept out of the way until the pointer is over the column - but only where
   there is a pointer to hover with. On a touch screen a button that only
   appears on hover is simply a button that is missing. */
@media (hover: hover) and (pointer: fine) {
  .ckcol__acts { opacity: 0; transition: opacity 0.12s ease; }
  .ckcol:hover .ckcol__acts,
  .ckcol__acts:focus-within { opacity: 1; }
}

.ckcol__head.is-collapsed {
  flex-direction: column; gap: 9px;
  height: 100%; padding: 3px 0 0; cursor: pointer;
}
.ckcol__head.is-collapsed .ckcol__pill {
  writing-mode: vertical-rl;
  padding: 9px 4px; max-height: 58vh;
}

/* ------------------------------ adding things ----------------------------- */

.ckadd {
  display: flex; align-items: center; gap: 7px; width: 100%;
  margin-top: 2px; padding: 9px 10px;
  border: 0; border-radius: 8px; background: none;
  color: var(--text-muted); font-family: inherit;
  font-size: var(--fs-sm); font-weight: var(--fw-semibold);
  text-align: left; cursor: pointer;
}
.ckadd:hover { background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); }
.ckadd:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

.ckb .rkk-column-adder { display: flex; align-items: flex-start; padding-top: 3px; }
.ckcoladd {
  display: flex; align-items: center; gap: 7px; align-self: flex-start;
  padding: 10px 14px; white-space: nowrap;
  border: 1.5px dashed var(--border-strong); border-radius: 12px;
  background: none; color: var(--text-muted); font-family: inherit;
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); cursor: pointer;
}
.ckcoladd:hover { border-color: var(--brand); color: var(--brand); }

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

/* A plain box. It exists to carry the long-press handler and the card markup,
   not to draw anything - .board__card inside it is still the card. */
.ckb .ckcard { display: block; }

.ckb .board__card {
  /* ClickUp does not stripe its cards by column: the colour lives in the
     header, and a board of cards each wearing a coloured edge is noisier than
     it is informative. The entity tag is still there to carry a colour. */
  border-left: 1px solid var(--border);
  border-radius: 10px; padding: 10px 11px;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: box-shadow 0.14s ease, border-color 0.14s ease, transform 0.14s ease;
  /* A card is a native HTML5 drag source here, so text selection inside one
     fights the drag rather than helping it. */
  -webkit-user-select: none; user-select: none;
}
.ckb .board__card--locked { border-left-style: solid; }
.ckb .board__card:hover {
  background: var(--surface); border-color: var(--border-strong);
  box-shadow: var(--shadow); transform: translateY(-1px);
}
.ckb .board__card:active { transform: none; }

@media (hover: hover) and (pointer: fine) { .ckb .board__card { cursor: grab; } }

/* Where the card will land. The library's own is a white block, which is
   invisible on a white card and wrong in the dark theme. */
.ckb .rkk-card-shadow,
.ckb .rkk-column-shadow {
  border-radius: 10px;
  border: 1.5px dashed var(--brand);
  background: color-mix(in srgb, var(--brand) 9%, transparent);
}
.ckb .rkk-column-shadow { border-radius: 12px; }

/* --------------------------- moving without a drag ------------------------ */
/*
 * The board drags through the native HTML5 API, which does not fire from a
 * touch screen at all. A long press opens this instead, so a card can still be
 * moved with a thumb - see the note at the top of client/projects-board.jsx.
 */

.movecols { display: flex; flex-direction: column; gap: 6px; }
.movecol {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 12px 13px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text); font-family: inherit;
  font-size: var(--fs-md); font-weight: var(--fw-semibold);
  text-align: left; cursor: pointer;
}
.movecol:hover { border-color: var(--border-strong); background: var(--surface-2); }
.movecol--on {
  border-color: var(--col);
  background: color-mix(in srgb, var(--col) 10%, transparent);
}
.movecol__dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--col); }
.movecol .icon { flex: none; color: var(--col); }

/* -------------------------------- sheets -------------------------------- */

.overlay {
  position: fixed; inset: 0; z-index: 80;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(15, 23, 42, 0.45);
  backdrop-filter: blur(2px);
  animation: fade 0.18s ease;
}
.sheet {
  width: 100%; max-height: 88vh; overflow-y: auto;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 8px 16px calc(20px + var(--safe-b));
  box-shadow: var(--shadow-lg);
  animation: slide-up 0.24s cubic-bezier(0.2, 0.9, 0.25, 1);
}
.sheet__grip { width: 40px; height: 4px; border-radius: 2px; background: var(--border-strong); margin: 6px auto 12px; }
.sheet__title { font-size: var(--fs-xl); margin-bottom: 4px; }
.sheet__actions { display: flex; gap: 10px; margin-top: 18px; }
.sheet__actions .btn { flex: 1; }
@media (min-width: 640px) {
  .overlay { align-items: center; }
  .sheet { max-width: 520px; border-radius: var(--radius-lg); padding-bottom: 20px; }
  /* For looking at something rather than answering something. 520px is right
     for a form and useless for a page of a PDF. */
  .sheet--wide { max-width: 900px; }
  .sheet__grip { display: none; }
}

/* ------------------------------- drawer --------------------------------- */

/* A panel down the right edge rather than a sheet up from the bottom. On a wide
   screen the board stays visible beside it, which is most of the reason to open
   a card from a board at all. */
.overlay--drawer { align-items: stretch; justify-content: flex-end; }
.overlay--drawer.is-closing { animation: fade 0.18s ease reverse; }

.drawer {
  display: flex; flex-direction: row;
  /* 460px was a phone layout shown on a desktop: a label column of 132px left
     224px for the value, which is why the deal name and the rep who owns it
     could not share a line and why a paragraph of notes arrived four words
     wide. Two thirds of a 1024px window, capped so it stays a panel over the
     board rather than a page that has replaced it. */
  width: min(680px, 100%); height: 100%;
  background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  animation: slide-in 0.26s cubic-bezier(0.2, 0.9, 0.25, 1);
}
.is-closing .drawer { animation: slide-in 0.2s cubic-bezier(0.4, 0, 1, 1) reverse; }

.drawer__panel { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* A column of icons down the outside edge, one per page of the record. Icons
   rather than a row of labelled tabs because the panel is 460px and a tab strip
   would eat a line of it to say two words. */
.drawer__rail {
  flex: none; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: calc(var(--safe-t) + 14px) 8px 14px;
  border-left: 1px solid var(--border);
  background: var(--surface-sunken);
}
.drawer__rail-btn {
  position: relative;
  display: grid; place-items: center;
  width: 38px; height: 38px; padding: 0;
  border: 0; border-radius: 11px; background: none;
  color: var(--text-muted); cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
/* The visible pill is 38px because a fatter one would eat panel width, but the
   thing a thumb has to land on is 44px. Same rule as everywhere else: minimal
   means less ink, not smaller things to hit. */
.drawer__rail-btn::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.drawer__rail-btn:hover { background: var(--surface); color: var(--text); }
.drawer__rail-btn.is-on {
  background: color-mix(in srgb, var(--brand) 14%, transparent);
  color: var(--brand);
}
.drawer__rail-badge {
  position: absolute; top: 1px; right: 1px;
  min-width: 15px; height: 15px; padding: 0 3px;
  display: grid; place-items: center;
  border-radius: 8px; border: 2px solid var(--surface-sunken);
  background: var(--text-muted); color: var(--surface);
  font-size: var(--fs-3xs); font-weight: var(--fw-bold);
  line-height: 1; font-variant-numeric: tabular-nums;
}
.drawer__rail-btn.is-on .drawer__rail-badge { background: var(--brand); color: var(--brand-contrast); }

.drawer__head {
  display: flex; align-items: flex-start; gap: 10px; flex: none;
  padding: calc(var(--safe-t) + 14px) 14px 12px 18px;
  border-bottom: 1px solid var(--border);
}
.drawer__eyebrow {
  display: block; margin-bottom: 3px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-faint);
}
.drawer__title { font-size: var(--fs-2xl); line-height: 1.2; letter-spacing: -0.02em; }
.drawer__body {
  flex: 1; min-height: 0;
  /* Setting only overflow-y makes the other axis compute to auto, so anything
     too wide turns into a sideways scroll - and a scrolled panel hides its own
     labels off the left edge, which looks like a broken layout rather than a
     wide field. Nothing here should ever scroll sideways. */
  overflow-y: auto; overflow-x: hidden;
  padding: 14px 18px calc(24px + var(--safe-b));
  overscroll-behavior: contain;
}

/* A field list rather than a stack of boxed inputs: label on the left, the
   value on the right, one line each where the value allows it. Twelve fields
   as labelled boxes is a form; as rows it is a record you can read down. */
.field-row {
  display: flex; align-items: center; gap: 12px;
  min-height: 44px; padding: 5px 0;
  border-bottom: 1px solid var(--border);
}
.field-row:last-child { border-bottom: 0; }
.field-row__label {
  display: flex; align-items: center; gap: 8px; flex: none;
  width: 132px; min-width: 0;
  font-size: var(--fs-sm); color: var(--text-muted);
}
.field-row__label .icon { flex: none; color: var(--text-faint); }
.field-row__value { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
/* .input carries width:100%, which in a flex row means "as wide as the whole
   container" and, with the default min-width:auto, refuses to shrink below its
   content. Two date pickers in one row then demand 504px of a 423px row. */
.field-row__value > * { min-width: 0; }
/* Anything you can change looks like a slot you can change; anything you cannot
   is plain text. That contrast is the whole affordance, and it has to be there
   at rest - an earlier version showed the border only on hover, which on a
   phone means never, so every field read as a label somebody had typed. */
.field-row__value .input,
.field-row__value .select,
.field-row__value .textarea {
  min-height: 38px;
  /* background-color, never the `background` shorthand: that resets
     background-image too, and the select's chevron IS a background image -
     losing it left three dropdowns looking exactly like static text. */
  background-color: var(--surface-sunken);
  border-color: transparent;
  padding-left: 10px;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.field-row__value .input:hover:not(:disabled),
.field-row__value .select:hover:not(:disabled),
.field-row__value .textarea:hover:not(:disabled) { border-color: var(--border-strong); }
.field-row__value .input:focus,
.field-row__value .select:focus,
.field-row__value .textarea:focus { background-color: var(--surface); border-color: var(--brand); }

/* The deal and the rep who sold it, on one line where there is room for both
   and on two where there is not. Every other row in this panel holds one
   control that fills the width; this one holds two facts, and a drawer 404px
   wide cannot always show a client name and a person's name side by side. */
.field-row--deal .field-row__value { flex-wrap: wrap; row-gap: 4px; }

/* Read-only: no slot, no fill, no border - it is a fact, not a field. */
.field-row__value .input:disabled,
.field-row__value .select:disabled,
.field-row__value .textarea:disabled {
  opacity: 1; color: var(--text);
  background-color: transparent; border-color: transparent;
  background-image: none; padding-left: 0;
  /* The grip in the corner is a handle offering a drag that does nothing. */
  resize: none;
}
/* A date picker wants 178px. Two of them plus an arrow need 378px, and a 460px
   drawer minus a label column leaves 279 - so side by side they were squeezed
   to 125px each and showed "15/10/202". The label takes its own line instead
   and the pair get about 200px each, at every width. */
.field-row--dates { flex-direction: column; align-items: stretch; gap: 6px; }
.field-row--dates .field-row__label { width: auto; }
/* Three controls now - starts, due, and the hour it is due. They share the row
   evenly where it is wide enough and wrap onto a second line where it is not:
   a browser's date picker renders "10/09/2026" in about 118px and below that
   quietly clips the year, which is the digit that matters least right up until
   it is January. */
.field-row--dates .field-row__value { flex-wrap: wrap; row-gap: 6px; }
.field-row--dates .input { flex: 1 1 118px; min-width: 118px; }
.field-row--dates .field-row__value > .faint { flex: none; }

/* What a panel that saves itself says instead of a button. Fixed height, so a
   line changing from "Saving..." to "Saved" does not move the two things under
   it - which on a card is a delete button. */
.autosave { margin: 0; min-height: 18px; }
.autosave--failed { color: var(--danger); }

.field-row--stacked { flex-direction: column; align-items: stretch; gap: 6px; }
.field-row--stacked .field-row__label { width: auto; }

@media (max-width: 480px) {
  /* The rail costs the panel 55px it does not have on a phone, and the values
     were the ones paying for it. Everything gives a little back: the label
     column, the body's gutters, the rail itself, and the arrow between the two
     dates - adjacent boxes under a "Dates" label read start-then-due without
     it, and the year fitting matters more. */
  .field-row__label { width: 88px; }
  .drawer__body { padding-left: 12px; padding-right: 12px; }
  .drawer__rail { padding-left: 5px; padding-right: 5px; }
  .drawer__rail-btn { width: 36px; height: 36px; }
  .field-row--dates .field-row__value > .faint { display: none; }
  .field-row--dates .input { padding-left: 6px; padding-right: 4px; }
  /* The pair drops under its own label on a phone rather than sitting beside
     it, which is where the width comes from. The arrow goes with it: wrapping
     would leave it dangling on the end of a line. */
}

@keyframes fade { from { opacity: 0; } }
@keyframes slide-up { from { transform: translateY(24px); opacity: 0.6; } }
@keyframes slide-in { from { transform: translateX(24px); opacity: 0.4; } }

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

.toasts {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 200;
  pointer-events: none;
  bottom: calc(var(--nav-space) + 78px);
  display: flex; flex-direction: column; gap: 8px; width: min(92vw, 420px);
}
.toast {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 14px; border-radius: var(--radius-sm);
  background: #111827; color: #fff; font-size: var(--fs-md); font-weight: var(--fw-medium);
  box-shadow: var(--shadow-lg); animation: slide-up 0.22s ease;
}
.toast--success { background: #14532d; }
.toast--error { background: #7f1d1d; }
@media (min-width: 900px) { .toasts { bottom: 26px; left: auto; right: 26px; transform: none; } }

/* ------------------------------- timeline ------------------------------- */

.timeline { position: relative; padding-left: 26px; }
.timeline::before {
  content: ''; position: absolute; left: 8px; top: 6px; bottom: 6px;
  width: 2px; background: var(--border);
}
.timeline__item { position: relative; padding: 0 0 18px; }
.timeline__dot {
  position: absolute; left: -24px; top: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--surface); border: 2.5px solid var(--brand);
}
.timeline__meta { font-size: var(--fs-2xs); color: var(--text-faint); margin-top: 2px; }

/* -------------------------------- states -------------------------------- */

.empty { padding: 44px 20px; text-align: center; }
.empty__icon {
  display: grid; place-items: center; width: 62px; height: 62px; margin: 0 auto 14px;
  border-radius: 50%; background: var(--brand-soft); color: var(--brand);
}
.empty h3 { font-size: var(--fs-xl); margin-bottom: 5px; }
.empty p { color: var(--text-muted); font-size: var(--fs-md); max-width: 320px; margin: 0 auto 18px; }

.skeleton {
  background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border) 50%, var(--surface-sunken) 75%);
  background-size: 200% 100%; border-radius: var(--radius-sm);
  animation: shimmer 1.3s infinite linear;
}
@keyframes shimmer { to { background-position: -200% 0; } }

.offline-banner {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; background: var(--warning-soft); color: var(--warning);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold);
}

/* --------------------------------- login -------------------------------- */

/* The one screen in this app that is allowed to be quiet.
 *
 * Everything else here is a dense tool: small type, tight rows, nothing
 * decorative. This page is seen for eight seconds, and its jobs are to say
 * which app this is and to give somebody something to look at while they type
 * - so it gets the serif, the whitespace and the artwork, and the rest of the
 * app gets none of them.
 *
 * Two columns at a desk, one on a phone, where the artwork becomes a band
 * above the form rather than disappearing: it is the only thing that makes
 * this screen the company's rather than a generic form.
 */
/* The host, which has no shell around it to size it. See renderLogin. */
.view--full { flex: 1 1 auto; min-width: 0; }

.signin {
  min-height: 100dvh; display: grid; grid-template-columns: 1fr;
  background: var(--surface);
}
@media (min-width: 900px) {
  .signin { grid-template-columns: 1fr 1fr; padding: 16px; gap: 16px; }
}

/* -------------------------- the artwork ---------------------------------- */

/* Drawn, not downloaded.
 *
 * Layered gradients over black: no image file to ship, sharp at any size, and
 * ours rather than a stock render. `background-blend-mode` is what stops five
 * flat washes reading as five flat washes - it is the blending that gives the
 * bands their edges. */
.signin__art {
  position: relative; overflow: hidden; isolation: isolate;
  min-height: 220px; padding: 30px;
  display: flex; flex-direction: column; justify-content: space-between;
  border-radius: var(--radius-lg); color: #fff;
  background-color: #05030a;
  background-image:
    radial-gradient(120% 70% at 12% 8%, rgba(236, 45, 138, 0.95), transparent 62%),
    radial-gradient(110% 80% at 78% 26%, rgba(255, 82, 92, 0.75), transparent 58%),
    radial-gradient(130% 90% at 22% 62%, rgba(126, 58, 242, 0.9), transparent 60%),
    radial-gradient(120% 80% at 84% 82%, rgba(37, 99, 235, 0.85), transparent 62%),
    linear-gradient(155deg, rgba(255, 0, 128, 0.55) 0%, transparent 42%),
    linear-gradient(200deg, transparent 48%, rgba(14, 165, 233, 0.6) 88%);
  background-blend-mode: screen, screen, screen, screen, soft-light, screen;
}

/* The bands. Repeating hairlines sheared across the whole panel, which is what
   turns a wash of colour into something that looks like it flows. */
.signin__art::after {
  content: ''; position: absolute; inset: -30%; z-index: -1;
  background: repeating-linear-gradient(
    172deg, rgba(0, 0, 0, 0.34) 0 1px, transparent 1px 14px
  );
  transform: rotate(-8deg);
}

.signin__label {
  position: relative;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  letter-spacing: 0.18em; text-transform: uppercase;
  display: flex; align-items: center; gap: 14px;
}
/* The rule that runs off the label, as on the reference. */
.signin__label::after {
  content: ''; flex: 1 1 auto; max-width: 180px;
  height: 1px; background: currentColor; opacity: 0.55;
}

.signin__quote { position: relative; max-width: 520px; }
.signin__quote h2 {
  font-family: var(--font-display);
  font-size: var(--fs-display-lg); font-weight: var(--fw-body);
  line-height: 1.02; letter-spacing: -0.01em;
}
.signin__quote p {
  margin-top: 18px; max-width: 34em;
  font-size: var(--fs-lg); line-height: 1.55; opacity: 0.86;
}

/* On a phone the artwork is a band, not a column - short enough to leave the
   form above the fold, tall enough to still be the thing you see first. */
@media (max-width: 899px) {
  .signin__art { border-radius: 0; min-height: 172px; padding: 20px; }
  .signin__quote h2 { font-size: var(--fs-3xl); }
  .signin__quote p { font-size: var(--fs-sm); margin-top: 8px; }
}

/* ---------------------------- the form side ------------------------------ */

.signin__panel {
  display: flex; flex-direction: column; align-items: center;
  padding: 26px 20px calc(26px + var(--safe-b));
}
.signin__brand {
  display: flex; align-items: center; gap: 9px;
  font-size: var(--fs-xl); font-weight: var(--fw-semibold); letter-spacing: -0.02em;
}
/* No border-radius: the mark is an SVG with its own corners, and rounding it
   a second time at 26px turns the squircle into a circle. */
.signin__brand img { width: 26px; height: 26px; }

/* Centred in what is left after the brand and the footer, so the form sits on
   the optical middle of the panel rather than hard under the logo. */
.signin__form {
  flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center;
  width: 100%; max-width: 380px;
}
.signin__form h1 {
  font-family: var(--font-display);
  font-size: var(--fs-display); font-weight: var(--fw-body);
  line-height: 1.1; letter-spacing: -0.01em; text-align: center;
}
.signin__sub {
  margin: 10px 0 30px; text-align: center;
  font-size: var(--fs-md); color: var(--text-muted);
}
.signin__form .field + .field { margin-top: 16px; }
.signin__form label { font-size: var(--fs-md); font-weight: var(--fw-medium); }
.signin__form .input { background: var(--surface-sunken); }

/* The eye sits inside the box, so the field keeps its full width. */
.signin__peek { position: relative; }
.signin__peek .input { padding-right: 44px; }
.signin__peek button {
  position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
  display: grid; place-items: center; width: 32px; height: 32px;
  border: 0; border-radius: var(--radius-sm); background: none;
  color: var(--text-faint); cursor: pointer;
}
.signin__peek button:hover { color: var(--text); }
.signin__peek button[aria-pressed='true'] { color: var(--brand); }

.signin__row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 14px;
}
.signin__check {
  display: flex; align-items: center; gap: 9px; cursor: pointer;
  font-size: var(--fs-md); font-weight: var(--fw-body); color: var(--text);
}
.signin__check input { width: 16px; height: 16px; accent-color: var(--brand); cursor: pointer; }
.signin__forgot {
  border: 0; background: none; padding: 0; cursor: pointer;
  font-size: var(--fs-md); font-weight: var(--fw-medium); color: var(--text);
}
.signin__forgot:hover { color: var(--brand); }

/* Taller than a button anywhere else in the app, because it is the only thing
   on the page anybody came here to press. */
/* Ink, not brand.
 *
 * The reference is a black button, and black is right here for a reason worth
 * keeping: this is the only button on the page, so it does not need a colour
 * to be found - and the indigo the app uses for "the one action on this
 * screen" means something different once you are inside. Written as --text so
 * it inverts with the theme instead of becoming a black hole on a dark
 * panel. */
.signin__go {
  margin-top: 24px; min-height: 50px; font-size: var(--fs-lg);
  background: var(--text); color: var(--surface); border-color: var(--text);
}
.signin__go:hover:not(:disabled) { background: var(--text-muted); border-color: var(--text-muted); }

.signin__foot {
  margin-top: 26px; text-align: center;
  font-size: var(--fs-md); color: var(--text-muted);
}
.signin__foot b { color: var(--text); font-weight: var(--fw-semibold); }

.login__demo {
  margin-top: 16px; padding: 13px 14px;
  background: var(--surface); border: 1px dashed var(--border-strong);
  border-radius: var(--radius); font-size: var(--fs-xs); color: var(--text-muted);
}
.login__demo button {
  display: flex; width: 100%; align-items: center; gap: 8px; padding: 7px 0;
  background: none; border: 0; border-top: 1px solid var(--border);
  color: var(--text); font-size: var(--fs-sm); cursor: pointer; text-align: left;
}
.login__demo button:first-of-type { border-top: 0; }

.alert {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 11px 13px; border-radius: var(--radius-sm);
  background: var(--danger-soft); color: var(--danger);
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
}

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

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.data th {
  text-align: left; padding: 6px 12px; white-space: nowrap;
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-faint); border-bottom: 1px solid var(--border);
}
table.data td { padding: 6px 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
table.data tr:last-child td { border-bottom: 0; }
table.data td.wrap { white-space: normal; min-width: 170px; }

.divider { height: 1px; background: var(--border); margin: 4px 0; }

.list-group { display: flex; flex-direction: column; }
.list-item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 14px 16px; background: var(--surface); border: 0;
  border-bottom: 1px solid var(--border); cursor: pointer; text-align: left; font-size: var(--fs-md);
}
.list-item:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.list-item:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
.list-item:only-child { border-radius: var(--radius); }
.list-item:active { background: var(--surface-2); }
.list-group--card { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }

/* ---------------------------- board columns ------------------------------ */

/* One row per column in the board editor: reorder, rename, recolour, mark the
   finished one, delete. Wraps rather than scrolls - an admin doing this on a
   phone should not have to drag sideways to reach the delete button. */
.column-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface);
}
.column-row .input { min-height: 40px; min-width: 120px; }
/* Two digits and a per-cent sign. Given the width a name needs it would push
   the delete button onto a second line, and a row that wraps reads as two
   settings rather than one. */
.column-row .column-row__pct { min-width: 0; width: 66px; flex: none; text-align: center; }
.column-row__move { display: flex; flex: none; }
/* The pair are one control: an up arrow is the down arrow upside down. */
.column-row__move .btn:first-child .icon { transform: rotate(180deg); }
.column-row__swatches { display: flex; gap: 4px; flex: none; }
.swatch {
  width: 22px; height: 22px; padding: 0; cursor: pointer;
  border: 2px solid transparent; border-radius: 50%;
  transition: transform 0.12s ease, border-color 0.15s ease;
}
.swatch:hover { transform: scale(1.15); }
.swatch--on { border-color: var(--text); transform: scale(1.15); }

/* ----------------------------- action menu ------------------------------ */

/* Rare and destructive things live behind the "more" button rather than on the
   header, where a mis-tap on a phone reaches them by accident. Full-width rows
   so the whole line is the target. */
.action-menu { display: flex; flex-direction: column; gap: 2px; margin: -4px 0 4px; }
.action-menu__item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: 52px; padding: 10px 12px;
  border: 0; border-radius: var(--radius-sm); background: none;
  color: var(--text); font: inherit; text-align: left; cursor: pointer;
  transition: background 0.15s ease;
}
.action-menu__item:hover { background: var(--surface-sunken); }
.action-menu__item:active { background: var(--surface-2); }
.action-menu__item > .icon { flex: none; color: var(--text-muted); }
.action-menu__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.action-menu__text b { font-size: var(--fs-md); font-weight: var(--fw-semibold); }
.action-menu__item--danger, .action-menu__item--danger > .icon { color: var(--danger); }
.action-menu__item--danger:hover { background: var(--danger-soft); }

/* ------------------------------- comments -------------------------------- */

.comment-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
.comment { display: flex; align-items: flex-start; gap: 10px; }
.comment__main { flex: 1; min-width: 0; }
.comment__head { display: flex; align-items: baseline; gap: 8px; }
.comment__head b { font-size: var(--fs-sm); }
.comment__body {
  margin-top: 3px; font-size: var(--fs-md); line-height: 1.5;
  overflow-wrap: anywhere; white-space: pre-wrap;
}
/* Only visible on hover or focus - a delete button on every comment in a thread
   is a row of small red invitations to lose something. */
.comment__delete { opacity: 0; color: var(--text-faint); transition: opacity 0.15s ease; }
.comment:hover .comment__delete, .comment__delete:focus-visible { opacity: 1; }
.comment__delete:hover { color: var(--danger); }
@media (hover: none) { .comment__delete { opacity: 0.5; } }

.mention {
  padding: 1px 5px; border-radius: 5px;
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--brand); font-weight: var(--fw-semibold);
}
/* Being named yourself is the one that has to catch your eye. */
.mention--me { background: var(--brand); color: var(--brand-contrast); }

.composer { position: relative; }
.composer__input { min-height: 68px; }
/* Over the thread rather than pushing it down, so the box does not jump away
   from under the thumb that is typing in it. */
.composer__people {
  position: absolute; left: 0; right: 0; bottom: calc(100% - 4px); z-index: 2;
  max-height: 232px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
}
.composer__person {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: 0; background: none; cursor: pointer; text-align: left;
}
.composer__person:hover { background: var(--surface-sunken); }

/* ------------------------------- entity tag ------------------------------ */

/* Which of the two companies a card belongs to. Quiet enough to sit beside a
   stage pill without competing with it, coloured enough to be read at a glance
   rather than actually read. */
.etag {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px; border-radius: 999px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); white-space: nowrap;
  color: var(--etag);
  background: color-mix(in srgb, var(--etag) 13%, transparent);
}
.etag::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--etag);
}

/* The two company buttons.
 *
 * Not tabs: a tab strip says "one of these", and these say "narrow to this
 * one, or press it again for both" - three states out of two controls, which
 * no tab strip can express. They live on the toolbar beside the view switcher
 * and the column menu, so they are shaped like those rather than like the
 * filters below, and the pill they used to be is gone with the rest.
 *
 * Pressed, each takes its own company's colour - the same colour already on
 * every row as its tag, so pressing Webcastle and watching the teal tags stay
 * put is the control explaining itself. */
.board-entities { display: flex; flex: none; gap: 6px; }

/* On a phone these two move into the filter sheet, where they are a labelled
   dropdown - "Organisation" - rather than two chips fighting the search box
   for a 375px row that also holds a view toggle.
 *
 * The modifier is on the screens whose sheet actually carries the same choice,
 * and only those. Accounts and Delivery show the same buttons and have no
 * filter sheet at all, so hiding theirs would take company filtering off a
 * phone rather than move it. */
.board-entities--in-filters { display: none; }
@media (min-width: 900px) { .board-entities--in-filters { display: flex; } }
.chip--entity {
  flex: none; padding: 7px 13px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-muted); cursor: pointer; white-space: nowrap;
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.chip--entity::after { content: none; }
.chip--entity:hover { background: var(--surface-sunken); color: var(--text); }
.chip--entity[aria-pressed='true'] {
  background: color-mix(in srgb, var(--etag) 16%, transparent);
  border-color: color-mix(in srgb, var(--etag) 45%, transparent);
  color: var(--etag); font-weight: var(--fw-semibold);
}
.chip--entity[aria-pressed='true']:hover {
  background: color-mix(in srgb, var(--etag) 24%, transparent);
  color: var(--etag);
}

/* --------------------------------- chat ---------------------------------- */

/* People on the left, one conversation on the right - and on a phone, one at a
   time, because 375px will not hold both and a squeezed thread is not a thread.
   The list gives way rather than the conversation: you arrive at this screen
   to say something. */
/* Fills what is left rather than guessing at it.
 *
 * The height used to be 100dvh minus a constant that stood for the header, the
 * padding and the tab bar - and the constant was wrong, leaving a hundred
 * pixels of nothing under the panels. This screen now uses the same
 * arrangement the boards do: the shell becomes a flex column, and the chat
 * takes the remainder. Nothing has to know how tall anything above it is. */
.chat {
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: 14px; flex: 1; min-height: 0;
}
body.chat-mode { overflow: hidden; }
body.chat-mode #app { height: 100dvh; overflow: hidden; }
body.chat-mode .shell { min-height: 0; }
body.chat-mode .main {
  display: flex; flex-direction: column; min-height: 0; overflow: hidden;
  padding-bottom: calc(var(--nav-space) + 6px);
  /* Tight to the menu on both sides.
   *
   * Every other screen is a column of content and wants a gutter to read
   * inside. This one is two panels that already have their own borders, and
   * the gutter was only pushing them away from the edge they belong against.
   *
   * The width cap goes too. .main stops at 1180px so a page of text does not
   * run to arm's length on a wide monitor - but a conversation is not a page
   * of text, and on a 1920 screen that cap was leaving five hundred pixels of
   * nothing to the right of the thread. */
  padding-left: 10px; padding-right: 10px; padding-top: 10px; max-width: none;
}
/* No tab bar on a wide screen, so nothing needs reserving for it - and with
   nothing to reserve, the same ten pixels go all the way round. */
@media (min-width: 900px) { body.chat-mode .main { padding-bottom: 10px; } }
@media (min-width: 860px) {
  .chat { grid-template-columns: 320px minmax(0, 1fr); }
}

.chat__list, .chat__thread {
  display: flex; flex-direction: column; min-height: 0;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
/* One panel at a time below 860px - which this used to promise and never did.
 *
 * The rule was `.chat__thread:not(:empty) ~ .chat__list`, and it could not
 * match: the list is written before the thread, and `~` only looks forwards. So
 * on a phone the list sat on top of every open conversation, and the thread had
 * whatever was left underneath it.
 *
 * What decides it now is the attribute paintThread() writes on the thread: the
 * id of the conversation on screen, or "none". Not :empty, because with nothing
 * open the thread holds its "pick someone" message - and no attribute at all is
 * the moment before the first paint, when the list is the one worth showing. */
@media (max-width: 859px) {
  .chat:has(> .chat__thread[data-conversation]:not([data-conversation="none"])) > .chat__list,
  .chat__thread:not([data-conversation]),
  .chat__thread[data-conversation="none"] { display: none; }
}
/* The way back to the list is the app header's arrow, drawn while a
   conversation is open. Beside the list there is nothing to go back to. */
@media (min-width: 860px) {
  body.chat-mode #header-back { display: none; }
}

/* The search sits in a field of its own rather than as a bare input on a rule,
   which is what makes the panel read as a panel. */
.chat__search {
  display: flex; align-items: center; gap: 8px; flex: none;
  padding: 12px 14px; border-bottom: 1px solid var(--border);
}
.chat__search .input {
  border: 0; background: none; padding: 0; min-width: 0; flex: 1;
}
.chat__search > .icon { color: var(--text-faint); flex: none; }

/* The two halves of the list, as the tab strip the rest of the app uses. */
.chat__kinds { flex: none; margin: 0; padding: 0 14px; }
.chat__kinds .chip { padding: 10px 14px; }
/* Unread waiting on the tab you are not looking at. Small and beside the
   label rather than a badge on the end, which would make the strip jump. */
.chat__kind-dot {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; margin-left: 6px; padding: 0 5px;
  border-radius: 999px; background: var(--danger); color: #fff;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
}

.chat__rows { overflow-y: auto; flex: 1; min-height: 0; padding: 6px; }

/* The line that stands in for a list that is not there yet, and the one that
   says a search found nobody - both offer the way out rather than only
   reporting the absence. */
.chat__none {
  display: block; width: 100%; padding: 22px 16px; text-align: center;
  border: 0; background: none; color: var(--text-muted); font: inherit;
  font-size: var(--fs-xs);
}
button.chat__none { cursor: pointer; }
button.chat__none:hover { color: var(--brand); }
.chat__none .icon { vertical-align: -3px; }

/* Inside the new-conversation sheet the search is a field, not a panel edge. */
.chat__search--sheet {
  margin: 12px 0; padding: 9px 12px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-sunken);
}
.chat__group-start { border-bottom: 1px solid var(--border); }
.chat__divider {
  padding: 14px 10px 6px; text-transform: uppercase; letter-spacing: 0.06em;
  font-weight: var(--fw-semibold);
}

/* A row is a card that lights up rather than a line with a border under it -
   the borders were drawing a table where this is a list of people. */
.chat__row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 10px 12px; margin-bottom: 2px;
  border: 0; border-radius: var(--radius-sm); background: none;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: background 0.15s ease;
}
.chat__row:hover { background: var(--surface-sunken); }
.chat__row.is-open { background: var(--brand-soft); }

/* The presence dot sits on the avatar, which is where everyone already looks
   for it, rather than beside the name where it competes with the text. */
.chat__face { position: relative; flex: none; }
.chat__dot {
  position: absolute; right: -1px; bottom: -1px;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--text-faint); border: 2px solid var(--surface);
}
.chat__dot.is-on { background: var(--s-won, #22c55e); }

.chat__who { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.chat__last { color: var(--text-muted); }
.chat__meta { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 5px; }

.chat__badge {
  min-width: 19px; height: 19px; padding: 0 6px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--brand); color: var(--brand-contrast);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
}

/* ------------------------------ the thread ------------------------------- */

.chat__head {
  display: flex; align-items: center; gap: 12px; flex: none;
  padding: 13px 16px; border-bottom: 1px solid var(--border);
}
.chat__on { color: var(--s-won, #22c55e); }

.chat__scroll {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 18px 16px; display: flex; flex-direction: column; gap: 3px;
}

/* A message is a face and a column beside it. The face is only drawn on the
   last of a run, and the gap is reserved on the others so the bubbles above
   stay in line with the one that has it. */
.chat__msg { display: flex; align-items: flex-end; gap: 9px; max-width: 100%; }
.chat__msg.is-mine { justify-content: flex-end; }
.chat__msg.is-last { margin-bottom: 12px; }
.chat__msg-face { width: 26px; flex: none; }

.chat__stack { display: flex; flex-direction: column; min-width: 0; max-width: min(78%, 520px); }
.chat__msg.is-mine .chat__stack { align-items: flex-end; }

/* Squared off on the side the speaker is on, so a run of them reads as one
   block coming from one direction. */
.chat__bubble {
  padding: 9px 13px; border-radius: 14px 14px 14px 4px;
  background: var(--surface-sunken); font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}
.chat__msg.is-mine .chat__bubble {
  background: var(--brand); color: var(--brand-contrast);
  border-radius: 14px 14px 4px 14px;
}
/* Sent, not yet acknowledged. Faded rather than hidden: clearly there, and
   clearly not finished. */
.chat__msg.is-pending .chat__bubble { opacity: 0.6; }

.chat__text { white-space: pre-wrap; }
.chat__text--gone { font-style: italic; opacity: 0.6; }

/* Under the bubble rather than inside it. A stamp tucked into the corner of a
   two-word message pushes the message around, and a column of bubbles all
   different widths for that reason reads as ragged. */
.chat__meta-line {
  display: flex; align-items: center; gap: 6px;
  margin: 4px 2px 0; color: var(--text-faint);
}
.chat__meta-line b { color: var(--text-muted); font-weight: var(--fw-semibold); }

/* One tick sent, two read - the convention people already know, so it needs no
   legend. The second tick overlaps the first. */
.chat__tick { display: inline-flex; }
.chat__tick .icon + .icon { margin-left: -5px; }
.chat__tick.is-seen { color: #38bdf8; }

.chat__typing { flex: none; min-height: 18px; padding: 0 16px; }
.chat__typing-text {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-xs); color: var(--text-muted); font-style: italic;
}

/* Three dots that actually move, because a static "typing..." reads as stuck. */
.chat__ellipsis { display: inline-flex; gap: 3px; align-items: center; }
.chat__ellipsis i {
  width: 4px; height: 4px; border-radius: 50%; background: currentColor;
  animation: chat-blink 1.2s infinite ease-in-out;
}
.chat__ellipsis i:nth-child(2) { animation-delay: 0.15s; }
.chat__ellipsis i:nth-child(3) { animation-delay: 0.3s; }
@keyframes chat-blink {
  0%, 60%, 100% { opacity: 0.25; }
  30% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .chat__ellipsis i { animation: none; opacity: 0.6; }
}

/* ------------------------- taking a message back -------------------------- */

/* The way in for a mouse: a button beside your own bubble while it can still be
   withdrawn, out of sight until the pointer is on the message. Only where there
   is a pointer to hover with - on a touch screen a button that only appears on
   hover is a button that is missing, so a phone holds the message instead. */
.chat__msg.can-withdraw .chat__bubble { position: relative; }
.chat__more {
  display: none; align-items: center; justify-content: center;
  position: absolute; top: 50%; right: calc(100% + 6px); transform: translateY(-50%);
  width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: 50%; background: none;
  color: var(--text-faint); cursor: pointer;
  transition: opacity 0.12s ease, background 0.15s ease, color 0.15s ease;
}
.chat__more:hover,
.chat__more[aria-expanded="true"] { background: var(--surface-sunken); color: var(--text); }
@media (hover: hover) and (pointer: fine) {
  .chat__more { display: inline-flex; opacity: 0; }
  .chat__msg:hover .chat__more,
  .chat__more:focus-visible,
  .chat__more[aria-expanded="true"] { opacity: 1; }
}
/* Holding one of these opens its menu, so the hold must not also start selecting
   its words, or bring up the phone's own menu for them. */
@media (hover: none) {
  .chat__msg.can-withdraw .chat__bubble {
    -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  }
}

/* Small, and beside the button that opened it: above the thread and the tab bar,
   below the sheet that asks whether you are sure. */
.chat__menu {
  position: fixed; z-index: 70; min-width: 190px; padding: 5px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
}
.chat__menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 11px; border: 0; border-radius: 8px; background: none;
  color: var(--text); font: inherit; font-size: var(--fs-sm);
  text-align: left; cursor: pointer;
}
.chat__menu-item:hover, .chat__menu-item:focus-visible { background: var(--surface-sunken); outline: none; }
.chat__menu-item--danger { color: var(--danger); }
.chat__menu-item--danger:hover, .chat__menu-item--danger:focus-visible { background: var(--danger-soft); }

/* ------------------------------ the composer ----------------------------- */

.chat__compose {
  display: flex; gap: 10px; align-items: center; flex: none;
  padding: 12px 16px; border-top: 1px solid var(--border);
}

/* One field carrying its own controls, rather than three things in a row. */
.chat__field {
  display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0;
  padding: 0 6px 0 16px; border-radius: 999px;
  background: var(--surface-sunken); border: 1px solid var(--border);
  transition: border-color 0.15s ease;
}
.chat__field:focus-within { border-color: var(--brand); }
.chat__field .input {
  border: 0; background: none; padding: 11px 0; min-width: 0; flex: 1;
}
.chat__field-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex: none;
  border: 0; border-radius: 50%; background: none;
  color: var(--text-faint); cursor: pointer;
}
.chat__field-btn:hover { color: var(--brand); background: var(--surface); }

/* Round and filled: the one thing on this bar that does something. */
.chat__send {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; flex: none;
  border: 0; border-radius: 50%; cursor: pointer;
  background: var(--brand); color: var(--brand-contrast);
  transition: background 0.15s ease;
}
.chat__send:hover { background: var(--brand-strong); }

/* A group has no face and no presence - it is a room, not a person. */
.chat__face--group {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: var(--radius-sm);
  background: var(--brand-soft); color: var(--brand);
}

/* Who said it, on the first of a run from the same person. Repeating it down a
   burst of six would be noise where the name is meant to be information. */
.chat__author {
  display: block; margin-bottom: 2px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); color: var(--brand);
}
.chat__msg.is-mine .chat__author { color: inherit; opacity: 0.85; }
.chat__text--gone { font-style: italic; opacity: 0.6; }

/* Picking people for a new group. */
.chat__picker {
  max-height: 280px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.chat__pick {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; cursor: pointer; font-size: var(--fs-sm);
}
.chat__pick + .chat__pick { border-top: 1px solid var(--border); }
.chat__pick:hover { background: var(--surface-sunken); }

.chat__member {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 4px; font-size: var(--fs-sm);
}
.chat__member + .chat__member { border-top: 1px solid var(--border); }

/* Files waiting to be sent, above the composer. */
.chat__tray { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px; }
.chat__tray:not(:empty) { padding-bottom: 8px; }
.chat__compose .btn--icon { flex: none; }

/* A picture inside a bubble is the message, so it gets room - but .shot is a
   fixed thumbnail and a max-width on the img inside it does nothing. The tile
   itself grows instead. */
.chat__bubble .attachments { margin-top: 6px; }
.chat__bubble .shot { width: 190px; height: 130px; }
.chat__msg.is-mine .chat__bubble .attach { color: inherit; border-color: rgba(255,255,255,0.25); }

/* ---------------------------- a record page ------------------------------ */

/* One deal, read the way a CRM record is read.
 *
 * This screen used to be a single narrow column with half the window empty
 * beside it - a phone layout shown on a desktop. It is two columns now: the
 * left is what you came to do (where the deal sits, what is next, the
 * conversation) and the right is what you came to look up, which is reference
 * and can sit still. Below 1000px they stack in that same order. */
.record { display: flex; flex-direction: column; }

.record__head {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 16px; margin-bottom: 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); border-left: 3px solid var(--pill-color, var(--brand));
}

/* The deal's initials, so the page has a face at the top rather than opening
   on a number. Tinted from the stage, which is the one colour already on it. */
.record__mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: none; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--pill-color, var(--brand)) 16%, transparent);
  color: var(--pill-color, var(--brand));
  font-size: var(--fs-md); font-weight: var(--fw-bold); letter-spacing: -0.02em;
}

.record__id { min-width: 0; flex: 1; }
.record__name {
  margin: 0; font-size: var(--fs-xl); font-weight: var(--fw-bold); letter-spacing: -0.02em;
  overflow-wrap: anywhere;
}
.record__sub { margin: 2px 0 0; font-size: var(--fs-sm); color: var(--text-muted); }
.record__tags { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 9px; }

.record__money { text-align: right; flex: none; display: flex; flex-direction: column; gap: 1px; }
.record__value { font-size: var(--fs-xl); font-weight: var(--fw-bold); letter-spacing: -0.02em; }

/* Actions on the left, the three ways to reach somebody on the right. */
.record__bar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
/* A contact route the deal does not have. Still readable, still says why. */
.record__bar .is-off { opacity: 0.4; pointer-events: none; }
.record__reach { display: flex; gap: 8px; flex: none; }

.record__note {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 11px 13px; margin-bottom: 12px; border-radius: var(--radius-sm);
  background: var(--surface-sunken); color: var(--text-muted); font-size: var(--fs-sm);
}
.record__note .icon { flex: none; margin-top: 1px; }

.record__cols { display: flex; flex-direction: column; gap: 0; }
.record__main, .record__aside { min-width: 0; }

@media (min-width: 1000px) {
  .record__cols { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 14px; align-items: start; }
}

/* Where the deal has got to, named rather than implied.
 *
 * This was a row of coloured bars you had to hover to identify, which is a row
 * nobody reads. The stage names are the information. */
.stepper { display: flex; list-style: none; margin: 0; padding: 0; gap: 4px; }
.stepper__step {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px;
}
.stepper__dot {
  height: 5px; border-radius: 3px; background: var(--surface-sunken);
  transition: background 0.2s ease;
}
.stepper__label {
  font-size: var(--fs-2xs); color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stepper__step.is-done .stepper__dot { background: var(--pill-color); }
.stepper__step.is-done .stepper__label { color: var(--text-muted); }
/* The stage it is sitting at now, as opposed to the ones it has been through. */
.stepper__step.is-now .stepper__label { color: var(--text); font-weight: var(--fw-semibold); }

.record__compose { display: flex; gap: 8px; align-items: stretch; margin-bottom: 14px; }

/* The payments panel is a button, so it has to stop looking like one. */
.record__money-open {
  display: block; width: 100%; border: 0; background: none;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.record__money-open:hover { background: var(--surface-sunken); }

.record__owner {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 12px 14px; border: 0; background: none; color: inherit;
  font: inherit; font-size: var(--fs-sm); text-align: left;
}
.record__owner--action { cursor: pointer; }
.record__owner--action:hover { background: var(--surface-sunken); }
.record__owner .icon { color: var(--text-faint); flex: none; }

/* Facts as pairs, because that is what they are.
 *
 * The label column is sized once from the widest label rather than guessed at,
 * so every value in a panel starts at the same place without a magic number. */
.dl { margin: 0; display: flex; flex-direction: column; }
.dl__pair {
  display: grid; grid-template-columns: minmax(88px, 38%) minmax(0, 1fr);
  gap: 10px; padding: 8px 0; align-items: baseline;
}
.dl__pair + .dl__pair { border-top: 1px solid var(--border); }
.dl__pair dt { font-size: var(--fs-xs); color: var(--text-faint); }
.dl__pair dd {
  margin: 0; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  overflow-wrap: anywhere;
}

/* A panel that is carrying a warning rather than a section of the record. */
.panel--flag { border-left: 3px solid var(--warning); }
.panel--danger { border-left-color: var(--danger); }

@media (max-width: 560px) {
  .record__head { flex-wrap: wrap; }
  .record__money { text-align: left; width: 100%; }
  .stepper__label { display: none; }
}

/* ------------------------- grouped, the way a task list groups ----------- */

/* Each stage its own block: a header carrying the stage, its count and its
   money, then that stage's own column headers and rows.
   
   Repeating the headers inside every block is the point rather than an
   oversight - it is what lets a stage be read on its own, and it is the
   difference between a grouped list and a table with dividers in it. */
.dtg + .dtg { margin-top: 18px; }
.dtg__head {
  display: flex; align-items: center; gap: 10px;
  padding: 0 2px 8px;
}
.dtg__fold {
  display: inline-flex; padding: 2px; border: 0; border-radius: var(--radius-sm);
  background: none; color: var(--text-faint); cursor: pointer;
  transition: transform 0.15s ease, color 0.15s ease;
}
.dtg__fold:hover { color: var(--text); }
.dtg--shut .dtg__fold { transform: rotate(-90deg); }

/* The stage's own name, in its own colour, sized like a label rather than a
   heading - the count beside it is the thing worth reading first. */
.dtg__chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--pill-color) 14%, transparent);
  color: var(--pill-color);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap;
}
.dtg__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.dtg__count { font-size: var(--fs-sm); font-weight: var(--fw-bold); }
.dtg__value { white-space: nowrap; }

.dtg__more {
  padding: 3px 9px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: none; color: var(--text-faint); cursor: pointer;
  font-size: var(--fs-2xs); white-space: nowrap;
}
.dtg__more:hover { color: var(--brand); border-color: var(--brand); }

/* A folded stage keeps its header and loses everything under it. */
.dtg--shut .dt__scroll { display: none; }

/* The ghost row at the foot of a stage - present, and quiet until wanted. */
.dt__adder td { padding: 0; }
.dt__add {
  display: inline-flex; align-items: center; gap: 7px; width: 100%;
  padding: 9px 12px; border: 0; background: none; cursor: pointer;
  color: var(--text-faint); font: inherit; font-size: var(--fs-sm); text-align: left;
}
.dt__add:hover { color: var(--brand); background: var(--surface-sunken); }

/* Grouped, each stage says its own "showing 40 of 5,183", so the strip at the
   bottom carries the totals alone and drops the paging buttons. */
.dt-foot--grouped .btn { display: none; }

/* --------------------------- the leads data table ----------------------- */

/* A table of deals, in the shape of shadcn's data table: one bordered, rounded
   surface with a muted header, rows that separate rather than float, and the
   selection count and paging sitting under it rather than above.
   
   Cards and table are both drawn every time and one of them is hidden - the
   table cannot be read at 375px and the cards waste a desk. Swapping them in
   CSS rather than in JavaScript means turning a phone sideways changes the
   layout without a repaint, and neither has to know the other exists. */
.dt { display: none; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }

/* Grouped, the outer frame goes: a stage block is its own object, and a border
   drawn around the whole run would tie five of them into one box. */
.dt--grouped { border: 0; border-radius: 0; overflow: visible; }
.dt--grouped .dt__table {
  border: 1px solid var(--border); border-radius: var(--radius-md);
  overflow: hidden; background: var(--surface);
}
.dt-cards { display: flex; flex-direction: column; gap: 10px; }

/* Paging belongs to the data, not to the table, so it stays on the phone.
   The list asks for one page whichever way it is drawn, and a phone with no
   Next button would be a phone that can only ever see the first 25 leads. */
.dt-foot { display: flex; align-items: center; gap: 8px; padding: 12px 2px 0; }

@media (min-width: 900px) {
  .dt { display: block; }
  .dt-cards { display: none; }
}

/* Wide tables scroll inside their own border rather than pushing the page. */
.dt__scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Fixed layout, and every column carrying its own width.
 *
 * Grouped, each stage is its own table, and an automatic layout would size
 * each one to its own contents - so Negotiation's Deal column would land at a
 * different width from New Lead's and the blocks would read as unrelated
 * tables rather than one list. Widths are declared once on the header and the
 * layout is told to honour them. */
.dt__table {
  width: 100%;
  table-layout: fixed; border-collapse: collapse; font-size: var(--fs-sm);
}

.dt__table thead th {
  position: sticky; top: 0; z-index: 1;
  text-align: left; padding: 6px 12px; white-space: nowrap;
  background: var(--surface-sunken); color: var(--text-faint);
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  text-transform: uppercase; letter-spacing: 0.04em;
  border-bottom: 1px solid var(--border);
}
.dt__table tbody td {
  padding: 6px 12px; border-bottom: 1px solid var(--border);
  white-space: nowrap; vertical-align: middle;
  /* A fixed layout gives a cell its width and then lets its contents spill
     straight over the next column - the client name was running underneath the
     stage pill beside it. The cell clips, and anything asking to truncate
     inside it gets a box to truncate within. */
  overflow: hidden; text-overflow: ellipsis;
}
.dt__table td .truncate { display: block; max-width: 100%; }
.dt__table td .dt__deal, .dt__table td .dt__owner { max-width: 100%; }
.dt__table tbody tr:last-child td { border-bottom: 0; }
.dt__table tbody tr { cursor: pointer; transition: background 0.12s ease; }
.dt__table tbody tr:hover { background: var(--surface-sunken); }
.dt__row--on, .dt__row--on:hover { background: color-mix(in srgb, var(--brand) 8%, transparent); }

.dt__right { text-align: right; }
.dt__table td.wrap { white-space: normal; min-width: 200px; max-width: 320px; }

/* The two narrow ends: the tick and the row menu. */
.dt__tick { width: 1%; padding-left: 12px; padding-right: 4px; }
.dt__check { width: 16px; height: 16px; accent-color: var(--brand); cursor: pointer; }

/* A sortable header is a button, so it has to stop looking like one. */
.dt__sort {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 0; margin: 0; border: 0; background: none; cursor: pointer;
  color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
}
.dt__sort:hover { color: var(--text); }
.dt__sort .icon { opacity: 0.45; }
.dt__sort:hover .icon { opacity: 0.8; }
.dt__sort--on { opacity: 1; color: var(--brand); }

.dt__deal { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.dt__owner { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }

/* A bar and its number in one cell, on the money table.
 *
 * The bar is what the column is for - a hundred of them read as a shape long
 * before any of the percentages are read as numbers - but the number is what
 * somebody quotes on the phone, so it sits beside rather than instead. Slimmer
 * than the bar on a card, because at row height a 9px bar is a stripe. */
.dt__meter { display: flex; align-items: center; gap: 8px; min-width: 0; }
.dt__meter .money-bar { flex: 1 1 auto; height: 6px; min-width: 40px; }

/* Two bars stacked in one cell, on the month-by-month table: in above out.
   Both are scaled against the same peak across every row, so the column can be
   read down as a shape - which is the one thing a table of numbers loses and
   the reason these survived the move out of the cards. */
.dt__bars { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.dt__bars .progress { height: 5px; }

/* A category and its colour. The dot is the stripe that used to run down the
   left of the card - the one thing that made that list scannable at a glance,
   and the only part of it worth a column. */
.dt__cat { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }

/* A row opened out underneath itself, for detail that belongs to one row and
   answers a different question from the columns - a retainer's months, say.
   Sunken so it reads as behind the row rather than as another one. */
.dt__open > td {
  padding: 12px 14px 14px; background: var(--surface-sunken);
  white-space: normal; border-bottom: 1px solid var(--border);
}
.dt__open:hover > td { background: var(--surface-sunken); }

/* Only worth a button once the table it configures is on screen. */
.dt-columns-btn { display: none; flex: none; }
@media (min-width: 900px) { .dt-columns-btn { display: inline-flex; } }

/* ------------------------------ pick-one --------------------------------- */

/* Two or three options that are all worth reading, as buttons rather than a
   dropdown - a choice hidden behind a tap is a choice made by whatever happened
   to be first. */
.pickone { display: flex; gap: 8px; flex-wrap: wrap; }
.pickone__opt { flex: 1 1 120px; min-width: 0; cursor: pointer; }
.pickone__opt input { position: absolute; opacity: 0; pointer-events: none; }
.pickone__opt span {
  display: block; padding: 10px 12px; text-align: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-muted);
  font-size: var(--fs-md); font-weight: var(--fw-medium);
}
.pickone__opt:hover span { border-color: var(--border-strong); }
.pickone__opt input:checked + span {
  border-color: var(--brand); color: var(--brand);
  background: color-mix(in srgb, var(--brand) 8%, var(--surface));
  font-weight: var(--fw-semibold);
}
.pickone__opt input:focus-visible + span { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ------------------------------- avatars --------------------------------- */

/* A photograph fills the disc the initials would have occupied, so every place
   that draws a person keeps its own size and spacing. */
.avatar--photo { padding: 0; overflow: hidden; background: var(--surface-sunken); }
.avatar--photo img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: inherit; }

/* A disc with a mark on its corner - the team list's trophy, which used to
   replace the initials and would have hidden a face. */
.avatar-badged { position: relative; display: inline-flex; flex: none; }
.avatar-badged__mark {
  position: absolute; right: -3px; bottom: -3px;
  display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%;
  background: #fef3c7; color: #b45309; border: 2px solid var(--surface);
}
.avatar-badged__mark .icon { width: 10px; height: 10px; }

.avatarpick { display: flex; align-items: flex-start; gap: 14px; }
.avatarpick__preview {
  width: 72px; height: 72px; flex: none; border-radius: 50%; overflow: hidden;
  display: grid; place-items: center;
  background: var(--surface-sunken); border: 1px solid var(--border);
}
.avatarpick__preview img { width: 100%; height: 100%; object-fit: cover; }
.avatarpick__empty { color: var(--text-faint); }
.avatarpick__actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
.avatarpick__actions .tiny { width: 100%; }

/* ============================ funnel dashboard ============================ */

/* A gap in the chip row, so "which desk" reads as its own question rather than
   as one more range button. */
.chips__split { flex: 0 0 1px; align-self: stretch; margin: 4px 4px; background: var(--border); }

/* --- the control strip ----------------------------------------------------
   Three buttons that each show their own answer, rather than eleven chips of
   which one is lit. */
.dash-controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.control {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 11px; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
}
.control:hover { border-color: var(--border-strong); }
.control > .icon { color: var(--text-faint); flex: none; }
.control__caret { margin-left: 2px; }
.control--on { border-color: var(--brand); color: var(--brand); }
.control--on > .icon { color: var(--brand); }

/* Accents for the four headline cards. Named for what they measure rather than
   for the colour, so a repaint is one edit here. */
:root {
  --k-pipeline: #7c5cff;
  --k-won: #16a34a;
  --k-leads: #0ea5e9;
  --k-deal: #f59e0b;
}

/* --- the grid -------------------------------------------------------------
   One column on a phone. The named rows are the proportions the design asks
   for and only apply once there is width to divide. */
.dash-row { display: grid; gap: 14px; margin-bottom: 14px; }
/* Two steps, not one. At 2fr:1fr the right-hand column is about 220px on a
   small laptop, which is not enough for a name and four figures - the names
   were truncating to a single letter. It takes a bigger share until there is
   room to be generous. */
@media (min-width: 900px) {
  .dash-row--2-1 { grid-template-columns: 1.5fr 1fr; }
  .dash-row--3-2 { grid-template-columns: 3fr 2fr; }
}
@media (min-width: 1200px) {
  .dash-row--2-1 { grid-template-columns: 2fr 1fr; }
}
.dash-row > * { min-width: 0; }

/* A column inside a row: two panels that together match the tall one beside
   them, sharing the leftover height between themselves. */
.dash-stack { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.dash-stack > .panel { margin-bottom: 0; }
.dash-stack > .panel:last-child { flex: 1; }

.panel {
  display: flex; flex-direction: column; margin-bottom: 14px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden;
}
.dash-row > .panel { margin-bottom: 0; }
.panel__head {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 14px 0;
}
.panel__head h3 {
  font-size: var(--fs-md); font-weight: var(--fw-semibold); margin: 0;
}
.panel__head .tiny { margin-left: auto; }
.panel__head .grow + .btn { margin-left: 0; }
.panel__body { padding: 12px 14px 14px; flex: 1; min-width: 0; }

/* --- headline cards -------------------------------------------------------
   Each carries a tinted wash of its own accent rather than a flat surface, so
   four numbers in a row are four things and not one striped block. */
.kpi--v {
  --kpi-accent: var(--brand);
  position: relative; display: flex; flex-direction: column; align-items: flex-start;
  gap: 2px; padding: 13px 14px 12px; overflow: hidden; text-align: left;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: inherit; width: 100%;
}
/* Only the ones that go somewhere claim to be clickable - these cards are a
   <div> as often as a <button>, and a pointer over a static figure is a
   promise the card cannot keep. */
button.kpi--v { cursor: pointer; }
.kpi--v::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(135deg, var(--kpi-accent) 0%, transparent 62%);
  opacity: 0.13;
}
button.kpi--v:hover::before { opacity: 0.2; }
button.kpi--v:hover { border-color: var(--kpi-accent); }
.kpi--v:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Named for the figure on the dashboard... */
.kpi--pipeline { --kpi-accent: var(--k-pipeline); }
.kpi--won      { --kpi-accent: var(--k-won); }
.kpi--leads    { --kpi-accent: var(--k-leads); }
.kpi--deal     { --kpi-accent: var(--k-deal); }
/* ...and for what it means anywhere else. Money in is green, money out is
   amber, something wrong is red, a plain count is blue. */
.kpi--good { --kpi-accent: var(--success); }
.kpi--warn { --kpi-accent: var(--warning); }
.kpi--bad  { --kpi-accent: var(--danger); }
.kpi--info { --kpi-accent: var(--info); }

.kpi--v .kpi__label {
  position: relative; font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted);
}
.kpi--v .kpi__value {
  position: relative; margin: 0; font-size: var(--fs-3xl); font-weight: var(--fw-bold);
  letter-spacing: -0.03em; line-height: 1.15;
}
/* The drawing sits under the figure and above the comparison, at a fixed
   height so four cards line up whatever is drawn in them. */
.kpi__art { position: relative; display: block; width: 100%; height: 30px; margin: 4px 0 2px; }
.kpi__art .spark { width: 100%; height: 100%; display: block; }
.kpi__art--ring, .kpi__art--faces { display: flex; align-items: center; height: 30px; }
.kpi__art--ring .ring { width: 30px; height: 30px; color: var(--text); }
.kpi__art--faces { gap: 0; }
.kpi__art--faces .avatar { margin-right: -7px; border: 2px solid var(--surface); }

.kpi__delta {
  position: relative; display: inline-flex; align-items: center; gap: 3px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: var(--text-muted);
}
.kpi__delta.is-up { color: var(--success); }
.kpi__delta.is-down { color: var(--danger); }
.kpi__delta.is-down .icon { transform: none; }

/* --- the funnel shape -----------------------------------------------------
   Drawing on the left, numbers on the right, sharing one row height so a
   stage's band lines up with its own figures. */
/* The body is a column so the funnel can take whatever height is going and
   the summary sits on the floor of the panel, rather than the drawing ending
   halfway down with nothing underneath it. */
.panel__body--split { display: flex; flex-direction: column; gap: 14px; }
.panel__body--split > .funnelshape { flex: 1; min-height: 190px; }

/* The drawing gives up width before the labels do. At 40% on a small laptop
   the stage names were truncating - and a funnel you cannot read the stages of
   is a decoration. It gets its generous share back once there is room. */
.funnelshape { display: grid; grid-template-columns: minmax(84px, 33%) 1fr; gap: 12px; align-items: stretch; }
@media (min-width: 1200px) {
  .funnelshape { grid-template-columns: minmax(96px, 40%) 1fr; }
}
.funnelshape__svg { width: 100%; height: 100%; display: block; }
.funnelshape__rows { display: flex; flex-direction: column; }
.funnelshape__row {
  display: grid; align-items: center; gap: 8px; flex: 1;
  grid-template-columns: 8px minmax(0, 1fr) auto auto auto;
  padding: 0 6px; margin: 0; border: 0; border-radius: var(--radius-sm);
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.funnelshape__row:hover { background: var(--surface-sunken); }
.funnelshape__dot { width: 8px; height: 8px; border-radius: 50%; }
.funnelshape__label {
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.funnelshape__count { font-size: var(--fs-md); font-weight: var(--fw-bold); }

/* The four figures the funnel implies, along the bottom of its own panel. */
.funnelstats {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
  padding-top: 12px; border-top: 1px solid var(--border);
}
.funnelstats > span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.funnelstats b { font-size: var(--fs-lg); font-weight: var(--fw-bold); letter-spacing: -0.02em; }
@media (max-width: 560px) { .funnelstats { grid-template-columns: repeat(2, 1fr); row-gap: 12px; } }
.funnelshape__value, .funnelshape__pct { white-space: nowrap; }
@media (max-width: 560px) {
  .funnelshape { grid-template-columns: 64px 1fr; gap: 8px; }
  .funnelshape__value { display: none; }
}

/* The leak.

   A funnel only draws the stages a deal travels through, so Lost - which is
   where most of them end up - had nowhere to appear, and the biggest number on
   the page was the one that was not on it. It sits below the shape rather than
   in it: an outcome, not a stage, tinted so it never reads as another step. */
.funnel-lost {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; margin: 0; border: 0; border-radius: var(--radius-sm);
  background: var(--danger-soft); color: var(--danger);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-semibold);
  text-align: left; cursor: pointer;
}
.funnel-lost:hover { filter: brightness(0.97); }
.funnel-lost:focus-visible { outline: 2px solid var(--danger); outline-offset: 1px; }
.funnel-lost b { font-size: var(--fs-md); }
.funnel-lost .faint { color: inherit; opacity: 0.7; }
.funnel-lost__dot {
  width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--danger);
}

/* And why, when anybody has written it down. */
.lostwhy { padding-top: 12px; border-top: 1px solid var(--border); }
.lostwhy__head { margin-bottom: 6px; }

/* --- how it splits -------------------------------------------------------
   One axis per block: a key, then a row per answer. The bar under each label
   carries the proportion, so the comparison is visible before you read a
   single figure. */
.split + .split { margin-top: 12px; }
/* The name column is what has to survive: "Webcastle" truncated to "W..." tells
   you nothing, while a money figure one character narrower still reads. So the
   numbers take only what they need and the name gets the rest. */
.split__key, .split__row {
  display: grid; align-items: center; gap: 6px;
  grid-template-columns: minmax(58px, 1fr) 32px minmax(44px, max-content) minmax(40px, max-content);
}
/* Only the axis is shouted. "LEADS OPEN WON" set in spaced capitals needs more
   width than the columns they head, and lowercase hints read as hints - which
   is what they are - rather than competing with the section title. */
.split__key { padding: 0 6px 4px; }
.split__key > :first-child { text-transform: uppercase; letter-spacing: 0.04em; }
/* Truncate rather than overlap: the axis label is the longest word in the row
   and was running straight into the column headings beside it. */
.split__key > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.split__row {
  width: 100%; padding: 6px; margin: 0; cursor: pointer; text-align: left;
  border: 0; border-radius: var(--radius-sm);
  background: none; color: inherit; font: inherit;
}
.split__row:hover { background: var(--surface-sunken); }
.split__row--on { background: color-mix(in srgb, var(--brand) 9%, transparent); }
.split__name {
  min-width: 0; font-size: var(--fs-sm); font-weight: var(--fw-semibold);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.split__bar {
  display: block; height: 3px; margin-top: 4px; border-radius: 2px;
  background: var(--surface-sunken); overflow: hidden;
}
.split__bar i { display: block; height: 100%; background: var(--brand); border-radius: 2px; }
.split__n { text-align: right; }
.split__num { font-size: var(--fs-sm); font-weight: var(--fw-semibold); text-align: right; white-space: nowrap; }

/* --- donut ---------------------------------------------------------------- */
.sourcepanel { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.donut { position: relative; width: var(--donut-size); height: var(--donut-size); flex: none; color: var(--text); }
.donut svg { width: 100%; height: 100%; display: block; }
.donut__center {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 0; pointer-events: none;
}
.donut__center b { font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -0.02em; }
.sourcepanel__key { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 140px; }
.sourcekey { display: grid; grid-template-columns: 9px 1fr auto auto; align-items: center; gap: 8px; font-size: var(--fs-sm); }
.sourcekey i { width: 9px; height: 9px; border-radius: 3px; }
.sourcekey b { font-weight: var(--fw-semibold); }

/* --- trend ---------------------------------------------------------------- */
.linetrend { color: var(--text); }
.linetrend__legend { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 8px; color: var(--text-muted); }
.linetrend__plot { position: relative; }
.linetrend__plot svg { width: 100%; height: 150px; display: block; }
.linetrend__tips { position: absolute; inset: 0; pointer-events: none; }
/* The end-of-line label. Right-aligned inside the plot so it never runs past
   the card, and stacked by its own line's height rather than a fixed slot. */
.linetrend__tip {
  position: absolute; right: 0; padding: 1px 6px; border-radius: 5px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold); color: #fff; white-space: nowrap;
}
.linetrend__months { display: flex; justify-content: space-between; margin-top: 6px; }

/* --- reps ----------------------------------------------------------------- */
.reprail {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(124px, 1fr);
  gap: 10px; overflow-x: auto; padding-bottom: 4px;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
}
.repcard {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 14px 10px 10px; scroll-snap-align: start; cursor: pointer; text-align: center;
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: inherit;
}
.repcard:hover { border-color: var(--border-strong); }
.repcard__rank {
  position: absolute; top: 8px; left: 8px; width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center; font-size: var(--fs-3xs); font-weight: var(--fw-bold);
  color: #3b2f00; background: var(--border-strong);
}
.repcard__rank--1 { background: #fbbf24; }
.repcard__rank--2 { background: #cbd5e1; }
.repcard__rank--3 { background: #e0a372; }
.repcard__name { font-size: var(--fs-sm); font-weight: var(--fw-semibold); max-width: 100%; }
.repcard__value { font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--s-won); }
.repcard__spark { display: block; width: 100%; height: 24px; margin-top: 4px; }
.repcard__spark .spark { width: 100%; height: 100%; }

/* --- deal table -----------------------------------------------------------
   Real columns above 760px. Below it the same rows become stacked cards -
   five columns in a phone's width is a horizontal scroll nobody performs. */
.dealtable { display: flex; flex-direction: column; }
.dealtable__head, .dealtable__row {
  display: grid; align-items: center; gap: 12px;
  grid-template-columns: minmax(0, 2.2fr) 110px minmax(0, 1.4fr) 92px 96px;
  padding: 6px 14px;
}
.dealtable__head {
  text-transform: uppercase; letter-spacing: 0.04em;
  border-bottom: 1px solid var(--border);
}
.dealtable__row {
  margin: 0; border: 0; border-bottom: 1px solid var(--border);
  background: none; color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.dealtable__row:last-child { border-bottom: 0; }
.dealtable__row:hover { background: var(--surface-sunken); }
.dealtable__name { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: var(--fs-sm); }
.dealtable__amount { text-align: right; font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
.dealtable__flag { justify-self: start; }

@media (max-width: 760px) {
  .dealtable__head { display: none; }
  .dealtable__row {
    grid-template-columns: 1fr auto; gap: 4px 10px; padding: 10px 14px;
  }
  .dealtable__name { grid-column: 1; }
  .dealtable__amount { grid-column: 2; grid-row: 1; }
  .dealtable__when { grid-column: 1 / -1; }
  .dealtable__stage { grid-column: 1; }
  .dealtable__flag { grid-column: 2; justify-self: end; }
}

/* The tag on a lead. Quiet - it is a fact about the deal, not a state of it. */
.region-tag {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--fs-2xs); font-weight: var(--fw-semibold);
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;
}

/* ------------------------------ attachments ------------------------------ */

.composer__actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.composer__hint { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Pasting a screenshot is a keyboard habit, so the line about it is the first
   thing to drop on a phone. The spacer beside it is what keeps Comment on the
   right, which is where every other action in the app sits. */
@media (max-width: 480px) { .composer__hint { display: none; } }

/* Files chosen but not yet sent. Between the box and the buttons, where the
   eye already is after picking them. */
.composer__tray { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.chipfile {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
  padding: 4px 4px 4px 8px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-sunken);
  font-size: var(--fs-sm);
}
.chipfile > .icon { color: var(--text-faint); flex-shrink: 0; }
.chipfile__thumb {
  width: 24px; height: 24px; border-radius: 4px; object-fit: cover; flex-shrink: 0;
}
.chipfile__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 140px; }
.chipfile__size { flex-shrink: 0; }
.chipfile__x {
  display: inline-flex; padding: 2px; border: 0; border-radius: 4px;
  background: none; color: var(--text-faint); cursor: pointer; flex-shrink: 0;
}
.chipfile__x:hover { background: var(--danger-soft); color: var(--danger); }

.attachments { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.attachments__shots { display: flex; flex-wrap: wrap; gap: 6px; }

/* A screenshot is shown, not named. Held at a size that reads at a glance and
   still leaves the thread readable. */
.shot {
  position: relative; width: 116px; height: 88px; padding: 0; overflow: hidden;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); cursor: zoom-in;
}
.shot img { width: 100%; height: 100%; object-fit: cover; display: block; opacity: 0; transition: opacity 0.2s ease; }
.shot--ready img { opacity: 1; }
.shot--ready .shot__spinner, .shot--failed .shot__spinner { display: none; }
/* The same shimmer the lists use while they load - a picture arriving should
   look like everything else arriving. */
.shot__spinner {
  position: absolute; inset: 0;
  background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border) 50%, var(--surface-sunken) 75%);
  background-size: 200% 100%; animation: shimmer 1.3s infinite linear;
}
.shot--failed { border-style: dashed; cursor: default; }
.shot--failed::after {
  content: 'Could not load'; position: absolute; inset: 0;
  display: grid; place-items: center; font-size: var(--fs-xs); color: var(--text-faint);
}
/* In the viewer it is the picture that matters, so it gets the room. */
.shot__full { display: block; max-width: 100%; max-height: 72vh; margin: 0 auto; border-radius: var(--radius-sm); }

/* Everything that is not a picture: a row that says what it is and downloads. */
.attach {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); cursor: pointer; text-align: left;
  font-size: var(--fs-sm); color: var(--text);
}
.attach:hover { background: var(--surface-sunken); border-color: var(--border-strong, var(--border)); }
.attach > .icon { color: var(--text-faint); flex-shrink: 0; }
.attach__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach__size { flex-shrink: 0; }
.attach:disabled { opacity: 0.6; cursor: default; }

/* Looking at a file rather than saving it. Sized to the sheet, which goes wide
   for anything that is not a picture - 520px is a column, and a page of a PDF
   read in a column is a page nobody reads. */
.viewer { min-height: 200px; display: flex; flex-direction: column; }
.viewer__wait { display: flex; align-items: center; gap: 8px; margin: auto; color: var(--text-faint); }
.viewer__frame { width: 100%; height: 68vh; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-sunken); }
.viewer__text {
  max-height: 68vh; overflow: auto; margin: 0; padding: 12px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); white-space: pre-wrap; word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--fs-sm);
}

/* The download, once the row itself opens the file instead. */
.attach__save {
  display: grid; place-items: center; width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: var(--radius-sm); background: none;
  color: var(--text-faint); cursor: pointer;
}
.attach__save:hover { background: var(--surface-sunken); color: var(--text); }

/* A file on a card can be taken back on its own, so it wears its own x.
   A comment's cannot - they go when the comment does - which is why the
   wrappers exist but the button inside them is optional. */
.shot-wrap { position: relative; display: block; }
/* A row, a download and sometimes an x - laid out rather than overlaid, so the
   name truncates against the buttons instead of underneath them. */
.attach-wrap { display: flex; align-items: center; gap: 2px; width: 100%; }
.attach-wrap .attach { flex: 1; min-width: 0; }
.shot__x {
  position: absolute; top: 3px; right: 3px;
  display: grid; place-items: center; width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: 50%; cursor: pointer;
  background: var(--surface); color: var(--text-faint);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.24);
}
.attach-wrap .shot__x { position: static; flex: none; box-shadow: none; background: none; }
.shot__x:hover { background: var(--danger-soft); color: var(--danger); }


/* The card's own files, under Notes.
   No align-items here: this row is also .field-row--stacked, which stretches
   its value to the full width, and an align-items of its own further down the
   sheet quietly won - leaving the textarea 241px wide in a 356px row while
   every other field filled it. */
.field-row--files .attachments { width: 100%; margin-top: 0; }
.notefiles { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.notefiles__bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* What the rep wrote on the deal, read on the card. Quiet and boxed, so it
   reads as something brought in from elsewhere rather than as this card's own
   notes - which sit directly above it in a field somebody can type into. */
.dealnote {
  white-space: pre-wrap; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* ----------------------------- notifications ---------------------------- */

.notif-list { display: flex; flex-direction: column; gap: 2px; }
.notif {
  display: flex; align-items: flex-start; gap: 4px;
  border-radius: var(--radius-sm); padding-right: 8px;
}
.notif:hover { background: var(--surface-2); }
.notif--unread { background: color-mix(in srgb, var(--brand) 7%, transparent); }
.notif--unread:hover { background: color-mix(in srgb, var(--brand) 11%, transparent); }

.notif__main {
  flex: 1; min-width: 0;
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px; border: 0; background: none;
  cursor: pointer; text-align: left; font: inherit; color: inherit;
}

.notif__icon {
  flex: none; width: 32px; height: 32px; display: grid; place-items: center;
  border-radius: 50%; background: var(--surface-2); color: var(--text-muted);
}
.notif__icon--unread { background: color-mix(in srgb, var(--brand) 16%, transparent); color: var(--brand); }

.notif__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.notif__text b { font-size: var(--fs-md); line-height: 1.3; }
.notif__body { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.4; }
.notif__when { font-size: var(--fs-2xs); color: var(--text-faint); }

/* Big enough to hit with a thumb without turning the row into two buttons. */
.notif__toggle {
  flex: none; align-self: center;
  width: 32px; height: 32px; display: grid; place-items: center;
  border: 0; background: none; border-radius: 50%;
  color: var(--text-faint); cursor: pointer;
}
.notif__toggle:hover { background: var(--surface); color: var(--text); }
.notif__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--brand); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* -------------------------------- rewards -------------------------------- */
/*
 * The one screen in this app that is meant to be enjoyed rather than worked.
 *
 * It is louder than everything else here on purpose - a gradient headline, tier
 * colours, a bar behind every name on the board. That is the point of it: the
 * funnel and the accounts pages are instruments, and this is a scoreboard.
 *
 * The loudness is still built out of the same tokens as the quiet screens, so
 * it moves with the theme instead of being a dark poster pasted into a light
 * app. Every accent is mixed from --col or a semantic token, never a hex.
 */

.rw { display: flex; flex-direction: column; gap: 18px; }

/* --------------------------------- hero ---------------------------------- */

.rw__hero { display: flex; flex-direction: column; gap: 8px; }

.rw__eyebrow {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--warning);
}

.rw__headline {
  margin: 0;
  /* Grows with the screen without a single literal size: the scale supplies
     both ends and the viewport picks between them. */
  font-size: clamp(var(--fs-2xl), 5.2vw, var(--fs-4xl));
  font-weight: var(--fw-bold); letter-spacing: -0.03em; line-height: 1.1;
}
.rw__headline em {
  font-style: normal;
  background: linear-gradient(96deg, var(--warning), var(--danger));
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
/* Safari and Firefox both do background-clip:text, but a printer or a forced
   -colours mode does not - and a transparent headline is an invisible one. */
@media (forced-colors: active) {
  .rw__headline em { color: var(--text); -webkit-text-fill-color: currentColor; }
}

/* ------------------------------- your season ------------------------------ */

.rw__mine {
  display: flex; flex-direction: column; gap: 14px;
  padding: 16px; border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--brand) 34%, transparent);
  background:
    radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--brand) 16%, transparent), transparent 60%),
    var(--surface);
  box-shadow: var(--shadow-sm);
}
.rw__mine-head { display: flex; align-items: center; gap: 10px; }
.rw__mine-who { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }

.rw__rank {
  flex: none; display: flex; flex-direction: column; align-items: center;
  padding: 6px 12px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--brand); line-height: 1.1;
}
.rw__rank b { font-size: var(--fs-xl); font-weight: var(--fw-bold); }
.rw__rank .tiny { color: var(--text-muted); }

.rw__mine-figure { display: flex; flex-direction: column; gap: 2px; }
.rw__mine-label {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-faint);
}
.rw__mine-value {
  font-size: clamp(var(--fs-3xl), 8vw, var(--fs-4xl));
  font-weight: var(--fw-bold); letter-spacing: -0.02em; color: var(--success);
}

.rw__mine-stats {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
  padding-top: 12px; border-top: 1px solid var(--border);
}
.rw__mine-stats > span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.rw__mine-stats b {
  font-size: var(--fs-lg); font-weight: var(--fw-bold);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 420px) {
  .rw__mine-stats { grid-template-columns: repeat(2, 1fr); }
}

/* ------------------------------- the board -------------------------------- */

.rw__block { display: flex; flex-direction: column; gap: 10px; }
.rw__block-title {
  display: flex; align-items: center; gap: 7px; margin: 0;
  font-size: var(--fs-md); font-weight: var(--fw-bold);
}
.rw__block-title .tiny { margin-left: auto; font-weight: var(--fw-medium); }

.rw__board { display: flex; flex-direction: column; gap: 6px; }

.rw__row {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text); font-family: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.14s, transform 0.14s;
}
/* How far along everyone is, as a rule along the bottom of the row.
 *
 * This started as a tint filling the row to the left, which was a mistake worth
 * recording: a 9% brand wash on a white row lands almost exactly on the page
 * background, so the *unfilled* remainder was the part that stood out as white,
 * and every bar read backwards. A meter that occupies a definite few pixels
 * cannot be misread that way.
 *
 * Comparative, not absolute - the leader fills the row and everyone else is
 * read against them, which is the only comparison a leaderboard is for. */
.rw__row::after {
  content: ''; position: absolute; inset: auto auto 0 0;
  height: 3px; width: var(--share, 0%);
  background: linear-gradient(90deg, var(--brand), var(--success));
  border-radius: 0 3px 3px 0;
  pointer-events: none;
  transition: width 0.4s ease;
}
.rw__row > * { position: relative; }
.rw__row:hover { border-color: var(--border-strong); transform: translateY(-1px); }
.rw__row--me {
  border-color: color-mix(in srgb, var(--brand) 55%, transparent);
  background: color-mix(in srgb, var(--brand) 5%, var(--surface));
}

.rw__row-rank {
  flex: none; display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 8px;
  background: var(--surface-sunken); color: var(--text-muted);
  font-size: var(--fs-sm); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
/* Only the podium is coloured. Colouring every place makes none of them mean
   anything, and eighth does not need celebrating in gold. */
.rw__row-rank--1 { background: color-mix(in srgb, var(--warning) 22%, transparent); color: var(--warning); }
.rw__row-rank--2 { background: color-mix(in srgb, var(--text-muted) 20%, transparent); color: var(--text); }
.rw__row-rank--3 { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }

.rw__row-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rw__row-figures {
  flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
}
.rw__row-figures b { font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--success); }

/* ------------------------------ the promises ------------------------------ */

.rw__claims { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
@media (max-width: 760px) { .rw__claims { grid-template-columns: 1fr; } }

.rw__claim {
  display: flex; flex-direction: column; gap: 4px;
  padding: 14px 16px; border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--claim) 40%, transparent);
  background:
    radial-gradient(130% 130% at 0% 0%, color-mix(in srgb, var(--claim) 14%, transparent), transparent 62%),
    var(--surface);
}
.rw__claim--base    { --claim: var(--success); }
.rw__claim--bonus   { --claim: var(--warning); }
.rw__claim--ceiling { --claim: #a855f7; }

.rw__claim-label {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--claim);
}
.rw__claim-value {
  font-size: var(--fs-2xl); font-weight: var(--fw-bold);
  letter-spacing: -0.02em; color: var(--claim);
}

/* -------------------------------- the ladder ------------------------------ */

.rw__tiers {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); overflow: hidden;
}

.rw__tier {
  display: grid;
  grid-template-columns: 110px 1.2fr 62px 78px 72px 1.6fr;
  align-items: center; gap: 10px;
  padding: 11px 14px; border-top: 1px solid var(--border);
}
.rw__tier:first-child { border-top: 0; }

.rw__tier--head {
  background: var(--surface-sunken);
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint);
}

.rw__tier-pill {
  display: inline-block; padding: 4px 11px; border-radius: 999px;
  background: color-mix(in srgb, var(--col) 18%, transparent);
  /* Mixed towards the page's own text colour so an amber tier is readable on
     white and the same amber is readable on black - the trick the board column
     headers use, for the same reason. */
  color: color-mix(in srgb, var(--col) 80%, var(--text));
  font-size: var(--fs-2xs); font-weight: var(--fw-bold); white-space: nowrap;
}
.rw__tier-bonus { color: color-mix(in srgb, var(--col) 80%, var(--text)); font-weight: var(--fw-semibold); }
.rw__tier-total { font-weight: var(--fw-bold); }
.rw__tier--top .rw__tier-total { color: var(--success); }
.rw__tier-eg { color: var(--text-muted); font-size: var(--fs-sm); }
.rw__tier-eg b { color: var(--text); }
.rw__tier--top .rw__tier-eg b { color: var(--success); }

/* Six columns will not fit a phone, and a sideways scroll on the one screen
   people open to feel good about themselves is a poor trade. Each tier becomes
   its own small card instead, with the labels the header row was carrying. */
@media (max-width: 720px) {
  .rw__tier--head { display: none; }
  .rw__tier {
    grid-template-columns: auto 1fr; gap: 4px 10px;
    padding: 12px 14px;
  }
  .rw__tier > span:nth-child(2) { text-align: right; font-size: var(--fs-sm); }
  .rw__tier > span:nth-child(3) { display: none; }
  .rw__tier > span:nth-child(4)::before { content: 'bonus '; color: var(--text-faint); }
  .rw__tier > span:nth-child(5) { text-align: right; }
  .rw__tier > span:nth-child(5)::before { content: 'total '; color: var(--text-faint); font-weight: var(--fw-medium); }
  .rw__tier-eg { grid-column: 1 / -1; padding-top: 6px; border-top: 1px dashed var(--border); }
}

/* ------------------------------- the target ------------------------------- */

.rw__season {
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px; border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--warning) 34%, transparent);
  background:
    radial-gradient(120% 160% at 100% 0%, color-mix(in srgb, var(--warning) 13%, transparent), transparent 58%),
    radial-gradient(100% 140% at 0% 100%, color-mix(in srgb, var(--brand) 12%, transparent), transparent 60%),
    var(--surface);
}
.rw__season-top { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
.rw__season-top > div:first-child { flex: 1 1 240px; min-width: 0; }
.rw__season-payout { flex: none; display: flex; flex-direction: column; align-items: flex-end; text-align: right; }

.rw__season-label {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-faint);
}
.rw__season-headline {
  display: block; margin-top: 4px;
  font-size: clamp(var(--fs-xl), 4vw, var(--fs-2xl));
  font-weight: var(--fw-bold); letter-spacing: -0.02em;
}
.rw__season-value {
  font-size: clamp(var(--fs-2xl), 5vw, var(--fs-3xl));
  font-weight: var(--fw-bold); color: var(--success); letter-spacing: -0.02em;
}

.rw__season-bar {
  height: 10px; border-radius: 999px; overflow: hidden;
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
.rw__season-bar span {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--brand), var(--success));
  transition: width 0.4s ease;
}
.rw__season-foot { display: flex; flex-wrap: wrap; gap: 4px 14px; }

.rw__empty {
  padding: 16px; text-align: center;
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius);
}
.rw__note { display: flex; align-items: flex-start; gap: 7px; margin: 0; }
.rw__note .icon { flex: none; margin-top: 2px; }

/* ------------------------- the scheme, being edited ----------------------- */

/* Welded to the right of a field the way __prefix is to the left. The prefix
   rule cannot simply be reused: the border and the corners have to come off the
   other side. */
.input-group__suffix {
  display: flex; align-items: center; flex: none;
  padding: 0 12px; min-height: 48px;
  background: var(--surface-sunken); color: var(--text-muted);
  border: 1px solid var(--border); border-left: 0;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: var(--fs-input); font-weight: var(--fw-semibold);
}
.input-group .input:has(+ .input-group__suffix),
.input-group__prefix + .input:has(+ .input-group__suffix) {
  border-radius: 0;
}
.input-group > .input:first-child:has(+ .input-group__suffix) {
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}

.tier-row {
  padding: 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  border-left: 3px solid var(--col);
  background: var(--surface);
}
.tier-row__top { display: flex; align-items: center; gap: 8px; }
/* A field with furniture on both sides. .input carries width:100%, which as a
   flex basis is the whole group - so the prefix and suffix squeezed the number
   out of existence and the bonus was an empty box. Basis zero instead, and it
   takes whatever the two of them leave. */
.tier-row__top .input-group { flex: none; width: 148px; }
.tier-row__top .input-group .input { flex: 1 1 0; min-width: 0; text-align: center; padding-left: 6px; padding-right: 6px; }
.tier-row__range { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.tier-row__range .input { min-width: 0; }
.tier-row__range .tiny { flex: none; white-space: nowrap; }
.tier-row__swatches { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }

.season-row {
  padding: 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface);
}
.season-row--on {
  border-color: color-mix(in srgb, var(--success) 50%, transparent);
  background: color-mix(in srgb, var(--success) 6%, var(--surface));
}
.season-row .tiny { flex: none; }
/* A season that has been paid.
 *
 * Deliberately calm rather than alarming - nothing is wrong, the figures are
 * simply final. Green because it is the same settled meaning the Live pill and
 * the Delivered column already carry. */
.rw__lock {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 13px 15px; border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--success) 34%, transparent);
  background: color-mix(in srgb, var(--success) 7%, var(--surface));
}
.rw__lock-icon {
  flex: none; display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 50%;
  background: color-mix(in srgb, var(--success) 18%, transparent);
  color: var(--success);
}
.rw__lock > span:last-child { flex: 1; min-width: 0; }
.rw__lock b { font-size: var(--fs-md); font-weight: var(--fw-semibold); }

/* Deals that arrived after the money did. Amber, because somebody still has to
   decide what to do about them - it is not settled and should not look it. */
.rw__lock-late {
  display: flex; align-items: flex-start; gap: 6px; margin-top: 8px;
  padding: 8px 10px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  color: color-mix(in srgb, var(--warning) 78%, var(--text));
}
.rw__lock-late .icon { flex: none; margin-top: 2px; }

.season-row__lock {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 10px; padding-top: 10px;
  border-top: 1px dashed var(--border);
}
.season-row__lock > span:first-child {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px;
}
.season-row--locked { border-color: color-mix(in srgb, var(--success) 45%, transparent); }
.season-row--locked .season-row__lock > span:first-child { color: var(--success); }
.season-row--locked .input:disabled { opacity: 0.72; }


/* The strip at the top of the funnel page.
 *
 * Rewards lives in the sidebar, and on a phone there is no sidebar - so without
 * this the one screen written for the sales team would be the one they could
 * not reach. It doubles as the gamified hook: the number is on the landing
 * page rather than two taps into a menu. */
.rwstrip {
  display: flex; align-items: center; gap: 12px; width: 100%;
  margin-bottom: 12px; padding: 12px 14px;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--warning) 32%, transparent);
  background:
    radial-gradient(120% 200% at 0% 0%, color-mix(in srgb, var(--warning) 12%, transparent), transparent 55%),
    var(--surface);
  color: var(--text); font-family: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.14s, transform 0.14s;
}
.rwstrip:hover { border-color: color-mix(in srgb, var(--warning) 60%, transparent); transform: translateY(-1px); }
.rwstrip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.rwstrip__icon {
  flex: none; display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 12px;
  background: color-mix(in srgb, var(--warning) 16%, transparent);
  color: var(--warning);
}
.rwstrip__main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rwstrip__main b { font-size: var(--fs-lg); font-weight: var(--fw-bold); }
.rwstrip__rank {
  flex: none; display: flex; flex-direction: column; align-items: center; line-height: 1.15;
  padding: 4px 10px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--brand) 12%, transparent); color: var(--brand);
}
.rwstrip__rank b { font-size: var(--fs-md); font-weight: var(--fw-bold); }
.rwstrip__rank .tiny { color: var(--text-muted); }
.rwstrip > .icon { flex: none; color: var(--text-faint); }

/* ------------------------------ roles and access -------------------------- */
/*
 * The permission matrix.
 *
 * Written to be read rather than scanned: every row says in a sentence what it
 * grants, because "finance: write" tells an admin nothing about whether the
 * person will see a salary. The three-way control is a segmented choice rather
 * than two checkboxes - none, view, edit are one decision, and two checkboxes
 * would let somebody express "edit but not view", which is not a thing.
 */

.rolecard {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); overflow: hidden;
}
.rolecard[open] { border-color: var(--border-strong); }
.rolecard--system { background: var(--surface-2); }

.rolecard__head {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; cursor: pointer; list-style: none;
}
.rolecard__head::-webkit-details-marker { display: none; }
.rolecard__head::after {
  content: ''; flex: none; width: 8px; height: 8px; margin-left: 2px;
  border-right: 2px solid var(--text-faint); border-bottom: 2px solid var(--text-faint);
  transform: rotate(45deg); transition: transform 0.16s;
}
.rolecard[open] .rolecard__head::after { transform: rotate(-135deg); }
.rolecard__head:hover { background: var(--surface-2); }

.rolecard__who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.rolecard__tags { flex: none; display: flex; align-items: center; gap: 6px; }

.rolecard__body { padding: 4px 14px 14px; border-top: 1px solid var(--border); }
.rolecard__group {
  font-size: var(--fs-2xs); font-weight: var(--fw-bold);
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--text-faint);
  margin: 10px 0 6px;
}

.permrow {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 0; border-top: 1px solid var(--border);
}
.permrow:first-child { border-top: 0; }
.permrow__what { flex: 1; min-width: 0; }
.permrow__pick { flex: none; display: flex; gap: 0; }

.permbtn {
  padding: 7px 11px; border: 1px solid var(--border); border-right-width: 0;
  background: var(--surface); color: var(--text-muted);
  font-family: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  cursor: pointer; white-space: nowrap;
}
.permbtn:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.permbtn:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; border-right-width: 1px; }
.permbtn:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
.permbtn:disabled { opacity: 0.55; cursor: not-allowed; }

/* Only the chosen one is coloured, and edit is coloured differently from view -
   handing out write access should not look the same as handing out a glance. */
.permbtn--on {
  background: color-mix(in srgb, var(--brand) 12%, var(--surface));
  border-color: var(--brand); color: var(--brand); z-index: 1;
}
.permbtn--on[data-level='none'] {
  background: var(--surface-sunken); border-color: var(--border-strong); color: var(--text-muted);
}
.permbtn--on[data-level='write'] {
  background: color-mix(in srgb, var(--warning) 15%, var(--surface));
  border-color: var(--warning); color: color-mix(in srgb, var(--warning) 82%, var(--text));
}

@media (max-width: 560px) {
  .permrow { flex-direction: column; gap: 8px; }
  .permrow__pick { width: 100%; }
  .permbtn { flex: 1; text-align: center; }
}

/* ------------------------------ finding a client -------------------------- */
/*
 * The search box at the top of a new lead.
 *
 * Deliberately not a combobox that takes over the client-name field: it sits
 * above the form and fills it in, so a rep can see exactly what was filled and
 * correct it. Something that silently replaced six fields would be faster and
 * much harder to trust.
 */

.contactfind { position: relative; }
.contactfind__results {
  position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20;
  max-height: 260px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
}
.contactfind__hit {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border: 0; border-top: 1px solid var(--border);
  background: none; color: var(--text); font-family: inherit;
  text-align: left; cursor: pointer;
}
.contactfind__hit:first-child { border-top: 0; }
.contactfind__hit:hover { background: var(--surface-2); }
/* Arrow keys land here. Same weight as hover, because they are the same act -
   a row about to be chosen - and two different highlights in one list reads as
   two different things being true at once. */
.contactfind__hit--on { background: var(--surface-2); }
.contactfind__hit b { font-size: var(--fs-md); font-weight: var(--fw-semibold); }

/* The way out for a client who is not on file. Always the last row, so its
   position never moves as the matches above it change - somebody who has
   learned it is at the bottom should not have to look for it. */
.contactfind__new { color: var(--brand); border-top: 1px solid var(--border-strong); }
.contactfind__new b { color: var(--brand); }
.contactfind__none { padding: 12px; margin: 0; }

/* ------------------------------ the address book -------------------------- */
/*
 * A row per client, built to be scanned rather than read. The money sits hard
 * right in a fixed column so several thousand of them line up: a list where
 * every figure starts in a different place cannot be compared down the page,
 * which is the only thing anybody does with a list this long.
 */

.contactrow__stats {
  flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  min-width: 88px;
}
.contactrow__stats b { font-size: var(--fs-md); font-weight: var(--fw-bold); }
.contactrow__stats b.faint { color: var(--text-faint); font-weight: var(--fw-medium); }

.contactsum {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
  padding: 14px; border-radius: var(--radius);
  border: 1px solid var(--border); background: var(--surface-2);
}
.contactsum > span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.contactsum b {
  font-size: var(--fs-lg); font-weight: var(--fw-bold);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 420px) { .contactsum { grid-template-columns: repeat(2, 1fr); } }

/* The client, under the project name it belongs to. Only drawn when the two
   differ - half this database is one job for one client, and printing the same
   name twice on every card says nothing twice. */
.lead-card__client { margin-top: 1px; }

/* The rest of a column, which is not drawn. Five thousand cards is not a
   board - the list is built to be paged through and this is the door to it. */
.board__more {
  display: block; width: 100%; margin-top: 6px; padding: 9px 10px;
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
  background: none; color: var(--text-muted); font-family: inherit;
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); cursor: pointer;
}
.board__more:hover { border-color: var(--brand); color: var(--brand); }

/* ---------------------------- the two businesses -------------------------- */
/*
 * Webcastle and Wizebix keep separate books. A single "₹19.5Cr due" is the sum
 * of two of them and answers a question neither owner is asking, so the money
 * screen shows each side and lets somebody tap through to just that one.
 */

.entity-split { display: flex; flex-direction: column; gap: 6px; }
.entity-split__row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text); font-family: inherit; text-align: left; cursor: pointer;
  border-left: 3px solid var(--etone);
  transition: border-color 0.14s, background 0.14s;
}
.entity-split__row:hover { background: var(--surface-2); border-color: var(--border-strong); border-left-color: var(--etone); }
.entity-split__who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.entity-split__who b { font-size: var(--fs-md); font-weight: var(--fw-semibold); }
.entity-split__money { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
.entity-split__money b { font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--warning); }

/* Shared inline status selector on record summaries. */
.record-status-control { border-left:3px solid var(--stage-color, #64748b); border-radius:8px; min-width:0; }
.record-status-control .select { width:100%; min-width:0; border-color:var(--stage-color, #64748b); font-size:14px; }

.record-status-control { position: relative; }
.record-status-control::before { content: ''; position:absolute; z-index:1; left:12px; top:50%; transform:translateY(-50%); width:8px; height:8px; border-radius:50%; background:var(--stage-color, #64748b); pointer-events:none; }
.wz-system .record-status-control .select { padding-left:28px; }

/* Approved wordmarks retain their original proportions in both themes. */
.brand-logo--dark { display:none; }
[data-theme='dark'] .brand-logo--light { display:none; }
[data-theme='dark'] .brand-logo--dark { display:block; }
.signin__brand .brand-logo { width:174px; height:auto; }

/* Let board cards grow with their actual content, without reserved blank rows. */
.ckb .board__card { height:auto; min-height:0; }
.ckb .board__card-name { white-space:normal; overflow-wrap:anywhere; }
.ckb .board__card-sub { min-height:0; margin-top:3px; white-space:normal; overflow-wrap:anywhere; }
.ckb .board__card-foot { margin-top:8px; align-items:flex-start; }
.ckb .board__card-metas { flex-wrap:wrap; gap:5px 6px; overflow:visible; mask-image:none; -webkit-mask-image:none; }
.ckb .board__card-metas > * { max-width:100%; white-space:normal; overflow-wrap:anywhere; }
.ckb .board__card-metas .etag::before { flex-shrink:0; }
