:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f0f2f5;
  --border: #dfe3e8;
  --text: #1c1f23;
  --text-dim: #6b7280;
  --accent: #2563eb;
  --accent-soft: #e8efff;
  --danger: #dc2626;
  --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 4px 12px rgba(16, 24, 40, .06);
  --radius: 10px;

  /* "Somebody is waiting on you." Amber rather than the blue accent, which is
     already spent on selection and primary actions, and rather than red, which
     would put a stalled question on the same footing as a P0 defect. */
  --waiting-bg: #fef3c7;
  --waiting-fg: #92400e;
  --waiting-border: #fcd34d;

  /* The weekly report's caveats — a partial week, a refused week. 🔴 These
     were originally written as a `var(--warn-bg, #fff6e5)` fallback with no
     token behind it, so dark mode painted near-white text on pale yellow: the
     two messages the report exists to make unmissable were the only ones
     nobody could read. A fallback that is never overridden is a light-mode
     hard-code wearing a token's clothes. */
  --warn-bg: #fff6e5;
  --warn-fg: #7c4a03;

  /* "Its time has come round." Rose rather than amber so a recurring job that
     is late and a question waiting on you stay tellable apart on a card that
     is both — and rather than plain red, which P0 already owns. */
  --due-bg: #ffe4e6;
  --due-fg: #9f1239;
  --due-border: #fda4af;

  /* "Something is holding the machine awake." Only ever used in the topbar, so
     sharing the orange family with chip-P1 costs nothing — no card carries
     both, and the ☕ does most of the talking anyway. */
  --awake-bg: #fff7ed;
  --awake-fg: #9a3412;
  --awake-border: #fed7aa;

  /* "You are on the personal board." Teal because every other named colour on
     this board already means something — amber waiting, rose due, orange awake,
     blue accent — and a board-identity signal must not be mistakable for any of
     them. Applied only to the topbar strip, so it stays quiet enough to sit
     behind a full screen of cards all day. */
  --personal-tint: #eef6f5;
  --personal-fg: #0f766e;

  /* "This card is waiting on something", and "the thing it was waiting on is
     done". Slate for the wait — deliberately duller than amber ⏳ and rose ⏰,
     because a blocked card is precisely the one you can do nothing about, and it
     must not outshout the two that are asking for you. Green for the release: it
     is the one moment the board has good news, and it earns a colour used
     nowhere else. */
  --blocked-bg: #eef1f5;
  --blocked-fg: #475569;
  --blocked-border: #cbd5e1;
  --clear-bg: #dcfce7;
  --clear-fg: #15803d;
  --clear-border: #86efac;

  /* "You are on the board's own backlog." Violet for the same reason personal
     got teal — it is the next hue on this board that carries no other meaning.
     Kept at the same lightness as the teal so neither board feels louder. */
  --self-tint: #f2f0fb;
  --self-fg: #5b21b6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --waiting-bg: #45280f;
    --waiting-fg: #fcd34d;
    --waiting-border: #78350f;

    --warn-bg: #3a2a10;
    --warn-fg: #fcd9a0;

    --due-bg: #4c0f1c;
    --due-fg: #fda4af;
    --due-border: #881337;

    --awake-bg: #3a1d08;
    --awake-fg: #fdba74;
    --awake-border: #7c2d12;

    --personal-tint: #14201f;
    --personal-fg: #5eead4;

    --blocked-bg: #232833;
    --blocked-fg: #a8b3c4;
    --blocked-border: #3a4150;
    --clear-bg: #10331f;
    --clear-fg: #86efac;
    --clear-border: #14532d;

    --self-tint: #1b1830;
    --self-fg: #c4b5fd;

    --bg: #0f1115;
    --surface: #171a21;
    --surface-2: #1f232c;
    --border: #2b303b;
    --text: #e6e8ec;
    --text-dim: #98a1b0;
    --accent: #5b8cff;
    --accent-soft: #1c2740;
    --danger: #f0645f;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 6px 18px rgba(0, 0, 0, .3);
  }
}

* { box-sizing: border-box; }

/* The UA stylesheet's [hidden] rule loses to any class rule that sets display
   (.drawer is display:flex), which would leave the drawer permanently open. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue",
        "PingFang SC", "Hiragino Sans GB", sans-serif;
}

/* ---------------------------------------------------------------- topbar */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* user-select off because a triple-click here is a gesture, not a selection —
   without it the egg leaves the title highlighted every time. */
.brand { display: flex; align-items: center; gap: 10px; user-select: none; }
.brand h1 { margin: 0; font-size: 16px; font-weight: 650; letter-spacing: -.01em; }

.dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-dim); transition: background .3s;
}
.dot.ok { background: #16a34a; }
.dot.bad { background: var(--danger); }

/* ---------------------------------------------------------------- spaces */

/* Segmented rather than a checkbox or a dropdown: both boards stay named and
   visible, so "which one am I on" is answered without opening anything, and
   the answer is one click from being changed. */
/* The track is --bg rather than --surface-2 so the selected pill (--surface) is
   the lighter of the two in both themes. --surface-2 is lighter than --surface
   in dark mode, which inverts the control there: the pill sinks into the track
   instead of sitting on it, and "which board am I on" stops being answerable at
   a glance — exactly what this thing is for. */
.spaces {
  display: inline-flex;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--bg);
}

.space-tab {
  padding: 4px 12px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  cursor: pointer;
  transition: background .15s, color .15s;
}
.space-tab:hover { color: var(--text); }

