/* HQ Hub design system.
   One stylesheet serves both the login page and the app shell.

   Built to DESIGN-BRIEF.md: calm surfaces, loud numbers, one orange.
   Everything is driven by the tokens below. Elevation is carried by lightness,
   not by shadow: cards never float, only the sheet and the toast do.

   Motion rule that shapes this whole file: every view is rebuilt with
   innerHTML on every keystroke, chip click and tab switch, so no @keyframes
   rule may target anything reachable inside #view-dashboard, #view-clients or
   #view-tasks except .skel. Movement here is interaction driven. */

:root {
  /* Color: a high-tech read on the Double Shot palette. Espresso-warm darks
     instead of blue-gray, sun-faded orange as the accent, paper-tinted text.
     Locked hues. Everything else below is a lightness or alpha step of one of
     them, and there is no pure black anywhere in the file. */
  --bg: #17110a;
  --surface: #211812;
  --surface-2: #2c2117;
  --surface-3: #372a1d;
  --well: #120d07;

  --text: #f1e9dc;
  --muted: #a6947e;
  /* A deeper muted for timestamps and tertiary meta. The value the brief
     proposed measures 3.5:1 on --surface and fails the brief's own 4.5:1
     floor, so it is stepped up one notch. Same hue, 4.89:1. */
  --muted-deep: #968571;
  --on-accent: #29231f;

  --accent: #d97e52;
  --accent-soft: rgba(217, 126, 82, 0.15);
  --accent-line: rgba(217, 126, 82, 0.32);
  --green: #45b06c;
  --red: #e0563f;
  --red-soft: rgba(224, 86, 63, 0.14);
  --red-line: rgba(224, 86, 63, 0.34);

  /* Hairlines: alpha steps of --text. --border inside a container,
     --border-strong where a container meets the canvas or where a separator
     sits inside an already 7%-bordered card, --border-hover on interaction. */
  --border: rgba(241, 233, 220, 0.07);
  --border-strong: rgba(241, 233, 220, 0.14);
  --border-hover: rgba(241, 233, 220, 0.22);

  /* Shadow ink is espresso, never black. Only the two layers that genuinely
     float carry one, so there is a single shadow token: the brief's
     --shadow-sticky was for a topbar that lifts once content scrolls under it,
     which cannot be detected without touching app.js, and a dead token in a
     stylesheet this small is worse than a missing one. */
  --shadow-float: 0 12px 32px -8px rgba(9, 5, 2, 0.62), 0 2px 6px rgba(9, 5, 2, 0.45);
  --scrim: rgba(9, 5, 2, 0.72);

  /* Shape: containers softer than their contents. */
  --radius-lg: 16px;
  --radius: 12px;
  --radius-sm: 8px;
  --radius-pill: 999px;

  /* Spacing scale: 4 / 8 / 12 / 16 / 24 / 32 / 48 */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s6: 24px;
  --s8: 32px;
  --s12: 48px;

  /* Type */
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Motion */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --dur-micro: 120ms;
  --dur-press: 90ms;
  --dur-state: 160ms;
  --dur-in: 200ms;
  --dur-out: 150ms;

  /* Layout */
  --tap: 44px;
  --sidebar-w: 220px;
  --tabbar-h: 60px;
}

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

html {
  background: var(--bg);
  color-scheme: dark;
}

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

h1,
h2,
h3 {
  margin: 0;
  text-wrap: balance;
}

h1 {
  font-size: 22px;
  font-weight: 650;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

h2 {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1.3;
}

h3 {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.35;
}

p { margin: 0; }

a { color: inherit; text-decoration: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Shared primitives ---------- */

/* The inline <symbol> library. Off the page, but not display:none, because a
   hidden ancestor stops <use> resolving in some engines. */
.svg-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* ---------- The Double Shot HQ lockup ---------- */

/* Mark plus wordmark, one unit. The mark is a redraw of the Double Shot
   stacked-cups logo, not the hand-inked original: geometric line art that
   still resolves into two cups with handles at 22px. */
.lockup {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}

.ds-mark {
  width: 22px;
  height: 22px;
  flex: none;
  color: var(--text);
}

.wordmark {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text);
}

/* The one new accent in the shell, and it is doing the job accent is for:
   marking which Double Shot surface you are standing on. */
.wordmark-hq { color: var(--accent); }

.muted { color: var(--muted); }

.placeholder {
  color: var(--muted);
  font-size: 14px;
  padding: var(--s6) 0;
  text-wrap: pretty;
}

/* Three tiers, and most buttons are not the loud one.
   Default is a transparent hairline box: it sits quietly on a card instead of
   glowing lighter than the surface it rests on. */
.btn {
  appearance: none;
  min-height: var(--tap);
  padding: 0 var(--s4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.btn:hover {
  background: var(--surface-2);
  border-color: var(--border-hover);
}

.btn:active { transform: translateY(1px); }

/* One per view, one per sheet. */
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.btn-primary:hover {
  background: var(--accent);
  border-color: var(--accent);
  filter: brightness(1.06);
}

.btn-primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-ghost {
  appearance: none;
  min-height: var(--tap);
  padding: 0 var(--s3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.btn-ghost:hover {
  background: var(--surface-2);
  color: var(--text);
}

.btn-ghost:active { transform: translateY(1px); }

/* Fields drop to the inset step on whatever surface they land on. */
.field {
  width: 100%;
  min-height: var(--tap);
  padding: 0 var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--well);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  /* An input clips its overflow at the padding box, so a long value ran its
     last glyph into the border and got sliced down the middle. Truncating
     instead ends the value inside the padding, where it reads as shortened
     rather than broken. Focus still scrolls the full value normally. */
  text-overflow: ellipsis;
  transition:
    border-color var(--dur-micro) var(--ease-out),
    box-shadow var(--dur-micro) var(--ease-out);
}

.field::placeholder { color: var(--muted-deep); }

.field:hover { border-color: var(--border-hover); }

.field:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

select.field {
  appearance: none;
  padding-right: var(--s6);
  /* Caret drawn from two gradients so it needs no new asset, in --muted. */
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 16px) calc(50% + 1px), calc(100% - 11px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s4);
}

/* ---------- Login ---------- */

.login-body {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s6);
  padding-bottom: calc(var(--s6) + env(safe-area-inset-bottom));
}

/* The one L1 surface that genuinely floats on the canvas. */
.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-float);
  /* Less at the foot than at the head: the status line below the button
     already reserves its own height, and counting it twice left the card
     visibly bottom-heavy. */
  padding: var(--s8) var(--s6) var(--s4);
  text-align: center;
}

/* Front door: the mark gets room to be looked at, stacked over the wordmark. */
.login-card .lockup {
  display: grid;
  justify-items: center;
  gap: var(--s3);
}

.login-card .ds-mark {
  width: 48px;
  height: 48px;
}

.login-card .wordmark {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.24em;
  /* The tracking pushes the block visually left; nudge it back to centre. */
  padding-left: 0.24em;
  display: block;
}

.login-sub {
  margin-top: var(--s2);
  color: var(--muted);
  font-size: 13px;
}

.people {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s3);
  margin-top: var(--s8);
}

/* The choice on this screen, so the selected one gets the full active
   treatment and the other stays a quiet control. */
.person {
  appearance: none;
  min-height: 56px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--muted);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--dur-state) var(--ease-out),
    border-color var(--dur-state) var(--ease-out),
    color var(--dur-state) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.person:hover { color: var(--text); border-color: var(--border-hover); }
.person:active { transform: translateY(1px); }

.person[aria-pressed='true'] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--text);
}

.pin-row {
  display: grid;
  gap: var(--s3);
  margin-top: var(--s4);
}

.pin-row .field {
  text-align: center;
  letter-spacing: 0.35em;
  font-size: 18px;
}

.login-msg {
  min-height: 20px;
  margin-top: var(--s2);
  font-size: 13px;
  font-weight: 500;
  color: var(--red);
}

.login-msg[data-tone='muted'] { color: var(--muted); }

@keyframes hq-shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}

.shake {
  animation: hq-shake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}

/* ---------- App shell ---------- */

.app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s3);
  height: 56px;
  padding: 0 var(--s4);
  padding-top: env(safe-area-inset-top);
  height: calc(56px + env(safe-area-inset-top));
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.topbar .spacer { flex: 1; }

.main {
  flex: 1;
  padding: var(--s4);
  /* The tab bar is fixed, so the scroll container has to reserve its full
     height plus the home-indicator inset or the last row hides underneath. */
  padding-bottom: calc(var(--tabbar-h) + var(--s6) + env(safe-area-inset-bottom));
  /* One container for all three views: same width, same left edge, centred.
     The bento needs the wide one, and a dashboard that starts 140px left of
     the Clients table is a worse problem than a wide table. */
  max-width: 1180px;
  width: 100%;
  margin-inline: auto;
}

.view[hidden] { display: none; }

/* Nav: one element, two layouts. Phone = bottom tab bar. */
.nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom);
}

.nav-item {
  flex: 1;
  min-height: var(--tabbar-h);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  color: var(--muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.nav-item svg {
  width: 22px;
  height: 22px;
  display: block;
}

.nav-item:hover { color: var(--text); }
.nav-item:active { transform: translateY(1px); }

.nav-item[aria-current='page'] { color: var(--accent); }

/* ---------- The opener ---------- */

/* Static, revealed per route with :has(), so no JavaScript and no handler is
   involved. The page starts with a person and two verbs instead of starting
   cold with four boxes of numbers. */
.opener { display: none; }

.main:has(#view-dashboard:not([hidden])) .opener {
  display: block;
  margin-bottom: var(--s6);
}

.opener-title {
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  text-wrap: balance;
}

.opener-title .who { color: var(--text); }

.opener-sub {
  margin-top: var(--s1);
  color: var(--muted);
  font-size: 13px;
}

.opener-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s3);
}

.opener-action {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: 0 var(--s3);
  margin-left: calc(var(--s3) * -1);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 14px;
  font-weight: 600;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.opener-action + .opener-action { margin-left: 0; }

.opener-action:hover {
  background: var(--surface-2);
  color: var(--text);
}

.opener-action:active { transform: translateY(1px); }

/* ---------- Desktop ---------- */

@media (min-width: 900px) {
  .app {
    display: grid;
    grid-template-columns: var(--sidebar-w) 1fr;
    grid-template-rows: auto 1fr;
  }

  .topbar {
    grid-column: 1 / -1;
    padding: 0 var(--s6);
  }

  .nav {
    position: sticky;
    top: calc(56px + env(safe-area-inset-top));
    align-self: start;
    grid-column: 1;
    grid-row: 2;
    left: auto;
    right: auto;
    bottom: auto;
    flex-direction: column;
    gap: var(--s1);
    height: calc(100dvh - 56px - env(safe-area-inset-top));
    padding: var(--s4) var(--s3);
    background: transparent;
    border-top: none;
    border-right: 1px solid var(--border);
  }

  .nav-item {
    flex: 0 0 auto;
    flex-direction: row;
    justify-content: flex-start;
    gap: var(--s3);
    min-height: var(--tap);
    padding: 0 var(--s3);
    border-radius: var(--radius-sm);
    font-size: 15px;
  }

  .nav-item:hover { background: var(--surface); color: var(--text); }

  .nav-item[aria-current='page'] {
    background: var(--accent-soft);
    color: var(--accent);
  }

  .main {
    grid-column: 2;
    grid-row: 2;
    padding: var(--s8);
    padding-bottom: var(--s12);
  }

  .opener-title { font-size: 30px; }

  .main:has(#view-dashboard:not([hidden])) .opener { margin-bottom: var(--s8); }
}

/* ---------- Toast ---------- */

.toast-host {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--tabbar-h) + var(--s4) + env(safe-area-inset-bottom));
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s2);
  pointer-events: none;
  width: max-content;
  max-width: min(92vw, 420px);
}