/* After .space-tab, deliberately: same specificity, so the selected state only
   wins by being written second. */
.space-tab.active {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(16, 24, 40, .10);
}

/* Which board you are on, said in a way you cannot read past. The topbar is
   tinted rather than the whole page: it is the one strip present in every
   screenshot and at every scroll position, and tinting the board itself would
   fight with the card colours that carry real meaning.

   Deliberately not done by overriding --accent. That variable is spent on
   primary buttons, focus rings and selection across the whole app, and moving
   it would re-test every one of those contrasts for a signal this strip already
   carries. */
body[data-space="personal"] .topbar,
body[data-space="personal"] .kindbar {
  background: var(--personal-tint);
}
body[data-space="personal"] .space-tab.active {
  background: var(--surface);
  color: var(--personal-fg);
}

body[data-space="taskboard"] .topbar,
body[data-space="taskboard"] .kindbar {
  background: var(--self-tint);
}
body[data-space="taskboard"] .space-tab.active {
  background: var(--surface);
  color: var(--self-fg);
}

#search {
  flex: 1;
  max-width: 460px;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
}
#search:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }

.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }

.sort-field { display: flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 13px; }
.sort-select {
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.sort-select:hover { border-color: var(--accent); }

/* ------------------------------------------------------------- keep awake */

/* The button and its menu share a hover target. The menu's own padding-top is
   what bridges the gap between the two — a `margin` there would leave a strip
   of dead pixels that closes the menu as the pointer crosses it. */
/* Two lines in the height of one control. Tabular figures so the numbers stop
   jittering sideways every poll — a figure that twitches reads as unstable
   even when the value has not moved. */
.usage {
  display: grid;
  gap: 1px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 11px;
  line-height: 1.35;
  cursor: default;
  font-variant-numeric: tabular-nums;
}
.usage-row { display: flex; align-items: baseline; gap: 6px; }
.usage-when { color: var(--text-dim); font-size: 10px; }
.usage-tok { color: var(--text); font-weight: 560; margin-left: auto; }
.usage-cost { color: var(--text-dim); min-width: 62px; text-align: right; }
.usage:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Quota on hover. Same construction as the caffeine menu, including the
   padding-top bridge — a margin would leave a dead gap the pointer falls
   through on the way down. */
.usage-wrap { position: relative; display: inline-flex; }
.usage-menu {
  position: absolute; top: 100%; right: 0; z-index: 30;
  padding-top: 6px;
  display: none;
  min-width: 236px;
}
.usage-wrap:hover .usage-menu,
.usage-wrap:focus-within .usage-menu { display: block; }
.usage-menu-inner {
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  animation: menu-pop .12s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .usage-menu-inner { animation: none; }
}
.usage-menu-head { margin-bottom: 9px; font-size: 12px; color: var(--text-dim); }

.lim-row + .lim-row { margin-top: 11px; }
.lim-top { display: flex; align-items: baseline; justify-content: space-between; }
.lim-label { font-size: 12.5px; color: var(--text); }
.lim-pct { font-size: 12.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
.lim-track {
  margin: 5px 0 3px;
  height: 6px;
  border-radius: 3px;
  background: var(--surface-2);
  overflow: hidden;
}
.lim-fill { height: 100%; border-radius: 3px; transition: width .2s ease-out; }
@media (prefers-reduced-motion: reduce) { .lim-fill { transition: none; } }
.lim-reset { font-size: 11px; color: var(--text-dim); }

/* Headroom, not window: 90% of the weekly is as urgent as 90% of the 5-hour. */
.lim-ok { color: var(--accent); }
.lim-warn { color: #b45309; }
.lim-crit { color: var(--danger); }
.lim-fill.lim-ok { background: var(--accent); }
.lim-fill.lim-warn { background: #f59e0b; }
.lim-fill.lim-crit { background: var(--danger); }

.usage-empty { font-size: 12.5px; color: var(--text-dim); }
.usage-note {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-dim);
}
.usage-note.is-stale { color: var(--danger); }

.usage-models:empty { display: none; }
.usage-models {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
.usage-models-head { margin-bottom: 5px; font-size: 11px; color: var(--text-dim); }
.mdl-row {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 11.5px; line-height: 1.7;
}
.mdl-name {
  color: var(--text);
  /* A long model id must not push the cost off the panel. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mdl-tok { margin-left: auto; color: var(--text-dim); }
.mdl-cost { min-width: 58px; text-align: right; color: var(--text); }

.caffeine-wrap { position: relative; display: inline-flex; }
.caffeine { font-size: 14px; line-height: 1.2; }
.caffeine.on {
  background: var(--awake-bg);
  border-color: var(--awake-border);
  color: var(--awake-fg);
  font-weight: 600;
}
.caffeine.on:hover { background: var(--awake-bg); border-color: var(--awake-fg); }

.caffeine-menu {
  position: absolute; top: 100%; right: 0; z-index: 30;
  padding-top: 6px;
  display: none;
  min-width: 208px;
}
.caffeine-wrap:hover .caffeine-menu,
.caffeine-wrap:focus-within .caffeine-menu { display: block; }

.caffeine-menu-inner {
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  animation: menu-pop .12s ease-out;
}
@keyframes menu-pop {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .caffeine-menu-inner { animation: none; }
}

.caffeine-menu-head {
  margin-bottom: 8px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-dim);
  /* The overdue explanation is a second line under the state — one long
     run-on sentence is the shape that made this unreadable to begin with. */
  white-space: pre-line;
}
.caffeine-presets { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.awake-preset { text-align: center; }
/* The one a bare click gives you, marked so the menu explains the button. */
.awake-preset.is-default { border-color: var(--awake-border); color: var(--awake-fg); }

.caffeine-custom { display: flex; gap: 6px; margin-top: 6px; }
/* The unit rides inside the box's border so it reads as part of the field
   rather than as a word sitting next to it. */
.caffeine-field {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.caffeine-field:focus-within { border-color: var(--accent); }
.caffeine-custom input {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12px;
}
.caffeine-custom input:focus { outline: none; }
.caffeine-unit { flex: none; font-size: 11px; color: var(--text-dim); }

.caffeine-error {
  margin-top: 6px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--danger);
}

.caffeine-stop { width: 100%; margin-top: 8px; color: var(--danger); }

/* The caveats, in the menu rather than a `title` — a native tooltip draws over
   the menu it is trying to explain. */
.caffeine-note {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-dim);
  white-space: pre-line;
}

/* Something else is holding the machine awake. Danger-coloured because the
   whole point is that it is happening without anyone having asked here. */
.caffeine-stray {
  padding: 6px 9px;
  color: var(--danger);
  border-color: var(--danger);
  font-weight: 600;
  font-size: 12.5px;
}
.caffeine-stray:hover { background: rgba(220, 38, 38, .1); }

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

.btn {
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.btn:hover { background: var(--surface-2); }
.btn-ghost { background: transparent; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); }
.btn-danger { color: var(--danger); border-color: transparent; }
.btn-danger:hover { background: rgba(220, 38, 38, .1); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

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

.board {
  display: grid;
  grid-auto-flow: column;
  /* 225 rather than 260 so six columns still land on screen at 1512pt: at 260
     the minimum binds and the board scrolls ~170px, which puts Done — the
     column you most want in peripheral vision — off the right edge. At 225 the
     columns stretch to ~231 instead and everything is visible. (`scrollWidth`
     still reads 40px over `clientWidth`, the horizontal padding; measured, and
     nothing is actually clipped.) The minimum only decides when scrolling
     starts; on wider screens 1fr stretches past it either way. */
  grid-auto-columns: minmax(225px, 1fr);
  gap: 14px;
  padding: 18px 20px 40px;
  overflow-x: auto;
  align-items: start;
}

.column {
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 10px;
  min-height: 120px;
}

.column-head {
  display: flex; align-items: center; gap: 8px;
  padding: 2px 4px 10px;
  font-size: 12px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-dim);
}
.column-count {
  margin-left: auto;
  padding: 1px 7px;
  background: var(--surface);
  border-radius: 20px;
  font-weight: 600;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 8px;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform .12s, border-color .12s;
  /* Card text is summaries people paste, so it is full of things a browser
     will not break: `fixtures/checklist_entry_a_D.json`, run lists like
     `(crea_r20/eco_r21/Prod_r12/…)`. CJK wraps anywhere by default, which is
     why most of a card looks fine and one line in ten shoots past the border
     into the next column — the card does not clip, so it simply paints
     outside itself.

     On `.card` rather than on `.card-title`/`.card-sub` so anything added to a
     card later is covered too. `break-word` rather than `anywhere`: only
     `anywhere` shrinks intrinsic min-content width, which would let the meta
     row's chips break mid-word when a column gets narrow. */
  overflow-wrap: break-word;
}
/* Waiting on the user. The thick left edge is what makes it findable while
   scanning a full column — the badge alone is one more chip among six.
   Deliberately ahead of :hover and .selected so those still win on colour: the
   width is the part that has to survive, not the hue. */
.card.needs-input { border-color: var(--waiting-border); border-left-width: 3px; }
/* After needs-input on purpose: a card that is both keeps the more time-critical
   edge. No information is lost — both chips still render in the meta row. */
.card.is-due { border-color: var(--due-border); border-left-width: 3px; }

.card:hover { transform: translateY(-1px); border-color: var(--accent); }
.card.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

/* The card being dragged is walked through the DOM live, so it *is* the
   placeholder: it shows where the card would land rather than where it came
   from. Dimmed and dashed because the cursor is already carrying the browser's
   own opaque drag image of it, and two solid copies read as two cards. */
.card.dragging { opacity: .4; border-style: dashed; }
.card.dragging:hover { transform: none; }

.column.drop-target {
  background: var(--accent-soft);
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}

/* The question itself, not just the fact that there is one: two lines is enough
   to decide whether you can answer it without opening the card. */
.card-question {
  margin-top: 7px;
  color: var(--waiting-fg);
  font-size: 12.5px;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-schedule {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-top: 7px;
}
.schedule-when { font-size: 12px; color: var(--text-dim); }
.card.is-due .schedule-when { color: var(--due-fg); font-weight: 600; }

.card-title { font-weight: 560; margin-bottom: 7px; line-height: 1.35; }
.card-meta { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.card-sub { margin-top: 6px; color: var(--text-dim); font-size: 12px; }
/* Always rendered, hidden once the column has anything in it. A drag needs
   something to aim at in an empty column, and a node that appeared halfway
   through the drag would move the very target the pointer was heading for. */
.empty-col { padding: 14px 4px; color: var(--text-dim); font-size: 12.5px; }
.column:has(.card) .empty-col { display: none; }

/* ---------------------------------------------------------------- chips */

.chip {
  display: inline-flex; align-items: center;
  padding: 1px 7px;
  border-radius: 5px;
  font-size: 11.5px; font-weight: 600;
  background: var(--surface-2);
  color: var(--text-dim);
  border: 1px solid var(--border);
}
.chip-P0 { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
.chip-P1 { background: #ffedd5; color: #9a3412; border-color: #fed7aa; }
.chip-P2 { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.chip-P3 { background: var(--surface-2); }
.chip-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* The attention chips. They must live *after* the bare `.chip` rule above, not
   next to the card styles they belong to: same specificity, and the later rule
   wins. Putting them earlier is how the "@我" badge shipped rendering as an
   ordinary grey tag — still legible, but no longer the thing that catches the
   eye, which was its entire job. */
.chip-waiting {
  background: var(--waiting-bg);
  color: var(--waiting-fg);
  border-color: var(--waiting-border);
}
.chip-due {
  background: var(--due-bg);
  color: var(--due-fg);
  border-color: var(--due-border);
}
/* Also not an attention chip. Whose card it is is a standing fact, true of the
   card the whole time it exists; the amber ones mean something needs doing now.
   Making assignment loud would compete with them and, once every card carries
   one, would drown them. */
.chip-assignee {
  background: transparent;
  color: var(--text-dim);
  cursor: help;
}

/* Not an attention chip, on purpose: this one reports how a card got where it
   is, and nothing about it needs doing. Dashed so it reads as provisional next
   to the solid chips — the card is in Done on a technicality, not a verdict. */
.chip-auto-done {
  background: transparent;
  color: var(--muted);
  border-style: dashed;
  cursor: help;
}

/* Waiting on something, and no longer waiting on it. Two chips rather than one
   that changes wording, because the second is the only *good* news the board
   ever puts on a card and it should not have to be read to be noticed. Same
   placement rule as the chips above — after the bare `.chip`. */
.chip-blocked {
  background: var(--blocked-bg);
  color: var(--blocked-fg);
  border-color: var(--blocked-border);
}
.chip-unblocked {
  background: var(--clear-bg);
  color: var(--clear-fg);
  border-color: var(--clear-border);
  font-weight: 650;
}
.chip-schedule, .chip-tick {
  font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
}
.chip-schedule:hover, .chip-tick:hover { filter: brightness(.96); }
.chip-tick {
  background: var(--due-fg);
  color: var(--surface);
  border-color: var(--due-fg);
}
@media (prefers-color-scheme: dark) {
  .chip-schedule:hover, .chip-tick:hover { filter: brightness(1.2); }
  .chip-tick { color: #1a0209; }
}

/* Work-type colours. Distinct hues rather than a shared accent, so the type is
   readable at a glance across a full column without reading the word. */
.kind-feature  { background: #e0e7ff; color: #3730a3; border-color: #c7d2fe; }
.kind-bugfix   { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
.kind-research { background: #f3e8ff; color: #6b21a8; border-color: #e9d5ff; }
.kind-doc      { background: #ccfbf1; color: #115e59; border-color: #99f6e4; }
.kind-ops      { background: #fef3c7; color: #92400e; border-color: #fde68a; }
.kind-chore    { background: #e5e7eb; color: #374151; border-color: #d1d5db; }

/* The one type only the taskboard board has. Violet, matching that board's own
   tint — it is the type that exists because that board exists. */
.kind-design   { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }

/* The personal board's types. Its own hues rather than a reuse of the six
   above: the two vocabularies never share a screen, so nothing forces them
   apart, but picking green for 健康 and grey for 其他 is worth more than the
   consistency of a lookup table. Keyed on the stored slug, which is why those
   are ASCII. */
.kind-life     { background: #dbeafe; color: #1e40af; border-color: #bfdbfe; }
.kind-health   { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
.kind-study    { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }
.kind-money    { background: #fef9c3; color: #854d0e; border-color: #fde047; }
.kind-home     { background: #ffe4e6; color: #9f1239; border-color: #fecdd3; }
.kind-other    { background: #e5e7eb; color: #374151; border-color: #d1d5db; }

@media (prefers-color-scheme: dark) {
  .kind-feature  { background: #23285c; color: #a5b4fc; border-color: #312e81; }
  .kind-bugfix   { background: #451a1a; color: #fca5a5; border-color: #7f1d1d; }
  .kind-research { background: #3b1d5c; color: #d8b4fe; border-color: #581c87; }
  .kind-doc      { background: #10403c; color: #5eead4; border-color: #134e4a; }
  .kind-ops      { background: #45280f; color: #fcd34d; border-color: #78350f; }
  .kind-chore    { background: #2b303b; color: #cbd5e1; border-color: #3a4150; }
  .kind-design   { background: #2e2258; color: #c4b5fd; border-color: #4c1d95; }

  .kind-life     { background: #172a56; color: #93c5fd; border-color: #1e3a8a; }
  .kind-health   { background: #10331f; color: #86efac; border-color: #14532d; }
  .kind-study    { background: #2e2258; color: #c4b5fd; border-color: #4c1d95; }
  .kind-money    { background: #3d3410; color: #fde047; border-color: #713f12; }
  .kind-home     { background: #4a1526; color: #fda4af; border-color: #881337; }
  .kind-other    { background: #2b303b; color: #cbd5e1; border-color: #3a4150; }
}

/* Title-line form of the type: keeps the hue as text colour, drops the chip's
   background so it reads as part of the heading. Must come after the .kind-*
   block above, which it overrides. */
.kind-label {
  background: none;
  border: none;
  padding: 0;
  margin-right: 2px;
  font-weight: 600;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- kindbar */

.kindbar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 12px 20px 0;
}
.kindbar-tabs { display: flex; flex-wrap: wrap; gap: 6px; }

.kind-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 20px;
  background: var(--surface);
  color: var(--text-dim);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: border-color .15s, color .15s;
}
.kind-tab:hover { border-color: var(--accent); }
.kind-tab.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}

.kind-count {
  padding: 0 6px;
  border-radius: 20px;
  background: var(--surface-2);
  font-size: 11.5px;
  font-weight: 600;
}
.kind-tab.active .kind-count { background: rgba(255, 255, 255, .25); }

/* Pushed to the far end and given the waiting hue, so it never reads as one
   more work type — it asks a different question about the same cards. Must come
   after the .kind-tab rules above: it carries both classes, and at equal
   specificity the later rule is the one that paints it. */
.waiting-tab {
  margin-left: auto;
  background: var(--waiting-bg);
  color: var(--waiting-fg);
  border-color: var(--waiting-border);
  font-weight: 600;
}
.waiting-tab:hover { border-color: var(--waiting-fg); }
.waiting-tab.active {
  background: var(--waiting-fg);
  border-color: var(--waiting-fg);
  color: var(--surface);
}
.waiting-tab .kind-count { background: rgba(120, 53, 15, .16); }
.waiting-tab.active .kind-count { background: rgba(255, 255, 255, .22); }

/* Sits next to the waiting pill at the far end; `margin-left: auto` on the
   first of the two is what pushes the pair over. */
.due-tab {
  margin-left: auto;
  background: var(--due-bg);
  color: var(--due-fg);
  border-color: var(--due-border);
  font-weight: 600;
}
.due-tab:hover { border-color: var(--due-fg); }
.due-tab.active {
  background: var(--due-fg);
  border-color: var(--due-fg);
  color: var(--surface);
}
.due-tab .kind-count { background: rgba(136, 19, 55, .16); }
.due-tab.active .kind-count { background: rgba(255, 255, 255, .22); }
/* Only one of the two may claim the auto margin, or the second gets pushed to
   the far edge on its own with a gap in between. */
.due-tab:not([hidden]) ~ .waiting-tab { margin-left: 0; }

.card-session { margin-top: 7px; }
.chip-session {
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
  background: #dcfce7;
  color: #166534;
  border: 1px solid #bbf7d0;
}
.chip-session:hover { filter: brightness(.96); }

/* No live tab behind this one — clicking reopens the conversation rather than
   jumping to it. Amber rather than green so the difference is visible without
   hovering for the tooltip. */
.chip-resume { background: #fef3c7; color: #92400e; border-color: #fde68a; }

/* Held on another machine. Deliberately grey and deliberately not a button:
   there is no tab on this computer to open, so anything that looks pressable
   would be a promise the board cannot keep. */
.chip-elsewhere {
  font-size: 11.5px;
  font-weight: 600;
  cursor: default;
  background: #e5e7eb;
  color: #4b5563;
  border: 1px solid #d1d5db;
}

@media (prefers-color-scheme: dark) {
  .chip-session { background: #14311f; color: #86efac; border-color: #14532d; }
  .chip-session:hover { filter: brightness(1.25); }
  .chip-resume { background: #45280f; color: #fcd34d; border-color: #78350f; }
  .chip-elsewhere { background: #262b33; color: #9ca3af; border-color: #374151; }
}

.session-actions { margin-top: 12px; }

@media (prefers-color-scheme: dark) {
  .chip-P0 { background: #451a1a; color: #fca5a5; border-color: #7f1d1d; }
  .chip-P1 { background: #45280f; color: #fdba74; border-color: #7c2d12; }
}

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

.scrim {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(10, 12, 16, .4);
}

.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 50;
  width: min(620px, 94vw);
  display: flex; flex-direction: column;
  background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: -8px 0 30px rgba(0, 0, 0, .18);
}

.drawer-head {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border);
}
.drawer-title-wrap { flex: 1; }
.drawer-head h2 { margin: 4px 0 0; font-size: 17px; line-height: 1.35; font-weight: 620; }
.task-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; color: var(--text-dim);
}

.drawer-body { flex: 1; overflow-y: auto; padding: 16px 18px 40px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.summary { margin: 12px 0 0; color: var(--text-dim); }
.summary:empty { display: none; }

.panel { margin-top: 22px; }
.panel h3 {
  margin: 0 0 10px;
  font-size: 11.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-dim);
}

.field { display: block; margin-bottom: 10px; }
.field > span { display: block; margin-bottom: 4px; font-size: 12px; color: var(--text-dim); }
.field input, .field select, .field textarea {
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
}
.field textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
.field-row { display: flex; gap: 10px; }
.field-row .field { flex: 1; }

.launch-row, .status-row { display: flex; flex-wrap: wrap; gap: 8px; }
.launch-note { margin-top: 8px; font-size: 12px; color: var(--text-dim); }

/* Under the board buttons in the drawer. Sits below rather than beside them
   because it is a consequence of the click, not a label for it. */
.space-note { margin: 10px 0 0; font-size: 12px; line-height: 1.5; color: var(--text-dim); }

/* What the conversation on this card has spent. Boxed like .brief rather than
   set as another <dl> row: it is a measurement of the session, not one more
   attribute of it, and the total wants to be readable without being hunted for
   among five other definition terms. */
.session-tokens {
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.tok-total { display: flex; align-items: baseline; gap: 6px; }
.tok-n {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 17px; font-weight: 650; color: var(--text);
}
.tok-unit { font-size: 12px; color: var(--text-dim); }
.tok-msgs { margin-left: auto; font-size: 12px; color: var(--text-dim); }
.tok-models {
  margin-top: 6px; font-size: 12px; color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.tok-split {
  margin-top: 4px; font-size: 11.5px; color: var(--text-dim);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* What the whole conversation cost, under what this card's share of it cost.
   Present so the share is legible: $3 out of $200 and $3 out of $4 are
   different statements and the number alone cannot tell them apart. */
.tok-session {
  margin-top: 6px; font-size: 12px; color: var(--text-dim);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* Everything that makes the figure above weaker than it looks. Given its own
   line rather than folded into the note: the note is fixed text describing the
   rule, these are facts about this particular card, and a reader scanning for
   "why might this be wrong" should not have to find them inside a paragraph. */
.tok-caveats {
  margin-top: 6px; font-size: 11.5px; line-height: 1.5; color: var(--waiting-fg);
}
/* The rule the number was produced by, not a footnote to it. An amount of money
   with no stated basis gets read as a bill; this one is a bounded share of a
   conversation and says so every time it is shown. */
.tok-note {
  margin-top: 8px; font-size: 11.5px; line-height: 1.5; color: var(--text-dim);
}

.brief {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  overflow-x: auto;
}
.brief h1, .brief h2, .brief h3 { font-size: 14px; margin: 14px 0 6px; }
.brief h1:first-child, .brief h2:first-child, .brief h3:first-child { margin-top: 0; }
.brief p { margin: 0 0 10px; }
.brief ul, .brief ol { margin: 0 0 10px; padding-left: 22px; }
.brief li { margin-bottom: 3px; }
.brief code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px;
  background: var(--surface-2);
  padding: 1px 5px; border-radius: 4px;
}
.brief pre {
  background: var(--surface-2);
  padding: 10px 12px; border-radius: 7px;
  overflow-x: auto;
}
.brief pre code { background: none; padding: 0; }
.brief a { color: var(--accent); }
.brief blockquote {
  margin: 0 0 10px; padding-left: 12px;
  border-left: 3px solid var(--border); color: var(--text-dim);
}
/* The scroll belongs to the table, not to the page: the drawer is narrow and a
   wide table would otherwise push the whole board sideways. */
.brief .md-table { margin: 0 0 10px; overflow-x: auto; }
.brief table { border-collapse: collapse; font-size: 12.5px; }
.brief th, .brief td {
  border: 1px solid var(--border);
  padding: 4px 9px; text-align: left; vertical-align: top;
}
.brief th { background: var(--surface-2); font-weight: 600; white-space: nowrap; }
.brief tbody tr:nth-child(even) td { background: var(--surface-2); }

.questions { margin: 0; padding-left: 20px; }
.questions li { margin-bottom: 6px; }

.meta { margin: 0; display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: 6px 14px; }
.meta dt { color: var(--text-dim); font-size: 12.5px; }
.meta dd {
  margin: 0; font-size: 12.5px; word-break: break-all;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.meta dd a { color: var(--accent); }

.launches { margin: 0; padding: 0; list-style: none; font-size: 12.5px; color: var(--text-dim); }
.launches li { padding: 4px 0; border-bottom: 1px solid var(--border); }
.launches li:last-child { border-bottom: none; }

.danger-row {
  display: flex; justify-content: space-between; gap: 10px;
  margin-top: 28px; padding-top: 16px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------------- waiting-on-you panel */

/* Sits at the top of the drawer, above the session and hand-off panels: if
   something is waiting on you, it is the reason you opened the card. */
.panel-waiting {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px solid var(--waiting-border);
  border-radius: var(--radius);
  background: var(--waiting-bg);
}
.panel-waiting h3 { color: var(--waiting-fg); }
.waiting-question {
  margin: 0 0 4px;
  font-size: 15px; font-weight: 560; line-height: 1.45;
  color: var(--text);
}
.waiting-since { margin: 0 0 12px; font-size: 12px; color: var(--waiting-fg); }
.waiting-since:empty { display: none; }
.panel-waiting .field > span { color: var(--waiting-fg); }
.panel-waiting textarea {
  background: var(--surface);
  border-color: var(--waiting-border);
  /* Prose, not a path or a command — the monospace default other fields use
     makes a two-sentence Chinese answer needlessly hard to read back. */
  font-family: inherit;
  font-size: 13.5px;
}
.panel-waiting .btn-ghost { color: var(--waiting-fg); }

/* The same amber, deliberately: flagging it yourself puts the card in exactly
   the inbox an agent's question would, so the panel that does it should not
   look like a different feature. Dashed rather than solid — this one is an
   offer, not something already demanding attention. */
.panel-flag {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px dashed var(--waiting-border);
  border-radius: var(--radius);
}
.panel-flag h3 { color: var(--waiting-fg); }
.flag-hint { margin: 0 0 12px; font-size: 12.5px; line-height: 1.5; color: var(--text-dim); }
.panel-flag .btn-ghost { color: var(--waiting-fg); border-color: var(--waiting-border); }
.panel-flag .btn-ghost:hover { background: var(--waiting-bg); }

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

/* Quiet by default and green once the wait is over — the same inversion the
   chips make, for the same reason: the release is the state worth noticing. */
.panel-blocked {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.panel-blocked.is-cleared {
  border-color: var(--clear-border);
  background: var(--clear-bg);
}
.panel-blocked.is-cleared h3, .panel-blocked.is-cleared .blocked-now {
  color: var(--clear-fg);
}
.panel-blocked.is-cleared .btn-ghost { color: var(--clear-fg); }
.blocked-now { margin: 0 0 12px; font-size: 13px; line-height: 1.5; }

/* A card id rendered as something you can follow. Not a .btn: these sit inside
   a <dd> in a run of prose, and a button's box would break the line. */
.linkish {
  padding: 0; border: 0; background: none;
  color: var(--accent); font: inherit; text-align: left;
  cursor: pointer;
}
.linkish:hover { text-decoration: underline; }
.origin-session {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  background: var(--surface-2);
  padding: 1px 5px; border-radius: 4px;
}
.dialog-parent { color: var(--accent); }

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

.panel-recurring {
  margin-top: 18px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.panel-recurring.is-due {
  border-color: var(--due-border);
  background: var(--due-bg);
}
.panel-recurring.is-due h3, .panel-recurring.is-due .recur-line { color: var(--due-fg); }
.panel-recurring.is-due .btn-ghost { color: var(--due-fg); }
.panel-recurring.is-due .btn-primary {
  background: var(--due-fg); border-color: var(--due-fg); color: var(--surface);
}
.recur-line { margin: 0 0 12px; font-size: 14px; font-weight: 560; }

/* The tick-ahead gate. Inset and outlined so the second click is visibly a
   different act in a different place, rather than the same button pressed
   again — pressing the same pixel twice is exactly how a day got spent. */
.recur-ahead {
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--danger);
  border-radius: 8px;
  background: rgba(220, 38, 38, .06);
}
.recur-ahead-text { margin: 0 0 10px; font-size: 13px; line-height: 1.5; }

/* The weekday picker: seven small toggles, not a multi-select, because picking
   Mon/Wed/Fri should be three taps and legible at a glance afterwards. */
.weekday-row { display: flex; gap: 6px; margin-bottom: 8px; }
.weekday {
  width: 30px; height: 30px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text-dim);
  font: inherit; font-size: 13px;
  cursor: pointer;
}
.weekday:hover { border-color: var(--accent); }
.weekday.on {
  background: var(--accent); border-color: var(--accent);
  color: #fff; font-weight: 600;
}
.btn-tiny { padding: 3px 9px; font-size: 12px; }

.dialog-sub { margin: -8px 0 14px; color: var(--text-dim); font-size: 12.5px; }
.dialog-note { margin: 4px 0 0; color: var(--text-dim); font-size: 12.5px;
               font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.dialog-note.warn { color: var(--danger); font-family: inherit; }

/* ---------------------------------------------------------------- dialog */

#new-dialog, #schedule-dialog, #edit-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  padding: 20px;
  width: min(560px, 92vw);
  box-shadow: var(--shadow);
}
#schedule-dialog { width: min(400px, 92vw); }
/* 🔴 The edit dialog is the only one that can grow while it is open: the
   conflict panel appears below the boxes and carries the other version's text.
   Measured on a 840px window, that pushed Save off the bottom of the screen —
   a dialog that reports a problem and hides the button which resolves it is
   worse than one that never reported. So the dialog scrolls, and the actions
   ride along at the bottom of it. The negative margin is the dialog's own
   20px padding, bled through so nothing shows underneath the bar. */
#edit-dialog { max-height: 88vh; overflow-y: auto; }
#edit-dialog .dialog-actions {
  position: sticky;
  bottom: -20px;
  margin: 16px -20px -20px;
  padding: 12px 20px 20px;
  background: var(--surface);
}
#new-dialog::backdrop, #schedule-dialog::backdrop,
#edit-dialog::backdrop { background: rgba(10, 12, 16, .45); }
#new-dialog h2, #schedule-dialog h2, #edit-dialog h2 { margin: 0 0 16px; font-size: 16px; }

/* A save that was refused says so here, between the boxes it is protecting and
   the button that was pressed. Amber, not red: nothing is broken and nothing is
   lost — somebody else wrote first, and this is a decision, not a failure. */
.edit-notice {
  margin-top: 14px;
  padding: 10px 12px;
  border: 1px solid var(--waiting-border);
  border-radius: 8px;
  background: var(--waiting-bg);
  color: var(--waiting-fg);
  font-size: 12.5px;
  line-height: 1.5;
}
.edit-notice p { margin: 0 0 8px; }
.edit-notice p:last-child { margin-bottom: 0; }
/* What the card says now, for merging by hand. Scrolls rather than growing:
   a Background can be pages long, and a panel that pushes Save off the bottom
   of the screen hides the only way out of the conflict it is reporting. */
.edit-theirs {
  margin: 0 0 8px;
  padding: 8px 10px;
  max-height: 160px;
  overflow: auto;
  border-radius: 6px;
  background: var(--surface);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }

/* ---------------------------------------------------------------- archive */

.topbar-btn {
  padding: 5px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
}
.topbar-btn:hover { color: var(--text); border-color: var(--text-dim); }

/* Wider than the other dialogs and taller than it needs to be for four rows:
   this is a list that grows for as long as the board is used, and a panel that
   resizes as the archive fills would move the row under the cursor. */
.archive-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  padding: 20px;
  width: min(820px, 94vw);
  height: min(600px, 82vh);
}
.archive-dialog::backdrop { background: rgba(10, 12, 16, .45); }
.archive-head { display: flex; align-items: center; gap: 10px; }
.archive-head h2 { margin: 0; font-size: 16px; }
.archive-head input {
  flex: 1;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
}
.archive-sub { margin: 8px 0 12px; font-size: 12px; color: var(--text-dim); }
.archive-body { overflow-y: auto; height: calc(100% - 84px); }
.ar-empty { padding: 24px 0; text-align: center; color: var(--text-dim); font-size: 13px; }

.ar-month { margin-bottom: 18px; }
.ar-month-head {
  display: flex; align-items: baseline; gap: 10px;
  padding: 0 0 6px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
  font-size: 13px; font-weight: 600;
}
.ar-month-n { font-weight: 400; font-size: 12px; color: var(--text-dim); }
.ar-month-cost {
  margin-left: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; color: var(--text-dim);
}

.ar-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 6px; border-radius: 6px;
  font-size: 13px; cursor: pointer;
}
.ar-row:hover, .ar-row:focus-visible { background: var(--surface-2); outline: none; }
.ar-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; color: var(--text-dim); min-width: 40px;
}
.ar-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ar-meta { font-size: 11.5px; color: var(--text-dim); white-space: nowrap; }
.ar-when {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px; color: var(--text-dim); min-width: 38px; text-align: right;
}
.ar-cost {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; min-width: 62px; text-align: right;
}
/* A whole conversation's cost standing in for a card's share. Dimmed and
   prefixed rather than shown as an equal, because it is a different quantity —
   a column where the two look alike is a column that adds up to nothing. */
.ar-cost-loose { color: var(--text-dim); font-style: italic; }
.ar-cost-none { color: var(--border); }

/* Sits under the topbar rather than inside the result area: it is a statement
   about where the answer is, not one of the answers. */
.archive-hint {
  position: fixed; top: 56px; left: 50%; transform: translateX(-50%); z-index: 30;
  padding: 6px 12px;
  border: 1px solid var(--waiting-border);
  border-radius: 999px;
  background: var(--waiting-bg);
  color: var(--waiting-fg);
  font-size: 12.5px;
  box-shadow: var(--shadow);
}
.ar-link {
  border: 0; background: none; padding: 0;
  color: inherit; font: inherit; text-decoration: underline; cursor: pointer;
}

/* ---------------------------------------------------------------- toast */

.toast {
  position: fixed; bottom: 20px; left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  padding: 10px 16px;
  border-radius: 8px;
  background: var(--text);
  color: var(--bg);
  box-shadow: var(--shadow);
  max-width: 80vw;
}
.toast.error { background: var(--danger); color: #fff; }

/* The toast paints itself inverted — text on --bg over a --text background — so
   the button borrows currentColor rather than any palette token, and stays
   legible in both themes and on the red error variant. */
.toast-action {
  margin-left: 12px;
  padding: 3px 10px;
  border: 1px solid currentColor;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.toast-action:hover { background: rgba(127, 127, 127, 0.25); }

/* Weekly report (#128). Reuses the archive dialog's frame; the rows are their
   own thing because every figure carries a caveat that has to sit beside it
   rather than in a footnote — a caveat somebody has to scroll to is one that
   does not exist. */
.wk-row {
  display: grid;
  grid-template-columns: 150px auto 1fr;
  gap: 10px;
  align-items: baseline;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}
.wk-label { color: var(--text-dim); font-size: 13px; }
.wk-value { font-size: 15px; font-variant-numeric: tabular-nums; }
.wk-note { font-size: 12px; color: var(--text-dim); }
.wk-sub { padding: 6px 0 10px 160px; font-size: 12px; color: var(--text-dim); }
.wk-warn {
  margin: 0 0 10px;
  padding: 8px 10px;
  border-radius: 6px;
  background: var(--warn-bg);
  color: var(--warn-fg);
  font-size: 12px;
}

/* ------------------------------------------------------------------ *
 * A board served from somewhere else.
 *
 * The cards are real and everything about them works: reading, editing,
 * moving, answering, claiming. What is missing is this machine — there is no
 * terminal to open a tab in, no checkout to start an agent in, and no laptop
 * to keep awake. /api/meta omits those keys rather than sending empty ones,
 * and app.js puts `no-machine` on <body> when it sees a shared board.
 *
 * Hidden, not disabled. A greyed-out Start button promises that starting an
 * agent is a thing this page could do under other circumstances, and from a
 * browser on somebody's phone it simply is not.
 * ------------------------------------------------------------------ */
body.no-machine #caffeinate,
body.no-machine #d-handoff-panel,
body.no-machine #d-launches-panel,
body.no-machine #d-session-panel .session-actions {
  display: none !important;
}

/* The page for a browser holding no invitation, or a spent one. */
.locked-out {
  max-width: 34rem;
  margin: 18vh auto;
  padding: 0 1.5rem;
  text-align: center;
  line-height: 1.6;
}
.locked-out h1 { font-size: 1.4rem; margin-bottom: 1.2rem; opacity: 0.75; }
.locked-out-hint { font-size: 0.85rem; opacity: 0.6; margin-top: 1.5rem; }