@media (min-width: 900px) {
  .toast-host { bottom: var(--s6); }
}

/* L3: genuinely floats, so it is the lightest surface in the file and the only
   one besides the sheet that casts a shadow. */
.toast {
  padding: var(--s3) var(--s4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  color: var(--text);
  font-size: 14px;
  box-shadow: var(--shadow-float);
  animation: hq-toast 2500ms var(--ease-out) both;
}

/* One keyframe carries in, hold and out, timed to TOAST_MS = 2500 in app.js,
   so the fade completes just before removeChild fires. No JS change needed. */
@keyframes hq-toast {
  0% { opacity: 0; transform: translateY(6px); }
  7% { opacity: 1; transform: none; }
  93% { opacity: 1; transform: none; animation-timing-function: var(--ease-in); }
  100% { opacity: 0; transform: translateY(4px); }
}

/* ---------- View furniture (dashboard + clients) ---------- */

/* Vertical rhythm for a column of cards.
   minmax(0, 1fr) rather than the default 1fr everywhere in this file: a grid
   item's automatic minimum size is its min-content width, so one long
   unbreakable string (a client name, an activity line) silently widens the
   whole page and the phone layout starts scrolling sideways. */
.stack {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
}

/* Pushes whatever follows it to the far end of a flex row. */
.grow { flex: 1 1 auto; min-width: 0; }

/* Opens a gap above a block that follows another block inside the same card. */
.spaced { margin-top: var(--s3); }
.spaced-lg { margin-top: var(--s6); }

.card-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s3);
}

.card-head h2 { flex: 1 1 auto; min-width: 0; }

/* An empty state should teach the interface, not just report absence: it gets
   real breathing room instead of reading as a stray line of grey text. */
.empty {
  color: var(--muted);
  font-size: 14px;
  padding: var(--s6) var(--s2);
  text-align: center;
  text-wrap: pretty;
}

.pill {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  flex: none;
}

/* A tag is data, not a state, so it is a neutral pill differentiated by a
   leading glyph rather than by colour. */
.pill-accent {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--muted);
}

.pill-accent::before {
  content: '#';
  color: var(--muted-deep);
  margin-right: 1px;
}

/* A warning should look like one: tinted fill and tinted edge, not a neutral
   pill with recoloured text. */
.pill-warn {
  background: var(--red-soft);
  border-color: var(--red-line);
  color: var(--red);
}

/* Round initial for "who owns this". Neutral: a dashboard used to print ten
   orange circles that only meant "a person exists". */
.who-badge {
  width: 22px;
  height: 22px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
}

/* ---------- Generic list rows ---------- */

.rows {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

/* Separators inside a card sit at 14%, not 7%: a 7% line on a 7%-bordered
   surface is invisible at normal screen brightness. */
.row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--tap);
  padding: 10px var(--s2);
  margin-inline: calc(var(--s2) * -1);
  border-top: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  transition:
    background-color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.rows > .row:first-child { border-top: none; }

.row-main {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
  font-weight: 550;
  line-height: 1.45;
  transition: color var(--dur-micro) var(--ease-out);
}

.row-side {
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
}

a.row:hover { background: var(--surface-2); }
a.row:hover .row-main { color: var(--accent); }
a.row:active { transform: translateY(1px); }

/* ---------- Dashboard stat band ---------- */

/* One instrument panel, not four cards competing with the three below them.
   A single surface field divided by internal hairlines. */
.stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

/* Rows rather than a plain stack: "Setup fees this month" wraps to two lines
   on a phone and its neighbour's label does not, and a row of numbers that
   does not share a baseline is the thing the eye notices first. The value row
   is auto, so the label row absorbs the difference. */
.stat {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--s2);
  padding: var(--s4);
}

/* Phone: a 2x2 hairline cross. */
.stat:nth-child(odd) { border-right: 1px solid var(--border-strong); }
.stat:nth-child(1),
.stat:nth-child(2) { border-bottom: 1px solid var(--border-strong); }

/* Two lines are reserved on every tile, because "Setup fees this month" wraps
   on a phone and its neighbours do not. Reserving the space on all four is
   what keeps the four value baselines on one line. */
.stat-label {
  min-height: calc(2 * 1.2 * 11px);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
}

/* The loud number. 1.45 on a 30px numeral is a hole in the layout. */
.stat-value {
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
}

@media (min-width: 900px) {
  .stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .stat { padding: var(--s6); }
  .stat:nth-child(1),
  .stat:nth-child(2) { border-bottom: none; }
  .stat:nth-child(odd) { border-right: none; }
  .stat:not(:last-child) { border-right: 1px solid var(--border-strong); }
  /* Four across, so every label fits on one line and no reserve is needed. */
  .stat-label { min-height: 0; }
  .stat-value { font-size: 30px; }
}

/* ---------- The bento (desktop dashboard) ---------- */

/* The dashboard's children are a fixed sequence, so the grid is addressable by
   position with no markup change: stat band across the top, the two task-ish
   cards down the 7-track, activity down the 5-track using its full height. */
@media (min-width: 900px) {
  #view-dashboard .stack {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    grid-auto-rows: min-content;
    align-items: start;
    gap: var(--s6);
  }

  #view-dashboard .stack > .stats {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  #view-dashboard .stack > :nth-child(2) { grid-column: 1; grid-row: 2; }
  #view-dashboard .stack > :nth-child(3) { grid-column: 1; grid-row: 3; }

  /* Activity fills the full height of the two rows it spans, so the bento
     reads as two balanced columns rather than one column with a stub beside
     it. The card stretches; its rows stay top-aligned. */
  #view-dashboard .stack > :nth-child(4) {
    grid-column: 2;
    grid-row: 2 / span 2;
    align-self: stretch;
  }
}

/* ---------- Task rows (dashboard + client checklist) ---------- */

/* Two columns on a phone: the checkbox, then a stack of title over pills. The
   pills are nowrap by design, so keeping them on the title's line would leave a
   long title a sliver of width and break it one letter per line. */
.task {
  display: grid;
  grid-template-columns: var(--tap) minmax(0, 1fr);
  align-items: center;
  column-gap: var(--s2);
  row-gap: var(--s1);
  min-height: var(--tap);
  padding: var(--s1) var(--s2);
  margin-inline: calc(var(--s2) * -1);
  border-top: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-micro) var(--ease-out);
}

/* The row reads as a target. */
.task:hover { background: var(--surface-2); }

/* Both of these are placed by hand. Left to auto-placement the pill box lands
   in the checkbox's 44px column on the second row and overflows it. */
.task-title { grid-column: 2; grid-row: 1; }

.task-meta {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}

.task-check { grid-column: 1; grid-row: 1; align-self: center; }

@media (min-width: 900px) {
  /* Room for one line: title left, pills right. */
  .task { grid-template-columns: var(--tap) minmax(0, 1fr) auto; }
  .task-meta { grid-column: 3; grid-row: 1; }
}

.rows > .task:first-child { border-top: none; }

.task-check {
  appearance: none;
  width: var(--tap);
  height: var(--tap);
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.task-box {
  width: 20px;
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--border-strong);
  border-radius: 50%;
  /* The most repeated interaction in the app gets the shortest duration. */
  transition:
    transform var(--dur-micro) var(--ease-out),
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out);
}

.task-box svg {
  width: 12px;
  height: 12px;
  opacity: 0;
}

.task-check:hover .task-box { border-color: var(--accent); }

.task-check:active .task-box {
  transform: scale(0.9);
  border-color: var(--accent);
  transition-duration: var(--dur-press);
}

.task[data-done='1'] .task-box {
  background: var(--green);
  border-color: var(--green);
  color: var(--on-accent);
}

.task[data-done='1'] .task-box svg { opacity: 1; }

.task[data-done='1'] .task-title {
  text-decoration: line-through;
  color: var(--muted);
}

/* Wraps rather than truncating. A task called "Send Dale th..." next to an
   intact pill is the wrong trade: the title is the thing being read. */
.task-title {
  min-width: 0;
  padding: var(--s1) 0;
  font-size: 15px;
  font-weight: 550;
  line-height: 1.45;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

/* ---------- Activity ---------- */

/* Actor leads, summary carries the line, timestamp sits hard right and never
   wraps under it. */
.act {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 40px;
  padding: 10px var(--s2);
  margin-inline: calc(var(--s2) * -1);
  border-top: 1px solid var(--border-strong);
}

.rows > .act:first-child { border-top: none; }

.act-summary {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 14px;
  line-height: 1.45;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

.act-when {
  flex: none;
  color: var(--muted-deep);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* ---------- Clients list ---------- */

/* This row is the Clients page header: search, scope toggle, and the single
   primary action. The hairline under it separates the controls from results. */
.list-controls {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  padding-bottom: var(--s4);
  margin-bottom: var(--s4);
  border-bottom: 1px solid var(--border-strong);
}

.list-controls .field {
  flex: 1 1 180px;
  width: auto;
  min-width: 0;
}

.toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: 0 var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.toggle input { accent-color: var(--accent); }

.toggle:hover {
  background: var(--surface-2);
  border-color: var(--border-hover);
  color: var(--text);
}

.toggle:active { transform: translateY(1px); }

@media (min-width: 900px) {
  .list-controls { padding-bottom: var(--s6); margin-bottom: var(--s6); }
}

/* Phone: cards. Desktop: the table below replaces them. */
.cards {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s2);
}

.client-card {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-height: 64px;
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.client-card:hover { border-color: var(--border-hover); }
.client-card:active { transform: translateY(1px); }

.client-card .who { flex: 1 1 auto; min-width: 0; }

.client-card .name {
  font-size: 15px;
  font-weight: 550;
  line-height: 1.45;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.client-card .sub {
  color: var(--muted);
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.client-card .price {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.table-wrap { display: none; }

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

table.data {
  width: 100%;
  border-collapse: collapse;
}

.data th {
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border-strong);
  text-align: left;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  transition: color var(--dur-state) var(--ease-out);
}

.data th.sortable { cursor: pointer; user-select: none; }
.data th.sortable:hover { color: var(--text); }
.data th[aria-sort] { color: var(--accent); }

.data td {
  padding: var(--s3);
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.data tbody tr {
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out);
}

.data tbody tr:hover { background: var(--surface-2); }

.data .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Monthly is money and reads as money. */
.data td.num:nth-child(4) { font-weight: 600; }

/* ---------- Client detail ---------- */

.detail-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  padding-bottom: var(--s4);
  margin-bottom: var(--s4);
  border-bottom: 1px solid var(--border-strong);
}

.detail-head .titles { flex: 1 1 240px; min-width: 0; }

.detail-head .muted { font-size: 13px; margin-top: 2px; }

.head-actions {
  display: flex;
  gap: var(--s2);
  flex: 0 0 auto;
}

.head-actions .btn { flex: 1 1 auto; }

.back {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  color: var(--muted);
  font-size: 14px;
  font-weight: 600;
  transition:
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.back:hover { color: var(--text); }
.back:active { transform: translateY(1px); }

@media (min-width: 900px) {
  .detail-head { padding-bottom: var(--s6); margin-bottom: var(--s6); }
  .detail-head h1 { font-size: 24px; }
}

.kv {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s3);
  margin: 0;
}

.kv dt { color: var(--muted); font-size: 14px; }

.kv dd {
  margin: 0;
  text-align: right;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

/* The paid marker on the setup fee, and the one other place green is allowed.
   Keyed by position because the pricing list is a fixed sequence (Monthly,
   Setup, Ad budget, Billing day) and the billing row's "not set" placeholder
   is the same span markup, which no structural selector can tell apart:
   :only-child ignores text nodes, so both would match. */
.kv dd:nth-of-type(2) > .muted {
  color: var(--green);
  font-weight: 500;
}

.pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

a.pill-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

a.pill-link:hover {
  background: var(--surface-2);
  border-color: var(--accent-line);
  color: var(--accent);
}

a.pill-link:active { transform: translateY(1px); }

.note {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s1);
  padding: var(--s3) 0;
  border-top: 1px solid var(--border-strong);
}

.notes > .note:first-child { border-top: none; }

.note-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--muted-deep);
}

.note-body {
  font-size: 15px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

.inline-form {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  margin-top: var(--s3);
}

.inline-form .field { flex: 1 1 auto; min-width: 0; }

.icon-btn {
  appearance: none;
  width: 34px;
  height: 34px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.icon-btn:hover {
  color: var(--red);
  border-color: var(--red-line);
  background: var(--red-soft);
}

.icon-btn:active { transform: translateY(1px); }

/* ---------- Overlay form (add / edit client) ---------- */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  overflow: auto;
  /* Espresso-tinted, never pure black in a warm room. */
  background: var(--scrim);
  animation: hq-scrim-in var(--dur-out) var(--ease-out) both;
}

/* The one layer in the app that genuinely floats over the page. */
.sheet {
  width: 100%;
  min-height: 100dvh;
  padding: var(--s4);
  padding-top: calc(var(--s4) + env(safe-area-inset-top));
  padding-bottom: calc(var(--s8) + env(safe-area-inset-bottom));
  background: var(--surface-3);
  animation: hq-sheet-in var(--dur-in) var(--ease-out) both;
}

@keyframes hq-scrim-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes hq-sheet-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

@media (min-width: 900px) {
  .overlay { align-items: center; padding: var(--s6); }

  .sheet {
    max-width: 560px;
    min-height: 0;
    padding: var(--s6);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-float);
  }
}

.sheet-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s4);
}

.sheet-head h2 { flex: 1 1 auto; font-size: 17px; }

/* Fields on the floating layer keep the inset step: it is two steps below the
   sheet, so the boxes still read as wells rather than as plates.
   background-color, not the background shorthand: the shorthand also resets
   background-image, which is where select.field keeps its caret, and a select
   in a sheet would silently lose it. */
.sheet .field { background-color: var(--well); }

/* Inputs only, never select.field: the select keeps the wider right padding
   that its caret is drawn into, and a two-class selector would out-specify it. */
.sheet input.field { padding-right: var(--s4); }

.form-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s3);
}

@media (min-width: 900px) {
  .form-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .form-grid.two .wide { grid-column: 1 / -1; }
}

/* An input carries a default intrinsic width of roughly 20 characters, which
   is wide enough to burst a narrow form column unless it is allowed to shrink. */
.lbl .field { min-width: 0; }

.lbl {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s2);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
}

/* No resize grip: the handle sat in the corner of every note and form box as
   the only piece of browser chrome on the page, and one textarea that can be
   dragged while another cannot is two component vocabularies. */
textarea.field {
  min-height: 84px;
  padding: var(--s2) var(--s3);
  line-height: 1.45;
  resize: none;
}

.link-rows {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s2);
}

.link-row {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.link-row .field { flex: 1 1 0; min-width: 0; }

/* One footer for every sheet: Delete quiet on the left, then a spacer, then
   Cancel and Save sharing the rest. Same order and same three tiers in the
   lead sheet, the event sheet and the client form, so a footer never has to be
   re-read to find out where Save went. */
.sheet-actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s6);
}

.sheet-actions .btn { flex: 1 1 0; }

/* Destructive sits below Cancel, not beside it: no border at rest, muted text,
   pushed to the far left by the spacer so it is never the button you hit on the
   way to Save. */
.sheet-actions .btn-danger {
  flex: 0 0 auto;
  margin-right: auto;
}

/* ---------- Loading skeletons ---------- */

/* Shape without content: the same row geometry the real list will have, so the
   card does not resize when the fetch lands. Pulsing opacity on --surface-2
   rather than a moving gradient keeps it to one property and one token.
   This is the one animation in a view that is allowed to loop, because it is
   the one animation that is supposed to. */
.skels {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.skel-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--tap);
  border-top: 1px solid var(--border-strong);
}

.skels > .skel-row:first-child { border-top: none; }

.skel {
  display: block;
  height: 12px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  animation: hq-skel 1.2s ease-in-out infinite;
}

.skel-line { flex: 1 1 auto; min-width: 0; }
.skel-pill { flex: none; width: 72px; }

.skel-stat {
  height: 24px;
  width: 70%;
  margin: var(--s1) 0;
}

@keyframes hq-skel {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.85; }
}

/* ---------- Tasks: composer ---------- */

.composer {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s2);
}

/* Edit mode has to be unmistakable: the same three inputs now overwrite an
   existing task instead of creating one. A rail plus the heading says so
   without moving anything on the page. */
.composer.is-editing {
  padding-left: var(--s3);
  border-left: 2px solid var(--accent-line);
}

.composer-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

.composer-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--accent);
}

.composer-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s2);
}

/* Phone: the client picker takes its own line (client names are long), tag and
   assignee share the next one. */
.composer-client { grid-column: 1 / -1; }

.composer-actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* Phone: the action fills the width instead of hanging off the right edge as a
   lone floating button. The spacer that pushes it right on desktop is not
   needed here, so it is taken out of the flow rather than out of the markup. */
.composer-actions .grow { display: none; }
.composer-actions .btn { flex: 1 1 0; }

@media (min-width: 900px) {
  .composer-row { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
  .composer-client { grid-column: auto; }

  /* Desktop: the action sits inline at the end of the client / tag / assignee
     row, so the composer is two lines instead of three with a stranded button.
     Placement is by column only, so it survives the extra head row that edit
     mode inserts. */
  .composer {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
  }

  .composer-head,
  .composer > .field { grid-column: 1 / -1; }

  .composer-row { grid-column: 1; }

  .composer-actions {
    grid-column: 2;
    align-self: end;
  }

  .composer-actions .grow { display: block; }
  .composer-actions .btn { flex: 0 0 auto; }
}

/* ---------- Tasks: filter chips ---------- */

/* Separated from the composer above so the filters stop reading as part of the
   create form. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  padding-top: var(--s4);
  margin-top: var(--s4);
  border-top: 1px solid var(--border-strong);
}

/* min-width keeps a two-letter filter from collapsing into a circle next to
   its neighbours: the radius is identical on every chip, and the engaged state
   changes fill and border only, never the shape. */
.chip {
  appearance: none;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  min-width: 72px;
  padding: 0 var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--muted);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-state) var(--ease-out),
    border-color var(--dur-state) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

/* The visible pill is 40px so a full row of filters still fits on a phone; this
   invisible box takes the actual tap target back up to 44. The 2px of overhang
   per side stays inside the 8px gap, so wrapped rows never overlap. */
.chip::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: var(--tap);
}

.chip:hover { color: var(--text); border-color: var(--border-hover); }
.chip:active { transform: translateY(1px); }

.chip.is-on {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}

/* ---------- Tasks: the done section ---------- */

/* .cal-earlier-head rides along: the calendar's "Earlier this month" is the
   same idea as Tasks' "Done", a finished pile folded away at the end of the
   list, and two disclosures that behave alike must look alike. */
.done-head,
.cal-earlier-head {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  margin-top: var(--s2);
  padding: 0;
  border: none;
  border-top: 1px solid var(--border-strong);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
  transition:
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.done-head:hover,
.cal-earlier-head:hover { color: var(--text); }

.done-head:active,
.cal-earlier-head:active { transform: translateY(1px); }

.done-caret,
.cal-earlier-caret {
  display: inline-block;
  font-size: 16px;
  line-height: 1;
  transition: transform var(--dur-state) var(--ease-out);
}

.done-head[aria-expanded='true'] .done-caret,
.cal-earlier-head[aria-expanded='true'] .cal-earlier-caret { transform: rotate(90deg); }

/* .rows sets display:grid, which out-specifies the browser's [hidden] rule, so
   the collapsed state has to be spelled out or the done rows never hide. */
.done-rows[hidden] { display: none; }

/* ---------- Leads pipeline ---------- */

/* The controls band mirrors the Clients view: one number on the left, one
   primary on the right, one hairline under the whole thing. */
.pipeline {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s1);
}

/* The two-line label reserve exists for the dashboard's four-tile band and is
   dead weight on a single figure. */
.pipeline .stat-label { min-height: 0; }

/* Phone: six stacked stage sections. Desktop: six columns on one horizontal
   rail. The rail scrolls inside itself, never the page, which is why the
   overflow lives here and not on .main. */
.board {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s6);
}

@media (min-width: 900px) {
  .board {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(220px, 1fr);
    gap: var(--s3);
    /* Stretch, not start: equal-height columns are what the separator hairlines
       hang off, and a stage with one card should still read as a column. */
    align-items: stretch;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    /* Room for the scrollbar so it never sits on top of the last card. */
    padding-bottom: var(--s3);
    /* Scroll-end room, so the last column stops short of the rail edge instead
       of ending flush against it. */
    padding-right: var(--s4);
  }

  /* Six stages will not fit six 220px columns on a 1440px screen, so the rail
     scrolls. macOS hides its overlay scrollbar until you touch it, which left
     the sixth column looking chopped off rather than scrolled past. A fade on
     whichever side still has content says "there is more" without inventing a
     custom scrollbar. app.js sets data-edge; nothing here animates.

     The fade starts 96px in and reaches zero 10px before the edge, so the rail
     is fully transparent by the time content meets the hard clip. The old 56px
     ramp still had ink left at the boundary, which sliced dollar figures down
     the middle of a numeral. */
  .board[data-edge='right'] {
    mask-image: linear-gradient(
      to right,
      #000 calc(100% - 96px),
      transparent calc(100% - 10px)
    );
  }

  .board[data-edge='left'] {
    mask-image: linear-gradient(
      to left,
      #000 calc(100% - 96px),
      transparent calc(100% - 10px)
    );
  }

  .board[data-edge='both'] {
    mask-image: linear-gradient(
      to right,
      transparent 10px,
      #000 96px,
      #000 calc(100% - 96px),
      transparent calc(100% - 10px)
    );
  }

  /* A board with one card per stage is still a board. Full-height hairlines
     between the columns carry that without adding six filled panels: the brief
     puts depth in lightness and keeps borders for where a surface meets
     content, so wells here would read as six more containers competing with the
     cards inside them. --border, the 7% step, because these lines sit on the
     canvas rather than inside a container. */
  .board-col {
    min-height: 240px;
    padding-right: var(--s3);
    border-right: 1px solid var(--border);
  }

  .board-col:last-child {
    padding-right: 0;
    border-right: none;
  }
}

.board-col {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s2);
  align-content: start;
  min-width: 0;
}

.board-head {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  padding: 0 var(--s1) var(--s2);
  border-bottom: 1px solid var(--border-strong);
}

.board-stage {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.board-count,
.board-value {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--muted-deep);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.board-value { margin-left: auto; }

.board-empty {
  padding: var(--s3) var(--s1);
  font-size: 13px;
  color: var(--muted-deep);
}

/* A lead card is a target, so it gets the client-card treatment rather than a
   flat panel: L1 surface, hairline, hover lift, press depress.

   The advance chevron rides the end of the meta row rather than getting a
   column or a row of its own. A column of its own cost the lead name 38px,
   which is a lot out of a 220px board column; a row of its own left a
   button-high hole in every card that had nothing else to put down there. */
.lead-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.lead-card:hover {
  background: var(--surface-2);
  border-color: var(--border-hover);
}

.lead-card:active { transform: translateY(1px); }

/* Lost is still readable and still interactive; it just stops competing with
   the stages that are still live. */
.board-col[data-stage='lost'] .lead-card { opacity: 0.6; }
.board-col[data-stage='lost'] .lead-card:hover { opacity: 1; }

.lead-top {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}

.lead-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

.lead-est {
  flex: none;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.lead-meta {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
  /* The chevron's own height, reserved whether or not this card has one, so a
     column of cards keeps one rhythm instead of two. */
  min-height: 30px;
}

.lead-contact {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: 12px;
}

.lead-foot {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-top: 2px;
}

/* A converted lead points at its client row. Muted, with a tick: it is a fact
   about the card, not a state you can act on. */
.lead-linked {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

.lead-linked svg {
  width: 13px;
  height: 13px;
  flex: none;
}

/* One tap to move a lead one stage down the funnel. Quiet at rest: a board of
   twelve cards must not print twelve orange buttons. Accent arrives on hover
   and on focus, where it means "this is the thing you are about to press". */
.lead-next {
  appearance: none;
  position: relative;
  margin-left: auto;
  flex: none;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    border-color var(--dur-micro) var(--ease-out),
    color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.lead-next svg {
  width: 16px;
  height: 16px;
  display: block;
}

.lead-next:hover {
  background: var(--surface-2);
  border-color: var(--accent-line);
  color: var(--accent);
}

.lead-next:active { transform: translateY(1px); }

/* A 30px button is right beside a 12px meta line and wrong for a thumb. Grow
   the hit area, leave the drawing alone. */
.lead-next::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap);
  height: var(--tap);
}

/* A card-sized quiet button. The 44px floor is for a control that stands on its
   own; inside a card the row itself is the tap target. */
.btn-sm {
  min-height: 30px;
  padding: 0 var(--s2);
  font-size: 12px;
  border-radius: var(--radius-sm);
}

/* Destructive, and it says so on hover only, the same way the note delete does.
   A permanently red button in a form is a dare. At rest it drops its border and
   its text to the quiet tier so it cannot be mistaken for Cancel, which is the
   default bordered tier sitting two buttons away. */
.btn-danger {
  border-color: transparent;
  color: var(--muted);
}

.btn-danger:hover {
  background: var(--red-soft);
  border-color: var(--red-line);
  color: var(--red);
}

/* ---------- Calendar ---------- */

/* The header band is the Clients and Leads band again: something to read on the
   left, the single primary on the right, one hairline under the lot. */
.cal-band { row-gap: var(--s3); }

.cal-title {
  font-size: 22px;
  font-weight: 650;
  letter-spacing: -0.02em;
  line-height: 1.2;
  white-space: nowrap;
}

/* The year is context, not the headline: you know what year it is, you are
   looking for the month. */
.cal-year {
  color: var(--muted);
  font-weight: 600;
}

.cal-steps {
  display: flex;
  align-items: center;
  gap: var(--s1);
}

/* Quiet default-tier buttons, narrowed. They keep the 44px floor rather than
   dropping to .btn-sm: these are standalone controls in a header band, not
   something sitting inside a row that is itself the tap target. */
.cal-steps .btn { padding: 0 var(--s3); }

/* A phone band cannot hold a month, three steps and a primary on one line. It
   wraps by group rather than by button: the month and its scope label take the
   first line, and the month controls take the second with the primary at their
   right end. That is the Clients and Leads band pattern (something to read on
   the left, the one primary on the right), and it means the primary is never
   stranded alone on a line of its own. */
.cal-band-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
  min-width: 0;
}

.cal-controls {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex: 1 1 100%;
  min-width: 0;
}

.cal-controls .btn-primary { margin-left: auto; }

/* Which slice of the month the agenda below is showing. Phone only: the desktop
   shows the whole month as a grid, so the month name says it all there. */
.cal-scope {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
}

@media (min-width: 900px) {
  .cal-controls { flex: 1 1 auto; }
  .cal-scope { display: none; }
}

.cal-step svg {
  display: block;
  width: 16px;
  height: 16px;
}

/* Phone: one column, agenda only. Desktop: month grid left, panel right. */
.cal-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
}

.cal-col {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
  min-width: 0;
}

/* Both shapes are in the DOM and the stylesheet picks one, so there is no
   breakpoint in JS and nothing to re-measure on resize. display:none also takes
   the hidden shape out of the accessibility tree, so nothing is announced twice. */
.cal-card,
.cal-panel { display: none; }

@media (min-width: 900px) {
  .cal-layout {
    grid-template-columns: minmax(0, 8fr) minmax(0, 4fr);
    gap: var(--s6);
    align-items: start;
  }

  .cal-agenda { display: none; }
  .cal-card,
  .cal-panel { display: block; }

  /* The grid wants its width, so this card runs tighter than the s6 every other
     desktop card gets: 24px of padding either side is a third of a day column.
     .card.cal-card, not .cal-card, because the s6 rule in the polish sweep is a
     single class further down the file and would otherwise win on order. */
  .card.cal-card { padding: var(--s3); }

  /* The panel follows you down a tall month, clearing the sticky topbar. */
  .cal-panel {
    position: sticky;
    top: calc(56px + env(safe-area-inset-top) + var(--s6));
  }
}

/* The 1px gap and 1px side padding are not decoration: they make this row's
   seven tracks line up with the grid's, which carries a 1px hairline mesh and a
   1px border. Without them the labels drift a whole pixel per column. */
.cal-week-head {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  padding-bottom: var(--s2);
}

.cal-week-head span {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
  text-align: center;
}

/* The hairline mesh is a 1px gap over a hairline background rather than 42
   borders that would double up on every shared edge. No outer border: the card
   the grid sits in already draws one, and two rounded rectangles 12px apart
   read as a frame inside a frame.

   Square corners, deliberately. A radius here clipped the mesh background into
   a visible arc outside the square corner of the corner cell, which printed a
   stray hairline curl at the top right of the grid. The cells are square, so
   the field they sit on is square. */
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  grid-auto-rows: minmax(96px, auto);
  gap: 1px;
  background: var(--border-strong);
  overflow: hidden;
}

.cal-day {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  gap: 3px;
  min-width: 0;
  padding: 6px;
  background: var(--surface);
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out);
}

.cal-day:hover { background: var(--surface-2); }

/* The days either side of the month are still readable, they just stop
   competing with the month you asked for.

   The contents fade, not the cell. Fading the cell made it translucent over the
   grid's own hairline background, so the six leading days rendered as one
   lighter block whose edge read as a stray vertical hairline beside the last
   cell of the top row. Elevation in this app is lightness, so an out-of-month
   day going lighter than an in-month day is the ladder running backwards. */
.cal-day[data-outside='1'] > * { opacity: 0.4; }

/* Selection is neutral on purpose. The orange in this view is spent on today
   and on meetings; a selected day is a place you are looking, not a state the
   data is in. */
.cal-day[data-selected='1'] {
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border-hover);
}

.cal-daynum {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 1px solid transparent;
  border-radius: 50%;
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* Today gets a hairline ring and full-strength text, which is the quietest
   accent expression in the file. It is the same idea as the nav's
   aria-current: the accent marks where you are, and nothing else. */
.cal-day[aria-current='date'] .cal-daynum {
  border-color: var(--accent-line);
  color: var(--text);
}

.cal-day-events {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
}

/* A chip is a flat bar with a type rail. No radius: at 18px tall a rounded bar
   reads as a pill, and a pill in this app means a tag. */
.cal-chip {
  display: flex;
  align-items: baseline;
  gap: 3px;
  min-width: 0;
  padding: 2px 3px 2px 4px;
  background: var(--surface-2);
  border-left: 3px solid var(--border-strong);
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out);
}

/* Hovering the cell lifts it to the chips' own resting colour, so the chips
   step up with it rather than dissolving into their own background. */
.cal-chip:hover,
.cal-day:hover .cal-chip { background: var(--surface-3); }

.cal-chip-time {
  flex: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--muted-deep);
  font-variant-numeric: tabular-nums;
}

.cal-chip-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  font-weight: 550;
  line-height: 1.4;
  color: var(--text);
}

.cal-more {
  padding-left: 5px;
  font-size: 11px;
  font-weight: 600;
  color: var(--muted-deep);
}

/* Type to colour, and only on the 3px rail. Meeting is the one that spends
   accent; presentation takes green; a plain event stays neutral. The word is
   printed on every row as well, so the mapping is never load-bearing. */
.cal-chip[data-type='meeting'],
.cal-row[data-type='meeting'] { border-left-color: var(--accent-line); }

.cal-chip[data-type='presentation'],
.cal-row[data-type='presentation'] { border-left-color: var(--green); }

.cal-rows {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
}

.cal-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
  align-content: center;
  min-height: var(--tap);
  padding: 10px var(--s3);
  background: transparent;
  border-left: 3px solid var(--border-strong);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  cursor: pointer;
  transition:
    background-color var(--dur-micro) var(--ease-out),
    transform var(--dur-press) var(--ease-out);
}

.cal-row:hover { background: var(--surface-2); }
.cal-row:active { transform: translateY(1px); }

.cal-row-time {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--muted-deep);
  font-variant-numeric: tabular-nums;
}

.cal-row-title {
  font-size: 15px;
  font-weight: 550;
  line-height: 1.35;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

.cal-row-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  min-width: 0;
}

.cal-type {
  flex: none;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
}

.cal-row-client {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--muted-deep);
}

/* Date headings inside an agenda or the Upcoming list. Today is marked by
   full-strength text, not by colour. */
.cal-day-head {
  margin: var(--s4) 0 var(--s1);
  padding: 0 0 var(--s2) var(--s3);
  border-bottom: 1px solid var(--border-strong);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.09em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--muted);
}

.cal-rows > .cal-day-head:first-child { margin-top: 0; }

.cal-day-head.is-today { color: var(--text); }

/* .cal-rows sets display:grid, which out-specifies the browser's [hidden] rule,
   so the collapsed state has to be spelled out or the pile never folds away.
   Same trap as .done-rows. */
.cal-earlier-rows[hidden] { display: none; }

.cal-earlier-rows { margin-top: var(--s2); }

/* ---------- Polish sweep ---------- */

/* .field:focus drops the outline in favour of its own ring, which out-specifies
   the global :focus-visible rule. Keyboard focus gets the outline back. */
.field:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  box-shadow: none;
}

/* A 34px button is the right size next to a 12px note header and the wrong size
   for a thumb. Grow the hit area, leave the drawing alone. */
.icon-btn { position: relative; }

.icon-btn::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--tap);
  height: var(--tap);
}

@media (min-width: 900px) {
  /* Cards get more air once there is room for it. */
  .card { padding: var(--s6); }
  .stack { gap: var(--s6); }
}

/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  /* Movement is removed outright rather than merely shortened. */
  .shake { animation: none !important; }

  .btn:active,
  .btn-ghost:active,
  .chip:active,
  .toggle:active,
  .person:active,
  .nav-item:active,
  .opener-action:active,
  .back:active,
  .icon-btn:active,
  .done-head:active,
  a.row:active,
  a.pill-link:active,
  .client-card:active,
  .lead-card:active,
  .lead-next:active,
  .cal-row:active,
  .task-check:active .task-box {
    transform: none !important;
  }

  .done-head[aria-expanded='true'] .done-caret { transform: rotate(90deg) !important; }

  /* The toast is removed by a timer, so zeroing its keyframe would end it on
     the closing frame and it would never be seen. It holds still instead. */
  .toast {
    animation: none !important;
    opacity: 1;
  }
}
