/* Reusable components: buttons, form controls, state panels, notices, stats, badges. */

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* 44px min target: comfortably operable by touch and by imprecise pointing. */
  min-height: 2.25rem;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition: background var(--transition), border-color var(--transition);
}

.btn:hover { background: var(--surface-sunken); }

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

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

/* A STRONG OUTLINE, NOT A QUIET ONE. It carries the accent on its border and its text so it
   reads as a real action, while leaving the filled accent to the one primary beside it. This
   is what lets New project and New task sit together without competing: same weight, one
   fill. Every colour comes from a token, so it follows the palette and both modes. */
.btn--outline {
  background: transparent;
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.btn--outline:hover {
  background: var(--accent);
  border-color: var(--accent-hover);
  color: var(--accent-contrast);
}

.btn--outline:active {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--accent-contrast);
}

/* The focus ring must stay visible against the transparent fill, so it is not left to the
   generic .btn rule to place over an accent background that is not there. */
.btn--outline:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

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

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

.btn--block {
  width: 100%;
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* RF-19 — the provider's own brand mark, decorative (aria-hidden, the button's text carries
   the accessible name). Never allowed to shrink away from a long label on a narrow button. */
.provider__logo {
  flex-shrink: 0;
}

/* Terry: "center the content of that tile." `.btn` already centers a flex row via
   `justify-content: center`; this makes the logo+label group's own text explicitly centered
   too, so nothing about the shorter label (once "(Default)" was removed) can read as
   left-leaning on a full-width `.btn--block` sign-in button. */
.btn--logo {
  text-align: center;
}

/* ---------- form controls ---------- */

/* RF-20260829-01 — IT HAS TO BE ABLE TO GET SMALLER, and until now it could not.
   A `<select>`'s default minimum width is its own content, and a flex item's default
   `min-width: auto` refuses to go below that — so at 320px the workspace picker's row (a
   nowrap label, this select, and the fixed 2rem "+" button) was 7px wider than the viewport
   and the whole page scrolled sideways. The MAX width is what keeps it sensible on a wide
   screen; the MIN is what lets it yield on a narrow one, and it had only the first.
   `text-overflow` is deliberately not set: a native select renders its own option text and
   truncates it itself. Measured at 320px in real Edge: 7px of page overflow, now 0. */
.picker__select {
  min-height: 2.25rem;
  min-width: 0;
  flex: 1 1 auto;
  max-width: 16rem;
  padding: var(--space-1) var(--space-3);
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  font-size: var(--text-sm);
}

.picker__select--compact { max-width: 9rem; }

/* ---------- state panels ---------- */

.state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 40vh;
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

.state__title { margin: 0; }

.state__text {
  margin: 0;
  color: var(--text-muted);
  max-width: 46ch;
}

/* RF-20260827-08 — A REFUSED ACCESS REQUEST SAYS WHY, and says it in the colour this product
   already uses for a refusal. Not colour alone: it is a sentence, and it carries role="alert". */
.state__text--error {
  color: var(--color-danger);
  font-weight: 500;
}

.state__detail {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-subtle);
  font-family: var(--font-mono);
}

.state__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(20rem, 100%);
}

/* The self-service access-request form on the "Waiting for approval" screen. */
.state__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(28rem, 100%);
}

/*
  Error panels are visually distinct from empty panels on purpose. An empty state invites
  you to create something; an error state tells you something failed. They must never be
  mistaken for one another.
*/
.state--error {
  border: 1px solid var(--color-danger);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-danger) 6%, var(--surface));
}

.state--forbidden {
  border-color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 6%, var(--surface));
}

.state--auth {
  gap: var(--space-4);
}

/* ---------- notices ---------- */

.notice {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  border-left: 3px solid var(--color-info);
  background: var(--surface);
  margin-bottom: var(--space-5);
}

.notice--warn {
  border-left-color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 8%, var(--surface));
}

.notice__text {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ---------- stats ---------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--space-4);
  margin: 0;
}

.stat {
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.stat__label {
  margin: 0 0 var(--space-1);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-subtle);
}

.stat__value {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat__detail {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

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

.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.1rem var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
  white-space: nowrap;
}

.badge--version { font-family: var(--font-mono); }

/* ---------- dialogs ----------
   There was no modal CSS at all, which is why the first dialog rendered as a form partway
   down the page. An overlay needs to be taken out of flow explicitly; nothing about
   role="dialog" does that on its own. */

.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--overlay);
  overflow-y: auto;
}

.modal__panel {
  position: relative;
  width: 100%;
  max-width: 30rem;
  max-height: calc(100vh - var(--space-8));
  overflow-y: auto;
  padding: var(--space-5);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.modal__panel--wide { max-width: 46rem; }
.modal__panel--full { max-width: 64rem; }

/* RF-20260827-11 — A DIALOG WITH TABS HAS A STABLE SHELL.
   MEASURED IN EDGE AT beta.246, switching from the task to its comments: the panel moved from
   top 24 to top 166 and shrank from 860 to 576, taking the tab strip (89 -> 231) and the buttons
   (771 -> 628) with it. The control somebody was reaching for moved 143px while they reached.
   TWO RULES MEETING CAUSED IT: the panel's height followed its content, and `.modal` centres.
   Either alone is fine; together they mean "the more this tab has to say, the further up the
   screen the dialog starts".
   SO: pinned near the top, a fixed height, and the panels scroll INSIDE. The outer frame stops
   being a function of which tab is open. `min()` keeps it responsive — on a short screen it is
   the viewport that decides, not a number somebody guessed. */
.modal--stable { align-items: flex-start; }
.modal__panel--stable {
  /* RF-20260901-05 — THE DIALOG USES THE SCREEN IT IS ON.
   *
   * MEASURED AT `beta.344`: a 908px viewport gave a 763px panel holding a 420px form body — less
   * than half the window, with 145px of empty overlay above and below it and a scrollbar inside a
   * dialog that had room to spare. `84vh` was chosen when the panel was centred and its content
   * was short; pinned to the top with fixed chrome at both ends, the remaining 16% buys nothing.
   *
   * `calc(100vh - 3rem)` IS ARITHMETIC, NOT A GUESS, and it is why this is still responsive. The
   * overlay contributes `--space-4` of padding above and below (2rem) and the panel adds
   * `--space-3` of margin above it (0.75rem) — 2.75rem of real chrome. Three leaves a quarter of a
   * rem of slack and never overflows. `64rem` caps it on a very tall display, where a dialog the
   * full height of a portrait monitor is a worse answer than one that stops.
   *
   * THE VERTICAL PADDING COMES DOWN WITH IT. `--space-5` top and bottom spent 48px of a fixed
   * height on air, inside a panel whose head, tab strip and action row are already separated by
   * their own margins. The horizontal padding is unchanged: that one is doing work. */
  margin-top: var(--space-3);
  height: min(calc(100vh - 3rem), 64rem);
  max-height: none;
  padding-top: var(--space-4);
  padding-bottom: var(--space-4);
  display: flex;
  flex-direction: column;
  /* The PANEL does not scroll; its panels do. A panel that scrolled as well would give the same
     content two scrollbars and let the head slide away from the tabs it labels. */
  overflow: hidden;
}
.modal__panel--stable .modal__head { flex: 0 0 auto; }
.modal__panel--stable .modal__form { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
/* The strip and the actions row are chrome: fixed at the two ends, so neither moves when the
   content between them changes. */
.modal__panel--stable .modal__tabs--sections,
.modal__panel--stable .modal__actions,
.modal__panel--stable .modal__guide { flex: 0 0 auto; }
/* WHICHEVER PANEL IS SHOWING FILLS WHAT IS LEFT AND SCROLLS ITSELF. */
.modal__panel--stable .modal__body,
.modal__panel--stable .modal__section { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* RF-20260827-11 — THE SUBJECT, KEPT VISIBLE ON EVERY TAB.
   One line, truncated rather than wrapped: a long task name must not grow the head and push the
   tab strip down, which would be the same defect one element along.

   RF-20260827-28 — THIS IS THE ACTUAL FLEX CHILD OF `.modal__head`, so `flex: 1 1 auto` belongs
   HERE, not on `.modal__title` below. `.modal__head` is `space-between` with up to three
   children: this wrapper, the optional waffle (`titleActions`), and close. Before this wrapper
   existed, `.modal__title` sat directly in `.modal__head` and growing IT was correct — see its
   own comment, now half true. Once the title gained a `.modal__context` sibling and both moved
   into `.modal__heading` (this RF), the h2's own flex-grow stopped reaching `.modal__head`
   at all: a nested element's `flex` property only matters to the flexbox it is DIRECTLY a
   child of. With nothing here to take the free space, `space-between` divided the row into two
   equal gaps and the waffle landed in the dead centre of the title row — the exact defect
   `RF-20260827-28` reported, reintroduced by this RF without either of the two touching the
   same line. `app/test/modalwaffle.test.js` still passed throughout, because it asserted the
   rule against `.modal__title`, which was never wrong on its own terms — just no longer the
   element that mattered. */
.modal__heading { min-width: 0; flex: 1 1 auto; }
.modal__context {
  margin: 2px 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.modal__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.modal__title {
  margin: 0;
  /* HISTORICAL: this `flex`/`min-width` pair is what made the waffle land beside close, back
     when this `<h2>` sat directly in `.modal__head`. It no longer does — `.modal__heading` (see
     above) sits between them now, and a nested element's `flex` only reaches the flexbox it is
     DIRECTLY a child of, so these two lines have had no effect on `.modal__head`'s layout since
     `RF-20260827-11`. Kept here because `min-width: 0` still does its own, unrelated job —
     truncating a long title instead of forcing this element wider than its container — and
     `flex: 1 1 auto` is inert rather than harmful. The row-layout fix now lives on
     `.modal__heading`, see `RF-20260827-28`. */
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
}

.modal__close {
  flex: none;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  font-size: var(--text-lg);
  line-height: 1;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.modal__close:hover { background: var(--surface-sunken); color: var(--text); }

.modal__description {
  margin: 0 0 var(--space-4);
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.modal__form { display: flex; flex-direction: column; gap: var(--space-4); }

.modal__error {
  padding: var(--space-3);
  color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-danger) 35%, transparent);
  border-radius: var(--radius);
  font-size: var(--text-sm);
}

.modal__error[hidden] { display: none; }

/* RF-20260827-11 — HIDDEN MEANS HIDDEN, and this is the same cascade trap RF-20260826-07 found
   on the drawer toggle. `[hidden]`'s own `display: none` comes from the user-agent stylesheet, so
   ANY author rule setting `display` beats it — and this row sets `display: flex` two lines down.
   The result measured in Edge: the actions row carried `hidden` and computed `display: flex`, so
   "Save changes" was still the most prominent control on the Comments tab, which is precisely the
   reported defect. A hiding rule that loses to the rule beside it is not a hiding rule. */
.modal__actions[hidden] { display: none; }
/* AND THE SAME FOR THE CONTROLS INSIDE IT. `.btn { display: inline-flex }` lives in this file and
   beats `[hidden]` for exactly the reason above; a Save button that carries `hidden` and renders
   anyway is the same defect wearing a different hat. */
.modal__actions .btn[hidden],
.modal__actions .modal__missing[hidden] { display: none; }

.modal__actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
}

/* RF-20260826-13 — WHY THE SUBMIT BUTTON IS DISABLED, beside the button it is about.
   `margin-right: auto` is what keeps it on the LEFT while the actions stay on the right: the row
   is `justify-content: flex-end`, so without it the note would be pushed up against Cancel.
   `:empty` is load-bearing rather than tidy — the row has a `gap`, so an empty <p> that still
   generated a box would add a permanent indent to the actions of EVERY dialog in the product,
   most of which have no required fields at all. */
/* RF-20260827-09 — THE TASK DIALOG'S HELP REGION.
   A RESERVED HEIGHT, and that is the whole point of the rule. The requirement is explicit that
   the dialog must not resize or jump; a region that grows from nothing to two lines the moment
   somebody focuses a checkbox would move every control below it, including the buttons they were
   reaching for. Two lines are reserved whether or not there is anything in them. */
.modal__guide {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  /* TWO LINES, PLUS 2px FOR THE ROUNDING. Measured in Edge: the two-line default rendered at
     58px against a 57px reservation, so the region SHRANK by a pixel when the text narrowed to
     one definition — and a region that changes height at all is a region that moves whatever is
     below it. Line boxes are rounded per engine and per font, so the allowance is deliberate
     rather than a magic number: it is the smallest amount that makes the reservation a ceiling
     instead of a near miss. `verify-taskguide.mjs` measures it and fails if it stops being one. */
  min-height: calc(2 * 1.45em + 2 * var(--space-2) + 2px);
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.45;
}

.modal__missing {
  margin: 0 auto 0 0;
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.modal__missing:empty { display: none; }

/* RF-20260826-14 — "ADD TO BACKLOG", beside the Title when creating.
   A row rather than a bare checkbox: the label and the one-line consequence sit together, so the
   decision can be made without opening anything. `align-items: center` keeps the box on the
   label's line; the hint wraps under it when the column is narrow. */
/* RF-20260901-05 — "Add to backlog" IS A CHECKBOX FIELD, so it wears the checkbox field's clothes.
 * It carried three rules of its own — a smaller label, a smaller hint, a different gap — and sat
 * directly beside MSR, which is a real `.field--check`. Two controls doing the same thing in the
 * same group looked like two different products. The row shape now comes entirely from
 * `.field--check` above; this rule only says that it IS a flex row and fills the cell it is given.
 * (`.field--check` supplies direction, alignment and gap but not `display` — that comes from
 * `.field`, which this element deliberately is not, to avoid nesting a field inside a field.) */
.tf-backlog { display: flex; width: 100%; }

/* RF-20260826-12 — the Required list under Field Defaults. One field per row, the box first so
   a column of them can be scanned down the left edge rather than hunted for at each line's end. */
/* RF-20260827-02 — the separate Required panel is gone; the switch lives on its field's own row.
   `.taskrequired__control` survives because it is the checkbox itself, now rendered inside
   `.taskdefaults__required`. The list/item/name rules went with the panel they described. */
.taskrequired__control { flex: 0 0 auto; }

/* On a narrow screen the note wraps above the buttons rather than squeezing them: a long list of
   missing fields must never shrink Cancel and Create to unreadable slivers. */
@media (max-width: 40rem) {
  .modal__actions { flex-wrap: wrap; }
  .modal__missing { flex: 1 0 100%; margin: 0 0 var(--space-1) 0; }
}

/* The page behind a dialog is inert. Without this the underlying call-to-action stays
   clickable and keeps competing for attention with the dialog on top of it. */
body.has-modal { overflow: hidden; }
[inert] { pointer-events: none; user-select: none; }
[inert], [inert] * { opacity: 0.55; }

/* ---------- form fields ---------- */

.field { display: flex; flex-direction: column; gap: var(--space-2); }

/* WITHOUT THIS, `hidden` DOES NOTHING TO A FIELD.
   `[hidden] { display: none }` is a USER-AGENT rule, and the author rule above beats it — so
   a field hidden by `showWhen` would carry the attribute, be skipped by the dialog's tab
   trap, and still be painted. Every other hideable component in this file has needed its own
   copy of this for the same reason. */
.field[hidden] { display: none; }

/* A CHECKBOX FIELD: control on the left, then a block that reads label-then-help.
   No `order` anywhere — the reading order is the DOM order, which is what stops helper
   text from being re-sorted in front of the label it describes. */
.field--check { flex-direction: row; align-items: flex-start; gap: var(--space-3); }
.field--check .field__check { margin-top: 0.15rem; flex: none; }
.field__check-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
/* The label is the field's NAME, so it carries the field weight; the help is secondary and
   is typographically subordinate to it rather than merely below it. */
.field--check .field__label { margin: 0; cursor: pointer; }
.field--check .field__help { margin: 0; }

/* RF-20260902-01 — A LABEL CARRIES NO MARGIN OF ITS OWN, AND THAT ONE LINE IS THE WHOLE FIX FOR
 * PROJECT SITTING BELOW TITLE.
 *
 * MEASURED IN EDGE, New Task at 1500x1000: Title's label 151→172 with its input at 180; Project's
 * label row 151→185 with its selector at 192. Twelve pixels, and the twelve had nothing to do with
 * the help icon — the icon is 20px inside a 22px line box and costs nothing.
 *
 * THE CAUSE IS THAT THE TWO LABELS ARE DIFFERENT ELEMENTS. `labelOf` in `modal.js` renders a
 * `<label>` for an ordinary control and a `<p>` for a CUSTOM one, because a custom control is not
 * a single focusable input for a `for=` to point at. `base.css` gives every `<p>` a
 * `margin-bottom: var(--space-3)`, so the paragraph form of the label brought 12px that the
 * `<label>` form never had — and `.field` is a flex column with its own `gap`, so that margin was
 * pure surplus stacked on top of the spacing the field already provides.
 *
 * SO IT AFFECTED EVERY CUSTOM-CONTROL FIELD, NOT JUST PROJECT: Category, Assignees, Owner, Parent
 * task, Depends on and Progress were all one `--space-3` lower than an ordinary field beside them,
 * in every arrangement, since long before the help icon existed. Project is simply where it was
 * visible, because Project and Title share one grid row and the eye can compare them.
 *
 * WHICH IS ALSO WHY THIS IS NOT A CAPTURE FIX, AND NOT A NUDGE TO PROJECT. Nothing is offset, no
 * arrangement is special-cased, and Title does not move: the two label elements simply stop
 * disagreeing about their own box, and the gap between a label and its control becomes `.field`'s
 * `gap` for both — one rule, one spacing model. */
.field__label {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text);
}

/* RF-20260901-05 — THE HELP ICON, BESIDE THE LABEL.
 *
 * The reusable answer to "this field needs three sentences of explanation, but not every time".
 * The text is still in the DOM and still in the field's `aria-describedby`; the icon decides only
 * whether it is on SCREEN. See `app/js/ui/helpicon.js`.
 *
 * NOT A `title` ATTRIBUTE, which is mouse-only, delayed, unstyleable and absent on touch. This is
 * a real button: hover, focus, click and Escape all work, and the bubble is ordinary flow content
 * rather than an overlay, so it can never be clipped by the dialog's scrolling body. */
/* WRAPS RATHER THAN SQUEEZES. The row carries the label, an optional short note and an optional
   help icon; on a narrow column the note drops to its own line instead of compressing the name of
   the field, which is the one part that must stay readable. */
.field__labelrow { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-1) var(--space-2); min-width: 0; }
.field__labelrow > .field__label { min-width: 0; }
/* THE ICON DOES NOT SIT ON THE TEXT BASELINE — it is a circle, and aligning a circle's baseline
   with a letter's leaves it visibly low. `align-self: center` puts it on the label's optical line
   without changing the row's height, which is what keeps Project's input level with Title's. */
.field__labelrow > .helpicon { align-self: center; }
/* RF-20260902-01 — the short inline helper. Secondary by weight and colour so the field's NAME is
   still what the eye lands on; it explains, it does not compete. */
.field__labelnote {
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  font-style: italic;
  color: var(--text-subtle);
}
.helpicon { display: inline-flex; flex: none; }
.helpicon__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A 20px target inside a 24px hit area — small enough not to compete with the label, large
     enough to press on a phone. */
  width: 1.25rem;
  height: 1.25rem;
  padding: 0;
  font: inherit;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  line-height: 1;
  color: var(--text-muted);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 50%;
  cursor: pointer;
  transition: color var(--transition), background var(--transition), border-color var(--transition);
}
.helpicon__btn:hover,
.helpicon__btn[aria-expanded="true"] {
  color: var(--text-inverse);
  background: var(--accent);
  border-color: var(--accent);
}
.helpicon__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.helpicon__glyph { pointer-events: none; }
/* THE EXPLANATION ITSELF — RF-20260902-01, AND IT FLOATS.
 *
 * IT USED TO BE FLOW CONTENT, and that was the defect. Opening a help note lengthened the field's
 * cell and pushed the form down; on the Capture head row, where Project and Title share one grid
 * row, it took the two inputs out of alignment with each other. Terry: "zero layout shift when
 * opened", "zero layout shift when closed", "must never be inserted into the normal form row".
 *
 * `position: fixed` IS WHAT MAKES IT COST NOTHING. Out of flow, so no ancestor's height changes;
 * against the VIEWPORT, so the dialog's scrolling body cannot clip it — which is the reason the
 * flow version existed in the first place, and is now solved properly rather than avoided.
 * `helpicon.js` supplies `--pop-top`/`--pop-left` from `computePlacement`, the same arithmetic the
 * date, command and task pickers use, so a bubble near the bottom of the dialog opens upward.
 *
 * THE SAME CHROME AS EVERY OTHER FLOATING PANEL — raised surface, strong border, shadow — because
 * a panel over the form has to read as being over it. The rule down its start edge is kept from
 * the flow version: it is what says this is the answer to the icon rather than a new paragraph. */
.helpicon__bubble {
  position: fixed;
  z-index: 1300;
  top: var(--pop-top, 50%);
  left: var(--pop-left, 50%);
  margin: 0;
  padding: var(--space-2) var(--space-3);
  /* Narrower than the 60ch it had in flow: a floating note is read in one pass, and a very wide
     one over a form is harder to place without covering the field it explains. */
  width: max-content;
  max-width: min(44ch, calc(100vw - 2rem));
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.helpicon__bubble[hidden] { display: none; }

.field__input {
  width: 100%;
  padding: var(--space-3);
  font: inherit;
  font-size: var(--text-base);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.field__input:focus-visible {
  outline: none;
  border-color: var(--color-info);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-info) 25%, transparent);
}

.field__input:disabled { background: var(--surface-sunken); color: var(--text-subtle); cursor: not-allowed; }

textarea.field__input { resize: vertical; min-height: 4.5rem; line-height: var(--leading-normal); }

.field__check { width: 1.1rem; height: 1.1rem; accent-color: var(--color-info); }

.field__help {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
  line-height: var(--leading-normal);
}

/* RF-20260827-15 — A CONSEQUENCE, NOT AN EXPLANATION.
 *
 * `.field__help` says what a control does. This says what turning it OFF will destroy, and it
 * has to be legible as a different KIND of sentence before it is read — a site administrator
 * skimming three switches must not have "clears every workspace's settings permanently" arrive
 * in the same muted grey as "the master switch for this deployment".
 *
 * COLOUR IS NEVER THE ONLY SIGNAL. The left rule carries the same weight for anybody who cannot
 * distinguish it, and the sentence itself always names the consequence in words. */
/* THE SAME MARK ON A CARD HINT, because the consequence sentence on a card-level control needs
   to read as a consequence exactly as the one under a field does. Declared as a shared selector
   rather than a copied block: two rules drawing the same warning is how they stop matching. */
.field__help--warn,
.card__hint--warn {
  color: var(--text);
  border-left: 3px solid var(--color-warning);
  padding-left: var(--space-2);
  background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}

/* RF-14 — SITE SETTINGS, AS ROWS THAT REPORT ON THEMSELVES.
 *
 * A ROW IS A SETTING, and the separator says so. Twelve controls in two anonymous columns gave
 * the presence heartbeat interval the same visual weight as the announcement audience; grouping
 * them into cards and ruling between rows is what lets somebody find one without reading all of
 * them.
 *
 * THE STATUS SITS WITH ITS OWN CONTROL, never at the foot of the page. A single message beneath
 * twelve settings cannot say which one it is about — which is the whole reason this exists. */
.siterows { display: flex; flex-direction: column; }
.siterow {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}
.siterow:first-child { border-top: 0; padding-top: 0; }
.siterow:last-child { padding-bottom: 0; }

/* EMPTY AND SILENT UNTIL IT HAS SOMETHING TO SAY. The element is always in the DOM so a screen
 * reader has a live region to announce into — creating it on demand is how the FIRST save on a
 * row ends up never being announced — but with no text it must take no space at all. */
.siterow__status { font-size: var(--text-xs); line-height: var(--leading-normal); color: var(--text-muted); }
.siterow__status:empty { display: none; }
.siterow__status[data-tone="ok"] { color: var(--color-success); }
.siterow__status[data-tone="error"] { color: var(--color-danger); }
/* COLOUR IS NEVER THE ONLY SIGNAL. Each state also says what it is in words — "Saving…",
 * "Saved", or the server's own sentence — so the tone is reinforcement rather than the message. */

.field__row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }

@media (max-width: 40rem) {
  .modal { padding: 0; align-items: stretch; }
  .modal__panel {
    max-width: none;
    max-height: none;
    min-height: 100vh;
    border: 0;
    border-radius: 0;
  }
  .field__row { grid-template-columns: 1fr; }
  .modal__actions { flex-direction: column-reverse; }
  .modal__actions .btn { width: 100%; }
}
.btn--danger {
  color: var(--text-inverse);
  background: var(--color-danger);
  border-color: var(--color-danger);
}

.btn--danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--color-danger) 85%, black);
  border-color: color-mix(in srgb, var(--color-danger) 85%, black);
}
.picker__action {
  flex: none;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  font-size: var(--text-lg);
  line-height: 1;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.picker__action:hover { color: var(--text); border-color: var(--color-info); }
/* ---------- views ---------- */

.view { display: flex; flex-direction: column; gap: var(--space-4); }

/* ---------- toolbar ---------- */

.toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.toolbar__row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3); }
.toolbar__row--actions { align-items: center; justify-content: flex-end; border-top: 1px solid var(--border); padding-top: var(--space-3); }
.toolbar__group { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.toolbar__group--toggle { flex-direction: row; align-items: center; gap: var(--space-2); }
/* The pills sit on the row's baseline with the fields beside them rather than under a label
   of their own — they carry their own name, so a toolbar label above would repeat it. */
.toolbar__group--pills { flex-direction: row; align-items: center; gap: var(--space-2); align-self: flex-end; flex-wrap: wrap; }

/* ---------- pills ----------
   A toggle BUTTON that carries its own state. Used for the task-view switches; anything added
   later that turns part of a view on or off uses this rather than inventing a fourth control.
   Pressed-ness is communicated by aria-pressed AND by fill — never by colour alone, so the
   border weight and background both change. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.pill:hover { color: var(--text); border-color: var(--border-strong); }
.pill--on,
.pill[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.pill[aria-pressed="true"]:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.toolbar__label { font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-muted); }
.toolbar__count { margin: 0; font-size: var(--text-sm); color: var(--text-muted); flex: none; }

/* RF-20260826-05 — THE TASK COUNT AND ACTIVE SORT.
   Secondary to the table, but deliberately so rather than by neglect. The count carries the
   weight because it is the number people look for; the sort is the same size but quieter,
   which distinguishes the two parts without making them look like two separate controls.
   `--text-muted` is the same token the rest of the secondary text uses and is already part of
   the palette contrast work, so this inherits AA in all twenty-one themes rather than
   introducing a colour that has to be checked separately. */
.tablesummary {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  /* WRAPS RATHER THAN COLLIDING. The view controls sit at the far end of this row, so on a
     narrow screen the two parts drop to the next line instead of overlapping them. */
  flex-wrap: wrap;
  min-width: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}
.tablesummary__count { font-weight: var(--weight-medium); color: var(--text); }
.tablesummary__sep { color: var(--border-strong); }
.tablesummary__sort { color: var(--text-muted); }

/* THE USED TAGS, IN THE SPACE BESIDE THE COUNT — see ui/tagfilter.js.
   It takes the room that is left and wraps inside itself rather than pushing the row wider,
   which is what keeps a workspace with ten tags from causing horizontal overflow. */
.tagfilter { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; flex: 1; }
.tagfilter--chips { justify-content: flex-end; }

/* A FILTER, NOT A LABEL. Tags render elsewhere as quiet metadata; these are controls, so
   they carry a border and a pressed state rather than looking like the same chip twice. */
.tagfilter__chip {
  display: inline-flex; align-items: center;
  min-height: 1.75rem; padding: 0 var(--space-3);
  font: inherit; font-size: var(--text-xs); font-weight: var(--weight-medium);
  color: var(--text-muted); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  cursor: pointer; max-width: 12rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tagfilter__chip:hover { border-color: var(--border-strong); color: var(--text); }
/* PRESSED IS NOT ONLY A COLOUR. The border thickens and the weight lifts, so the state
   survives a palette where the accent and the surface are close together. */
.tagfilter__chip--on {
  color: var(--accent-contrast, var(--text));
  background: var(--accent);
  border-color: var(--accent);
  font-weight: var(--weight-bold);
}

.tagfilter__trigger {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: 1.75rem; padding: 0 var(--space-3);
  font: inherit; font-size: var(--text-xs); font-weight: var(--weight-medium);
  color: var(--text-muted); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  cursor: pointer;
}
.tagfilter__trigger:hover { border-color: var(--border-strong); color: var(--text); }
.tagfilter--on .tagfilter__trigger { color: var(--text); border-color: var(--accent); font-weight: var(--weight-bold); }
.tagfilter__caret { color: var(--text-subtle); }

/* The same popover surface as every other panel in the product. */
.tagfilter__panel {
  position: fixed; z-index: 1300;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  width: min(18rem, calc(100vw - 1rem));
  max-height: var(--pop-max, 20rem);
  display: flex; flex-direction: column;
  background: var(--surface-raised, var(--surface));
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.tagfilter__panel[hidden] { display: none; }
.tagfilter__hint {
  margin: 0; padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs); color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.tagfilter__list { overflow-y: auto; padding: var(--space-1); }
.tagfilter__opt {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  padding: var(--space-2); font: inherit; font-size: var(--text-sm); text-align: start;
  background: none; border: 0; border-radius: var(--radius-sm); color: var(--text); cursor: pointer;
}
.tagfilter__opt:hover { background: var(--surface-sunken); }
/* RF-20260824-12 — the panel's own actions row. Separated from the option list by a rule so
   Done, Cancel and Clear read as decisions about the whole selection rather than as more
   options to pick from. */
.tagfilter__actions { display: flex; justify-content: flex-end; gap: var(--space-2); padding-top: var(--space-2); margin-top: var(--space-2); border-top: 1px solid var(--border); }
.tagfilter__opt--on { font-weight: var(--weight-bold); }
.tagfilter__tick { flex: none; color: var(--text-muted); }
.tagfilter__opt--on .tagfilter__tick { color: var(--accent); }
.tagfilter__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* AN ICON BUTTON. Square, so the mark sits in the middle of a hit target big enough to press
   on a phone, and aligned with the toggles it now sits beside rather than with the fields. */
.toolbar__group--clear { justify-content: flex-end; }
.btn--icon {
  display: inline-grid; place-items: center;
  width: 2.25rem; height: 2.25rem; padding: 0;
  line-height: 1;
}
.icon { display: block; }

.toolbar__search {
  min-width: 14rem;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  color: var(--text);
  background: var(--surface-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}

.field__input--inline { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); width: auto; }
.field__input--multi { min-height: 6rem; }

/* ---------- table ---------- */

.table__scroll {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }

.table th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--space-3);
  text-align: left;
  white-space: nowrap;
  background: var(--surface-sunken);
  border-bottom: 1px solid var(--border);
}

.table td { padding: var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--surface-sunken); }

.table__sort {
  font: inherit;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.table__sort:hover { color: var(--text); }
.table__sort--active { color: var(--text); }
.table__sortmark { margin-left: var(--space-1); font-size: 0.7em; color: var(--color-info); }

.table__row--done .table__titletext { text-decoration: line-through; color: var(--text-muted); }
.table__row--overdue .cell--overdue { color: var(--color-danger); font-weight: var(--weight-medium); }

.table__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  font: inherit;
  font-weight: var(--weight-medium);
  text-align: left;
  color: var(--text);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.table__title:hover .table__titletext { color: var(--color-info); text-decoration: underline; }

.cell--check { width: 1px; }
.cell--seq { color: var(--text-subtle); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cell--title { min-width: 16rem; }
.cell--due { white-space: nowrap; color: var(--text-muted); }
/* RF-20260827-27 — A STABLE WIDTH, INDEPENDENT OF NAME LENGTH. `assignedToCell`'s compact
   label ("Ada +2") is already bounded past one assignee, but a single very long name is still
   plain text in this cell — the cap here is the second half of "independent of assignee count
   AND name length" the acceptance criteria names, not a duplicate of the JS-side compacting. */
.cell--assignees { white-space: nowrap; color: var(--text-muted); max-width: 12rem; overflow: hidden; text-overflow: ellipsis; }
.cell--progress { min-width: 7rem; }
.cell--actions { white-space: nowrap; }
.cell__sub { display: block; margin-top: var(--space-1); font-size: var(--text-xs); color: var(--text-subtle); }
.cell__tags { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-1); }

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

.chip {
  display: inline-block;
  padding: 0.1rem var(--space-2);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  white-space: nowrap;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  color: var(--text-muted);
}

.chip--tag { background: transparent; }
.chip--muted { color: var(--text-subtle); }
/* The priority chip's id-keyed colouring is gone — see the .prio block below. A workspace
   that renamed its priorities got nothing from these rules, silently. */

/* ---------- progress ---------- */

.progress {
  position: relative;
  height: 1.15rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

/* DYNAMIC VALUES ARRIVE AS CUSTOM PROPERTIES, set through the CSSOM rather than as a style
   attribute — a strict style-src blocks the attribute and has never applied to the CSSOM.
   The RULE stays here, in CSS, where a rule belongs. */
.progress__fill { height: 100%; width: var(--fill, 0%); background: var(--color-success); transition: width var(--transition); }

.progress__text {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* ---------- bulk actions ---------- */

.bulkbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: color-mix(in srgb, var(--color-info) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--color-info) 35%, transparent);
  border-radius: var(--radius);
}

.bulkbar__count { margin: 0; margin-right: auto; font-weight: var(--weight-medium); }
/* RF-53 — the Export "include tasks" checkbox, beside the Export control it modifies. */
.bulkbar__checklabel { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-sm); color: var(--text-muted); }

/* ---------- notices ---------- */

.notice {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: var(--text-sm);
}

.notice p { margin: 0; margin-right: auto; }
.notice--success { border-color: color-mix(in srgb, var(--color-success) 40%, transparent); background: color-mix(in srgb, var(--color-success) 10%, var(--surface)); }
.notice--error { border-color: color-mix(in srgb, var(--color-danger) 40%, transparent); background: color-mix(in srgb, var(--color-danger) 10%, var(--surface)); }
.notice--warn { border-color: color-mix(in srgb, var(--color-warning) 40%, transparent); background: color-mix(in srgb, var(--color-warning) 10%, var(--surface)); }
.notice--announcement { border-color: color-mix(in srgb, var(--color-info) 45%, transparent); background: color-mix(in srgb, var(--color-info) 14%, var(--surface)); }
.notice--announcement p { font-weight: var(--weight-medium); }
.notice__dismiss { background: none; border: 0; font-size: var(--text-lg); line-height: 1; color: var(--text-muted); cursor: pointer; }

.btn--sm { padding: var(--space-1) var(--space-2); font-size: var(--text-xs); }

/* ---------- comments and details ---------- */

.comments__section { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--border); }
.comments__title { margin: 0 0 var(--space-3); font-size: var(--text-sm); font-weight: var(--weight-bold); }
.comments { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.comments__item { padding: var(--space-3); background: var(--surface-sunken); border-radius: var(--radius); }
.comments__meta { margin: 0 0 var(--space-1); font-size: var(--text-xs); color: var(--text-subtle); }

/* RF-61 — created/last-modified-by, at the very bottom of the task and project editors. */
.audit-footer {
  margin: var(--space-3) 0 0; padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  font-size: var(--text-xs); color: var(--text-subtle);
}
/* RF-51 — a pasted comment with no natural break points (one long word/URL/unbroken line)
   overflowed its own `.comments__item` box instead of wrapping, matching the same fix already
   applied to other long-text surfaces in this file (see the other two `overflow-wrap: anywhere`
   rules). */
.comments__text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.comments__empty { font-size: var(--text-sm); color: var(--text-subtle); }
/* RF-20260827-11 — A COLUMN, NOT A ROW.
   The composer sat beside its button in a horizontal flex, so the box somebody types a paragraph
   into measured 275px inside an 878px dialog while the widest thing in the region was the gap
   between the two. Composer first, at full width; its action beneath it. */
.comments__compose { display: flex; flex-direction: column; align-items: stretch; gap: var(--space-2); min-width: 0; }
.comments__compose .prose__field { width: 100%; min-width: 0; }
.comments__actions { display: flex; align-items: center; gap: var(--space-3); }
.comments__post { flex: 0 0 auto; }
.comments__status { margin: 0; min-width: 0; }

/* RF-20260828-13 — "somebody said something while you were reading further up."
   Sits between the scrolling thread and the pinned composer, centred, so it is the one thing in
   this region that can neither scroll away nor push the composer down. Outline rather than
   primary: "Post comment" is the primary action here and two competing filled buttons in one
   small region is a worse answer than one.
   THE `[hidden]` RULE IS NOT OPTIONAL — `[hidden] { display: none }` is a USER-AGENT rule and
   `.btn`'s own `display: inline-flex` beats it, exactly as `.modal__actions .btn[hidden]` above
   already records. Without this line the control would be permanently visible, offering to jump
   to comments that do not exist. */
.comments__new { align-self: center; flex: 0 0 auto; margin-bottom: var(--space-2); }
.comments__new[hidden] { display: none; }

/* RF-20260827-36 — TASK/PROJECT ATTACHMENTS. Simple hairline-divided rows, not a stack of
   `.comments__item`-style cards: a comment is prose of unpredictable length and reads well as a
   card; an attachment is one line of facts (name, size, who, when) and a stack of shaded boxes
   around single lines reads as visual noise once more than one or two are attached.
   THE ADD BUTTON RENDERS ABOVE THE LIST (see taskform.js's own note) so it never scrolls out of
   reach behind a long list — the opposite of Comments, whose composer belongs after the thread. */
.attachments__section { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--border); }
.attachments__title { margin: 0 0 var(--space-3); font-size: var(--text-sm); font-weight: var(--weight-bold); }
.attachments { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.attachments__item { padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
.attachments__item:last-child { border-bottom: none; }
.attachments__row { display: flex; align-items: baseline; gap: var(--space-3); }
.attachments__name { flex: 1 1 auto; min-width: 0; font-weight: var(--weight-medium); overflow-wrap: anywhere; }
.attachments__meta { display: block; margin-top: var(--space-1); font-size: var(--text-xs); color: var(--text-subtle); white-space: nowrap; }
.attachments__remove { flex: 0 0 auto; }
.attachments__empty { padding: var(--space-2) 0; font-size: var(--text-sm); color: var(--text-subtle); }
.attachments__actions { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.attachments__add { flex: 0 0 auto; }
.attachments__status { margin: 0; min-width: 0; }

/* RF-20260827-36, INCREMENT 3/4 — DROPPING FILES, AND WATCHING THEM GO.
 *
 * THE DROP ZONE IS A VISIBLE INVITATION, NOT AN INVISIBLE ONE. A panel that silently accepts a
 * drop is a feature nobody discovers; the dashed outline and the sentence inside it are how
 * somebody learns it is there, and the button above it is how somebody without a pointer does the
 * same job.
 *
 * `--over` IS A BORDER AND A TINT, NOT A TRANSFORM. Moving or scaling the target under a dragged
 * pointer is exactly when a person is least able to correct for it. */
.attachments__dropzone {
  margin-bottom: var(--space-3);
  padding: var(--space-4);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  text-align: center;
  transition: border-color var(--transition), background-color var(--transition);
}
.attachments__dropzone--over { border-color: var(--accent); background: var(--surface-raised); }
.attachments__dropbody { margin: 0; font-size: var(--text-sm); color: var(--text-subtle); }

/* THE QUEUE IS ABOVE THE LIST AND LOOKS LIKE IT, so an upload in flight reads as a row that is
 * about to join the list rather than as a separate widget somewhere else on the panel. */
.attachments__queue { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: flex; flex-direction: column; }
.attachments__queue[hidden] { display: none; }
.attachments__progress { display: block; width: 100%; margin-top: var(--space-1); height: 4px; }
.attachments__item--failed .attachments__meta { color: var(--color-danger); }
.attachments__item--cancelled .attachments__meta { color: var(--text-subtle); }
.attachments__cancel,
.attachments__retry,
.attachments__dismiss { flex: 0 0 auto; }

/* ---------------------------------------------------------------------------
   PRIORITY 9 — THE REUSABLE PROSE EDITOR/RENDERER (`prosetext.js`).
   The read side (`.prose`) and the edit side (`.prose__field`, wrapping a real
   `.field__input`/`textarea`) share the same small vocabulary rather than each view inventing
   its own "how a description looks" rule.
   --------------------------------------------------------------------------- */

.prose { color: var(--text); }
.prose__p { margin: 0 0 var(--space-3); }
.prose__p:last-child { margin-bottom: 0; }
.prose__list { margin: 0 0 var(--space-3); padding-inline-start: var(--space-5); }
.prose__list:last-child { margin-bottom: 0; }

/* RF-20260827-10 — HEADINGS, LINKS, STRIKETHROUGH AND CHECKLISTS.
   Sized in `em` so a heading is proportional to the text around it rather than to a page whose
   scale this prose knows nothing about — the same string renders inside a dialog, a card and a
   table row. */
.prose__h { margin: var(--space-3) 0 var(--space-2); font-weight: var(--weight-bold); line-height: 1.3; }
.prose__h:first-child { margin-top: 0; }
h3.prose__h { font-size: 1.25em; }
h4.prose__h { font-size: 1.1em; }
h5.prose__h { font-size: 1em; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }

.prose__a { color: var(--accent); text-decoration: underline; }
.prose__a:hover { text-decoration-thickness: 2px; }

.prose__s { text-decoration: line-through; text-decoration-thickness: 1px; }

/* A CHECKLIST ITEM CARRIES A REAL CHECKBOX, so the marker is removed — two markers for one item
   reads as a bullet that also happens to have a box. */
.prose__list .prose__checkitem { list-style: none; margin-inline-start: calc(-1 * var(--space-5)); display: flex; align-items: flex-start; gap: var(--space-2); }
.prose__check { margin-top: 0.25em; flex: 0 0 auto; }
.prose__checklabel { min-width: 0; }

/* RF-20260827-10 STAGE 4 — AN IMAGE, AND THE PLACEHOLDER FOR ONE THAT IS NOT THERE.
   BOUNDED BY ITS CONTAINER, NEVER BY ITS OWN PIXELS. The same description renders inside a task
   dialog, a card, a hover preview and an export, and a 4000px screenshot that pushed a dialog
   past the viewport would be a formatting control that breaks the page it is used on.
   `max-height` as well as `max-width`, because a tall narrow image is the case a width cap alone
   does not answer. */
.prose__img { display: block; max-width: 100%; max-height: 28em; height: auto; border-radius: var(--radius-sm); margin: var(--space-2) 0; }

/* THE PLACEHOLDER READS AS A MISSING THING, NOT AS A BROKEN ONE. Muted, dashed and inline-block:
   it occupies the flow where the picture would be and says what the picture was, which is the
   whole difference between "this image is unavailable" and a browser's broken-image glyph. */
.prose__img--missing {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 100%;
  padding: var(--space-1) var(--space-2);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--text-sm);
  vertical-align: baseline;
}
.prose__imgmark { flex: 0 0 auto; }
.prose__imgalt { min-width: 0; overflow-wrap: anywhere; }

/* RF-20260827-10 WAVE 1 — `.prose__imgnode`, the host element `AttachmentImage`'s node view owns in
   the LIVE editor, IS DELIBERATELY UNSTYLED, and that is a measured decision rather than an
   omission. The host exists because ProseMirror needs one stable element per node (see the
   extension's own note); how the picture LOOKS is `.prose__img` and `.prose__img--missing` above,
   which is what keeps the editor and the read view from having two answers.

   THE FIRST ATTEMPT DECLARED `display: inline-block; max-width: 100%` AND MADE THE IMAGE ZERO
   PIXELS WIDE. Measured in real Edge by `scripts/dev/verify-tiptapeditor.mjs`: the image loaded
   (`complete: true`, natural 150x150) and computed `width: 0px`. An inline-block shrink-to-fits its
   width from its content, `.prose__img` is `display: block` with `max-width: 100%`, and a
   percentage max-width resolved against a containing block whose width is itself being derived from
   that content collapses to zero. Left inline, the image's containing block is the paragraph — the
   same containing block it has in the read view — and it draws at its intrinsic size, exactly as
   the read view draws it. Do not give this element a formatting context. */

/* ---- RF-20260827-10 WAVE 1 — the seven classes the structured renderer emits and nothing styled.

   `render.js` has emitted `prose__quote`, `prose__pre`, `prose__hr`, `prose__code`,
   `prose__font--*`, `prose__size--*` and `prose__align--*` since Checkpoint A, and PROJECT_STATE
   §J.52/§J.53 record deliberately leaving them unstyled: no legacy string can produce any of them
   and no surface offered the controls, so nothing was unstyled on screen. Wave 1 is the wave that
   OFFERS them, so this is the wave that writes the rules.

   THE FOUR BLOCK KINDS FOLLOW THIS PRODUCT'S EXISTING VOCABULARY rather than inventing one: the
   quote's `3px solid` edge rule is the same one `.stat` and `.notice` already draw, the code block
   is the sunken surface `.prose__table th` already uses with the `--font-mono` stack from
   `tokens.css`, and the horizontal rule is the same hairline `--border` every separator in this
   stylesheet is drawn with. */
.prose__quote {
  margin: 0 0 var(--space-3);
  padding: var(--space-1) 0 var(--space-1) var(--space-3);
  border-inline-start: 3px solid var(--border);
  color: var(--text-muted);
}
.prose__quote > :last-child { margin-bottom: 0; }
.prose__pre {
  margin: 0 0 var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* A CODE BLOCK KEEPS ITS OWN LINE BREAKS AND WRAPS RATHER THAN SCROLLING SIDEWAYS. A horizontal
     scrollbar inside a dialog inside a narrow screen is a second thing to find before the words
     can be read. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}
.prose__pre > code { font: inherit; background: none; padding: 0; }
.prose__hr {
  margin: var(--space-4) 0;
  border: 0;
  border-top: 1px solid var(--border);
}
.prose__code {
  padding: 0.1em 0.35em;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* THE THREE FONT NAMES RESOLVE TO STACKS HERE, WHICH IS THE WHOLE REASON A NAME IS WHAT IS STORED.
   A document says `serif`; what `serif` looks like is this application's decision, revisable
   without touching a single saved description. */
.prose__font--sans { font-family: var(--font-sans); }
.prose__font--serif { font-family: var(--font-serif); }
.prose__font--mono { font-family: var(--font-mono); }

/* AND THE FOUR SIZES ARE STEPS ON THE EXISTING TYPE SCALE, not absolute pixels. `em` rather than
   `rem` deliberately: a size applied inside a heading should be a step relative to THAT heading,
   not a jump back to body text. */
.prose__size--sm { font-size: 0.85em; }
.prose__size--base { font-size: 1em; }
.prose__size--lg { font-size: 1.25em; }
.prose__size--xl { font-size: 1.5em; }

/* THE STORED VALUE IS `left`, SO THE RULE IS `left`. The rest of this stylesheet uses the logical
   `start`/`end` for a DEFAULT, which is the right choice when nobody has expressed a preference.
   These four are somebody's explicit choice, and quietly resolving "left" to "whichever side the
   writing system starts on" would be answering a different question from the one they asked. */
/* ---- RF-20260827-10 WAVE 1 — the two places the LIVE editor's markup differs in shape from the
   read renderer's, reconciled so the same class produces the same picture in both.

   BOTH DIFFERENCES ARE THE ENGINE'S, NOT A CHOICE. ProseMirror requires a list item to hold BLOCK
   content, so every item carries a real `<p>` that the read renderer unwraps; and Tiptap's TaskItem
   builds `<li><label><input><span></span></label><div>…</div></li>` where the read renderer builds
   `<li><input><span class="prose__checklabel">…</span></li>`. Fighting either one would mean
   replacing a node view, which is a much larger change than making the same class draw the same
   thing. */

/* A paragraph that IS a list item's whole content carries no gap of its own; the list's own
   spacing is the item spacing. Without this every item in the editor sits three spaces below the
   last, and the same list closes up the moment it is saved and re-read. */
.prose__doc li > .prose__p { margin-bottom: 0; }
.prose__doc li > .prose__p + * { margin-top: var(--space-2); }
/* The checklist item's label wrapper takes the place `.prose__check` holds in the read view, and
   its content div the place `.prose__checklabel` holds. */
.prose__doc .prose__checkitem > label { flex: 0 0 auto; margin-top: 0.25em; display: inline-flex; }
.prose__doc .prose__checkitem > div { min-width: 0; flex: 1 1 auto; }

.prose__align--left { text-align: left; }
.prose__align--center { text-align: center; }
.prose__align--right { text-align: right; }
.prose__align--justify { text-align: justify; }

/* RF-20260827-10 AMENDMENT 5 — THE BOUNDED TABLE SPIKE.

   THE SCROLL WRAPPER IS THE `*__scroll { overflow-x: auto; }` PATTERN THIS CODEBASE ALREADY USES
   TWICE — `.datatable__scroll` for the settings tables and `.table__scroll` for the task table.
   Same declaration, third user, not a third idea. A table wider than the dialog scrolls inside its
   own box rather than widening the dialog past the screen, which is the narrow-screen requirement.

   `max-width: 100%` ON THE WRAPPER AND `width: max-content` ON THE TABLE is what makes the scroll
   actually happen: a `width: 100%` table would compress its columns to fit forever and never
   overflow, so the scrollbar the wrapper offers would never appear and the cells would just get
   narrower until they were unreadable. */
.prose__tablescroll { overflow-x: auto; max-width: 100%; margin: 0 0 var(--space-3); }
.prose__tablescroll:last-child { margin-bottom: 0; }
.prose__table {
  border-collapse: collapse;
  font-size: var(--text-sm);
  width: max-content;
  min-width: 100%;
}
/* EVERY CELL HAS A VISIBLE EDGE, and in an EDITOR that is functional rather than decorative: an
   empty cell with no border is an invisible target, so somebody typing into a new table would have
   no way to see where one cell ends and the next begins. `min-width` for the same reason — an
   empty column must still be wide enough to click into. */
.prose__table th,
.prose__table td {
  border: 1px solid var(--border);
  padding: var(--space-1) var(--space-2);
  text-align: start;
  vertical-align: top;
  min-width: 4rem;
}
.prose__table th { font-weight: var(--weight-medium); background: var(--surface-sunken); }

/* THE ROW/COLUMN CONTROLS. A group of ordinary buttons that appears when the caret is inside a
   table; `hidden` takes it out of the tab order and the accessibility tree entirely when it is
   not. It wraps, unlike the formatting toolbar above it, because these four carry WORDS rather
   than a glyph and a narrow dialog must not clip the fourth one off the end. */
.prose__tablebar { display: flex; flex-wrap: wrap; gap: var(--space-1); align-items: center; }
.prose__tablebar[hidden] { display: none; }
.prose__tablebtn {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0 var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-xs);
  cursor: pointer;
}
.prose__tablebtn:hover:not(:disabled) { color: var(--text); background: var(--surface-sunken); }
.prose__tablebtn:disabled { cursor: default; opacity: 0.4; }

.prose__field { display: flex; flex-direction: column; gap: var(--space-1); }

/* Where an image upload says what happened. It carries no text until there is something to say,
   so a field that has never been asked to insert an image shows nothing at all. */
.prose__status:empty { display: none; }

/* RF-20260827-10 — THE STRUCTURED DOCUMENT.
   The canonical editor is a rendered `contenteditable`, not a textarea, so a checklist is a
   checklist while somebody is writing it. It has to LOOK like the field it replaced — same
   border, same padding, same focus ring — because the surface changed and the affordance did
   not: this is still the box you type in.
   HEIGHT FOLLOWS THE SAME `rows` EVERY CALLER ALREADY PASSES, through `--prose-rows`, so no
   screen had to learn a second sizing vocabulary. `min-height` rather than a fixed height, so
   the document grows with its content, and `max-height` with scrolling so a long checklist
   cannot push Save off the dialog. */
.prose__doc {
  min-height: calc(var(--prose-rows, 3) * 1.5em + 2 * var(--space-2));
  max-height: 32vh;
  overflow-y: auto;
  overflow-x: hidden;
  cursor: text;
  /* RF-20260827-29 — a pasted URL, a long word or a deep nest must wrap, never widen the box. */
  overflow-wrap: anywhere;
}
.prose__doc:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
/* The document's own blocks carry the read-side spacing, which is the point: what you edit is
   what you will see. Only the first and last need trimming so the box is not padded twice. */
.prose__doc > :first-child { margin-top: 0; }
.prose__doc > :last-child { margin-bottom: 0; }
/* A BOX INSIDE THE EDITOR IS A CONTROL, NOT TEXT. `contenteditable="false"` keeps the caret out
   of it; this keeps the pointer honest about that. */
.prose__doc .prose__check { cursor: pointer; }
.prose__doc[aria-disabled="true"] { opacity: 0.6; cursor: not-allowed; }

/* RF-20260827-22 — READING AND EDITING MUST NOT LOOK THE SAME.
   A field with saved content opens rendered; one deliberate press of "Edit" swaps the read body
   for the editable document. The two states are told apart by chrome, which is the honest
   difference: the editor is a BOX you type in (border, padding, focus ring, a toolbar above it);
   the read view is words on the dialog, with one named action where the toolbar will be.
   THE BAR SITS WHERE THE TOOLBAR SITS so the words do not jump down the dialog on the switch, and
   the read body takes the same `max-height` and scrolling the document does — a long checklist
   must not push Save off a modal in either mode.
   `--prose-rows` IS DELIBERATELY NOT APPLIED HERE: a rendered paragraph is as tall as its words,
   and reserving seven rows for two lines of read-only text is the empty space the editor's own
   `min-height` exists to give a typist and a reader has no use for. */
.prose__modefield { display: flex; flex-direction: column; gap: var(--space-1); }
.prose__readbar { display: flex; align-items: center; justify-content: flex-end; }
.prose__readbody {
  max-height: 32vh;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-2) 0;
  /* The same wrapping rule the document has (RF-20260827-29): a pasted URL, a long word or a deep
     nest must wrap rather than widen the field. */
  overflow-wrap: anywhere;
}
.prose__readbody .prose__check { cursor: pointer; }
/* A box nobody can press during a save must look like it. Matches `.prose__doc[aria-disabled]`. */
.prose__readbody .prose__check:disabled { cursor: not-allowed; opacity: 0.6; }

/* RF-20260827-10 — AND THE WORDS THEMSELVES SAY THEY CAN BE EDITED.
   Terry, 2026-08-29: "the current small Edit control is not sufficiently discoverable." Clicking
   the text now enters edit mode, and an affordance nobody can see is not an affordance — so the
   panel picks up the SAME quiet hover tint every other clickable row and card in this stylesheet
   uses (`.table tbody tr:hover`, `.datatable tbody tr:hover`, `.dock__person:hover`). It is the
   existing convention for "this responds to a press", not a new one invented for this field.
   `cursor: text`, NOT `pointer`, AND THAT IS THE HONEST GLYPH. A pointer says "button", and this
   is not one — every word here is ordinary prose, and a pointer over all of it would say the
   paragraph is a control. `text` is what `.prose__doc` shows, which is exactly what a press
   produces: the same words, now typeable.
   THE PADDING IS NEGATIVE-MARGINED so the tint reads as a panel rather than as a highlight
   clipped to the text, without moving a single word — read and edit must stay aligned.
   `:focus-within` COVERS THE KEYBOARD, so somebody tabbing to a link inside the description gets
   the same panel feedback a mouse gets. */
.prose__readbody--editable {
  cursor: text;
  border-radius: var(--radius);
  padding-left: var(--space-2);
  padding-right: var(--space-2);
  margin-left: calc(var(--space-2) * -1);
  margin-right: calc(var(--space-2) * -1);
  transition: background var(--transition), box-shadow var(--transition);
}
.prose__readbody--editable:hover,
.prose__readbody--editable:focus-within { background: var(--surface-sunken); }

/* THE ONE WAY IN THAT IS NAMED IS ALSO THE ONE THAT HAD TO BE FOUND.
   It was `btn--ghost` — transparent fill, transparent border, muted text — sitting above a read
   view that is itself deliberately plain, so the only affordance on screen looked like a caption.
   It now takes the ordinary small-button chrome (a real border, real text colour) this file
   already gives every other secondary action, and takes the ACCENT while the panel it belongs to
   is under the pointer, which ties the two halves of one affordance together: the words tint, the
   control lights up, and both mean the same thing.
   NOT A COLOUR ON ITS OWN. The border changes with it, so this reads for somebody who cannot
   distinguish the accent from the surrounding text. */
.prose__edit { gap: var(--space-1); padding: var(--space-1) var(--space-3); min-height: 1.9rem; }
.prose__editmark { font-size: 0.85em; line-height: 1; }
.prose__modefield[data-mode="read"]:hover .prose__edit:not(:disabled),
.prose__modefield[data-mode="read"]:focus-within .prose__edit:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

/* RF-20260826-03 (EXTENDED) — THE DESCRIPTION IS TALLER, BUT NOT ON A PHONE.
   The `rows` attribute sets the initial height; this stops that height from turning a modal on
   a short screen into something you have to scroll before you can reach Save. `vh` rather than
   a fixed pixel cap, so it responds to the actual viewport instead of to one assumed device. */
@media (max-height: 44rem), (max-width: 40rem) {
  .prose__input { max-height: 28vh; }
}

/* RF-20260827-10 — ONE ROW. NEVER TWO.
   `nowrap` is not cosmetic: a toolbar that wraps changes the HEIGHT of the field, which moves
   every control below it — on a dialog whose geometry is supposed to be stable.
   BUT `nowrap` ALONE ONLY TURNS WRAPPING INTO OVERFLOWING, and that is the correction made on
   2026-08-30. With a FIXED set of controls on the row, a row too narrow to hold them did not wrap
   — it pushed them past its own right edge instead. Measured in Edge at `96ae857`: at a 380px
   viewport with a 24px root the row needed 330px inside a 278px container, "Bulleted list" and the
   "⋯" trigger itself hung outside it, and the page's scrollWidth went to 468 against a 380px
   viewport. So WHICH controls are on the row is now measured from this element's own width by
   `fitToolbar` in `prosetext.js`, and `nowrap` is what makes the result a guarantee rather than a
   hope. There is no breakpoint here, deliberately: this row lives inside a dialog whose width is
   its own, so a viewport media query would answer a question nobody asked.
   THE OVERFLOW BUTTON IS PINNED TO THE END and never shrinks, and its width is reserved before any
   control takes any, so the one control that reaches everything else cannot be the one squeezed
   out. It is hidden outright when the row holds everything, because a "⋯" over an empty panel is a
   control that lies about having something to offer. */
.prose__toolbar {
  display: flex;
  gap: var(--space-1);
  flex-wrap: nowrap;
  align-items: center;
  position: relative;
  min-width: 0;
}
.prose__toolbar > .prose__toolbtn { flex: 0 0 auto; }
.prose__toolbar > .prose__toolmore { margin-inline-start: auto; }

/* RF-20260827-10 — the comment composer uses the width it has. */
.prose__field--wide { width: 100%; }
/* RF-20260827-10 — A RADIO GROUP WHOSE OPTIONS EXPLAIN THEMSELVES.
   Two alternatives, each naming what it offers, so the choice can be made without opening a task
   to find out what changed. The detail line is part of the label, so clicking it selects the
   option — a label people can read but not press is a target that looks bigger than it is. */
.field__radios { display: flex; flex-direction: column; gap: var(--space-3); }
.field__radio { display: flex; align-items: flex-start; gap: var(--space-2); }
.field__radio .field__check { margin-top: 0.25em; flex: 0 0 auto; }
.field__radiolabel { display: flex; flex-direction: column; gap: 2px; cursor: pointer; min-width: 0; }
.field__radiotitle { font-weight: var(--weight-bold); }
.field__radiodetail { font-size: var(--text-sm); color: var(--text-muted); }

.prose__field--wide .prose__input { width: 100%; }

.prose__toolbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
}

.prose__toolbtn:hover:not(:disabled) { color: var(--text); background: var(--surface-sunken); }
.prose__toolbtn:disabled { cursor: default; opacity: 0.4; }
/* `hidden` HAS TO BE SAID OUT LOUD HERE, and leaving it unsaid was a real defect for as long as
   this rule existed alongside anything that hides a toolbar button. The `hidden` attribute is
   `display: none` in the USER-AGENT stylesheet, which any author rule carrying a class selector
   outranks — so `.prose__toolbtn { display: inline-flex }` quietly won, and a button marked hidden
   went on taking part in the layout. Measured in Edge: with the "⋯" trigger correctly hidden
   because everything fitted on the row, Advanced at a 1400px viewport still laid out 28 boxes in an
   878px row and reported a scrollWidth of 892 — the trigger, invisible to nobody, hanging past the
   right edge. `.prose__more[hidden]` below says the same thing for the same reason. */
.prose__toolbtn[hidden] { display: none; }

.history__section { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--border); }
.history__title { margin: 0 0 var(--space-3); font-size: var(--text-sm); font-weight: var(--weight-bold); }
/* RB-3/RB-4 — collapsed by default; the toggle IS the section's own label now, so there is no
   separate .history__title above it once opened. */
.history__panel { margin-top: var(--space-3); }
.history { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.history__item { padding: var(--space-3); background: var(--surface-sunken); border-radius: var(--radius); }
.history__meta { margin: 0 0 var(--space-1); font-size: var(--text-xs); color: var(--text-subtle); }
.history__text { margin: 0; }
.history__empty { font-size: var(--text-sm); color: var(--text-subtle); }

.modal__danger { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--border); }

.detail { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-2) var(--space-4); margin: 0 0 var(--space-4); font-size: var(--text-sm); }
.detail dt { color: var(--text-muted); }
.detail dd { margin: 0; }
.detail__description { margin: 0 0 var(--space-4); white-space: pre-wrap; }

@media (max-width: 48rem) {
  .toolbar__row { flex-direction: column; align-items: stretch; }
  .toolbar__search { min-width: 0; width: 100%; }
  .bulkbar { flex-direction: column; align-items: stretch; }
}
/* ---------- dashboard ---------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--space-3); }

.stat {
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
}

.stat--wide { grid-column: 1 / -1; }
.stat--success { border-left-color: var(--color-success); }
.stat--danger { border-left-color: var(--color-danger); }
.stat--warning { border-left-color: var(--color-warning); }

.stat__value { margin: 0; font-size: var(--text-2xl); font-weight: var(--weight-bold); font-variant-numeric: tabular-nums; line-height: 1; }
.stat__label { margin: var(--space-2) 0 0; font-size: var(--text-sm); font-weight: var(--weight-medium); }
.stat__detail { margin: var(--space-1) 0 0; font-size: var(--text-xs); color: var(--text-muted); }

.progress--lg { height: 1.75rem; margin-top: var(--space-3); }
.progress--sm { height: 0.35rem; margin-top: var(--space-2); border-radius: var(--radius-sm); }
.progress--sm .progress__text { display: none; }

.dash__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: var(--space-4); }

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

.card--wide { grid-column: 1 / -1; }
.card__title { margin: 0 0 var(--space-3); font-size: var(--text-sm); font-weight: var(--weight-bold); }
/* RF-20260903-05 — THE NEGATIVE TOP MARGIN NOW APPLIES ONLY WHERE ITS PREMISE HOLDS.
   It exists to cancel `.card__title`'s bottom margin so a hint sits flush under the title. A hint
   further down a card has no such margin to cancel, so the same rule pulled it UP into whatever
   preceded it. RF-20260828-15 diagnosed exactly this after it overlapped two lines under
   Workspace Settings -> Kanban -> Completed Tasks, and mitigated it with an OPT-IN modifier
   (`--note`) — which left every hint that is not first overlapping by default, unless an author
   remembered a class. Terry hit the same defect again on Workspace Settings -> General, where the
   paragraph under "Show tasks when the due date is within" is struck through by the controls above
   it. So the premise is now expressed structurally: the adjacent-sibling selector cannot be applied
   where it does not hold, and nothing has to be remembered. `--note` is kept because callers use
   it and it still means what it says. */
.card__hint { margin: 0 0 var(--space-3); font-size: var(--text-xs); color: var(--text-subtle); }
.card__title + .card__hint { margin-top: calc(var(--space-3) * -1); }
.card__hint--note { margin-top: var(--space-1); }
.card__empty { margin: 0; font-size: var(--text-sm); color: var(--text-subtle); }

.bars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.bars__row { display: grid; grid-template-columns: minmax(6rem, 9rem) 1fr auto; align-items: center; gap: var(--space-3); font-size: var(--text-sm); }
.bars__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted); }
.bars__track { height: 0.6rem; background: var(--surface-sunken); border-radius: var(--radius-pill); overflow: hidden; }
.bars__fill { display: block; height: 100%; width: var(--fill, 0%); background: var(--bar, var(--color-info)); border-radius: var(--radius-pill); }
.bars__value { font-variant-numeric: tabular-nums; font-size: var(--text-xs); color: var(--text-muted); white-space: nowrap; }

.attention { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.attention__item { display: flex; align-items: center; gap: var(--space-3); font-size: var(--text-sm); }
.attention__link { flex: 1; text-align: left; font: inherit; color: var(--text); background: none; border: 0; padding: 0; cursor: pointer; }
.attention__link:hover { color: var(--color-info); text-decoration: underline; }
.chip--overdue { color: var(--color-danger); border-color: currentColor; }

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

/* RF-20260824-16 — STRETCH, NOT flex-start.
   The drag handlers live on `.board__list`, so a column's drop target is only as tall as the
   column itself. With `flex-start` each column shrank to its own content, which meant picking a
   card up from the bottom of a long column put the pointer BELOW every short column's list —
   nothing under it to drop onto, and a board that looked like it was ignoring you.
   Stretching makes every column as tall as the tallest, so all of them are reachable at any
   height the pointer can be at. It adds no page height: the tallest column already set it. */
.board { display: flex; gap: var(--space-4); overflow-x: auto; padding-bottom: var(--space-3); align-items: stretch; }

.board__column {
  flex: 0 0 17rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* A COLUMN HEADING SHOULD READ AS A HEADING.
   It used to sit in the same visual register as the cards beneath it — same surface, same
   weight family, no separation — so the eye had to work out which words were the column and
   which were the work. That is a hierarchy problem, and it is fixed with hierarchy.

   FOUR RESTRAINED MOVES, and deliberately no fifth:
     1. it spans the column's own padding, so its rule runs the full width and reads as a band
        rather than as a line floating between two margins;
     2. a bottom rule and real padding, which is what separates a heading from its content;
     3. the label takes the small-caps treatment this product already uses for section headings
        (.dock__section, .toolbar__label) — so it is recognisably A HEADING here, rather than a
        new idea invented for the board;
     4. the count moves onto its own plate, which stops "In Progress 12" reading as one phrase
        and gives the number a fixed place to sit as it changes.

   WHAT IS NOT DONE: no coloured banner, no new status colour, no heavier surface than the
   cards it heads. The status dot keeps the workspace's own colour and remains the only colour
   in the header. */
.board__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* Out through the column's padding, so the rule is the full width of the column. */
  margin: calc(var(--space-3) * -1) calc(var(--space-3) * -1) 0;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.board__swatch { width: 0.65rem; height: 0.65rem; border-radius: 50%; flex: none; background: var(--swatch, var(--border-strong)); }
.board__title {
  margin: 0; flex: 1; min-width: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  /* A narrow column truncates the label rather than wrapping the header to two lines and
     making one column taller than its neighbours. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.board__count {
  flex: none;
  min-width: 1.5rem;
  padding: 0 var(--space-2);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
}
/* RF-20260824-16 — `flex: 1` is the other half. The column stretches, but without this the
   LIST inside it stays exactly as tall as its cards and the empty space beneath it belongs to
   the section, which has no drop handler. `flex: 1` makes the list fill the column, so the
   whole column is the target. `min-height` stays as the floor for a board whose columns are
   all empty, where there is nothing to stretch to. */
.board__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; flex: 1; gap: var(--space-2); min-height: 3rem; border-radius: var(--radius); }
.board__list--over { outline: 2px dashed var(--color-info); outline-offset: 3px; }
.board__empty { padding: var(--space-3); font-size: var(--text-xs); color: var(--text-subtle); text-align: center; }

/* COMPACT BY DEFAULT. Reported: "THE CARDS ARE TOO TALL. There is too much vertical
   whitespace for the amount of information displayed." The card is a flex column now with a
   small gap, so its rows sit against each other instead of each carrying its own margin. */
.card--task {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-2);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}
/* The title and the priority on one line: the title takes the room, the chip keeps its size. */
.card__head { display: flex; align-items: flex-start; gap: var(--space-2); }
.card__head .card__open { flex: 1; min-width: 0; }
.card__head .chip--priority { flex: none; }
/* A card whose head is a title plus one action button (the vocabulary editor's "New status"
   etc.) — the title takes the room the same way `.card__open` does above, pushing the button
   to the far side rather than sitting flush beside it. */
.card__head .card__title { flex: 1; min-width: 0; margin: 0; }
/* Progress and the status control share the last line. */
.card__foot { display: flex; align-items: center; gap: var(--space-2); }
.card__foot .progress { flex: 1; min-width: 0; }
.card--task[draggable="true"] { cursor: grab; }
.card--archived { opacity: 0.6; }
.card__open { font: inherit; font-weight: var(--weight-medium); text-align: left; color: var(--text); background: none; border: 0; padding: 0; cursor: pointer; width: 100%; }
.card__open:hover { color: var(--color-info); }
/* The margins are gone: the card's own gap separates its rows, so nothing is spaced twice. */
.card__meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
/* SIZED TO ITS CONTENT, not to the card. `width: 100%` made a select the widest and loudest
   thing on every card, in a column that already says what the status is. It keeps its full
   padding — a smaller target would be a real cost, and density is not that. */
.card__status {
  flex: none;
  max-width: 100%;
  padding: var(--space-1) var(--space-2);
  font: inherit;
  font-size: var(--text-xs);
  color: var(--text-muted);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* ---------- matrix ---------- */

.matrix { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.matrix__note { margin: 0; padding: var(--space-3); font-size: var(--text-sm); color: var(--text-muted); background: var(--surface-sunken); border-radius: var(--radius); }

.matrix__cell {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 12rem;
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-top: 3px solid var(--border-strong);
  border-radius: var(--radius);
}

.matrix__cell--danger { border-top-color: var(--color-danger); }
.matrix__cell--info { border-top-color: var(--color-info); }
.matrix__cell--warning { border-top-color: var(--color-warning); }
.matrix__cell--over { outline: 2px dashed var(--color-info); outline-offset: 3px; }

.matrix__head { display: grid; grid-template-columns: 1fr auto; gap: var(--space-1); }
.matrix__title { margin: 0; font-size: var(--text-base); font-weight: var(--weight-bold); }
.matrix__count { font-size: var(--text-sm); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.matrix__subtitle { grid-column: 1 / -1; margin: 0; font-size: var(--text-xs); color: var(--text-muted); }
.matrix__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); flex: 1; }
.matrix__empty { font-size: var(--text-xs); color: var(--text-subtle); }
.matrix__advice { margin: 0; padding-top: var(--space-2); border-top: 1px solid var(--border); font-size: var(--text-xs); color: var(--text-subtle); }

@media (max-width: 52rem) {
  .matrix { grid-template-columns: 1fr; }
  .board__column { flex-basis: 14rem; }
}
/* ---------- calendar ---------- */

.cal__header { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.cal__title { margin: 0; font-size: var(--text-lg); min-width: 10rem; }
.cal__count { margin: 0; margin-left: auto; font-size: var(--text-sm); color: var(--text-muted); }

.cal { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.cal__weekdays { display: grid; grid-template-columns: repeat(7, 1fr); background: var(--surface-sunken); border-bottom: 1px solid var(--border); }
.cal__weekday { padding: var(--space-2); font-size: var(--text-xs); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); text-align: center; }

.cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); }

.cal__day {
  min-height: 6.5rem;
  padding: var(--space-2);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.cal__day:nth-child(7n) { border-right: 0; }
.cal__day--outside { background: var(--surface-sunken); }
.cal__day--outside .cal__daynum { color: var(--text-subtle); }
.cal__day--today { background: color-mix(in srgb, var(--color-info) 8%, var(--surface)); }
.cal__day--today .cal__daynum { color: var(--color-info); font-weight: var(--weight-bold); }
.cal__day--over { outline: 2px dashed var(--color-info); outline-offset: -2px; }

.cal__daynum { font-size: var(--text-xs); font-variant-numeric: tabular-nums; color: var(--text-muted); }
.cal__tasks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }

.cal__task {
  padding: 0.15rem var(--space-2);
  background: var(--surface-sunken);
  border-left: 2px solid var(--color-info);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
}

.cal__task--done { opacity: 0.6; border-left-color: var(--color-success); }
.cal__task--done .cal__tasklink { text-decoration: line-through; }
.cal__tasklink { display: block; width: 100%; font: inherit; text-align: left; color: var(--text); background: none; border: 0; padding: 0; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal__tasklink:hover { color: var(--color-info); }
.cal__date { width: 100%; margin-top: var(--space-1); font: inherit; font-size: var(--text-xs); color: var(--text-muted); background: transparent; border: 0; border-top: 1px solid var(--border); }

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

.gantt__range { margin: 0; font-size: var(--text-sm); color: var(--text-muted); }
.gantt__scroll { overflow-x: auto; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.gantt { min-width: 44rem; }

.gantt__head { display: grid; grid-template-columns: 14rem 1fr; align-items: center; padding: var(--space-2) var(--space-3); background: var(--surface-sunken); border-bottom: 1px solid var(--border); }
.gantt__headlabel { font-size: var(--text-xs); font-weight: var(--weight-bold); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.gantt__months { position: relative; height: 1.1rem; }
.gantt__month { position: absolute; left: var(--left, 0%); font-size: var(--text-xs); color: var(--text-subtle); white-space: nowrap; }

.gantt__rows { list-style: none; margin: 0; padding: 0; }
.gantt__row { display: grid; grid-template-columns: 14rem 1fr; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); }
.gantt__row:last-child { border-bottom: 0; }
.gantt__row--child .gantt__label { padding-left: var(--space-4); }
.gantt__label { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.gantt__link { flex: 1; min-width: 0; font: inherit; font-size: var(--text-sm); text-align: left; color: var(--text); background: none; border: 0; padding: 0; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gantt__link:hover { color: var(--color-info); }

.gantt__track { position: relative; height: 1.4rem; background: var(--surface-sunken); border-radius: var(--radius-sm); }
.gantt__bar { position: absolute; top: 0.15rem; left: var(--left, 0%); width: var(--width, 0%); height: 1.1rem; min-width: 2px; background: var(--color-info); border-radius: var(--radius-sm); overflow: hidden; }
.gantt__bar--done { background: var(--color-success); }
.gantt__bar--late { background: var(--color-danger); }
.gantt__bar--estimated { background: repeating-linear-gradient(45deg, var(--color-info), var(--color-info) 4px, transparent 4px, transparent 8px); border: 1px solid var(--color-info); }
.gantt__progress { height: 100%; width: var(--fill, 0%); background: rgb(255 255 255 / 0.45); }
.gantt__today { position: absolute; top: -0.2rem; bottom: -0.2rem; left: var(--left, 0%); width: 2px; background: var(--color-warning); }

.gantt__legend { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-4); font-size: var(--text-xs); color: var(--text-muted); }
.gantt__legend li { display: flex; align-items: center; gap: var(--space-2); }
.gantt__key { width: 1.4rem; height: 0.7rem; border-radius: var(--radius-sm); }
.gantt__key--bar { background: var(--color-info); }
.gantt__key--estimated { background: repeating-linear-gradient(45deg, var(--color-info), var(--color-info) 3px, transparent 3px, transparent 6px); border: 1px solid var(--color-info); }
.gantt__key--late { background: var(--color-danger); }
.gantt__key--done { background: var(--color-success); }

@media (max-width: 48rem) {
  .cal__day { min-height: 4.5rem; }
  .gantt__row, .gantt__head { grid-template-columns: 8rem 1fr; }
}
/* ---------- settings ---------- */

.tabs { display: flex; flex-wrap: wrap; gap: var(--space-1); border-bottom: 1px solid var(--border); }
.tabs__tab { padding: var(--space-3) var(--space-4); font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-muted); background: none; border: 0; border-bottom: 2px solid transparent; cursor: pointer; }
.tabs__tab:hover { color: var(--text); }
.tabs__tab--active { color: var(--color-info); border-bottom-color: var(--color-info); }
.tabs__panel { padding-top: var(--space-4); }

/* SUBORDINATE, AND VISIBLY SO.
   Settings is five sections, two of which have parts. A second row of equals
   would say there are nine destinations; these are parts OF the section above
   them, so they are smaller, quieter, inset, and carry no rule of their own —
   the section's rule is already the boundary. */
.tabs--sub {
  gap: var(--space-1);
  margin-top: var(--space-2);
  padding-left: var(--space-3);
  border-bottom: 0;
}
/* RF-20260903-10 — THE HELP ICON BESIDE A SUB-TAB, NOT INSIDE IT.
   A `<button>` cannot contain a `<button>`, so the icon is the tab's SIBLING in the strip. It is
   pulled in close so it reads as belonging to the tab on its left rather than to the one on its
   right, and it takes the strip's own alignment so it cannot ride high or low against the pills. */
.tabs__tabhelp {
  display: inline-flex;
  align-items: center;
  margin-inline: calc(var(--space-1) * -1) var(--space-2);
}
.tabs__tab--sub {
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  border-bottom: 0;
  border-radius: var(--radius-pill);
}
.tabs__tab--sub.tabs__tab--active {
  color: var(--accent-contrast, #fff);
  background: var(--accent);
  border-bottom-color: transparent;
}

/* MEMBERS VERSUS CONTACTS, said once where the choice is made. The distinction
   is which record grants access, and it should be obvious without knowing
   anything about how authorization is implemented. */
.settings__legend {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin: var(--space-3) 0 0;
  padding: var(--space-3);
  font-size: var(--text-sm);
  background: var(--surface-sunken);
  border-radius: var(--radius);
}
.settings__legend dt { font-weight: var(--weight-bold); color: var(--text); }
.settings__legend dd { margin: 0; color: var(--text-muted); }
@media (max-width: 40rem) {
  .settings__legend { grid-template-columns: 1fr; gap: 0 0; }
  .settings__legend dd { margin-bottom: var(--space-2); }
}

.panel { display: flex; flex-direction: column; gap: var(--space-4); }

/* LOAD OLDER ACTIVITY. Below the table it extends, quiet, and centred so it reads as the end
   of the list rather than as an action belonging to the last row. */
.activity__more { display: flex; justify-content: center; padding-top: var(--space-2); }

/* WHICH WORKSPACES, AND WHICH ROLE IN EACH — see views/settings.js. Closed by default, so a
   table of twenty people is still a table rather than twenty lists. */
.memberships { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.memberships__none { color: var(--text-subtle); }
.memberships__toggle {
  align-self: flex-start;
  padding: 0; font: inherit; font-size: var(--text-xs);
  color: var(--color-info); background: none; border: 0; cursor: pointer;
  text-align: start;
}
.memberships__toggle:hover { text-decoration: underline; }
.memberships__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.memberships__list[hidden] { display: none; }
.memberships__item { font-size: var(--text-xs); color: var(--text-muted); }

/* ACTIVE INVITATIONS AND INVITATION HISTORY are two sections of one card, so they are
   separated by space and a subheading rather than by a border that would make them look like
   two cards that failed to align. */
.invites { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-4); }
.card__subtitle { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-bold); color: var(--text); }

/* TWO COLUMNS, WHERE TWO COLUMNS SAVE SPACE.
   Used only on the rows that were asked for — see views/settings.js, which names them and
   explains why Task form layout is deliberately not one of them. `align-items: start` keeps
   each card at its natural height instead of stretching the shorter one to match. */
.panel__pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  align-items: start;
}
/* The merged Projects & Task Categories page holds two whole management areas rather than two
   short preference cards, so its halves need more room before they are worth splitting. */
.panel__pair--wide { gap: var(--space-5); }
.panel__col { display: flex; flex-direction: column; min-width: 0; }

/* THE SETTINGS NAVIGATION STAYS PUT.
   ONE sticky container holding both rows — the section tabs and, when a section has parts,
   its sub-tabs. Two independently sticky rows would overlap the moment the second caught up
   with the first.
   THE OFFSET IS NOT ZERO. The application header is sticky at 0 and the view navigation is
   sticky at --header-height, so this sits below both or it slides underneath them. */
.settings__nav {
  position: sticky;
  top: calc(var(--chrome-header) + var(--chrome-nav));
  z-index: 10;
  background: var(--surface-page, var(--surface));
  /* Out through the main region's padding, so the bar's background covers the full width of
     the content it is pinned over rather than leaving a strip of scrolling page either side. */
  margin: calc(var(--space-5) * -1) calc(var(--space-4) * -1) 0;
  padding: var(--space-5) var(--space-4) 0;
}

/* A FOCUSED OR SCROLLED-TO CONTROL MUST NOT LAND UNDERNEATH IT. */
.view--settings .card,
/* WHAT THIS IS FOR NOW: KEYBOARD FOCUS, NOT NAVIGATION.
   Navigating to a section resets the DOCUMENT to zero (see app/js/ui/scroll.js), so nothing
   scrolls this panel into view any more and no arithmetic here has to clear the sticky bars.
   The margin stays because the browser ALSO scrolls on its own — tabbing to a control below
   the fold — and without it that lands underneath the sticky settings navigation. */
.view--settings .tabs__panel {
  scroll-margin-top: calc(var(--chrome-header) + var(--chrome-nav) + var(--space-6));
}

/* IMMEDIATELY AFTER THE RULES THEY OVERRIDE, AND THAT IS NOT A STYLE CHOICE.
   These have the same specificity as the declarations above, so LATER WINS — the repository's
   general narrow-width block sits several hundred lines earlier in this file and an override
   written there would have been silently dead. That is the same class of cascade bug as the
   Full Sheet defect at beta.44, and app/test/settingslayout.test.js checks the ORDER rather
   than merely the presence of these rules. */
@media (max-width: 48rem) {
  /* One column: two 20rem halves do not fit, and a horizontal scrollbar is never the answer
     to a narrow screen. */
  .panel__pair { grid-template-columns: 1fr; }

  /* THE VIEW NAVIGATION IS STATIC AT THIS WIDTH (see layout.css), so an offset still allowing
     for its height would leave this bar floating with nothing above it. */
  .settings__nav { top: var(--chrome-header); }
  .view--settings .card,
  .view--settings .tabs__panel { scroll-margin-top: calc(var(--chrome-header) + var(--space-6)); }
}
.panel__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.panel__head .card__hint { margin: 0; margin-right: auto; }
/* RF-20260903-09 — AN ACTION ROW THAT BELONGS TO THE TAB RATHER THAN TO A CARD'S HEAD.
   `.panel__actions` has never had styling of its own: inside `.panel__head` it inherited that
   flex row, which is why it looked right and why nobody noticed. Standing on its own it needs to
   say what it is. Right-aligned, because that is where both of these buttons already sat in their
   separate card heads — this puts them beside each other without moving either across the page. */
.panel__actions--row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); margin-bottom: var(--space-3); }

.rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.rows__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.rows__row--muted { opacity: 0.65; }
.rows__main { flex: 1; min-width: 12rem; }
/*
  RF-20260828-11 — a whole row that IS a link (the notification bell).

  It carries `.rows__main` as well, so the geometry is the plain row's geometry and nothing
  about the layout depends on which of the two a given row happens to be. `display: block`
  is what lets its two paragraphs sit inside an anchor and still stack; the colour is
  inherited rather than the link colour, because underlining a whole two-line block would
  make a panel of them unreadable. The affordance is the hover surface and the cursor, and
  the keyboard ring is the global :focus-visible one in base.css — never suppressed.
*/
.rows__link { display: block; color: inherit; text-decoration: none; border-radius: var(--radius-sm); cursor: pointer; }
.rows__link:hover .rows__title { text-decoration: underline; }
.rows__title { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-medium); }
.rows__sub { margin: var(--space-1) 0 0; font-size: var(--text-xs); color: var(--text-muted); }
.rows__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.rows__note { font-size: var(--text-xs); color: var(--text-subtle); }

.card--danger { border-color: color-mix(in srgb, var(--color-danger) 40%, transparent); }
.card--danger .card__title { color: var(--color-danger); }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }

.vocab { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.vocab__item { display: flex; align-items: center; gap: var(--space-3); font-size: var(--text-sm); }
.vocab__swatch { width: 0.7rem; height: 0.7rem; border-radius: 50%; flex: none; background: var(--swatch, var(--border-strong)); }
.vocab__label { flex: 1; }
.vocab__id { font-size: var(--text-xs); color: var(--text-subtle); }
.mono { font-family: var(--font-mono); }

/* ---------- the people picker ---------- */

.people { position: relative; display: flex; flex-direction: column; gap: var(--space-2); }
.people__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.people__chips:empty { display: none; }

/* THE SAME LANGUAGE AS EVERY OTHER CONTROL. The people picker keeps everything that makes it
   a people picker — the chip rail, the directory search, the manual-address rule — and simply
   stops being a different height and a different roundness from the palette above it. */
.people__input {
  min-height: var(--control-height);
  padding: var(--control-padding-y) var(--control-padding-x);
  font-size: var(--control-font-size);
  background: var(--control-surface);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
}
.people__input:hover { border-color: var(--control-border-hover); }
.people__input:disabled { opacity: var(--control-disabled-opacity); cursor: not-allowed; }

.people__chip { display: inline-flex; align-items: center; gap: var(--space-2); padding-inline-end: var(--space-1); }
.people__chip-name { font-weight: var(--weight-medium); }
.people__chip-email { color: var(--text-subtle); font-size: var(--text-xs); }
/* The source is a label, not a colour: "member" and "contact" mean different things and
   must be readable without seeing the difference between two greys. */
.people__chip-source { font-size: var(--text-xs); color: var(--text-subtle); border-inline-start: 1px solid var(--border); padding-inline-start: var(--space-2); }
.people__chip--member .people__chip-source { color: var(--accent); }
.people__chip--manual .people__chip-source { color: var(--color-warning); }

/* THE COMPACT [M]/[C] BADGE. A small square rather than a full word — Member and Contact are
   distinguished by a letter AND a border colour, never by colour alone, and the accessible
   name ("Member"/"Contact") comes from role="img" + aria-label on the span itself rather than
   relying on `title`, which many screen readers do not expose. */
.people__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding-inline: 2px;
  border-radius: var(--radius-sm);
  border: 1px solid currentColor;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  line-height: 1;
}
/* Its own square border replaces the divider the full-word label used, on both surfaces. */
.people__chip-source.people__badge,
.people__option-source.people__badge {
  border-inline-start: none;
  padding-inline-start: 2px;
}
.people__chip-source.people__badge { margin-inline-start: var(--space-2); }
.people__badge--member { color: var(--accent); }
.people__badge--contact { color: var(--text-subtle); }
.people__chip-remove { background: none; border: 0; color: var(--text-muted); cursor: pointer; font-size: var(--text-md); line-height: 1; padding: 0 var(--space-1); }
.people__chip-remove:hover { color: var(--color-danger); }

/* OVER THE PAGE, NOT INSIDE THE FIELD.
   It was `position: absolute; top: 100%` inside `.modal__body`, which scrolls and — since the
   dialog was given a fixed height — clipped anything running past its edge. The names were
   rendered and unreachable. Fixed, because the coordinates are measured against the viewport;
   they arrive as custom properties through the CSSOM, so a list placed at runtime still needs
   no style attribute. The fallbacks keep it usable anywhere the coordinates cannot be
   measured. Above the dialog, which is 1200. */
.people__suggestions {
  list-style: none; margin: 0; padding: var(--space-1);
  position: fixed; z-index: 1300;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  min-width: var(--pop-width, 16rem);
  max-width: min(28rem, calc(100vw - 1rem));
  /* BOUNDED, AND IT SCROLLS INSIDE ITSELF. A list longer than the room available scrolls;
     it never runs off the screen and it never makes the dialog grow. */
  max-height: var(--pop-max, 16rem); overflow-y: auto;
  background: var(--surface-raised, var(--surface)); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-lg, 0 8px 24px rgb(0 0 0 / 0.3));
}
.people__suggestions[hidden] { display: none; }
.people__option {
  display: flex; align-items: baseline; gap: var(--space-2); width: 100%;
  padding: var(--space-2) var(--space-3); background: none; border: 0; border-radius: var(--radius-sm);
  color: inherit; font: inherit; text-align: start; cursor: pointer;
}
.people__option:hover, .people__option--active { background: var(--surface-hover, var(--border)); }
.people__option-name { font-weight: var(--weight-medium); }
.people__option-email { color: var(--text-subtle); font-size: var(--text-xs); flex: 1; }
.people__option-source { font-size: var(--text-xs); color: var(--text-subtle); }
.people__option-source--member { color: var(--accent); }
.people__option-source--manual { color: var(--color-warning); }
.people__status { margin: 0; }

/* Suggested categories: real buttons, so keyboard and screen-reader behaviour comes for
   free rather than being reimplemented on a div. */
.card__empty-block { display: flex; flex-direction: column; gap: var(--space-3); }
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip--action { cursor: pointer; border-style: dashed; background: transparent; color: var(--text-muted); font: inherit; font-size: var(--text-xs); }
.chip--action:hover { color: var(--text); border-color: var(--border-strong); border-style: solid; }
/* ---------- tabbed, compact dialogs ---------- */

.modal__panel--full { max-width: 58rem; }

.modal__tabs { display: flex; gap: var(--space-1); border-bottom: 1px solid var(--border); margin-bottom: var(--space-1); }

.modal__tab {
  padding: var(--space-2) var(--space-4);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-muted);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  cursor: pointer;
}

.modal__tab:hover { color: var(--text); }
.modal__tab--active { color: var(--color-info); border-bottom-color: var(--color-info); }

/* THE COMPACT GRID. Ordinary fields take one column of a responsive track; a date or a
   percentage is sized for its content rather than stretched across the dialog. */
/* A DETERMINISTIC THREE COLUMNS, not auto-fit.
   auto-fit is what produced the stair-step: the column count changed with the dialog's width,
   so which field landed beside which was a matter of arithmetic rather than of meaning —
   Project could end up nowhere near Category. A fixed count makes the grouping a decision.
   The grid supports the information hierarchy; it does not get to decide it. */
.modal__tabpanel {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3) var(--space-4);
  align-items: start;
}

.modal__tabpanel--hidden { display: none; }

.field--full { grid-column: 1 / -1; }
.field--narrow { max-width: 11rem; }

/* THE TITLE ESTABLISHES THE TOP OF THE FORM, and Important rides on its row.
   The title takes every column but the last; the flag takes the last. It was the other way
   round, which left the checkbox alone in a left column while the title began a third of the
   way across the dialog — the single most visible symptom of a form laid out by grid cell
   rather than by meaning. */
.field--grow { grid-column: 1 / -2; }
.field--flag { grid-column: -2 / -1; align-self: end; }
/* The checkbox reads as one control with its word, at the end of the title's row. */
.field--flag.field--check { padding-bottom: var(--space-2); }

/* Denser than a standalone form: the point of the Task tab is that everything ordinary fits
   without scrolling the page. */
.modal__tabpanel .field { gap: var(--space-1); }
.modal__tabpanel .field__input { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }

/* THE FIELDS THAT WERE NEVER CONVERTED — Repeat's selects and numbers, Tags, Progress, and
   anything added later — speak the same language as the palettes they sit beside.
   SCOPED TO THE TASK FORM, deliberately. `.field__input` is the whole product's text input and
   restyling it everywhere would be a redesign of every dialog on the way past, which is not
   what was asked for. The task form is where the inconsistency was reported and where the
   modern controls live. */
.modal__form .field__input {
  min-height: var(--control-height);
  padding: var(--control-padding-y) var(--control-padding-x);
  font-size: var(--control-font-size);
  background: var(--control-surface);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
}
.modal__form .field__input:hover:not(:disabled) { border-color: var(--control-border-hover); }
.modal__form .field__input:disabled { opacity: var(--control-disabled-opacity); }
/* A textarea is the one control the height token cannot describe: it is sized by its rows. */
.modal__form textarea.field__input { min-height: 3.5rem; }
.modal__tabpanel textarea.field__input { min-height: 3.5rem; }
/* AND NEITHER IS THE STRUCTURED PROSE DOCUMENT — the same exemption, for the surface that
   replaced the textarea.

   THIS IS A REAL REGRESSION, FOUND IN A BROWSER AND FIXED HERE. `.modal__form .field__input`
   above has specificity (0,2,0) and comes later in this file than `.prose__doc` (0,1,0), so it
   won, and every prose field in the task dialog collapsed to ONE control-height row: measured
   at 40px where seven rows should be ~168px. The `textarea` line directly above is the escape
   hatch that kept this from happening before — and the structured editor is a `div`, so it
   never got one. The Description simply became a slot the size of a text input.

   Nothing in the unit suite could see it: a DOM harness has no cascade, and the editor was
   still asking for seven rows through `--prose-rows` (the attribute was right, the pixels were
   not). `scripts/dev/verify-prose.mjs` had asserted exactly this — "GENUINELY TALL — measured,
   not inferred from an attribute" — but had been left selecting `textarea[data-field=...]`,
   so it found nothing and threw instead of failing. Repointing it is what surfaced this. */
.modal__form .prose__doc,
.modal__tabpanel .prose__doc {
  min-height: calc(var(--prose-rows, 3) * 1.5em + 2 * var(--space-2));
}
.modal__tabpanel .field__help { font-size: 0.7rem; }
.modal__tabpanel .field--check { align-items: flex-start; }

/* THE RECURRENCE TAB IS TWO COLUMNS. Its fields pair naturally — pattern with interval, the
   two schedule controls, the two ways a series ends — and three columns would split those
   pairs across rows for no gain. Less scrolling, better grouping, faster comprehension. */
.modal__tabpanel[data-tab="recurrence"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Between the two, two columns: the pairs that matter — Project/Category, Start/Due — still
   land together, and nothing is squeezed to an unusable width. */
@media (max-width: 62rem) {
  .modal__tabpanel { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 40rem) {
  /* Narrow screens scroll, which is normal there. One column, full-size controls — and the
     title row stops being a row, because there is only one column to share. */
  .modal__tabpanel { grid-template-columns: 1fr; }
  .field--narrow { max-width: none; }
  .field--flag, .field--grow { grid-column: 1 / -1; }
  .modal__tabs { overflow-x: auto; }
}
/* ---------- the notification bell ---------- */

.bell { position: relative; display: inline-flex; align-items: center; gap: var(--space-1); }
.bell__icon { font-size: var(--text-md); line-height: 1; }
/* The count is TEXT, not a coloured dot: a dot says nothing to a screen reader, and the
   button's accessible name says what the number counts. */
.bell__count { min-width: 1.25rem; padding: 0 var(--space-1); border-radius: 999px; background: var(--accent); color: var(--surface); font-size: var(--text-xs); font-weight: var(--weight-medium); text-align: center; }
.bell__count--error { background: var(--color-danger); }
.bell--unread { color: var(--text); }

.chip--success { color: var(--color-success, #10b981); border-color: currentColor; }
.chip--error { color: var(--color-danger); border-color: currentColor; }
/* RB-14 — a task's CURRENT status, wherever it is shown beside an actions waffle: the same
   thin-outline recipe every other state chip here uses, keyed to the workspace's own colour
   for that status rather than a fixed palette id, since a status's colour is configurable. */
.chip--status { color: var(--chip-color, var(--text-muted)); border-color: var(--chip-color, var(--border)); }
/* Waiting on somebody, rather than wrong. A pending account is nobody's fault and the
   colour says "look at this", not "this is broken". */
.chip--warn { color: var(--color-warning); border-color: currentColor; }

/* A statement of fact about how access was obtained, not a condition to act on. Warning
   banners are reserved for the latter; this is one compact line above the view. */
.access-note {
  display: flex; align-items: center; gap: var(--space-2);
  margin: 0 0 var(--space-4); padding: var(--space-2) var(--space-3);
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius);
  font-size: var(--text-xs); color: var(--text-muted);
}
.access-note__badge {
  flex: none; padding: 0.1rem var(--space-2); border-radius: var(--radius-pill);
  background: var(--accent-soft); border: 1px solid var(--accent); color: var(--accent);
  font-weight: var(--weight-medium);
}

/* Why a control is switched off. Rendered beside the field and joined to its accessible
   description, because a greyed-out box with no explanation is the thing people file bugs
   about. */
.field__reason { margin: 0; font-size: var(--text-xs); color: var(--color-warning); }
.field__reason[hidden] { display: none; }
.field__input[aria-disabled="true"], .field__check[aria-disabled="true"] { opacity: 0.55; }

/* ---------- compact data tables ----------
   Settings holds short structured datasets — six statuses, five priorities, a handful of
   contacts. Rendering them as full-width rows put a river of empty space between the label
   and the internal id, so a six-row list filled a desktop screen. A table gives each value
   a column of its own content's width and lets the eye scan down instead of across.

   INFORMATION PRIORITY, left to right: human label → meaning → usage/actions → internal id.
   The id stays visible because it is real administrative metadata, but it is last, muted
   and monospaced, because it is looked up rather than read. */

.datatable__scroll { overflow-x: auto; }
.datatable { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.datatable th {
  text-align: start; padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs); font-weight: var(--weight-medium);
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-subtle);
  border-bottom: 1px solid var(--border-strong); white-space: nowrap;
}
.datatable td { padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: middle; }
.datatable tr:last-child td { border-bottom: 0; }
.datatable tbody tr:hover { background: var(--surface-sunken); }
.datatable__row--muted { opacity: 0.6; }

/* The name column carries the weight and takes the slack, so every other column stays at
   its content's width instead of being stretched apart. */
.datatable__name { width: 100%; display: table-cell; }
.datatable__name .vocab__swatch { display: inline-block; margin-inline-end: var(--space-2); vertical-align: middle; }
.datatable__primary { display: block; font-weight: var(--weight-medium); }
.datatable__secondary { display: block; font-size: var(--text-xs); color: var(--text-muted); }

.datatable__num { text-align: end; font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text-muted); }
/* RF-20260903-12 — WHO IS AS WIDE AS A NAME; WHAT GETS THE REST.
   The Activity table used `.datatable__name`, which is `width: 100%` — the greedy column in the
   tables that class was written for. Here that made WHO take the whole table and squeezed WHAT into
   the remainder, wrapping short messages over three lines beside acres of empty space. `width: 1%`
   with `nowrap` is the standard way to say "as narrow as your content"; the 100% then lands on WHAT,
   which is the column with something to say. WHAT is deliberately left wrapping: a pathological
   message should wrap rather than push the table into horizontal scroll. */
.datatable__who { white-space: nowrap; width: 1%; }
.datatable__what { width: 100%; }
.datatable__when { white-space: nowrap; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* SECONDARY BY CONSTRUCTION. */
.datatable__id { white-space: nowrap; font-size: var(--text-xs); color: var(--text-subtle); }
.datatable__actions { white-space: nowrap; text-align: end; }
.datatable__actions > * + * { margin-inline-start: var(--space-2); }

.btn--sm { min-height: 1.75rem; padding: var(--space-1) var(--space-3); font-size: var(--text-xs); }
.chip--open { color: var(--color-info); border-color: currentColor; }
.card--tight { padding: var(--space-4); }

/* Efficient use of desktop space: the short vocabularies sit side by side rather than each
   claiming a full-width band of its own. */
/* SIBLING PANELS START AT THE SAME LINE. `align-items: start` is what stops a grid stretching
   every cell to the tallest row and is also what keeps each panel at its own natural height —
   without it the shorter tables were vertically centred against the taller ones and the set
   read as staggered. */
/* RF-20260828-03 — CAPPED AT TWO COLUMNS, ON INSTRUCTION, REVERSING WHAT THIS COMMENT USED TO
   SAY. A 16rem minimum let `repeat(auto-fit, ...)` pack THREE (or, for a five-card panel like
   Site Administration -> Settings, FOUR) cards into one row at `--content-max: 80rem` — every
   `.cards--split` consumer inherited that, not just Labels, and it is exactly the "three or
   four columns" the instruction explicitly rules out everywhere. `minmax(27rem, 1fr)` is the
   smallest floor that STRUCTURALLY cannot fit a third column within `--content-max` itself
   (3 x 27rem + 2 gaps > 80rem), so this is not tuned to one screenshot's width — it holds at
   any width up to the app's own maximum. Two columns where there is genuinely room, one when
   there is not, the same "auto-fit decides, never a fixed guess" principle this class already
   used, just with a floor wide enough to keep the ceiling at two.

   RF-20260829-01 — EQUAL HEIGHTS ARE NOW OPT-IN, AND THIS REVERSES THE DEFAULT.
   The stretch that used to live here was justified in its own comment ENTIRELY by the Labels
   tab: three small vocabulary cards holding different numbers of rows, which sat at three
   visibly different depths for no reason a reader could see. That reasoning is sound and is
   preserved verbatim on `.cards--split--even` below. What was wrong is that `.cards--split` is
   SHARED, so six other surfaces inherited a rule argued from one tab's specific shape.

   MEASURED, IN A REAL BROWSER, BEFORE CHANGING ANYTHING (`verify-settings-layout-contract.mjs`):
   Site Administration -> Settings' *Workspaces* card rendered as a 583px box holding 459px of
   dead space (79% empty) beside a taller Announcement card; Workspace Settings -> Activity's
   *Delivery history* held **2814px of dead space in a 4367px box**. A card whose natural height
   is 71px was rendering at 549px against an injected fixture.

   `start` IS THE DEFAULT BECAUSE IT IS THE HONEST ONE. A panel should be as tall as what is in
   it. Equalising is a deliberate visual choice for cards that are genuinely the same KIND of
   thing, and it now has to say so — which is what `RF-20260826-01` already concluded when it
   built `.panel__columns` expressly so "Landing View was not stretched to match whatever sat
   beside it". Those two rules contradicted each other for two days; this settles it. */
.cards--split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(27rem, 1fr));
  gap: var(--space-4);
  /* SIBLING PANELS START AT THE SAME LINE and end where their own content ends. Without this a
     grid stretches every cell to the tallest row; with it each panel keeps its natural height
     and the set still shares one top edge. */
  align-items: start;
}

/* THE LABELS PANELS SHARE ONE GEOMETRY, AND SAY SO.
   Statuses, Priorities and Efforts hold different numbers of rows, and with `align-items:
   start` each card was only as tall as its own content — so the three sat at three different
   depths and one visibly began lower than its neighbour for no reason a reader could see.
   `stretch` gives them a common top AND a common bottom, which is what "consistent panel
   geometry" actually means here.

   IT HAS TO ARGUE FOR ITSELF NOW. Three cards of the same kind, of similar size, whose shared
   baseline reads as intentional — that is the case this exists for, and the browser pass
   confirms Labels is the only surface that qualifies (it reports one minor sub-120px gap, where
   every other stretched surface reported hundreds or thousands of pixels of dead space). A
   consumer reaching for this modifier is claiming its cards are that shape. */
.cards--split--even { align-items: stretch; }

/* The cards are the grid items themselves, so the alignment above is what sizes them — there
   is no wrapper in between that would need its height passing on. `min-width: 0` keeps a wide
   table inside one from forcing the column wider than its share. */
.cards--split > .card { min-width: 0; }

@media (max-width: 48rem) {
  .cards--split { grid-template-columns: 1fr; }
}

/* Presence: a dot AND a word. Colour alone says nothing to a screen reader, and the word
   is what the accessible name carries. */
.presence { display: flex; align-items: center; gap: var(--space-2); white-space: nowrap; }
.presence__dot { font-size: 0.7rem; line-height: 1; }
.presence__dot--online { color: var(--color-success, #10b981); }
.presence__dot--away { color: var(--color-warning); }
.presence__dot--muted { color: var(--text-subtle); }

/* ---------- visibility ---------- */
/* The level is a small statement of fact beside a name, not a warning. Each level gets its
   own hue so a list is scannable, but the WORD is always present — the colour is never the
   only thing carrying the meaning. */
.chip--level { margin-inline-start: var(--space-2); font-size: var(--text-xs); }
.chip--level-public { color: var(--accent); border-color: currentColor; }
.chip--level-people { color: var(--text-muted); border-color: currentColor; }
.chip--level-private { color: var(--color-warning); border-color: currentColor; }

.levelrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); }
.levelrow .card__hint { flex: 1; min-width: 16rem; margin: 0; }

/* ---------- the notification matrix, and every other split-list card ----------
   TWO SECTIONS SIDE BY SIDE, NEVER THREE OR FOUR — ON INSTRUCTION, REVERSING WHAT THIS
   COMMENT USED TO SAY ("three on a wide desktop"). The Notification card's own three event
   groups could fill a whole row on a wide monitor at a 17rem floor, and every other user of
   this class (Task Form Defaults, Categories, Tags — see RF-20260828-03) inherited the same
   risk even though `splitIntoColumns()` already caps THEM at two sections structurally; the
   Notification matrix has no such JS-side cap, so the CSS floor is what has to hold the
   ceiling at two here. `minmax(27rem, 1fr)` is comfortably past the point a third column
   could fit within `--content-max: 80rem` itself (3 x 27rem + 2 gaps = 83rem > 80rem) — not
   tuned to one screenshot, true at any width up to the app's own maximum. Two columns where
   there is genuinely room, one when there is not. */
.matrixgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(27rem, 1fr));
  gap: var(--space-4) var(--space-5);
  align-items: start;
}

/* RF-20260828-03 — EACH COLUMN IS ITS OWN QUERY CONTAINER, not a fixed guess at how wide a
   column will end up. A first attempt at this fix widened `.matrixgrid`'s own `minmax` floor
   for Task Form Defaults specifically (a `matrixgrid--wide` modifier, `minmax(28rem, 1fr)`) —
   but that traded the alignment bug for a worse one: at `--content-max: 80rem`'s realistic
   settings-panel widths, 2 x 28rem rarely had room to fit at all, so the "two columns where
   there is room" half of the requirement stopped being true in practice, even though nothing
   was visibly broken. `container-type: inline-size` here lets any content INSIDE a section
   query that section's own rendered width directly, so a row can wrap gracefully at whatever
   width its column actually ends up — narrower columns fit more often AND never overflow,
   instead of picking one fixed number that has to be safe for every case at once. */
.matrixgrid__section { min-width: 0; container-type: inline-size; }
.matrixgrid__title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-subtle);
}

/* THE NOTIFICATION MATRIX — Event | In app | Email.

   IT IS DELIBERATELY NOT CALLED `.matrix`. It was, and the Eisenhower view already owned
   that name with `display: grid; grid-template-columns: 1fr 1fr`. Two unrelated components
   sharing a class is not a naming untidiness — the table inherited `display: grid` and
   stopped being a table, so `table-layout` and the column widths below applied to nothing,
   the header became one grid item and the body another, and the two ended up in different
   columns with no shared geometry at all. That is exactly what alignment-by-accident looks
   like when the accident stops. `.notifmatrix` cannot collide.

   ONE COLUMN DEFINITION, IN ONE PLACE. `table-layout: fixed` plus the <colgroup> means the
   header row and every event row resolve to the SAME columns by construction — not because
   two sets of widths happen to agree today. No cell declares a width, so there is nothing
   to drift. */
.notifmatrix { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: var(--text-sm); }

/* The whole column definition. Event takes what is left; the channels are sized to their
   headings and stay put however long a label is. */
.notifmatrix__col--event { width: auto; }
.notifmatrix__col--channel { width: 3.5rem; }

.notifmatrix th, .notifmatrix td { padding: var(--space-1) var(--space-2); border-bottom: 1px solid var(--border); }
.notifmatrix thead th {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-subtle);
  font-weight: var(--weight-medium);
}
.notifmatrix tbody tr:last-child th, .notifmatrix tbody tr:last-child td { border-bottom: 0; }

/* Alignment only. The geometry is the colgroup's job, and these must never take it back. */
.notifmatrix__event { text-align: start; font-weight: var(--weight-regular); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.notifmatrix__channel { text-align: center; }

/* Dense is not the same as tiny. The row is compact; the target somebody has to hit is not —
   the checkbox keeps a full-size hit area inside a short row. */
.notifmatrix__channel .field__check { margin: 0; width: 1rem; height: 1rem; }
.notifmatrix__channel label, .notifmatrix__channel .field__check { cursor: pointer; }
.notifmatrix tbody tr:hover { background: var(--surface-sunken); }

/* ---------- a dialog that does not move under you ----------
   The head, the tab strip and the actions are FIXED; only the content between them scrolls.
   Before this the whole panel scrolled and every tab was a different height, so changing tab
   moved the Save button somebody was already reaching for. */
.modal__panel {
  display: flex;
  flex-direction: column;
  /* The panel itself no longer scrolls — .modal__body does. */
  overflow: hidden;
}
.modal__form { flex: 1; min-height: 0; }
.modal__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Room for a focus ring against the scroll edge, and for a dropdown to open without
     being clipped the instant it appears. */
  padding: var(--space-1) var(--space-1) var(--space-2);
  margin: calc(var(--space-1) * -1) calc(var(--space-1) * -1) 0;
}
.modal__head, .modal__tabs, .modal__actions, .modal__description { flex: none; }
/* A TABBED DIALOG HOLDS ONE SHAPE ACROSS ITS TABS.
   A MINIMUM WAS NOT ENOUGH. The body had a floor and no ceiling, so it still grew to fit
   whichever panel was showing — the Task tab is tall, Recurrence is short, and changing tab
   resized the dialog and moved everything under the pointer. Three tabs of three different
   heights need ONE height, not a minimum each of them clears by a different amount.
   Bounded by the viewport so a short screen never gets a dialog taller than itself, and with
   a floor of its own so a very short screen still leaves something to work in. */
.modal__panel--full .modal__body {
  flex: none;
  height: max(14rem, min(30rem, calc(100vh - 16rem)));
}

/* RF-20260827-11 — AND THE STABLE SHELL SUPERSEDES IT.
   The rule above is beta.236's answer to the same problem one level down: give the BODY a fixed
   height so three field tabs of three different heights cannot resize the dialog. The stable
   shell fixes the PANEL instead, which is strictly stronger — it also covers the section panels,
   which the body rule cannot see. Both at once means a body frozen at 30rem inside a panel of a
   different height, with the footer floating wherever the sum happens to land. So on a stable
   panel the body goes back to filling what it is given, and the panel's own height is the only
   fixed number. */
.modal__panel--stable .modal__body { flex: 1 1 auto; height: auto; }
.modal__actions { background: var(--surface-raised); }

@media (max-width: 40rem) {
  /* On a narrow screen the dialog is effectively the page. Give it the whole height rather
     than a floating card with two scroll regions competing. */
  .modal__panel { max-height: calc(100vh - var(--space-4)); }
  /* AND THE STABLE PANEL STOPS RESERVING A MARGIN IT NO LONGER HAS ROOM FOR. `.modal` drops its
     padding here and the panel is `min-height: 100vh`, so a top margin pushed the whole dialog
     down and put a scrollbar on the overlay for the sake of 12px of nothing. */
  .modal__panel--stable { margin-top: 0; height: auto; }
  /* The fixed height is RELEASED here: on a narrow screen the dialog is effectively the page,
     where one tall scrolling column is right and an inner fixed height would create two
     scroll regions competing with each other. */
  .modal__panel--full .modal__body { flex: 1; height: auto; }
}

/* ---------- polish: restraint over decoration ----------
   Everything here removes something rather than adding it. The complaints these answer were
   all the same shape — space that carries no information, and controls stretched far past
   the width of what they hold. */

/* A card is a grouping, not a poster. Tightened so a settings panel reads as a list of
   related facts rather than a stack of billboards. */
.card { padding: var(--space-4); }
.card--tight { padding: var(--space-3) var(--space-4); }
/* RF-20260903-05..-14 — A CAP FOR A NOTE BESIDE A CONTROL, AND NONE FOR A SECTION'S INTRODUCTION.

   Terry filed the same complaint against SEVEN surfaces in one sitting — General, Task Form, View
   Tabs, Kanban, Notification settings, Site Administration -> Accounts and -> Task form defaults —
   each time as "the text is all scrunched on the left with all that space open to the right".
   Measured, every one of them was the SAME NUMBER: `78ch` at `--text-xs` resolves to 504.562px, in
   panels between 943px and 1248px wide. One rule, seven reports.

   THIS LESSON WAS ALREADY LEARNED ONCE AND APPLIED TO ONE SURFACE. `.rte__intro .card__hint` got
   `max-width: none` for precisely this reason, and the note above it says why: a cap makes "a
   narrow column of text down the left of a wide panel, reading as a note about the first setting
   rather than as the description of the section". That was true of every other section too.

   THE DISTINCTION IS STRUCTURAL, NOT A LIST OF SURFACES. A hint that is a DIRECT CHILD of a card
   or panel, or that sits in a panel heading, INTRODUCES that section and should be as wide as the
   thing it introduces. A hint nested inside a row, a level indicator or a field is a note attached
   to one control, and keeps a reading measure — those are the ones the cap was written for.

   THE MEASURE IS STILL BOUNDED ONE LEVEL UP: `.app__main` caps the application at 1280px, so an
   uncapped introduction is a panel-wide line, not a metre of text. */
.card__hint { max-width: 78ch; }
.card > .card__hint,
.panel > .card__hint,
.panel__head > .card__hint,
.panel__heading > .card__hint,
.card__disclosure > .card__hint,
.rte__intro > .card__hint { max-width: none; }
.card + .card { margin-top: var(--space-3); }

/* ...BUT NOT WHEN THE CARDS ARE SIDE BY SIDE, and this is what made the paired rows look
   wrong while their markup was right.
   The rule above exists for cards STACKED in normal flow, where the second needs breathing
   room below the first. Inside `.panel__pair` and `.cards--split` the cards are GRID ITEMS in
   the same row — and they are still adjacent siblings, so `.card + .card` matched and pushed
   the second one down by a whole step. The row was correctly built and visibly staggered, and
   no assertion about grid columns or `align-items` could have caught it, because neither was
   the thing that was wrong.
   A grid already spaces its items with `gap`; a margin on top of that is duplicated spacing
   applied to only one of the two. */
.panel__pair > .card + .card,
.cards--split > .card + .card,
/* RF-20260903-16 — AND THE SITE EDITOR MATRIX, WHICH HIT THIS EXACT DEFECT ON ITS FIRST RUN.
   Its disclosures carry `.card`, so the second cell of every row matched `.card + .card` and sat
   12px lower than the first — measured at tops 470 vs 482, a row that was correctly built and
   visibly staggered, which is word for word what the note above predicted. */
.rte__matrix > .card + .card { margin-top: 0; }

/* A definition list of short values does not need a full-width column each. */
.detail { grid-template-columns: minmax(8rem, max-content) 1fr; column-gap: var(--space-4); }

/* Inputs sized for their content. A date field stretched across a dialog reads as an
   invitation to type an essay into it. */
.field__input[type="date"], .field__input[type="number"] { max-width: 11rem; }
.field__input--inline { max-width: 12rem; }

/* Actions sit in the same place on every panel: at the end of the row they belong to. */
.panel__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.panel__head .card__hint { flex: 1; min-width: 16rem; margin: 0; }

/* A TITLED HEAD: name, then description BENEATH it, then the action at the end of the row.
   Two panels standing side by side line their titles up along the top this way however long
   either description turns out to be — which a title sitting beside its own sentence cannot
   do. The heading block takes the free space so the action still lands on the far right. */
.panel__head--titled { align-items: flex-start; }
.panel__heading { display: flex; flex-direction: column; gap: var(--space-2); flex: 1 1 16rem; min-width: 0; }
.panel__heading .card__title { margin: 0; }
.panel__heading .card__hint { flex: 0 1 auto; min-width: 0; margin: 0; }
/* The action keeps its own line height rather than stretching down the description. */
.panel__head--titled > .btn { flex: 0 0 auto; }

/* The tag vocabulary, read-only: a chip and how many tasks carry it. The rows are the same
   rows as everywhere else; only the tag chip needs to sit on its own baseline. */
.catalog__tags .rows__title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }

/* Focus is always visible, including on a control that is otherwise borderless. Removing an
   outline without replacing it is how a keyboard user loses their place. */
.btn:focus-visible, .field__input:focus-visible, .field__check:focus-visible,
.modal__tab:focus-visible, .nav__link:focus-visible, .tabs__tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  /* Motion is decoration here. Somebody who has asked for less of it gets none. */
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}

/* ---------- the people dock ----------
   A persistent panel in the bottom-right: collapsed it is one pill, expanded it is who is
   here and what is waiting. It never covers what somebody is working on, and on a narrow
   screen it becomes a sheet across the bottom rather than a floating card fighting for
   half the viewport. */

.dock {
  position: fixed;
  inset-block-end: var(--space-4);
  inset-inline-end: var(--space-4);
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  /* The dock is not a dialog. Only its own controls take pointer events, so the page
     underneath stays fully usable around it. */
  pointer-events: none;
}
.dock > * { pointer-events: auto; }

/* RF-39 — THE POPOUT'S OWN LAYOUT (chat.html). In its own window there is no page underneath
   to avoid covering, and no corner to sit in — the whole window IS the chat, so this fills it
   rather than floating a small panel inside a mostly-empty browser window. */
.dock--standalone {
  position: static;
  inset: auto;
  width: 100vw;
  height: 100vh;
  align-items: stretch;
  pointer-events: auto;
}
.dock--standalone .dock__panel { width: 100%; height: 100%; max-height: none; }

.dock__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.25rem;
  padding: var(--space-2) var(--space-4);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.dock__pill:hover { background: var(--surface-sunken); }

/* RF-16 — A NEW MESSAGE PULSES THE PILL, briefly, in the theme's own accent — never a
   hardcoded colour, so it repaints correctly across every palette and both light/dark modes
   the same way `.dock__badge` below already does. THREE SHORT PULSES, then it stops on its
   own; the persistent `.dock__badge` count is what carries the unread state afterward, not
   this animation. The blanket `prefers-reduced-motion: reduce` rule earlier in this file
   already collapses this to a single near-instant frame for anyone who has asked for less
   motion — no separate reduced-motion rule is needed here. */
@keyframes dock-pulse {
  0%, 100% { border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
  50% { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent); }
}
.dock__pill--flash { animation: dock-pulse 600ms ease-in-out 3; }

.dock__panel {
  display: flex;
  flex-direction: column;
  /* The "medium" size (`.dock__panel--md`) — no separate rule needed, this IS that default,
     kept here rather than duplicated in a `--md` class of its own. */
  width: min(22rem, calc(100vw - var(--space-6)));
  max-height: min(30rem, calc(100vh - var(--space-8)));
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* Restrained elevation. It is a dock, not a modal. */
  box-shadow: var(--shadow);
  overflow: hidden;
}

/* RF-20 STAGE 5 — RESIZE. Three named sizes cycled by `.dock__resize`, not a drag handle —
   see the note above `DOCK_SIZES` in dock.js for why a free-drag handle is off the table
   under this app's CSP. `--md` is deliberately absent: it is `.dock__panel`'s own base size
   above. */
.dock__panel--sm { width: min(18rem, calc(100vw - var(--space-6))); max-height: min(22rem, calc(100vh - var(--space-8))); }
.dock__panel--lg { width: min(30rem, calc(100vw - var(--space-6))); max-height: min(42rem, calc(100vh - var(--space-8))); }

.dock__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  flex: none;
}
.dock__title { margin: 0; flex: 1; font-size: var(--text-sm); font-weight: var(--weight-bold); }
/* RF-28 — Terry: "Make the '# online' look more like something to push." A real button now,
   not text with a subtle hover — a filled pill with its own border, matching the weight of
   every other clickable control in this header rather than reading as a passive label. */
.dock__count--toggle {
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3); font: inherit; font-size: var(--text-xs); font-weight: var(--weight-medium);
  color: var(--text); cursor: pointer;
}
.dock__count--toggle:hover { background: var(--surface-raised, var(--surface)); border-color: var(--border-strong); }
.dock__count--toggle:focus-visible { border-color: var(--accent); }
.dock__count--toggle[aria-expanded="true"] { background: var(--accent-soft, var(--surface-sunken)); border-color: var(--accent); }
.dock__close, .dock__back, .dock__resize, .dock__popout {
  flex: none; width: 1.75rem; height: 1.75rem; display: grid; place-items: center;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text-muted); cursor: pointer; font-size: var(--text-md); line-height: 1;
}
.dock__close:hover, .dock__back:hover, .dock__resize:hover, .dock__popout:hover { background: var(--surface-sunken); color: var(--text); }

.dock__body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-2); }
.dock__body--conversation { display: flex; flex-direction: column; padding: 0; }

/* RF-48 — GROUPS the message list, send-error and attach controls into one swappable unit
   distinct from the compose form beside it, so a poll-triggered refresh can replace this
   whole group in a single DOM operation while the compose form (and whatever has focus
   inside it) is never touched at all. `display: contents` removes this element from the box
   tree entirely — its children lay out exactly as if they were direct children of
   `.dock__body--conversation`, so grouping them changes nothing about how the panel looks. */
.dock__volatile { display: contents; }

.dock__section {
  margin: var(--space-2) var(--space-2) var(--space-1);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--text-subtle);
}
.dock__list { list-style: none; margin: 0; padding: 0; }
.dock__empty { margin: var(--space-3) var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }
.dock__error { margin: var(--space-3) var(--space-2); font-size: var(--text-sm); color: var(--color-danger); display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
/* RF-20260827-17 — WHY THIS LIST IS SHORTER THAN SOMEBODY EXPECTED.
 *
 * Deliberately NOT `.dock__error`: nothing has gone wrong and nothing needs fixing, so it must
 * not arrive in the danger colour and send somebody looking for a fault. It is an explanation,
 * so it reads as ordinary muted text like `.dock__empty` — with a left rule, because it is about
 * the absence of items rather than being an item itself, and the eye needs to tell those apart at
 * a glance in a narrow column. */
.dock__note {
  margin: var(--space-2) var(--space-2) var(--space-3);
  padding-left: var(--space-2);
  border-left: 2px solid var(--border);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}
/* RF-25 — the offline roster the header's "N online" control reveals. A top border marks it
   as a distinct, opened-on-demand section rather than a continuation of "Recent" above it. */
.dock__offline { margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--border); }

.dock__person {
  display: flex; align-items: center; gap: var(--space-2); width: 100%;
  padding: var(--space-2); font: inherit; font-size: var(--text-sm); text-align: start;
  background: none; border: 0; border-radius: var(--radius); color: var(--text); cursor: pointer;
}
.dock__person:hover:not(:disabled) { background: var(--surface-sunken); }

/* RF-28 — THE TAB BAR: People (home), Public, Workspace Chat, and one closable tab per open
   direct conversation. Real tabs, not a list — Terry: "they should be visible tabs." An
   underline on the active one is the same idiom this app's own settings sub-tabs already use,
   so a tab reads as a tab on sight rather than needing its own new visual language. */
.dock__tabs {
  display: flex; align-items: stretch; gap: var(--space-1); flex: none;
  padding: 0 var(--space-2); border-bottom: 1px solid var(--border); overflow-x: auto;
}
.dock__tab {
  display: inline-flex; align-items: center; gap: var(--space-1);
  border-bottom: 2px solid transparent; white-space: nowrap;
}
.dock__tab-label {
  padding: var(--space-2) var(--space-2); background: none; border: 0; font: inherit;
  font-size: var(--text-sm); color: var(--text-muted); cursor: pointer;
}
.dock__tab:hover { background: var(--surface-sunken); }
.dock__tab--active { border-bottom-color: var(--accent); }
.dock__tab--active .dock__tab-label { color: var(--text); font-weight: var(--weight-medium); }
.dock__tab-close {
  flex: none; width: 1.1rem; height: 1.1rem; margin-inline-end: var(--space-1);
  display: grid; place-items: center; background: none; border: 0; border-radius: var(--radius-sm);
  color: var(--text-subtle); cursor: pointer; font-size: var(--text-sm); line-height: 1;
}
.dock__tab-close:hover { background: var(--surface-sunken); color: var(--text); }

/* CHAT IS UNAVAILABLE — fewer than two people online. See dock.js.
   RESTRAINED, AND CLEARLY NOT BROKEN. The existing disabled language: reduced opacity and a
   not-allowed cursor, no new colour, no warning treatment, nothing that reads as an error.
   Presence is NOT dimmed with it — who is here is the fact that explains the state, and the
   panel would be answering a question with the answer greyed out. */
.dock__pill--nochat { color: var(--text-muted); }
.dock__pill--nochat .dock__dot--on { background: var(--text-subtle); }

.dock__input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.dock__person-main { flex: 1; min-width: 0; }
.dock__person-name { display: block; }
.dock__person-preview {
  display: block; font-size: var(--text-xs); color: var(--text-subtle);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Presence is a word to a screen reader and a dot to everybody else — the dot is never the
   only thing carrying the meaning, because every row is also grouped under "Online". */
.dock__dot { flex: none; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--text-subtle); }
.dock__dot--on { background: var(--color-success, #10b981); }
.dock__dot--off { background: var(--border-strong); }

.dock__badge {
  flex: none; min-width: 1.25rem; padding: 0 var(--space-1); border-radius: var(--radius-pill);
  background: var(--accent); color: var(--accent-contrast);
  font-size: var(--text-xs); font-weight: var(--weight-bold); text-align: center;
}

/* RF-25 — THE SAME `dock-pulse` KEYFRAME the pill's own `.dock__pill--flash` already uses
   (defined above), reused rather than duplicated: a person or channel row pulses briefly when
   ITS OWN unread count just rose, so "something arrived" (the pill) and "here specifically"
   (the row) read as the same visual language. The persistent `.dock__badge` count is what
   carries the unread state once the pulse ends, exactly as the pill's own comment describes. */
.dock__person--flash, .dock__tab--flash { animation: dock-pulse 600ms ease-in-out 3; }

/* RF-45 — "pulse it until actually read." SLOW AND INFINITE, unlike `dock-pulse` above's brief
   3-beat burst — a second, gentler tempo of the same visual language (a soft ring, not a
   colour change) so "just arrived" and "still sitting unread" read as related but distinct.
   Reduced-motion is already handled: the blanket `*` rule earlier in this file collapses any
   `animation-iteration-count` to 1, so this never actually loops for somebody who asked for
   less motion. */
@keyframes dock-pulse-soft {
  0%, 100% { box-shadow: none; }
  50% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
}
.dock__tab--unread .dock__tab-label { animation: dock-pulse-soft 2400ms ease-in-out infinite; border-radius: var(--radius-sm); }
.dock__pill--unread { animation: dock-pulse-soft 2400ms ease-in-out infinite; }

.dock__messages { flex: 1; min-height: 0; overflow-y: auto; list-style: none; margin: 0; padding: var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }
.dock__message { max-width: 85%; align-self: flex-start; }
.dock__message--mine { align-self: flex-end; text-align: end; }
.dock__message-text {
  margin: 0; padding: var(--space-2) var(--space-3); border-radius: var(--radius-lg);
  background: var(--surface-sunken); font-size: var(--text-sm);
  /* Line breaks are preserved exactly as typed, and the text is never treated as markup. */
  white-space: pre-wrap; overflow-wrap: anywhere; text-align: start;
}
.dock__message--mine .dock__message-text { background: var(--accent-soft, var(--surface-sunken)); }
.dock__message-meta { margin: var(--space-1) 0 0; font-size: var(--text-xs); color: var(--text-subtle); }

/* RF-55 — a pasted or shared task/project deep link, rendered inline by linkifyMessage(). */
.dock__message-link { color: var(--accent); font-weight: 600; text-decoration: underline; }
.dock__message-link:hover { text-decoration: none; }

/* RF-20 STAGE 4 — attachments. An image on a message renders inline, bounded so one large
   photo cannot push the whole thread wide; anything else is a plain filename link. */
.dock__attach-image {
  display: block; max-width: 100%; max-height: 12rem; border-radius: var(--radius);
  margin-bottom: var(--space-1);
}
.dock__attach-file {
  display: inline-block; margin-bottom: var(--space-1); font-size: var(--text-sm);
  color: var(--accent); text-decoration: none;
}
.dock__attach-file:hover { text-decoration: underline; }
.dock__attach-row { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-2); flex-wrap: wrap; }
.dock__attach-pending {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: var(--space-1) var(--space-2); border-radius: var(--radius);
  background: var(--surface-sunken); font-size: var(--text-xs); max-width: 12rem;
}
.dock__attach-pending span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dock__attach-remove {
  background: none; border: 0; color: var(--text-muted); cursor: pointer;
  font-size: var(--text-sm); line-height: 1; padding: 0;
}
.dock__attach-remove:hover { color: var(--text); }

.dock__compose { flex: none; display: flex; gap: var(--space-2); padding: var(--space-2); border-top: 1px solid var(--border); position: relative; }
.dock__input {
  flex: 1; min-width: 0; resize: none;
  padding: var(--space-2) var(--space-3);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  font: inherit; font-size: var(--text-sm);
}

/* RF-56 — "Link a task". `createTaskPicker`'s own search box, in a small popover anchored to
   the compose form — its results/preview panels float against the viewport on their own
   (core/popover.js), so this only positions the search field itself. */
.dock__linktask { flex: none; align-self: flex-end; }
.dock__linktaskpop {
  position: absolute; z-index: 20; inset-inline: var(--space-2); bottom: 100%;
  margin-bottom: var(--space-1); padding: var(--space-2);
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.dock__linktaskpop[hidden] { display: none; }

.dock__toasts { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2); }
.dock__toast {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  font-size: var(--text-sm); color: var(--text-muted);
}

@media (max-width: 40rem) {
  /* A sheet across the bottom, not a floating card covering half a phone. */
  .dock { inset-inline: var(--space-2); inset-block-end: var(--space-2); align-items: stretch; }
  .dock__panel { width: 100%; max-height: 70vh; }
  .dock__pill { justify-content: center; }
}

/* ---------- toasts ----------
   Short-lived, bottom-left so they never sit under the people dock, and restrained: a toast
   that has to be dismissed is a toast that will be resented. */
.toasts {
  position: fixed;
  inset-block-end: var(--space-4);
  inset-inline-start: var(--space-4);
  z-index: 70;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: min(28rem, calc(100vw - var(--space-6)));
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--color-info);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  pointer-events: auto;
}
.toast--warn { border-inline-start-color: var(--color-warning); }
.toast--success { border-inline-start-color: var(--color-success, #10b981); }
.toast__text { margin: 0; font-size: var(--text-sm); }
.toast__dismiss {
  flex: none; background: none; border: 0; color: var(--text-muted);
  font-size: var(--text-md); line-height: 1; cursor: pointer; padding: 0 var(--space-1);
}
.toast__dismiss:hover { color: var(--text); }

@media (max-width: 40rem) {
  /* Above the dock's sheet, not underneath it. */
  .toasts { inset-inline: var(--space-2); inset-block-end: calc(var(--space-8) + var(--space-4)); max-width: none; }
}

/* You, in the people list: present, marked as you, and not something to click. */
.dock__self { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }

/* ---------- the features page ----------
   Grouped capability cards in one responsive grid. auto-fit decides how many fit, so it is
   several across on a wide desktop and one on a phone without a pixel width chosen to suit
   a single screen. Compact by construction: the point is that it scans. */
.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: var(--space-4);
  align-items: start;
}

.features__head { margin-bottom: var(--space-5); }

.features__group {
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  min-width: 0;
}

.features__grouphead { display: flex; gap: var(--space-3); align-items: flex-start; margin-bottom: var(--space-3); }
.features__icon {
  flex: none; width: 1.75rem; height: 1.75rem; display: grid; place-items: center;
  border-radius: var(--radius); background: var(--accent-soft, var(--surface-sunken));
  color: var(--accent); font-size: var(--text-sm); line-height: 1;
}
.features__grouptitle { margin: 0; font-size: var(--text-md); font-weight: var(--weight-bold); }
.features__groupsummary { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--text-muted); }

.features__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.features__item { padding-top: var(--space-3); border-top: 1px solid var(--border); }
.features__title { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-medium); }
.features__detail { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-normal); }
.features__go { margin-top: var(--space-2); }

/* Always reachable, never shouting. */
.footer__link { color: var(--text-muted); text-decoration: none; font-size: var(--text-sm); }
.footer__link:hover { color: var(--text); text-decoration: underline; }

/* ---------- the delivery history ----------
   The name leads and the address follows on its own line, always. A row that shows only a
   display name cannot answer "which address did this go to", which is the question a
   delivery failure actually raises. */
.delivery__who { line-height: var(--leading-snug); }
.delivery__name { display: block; }
.delivery__address { display: block; font-size: var(--text-xs); color: var(--text-muted); word-break: break-all; }

/* COMPACT TO SCAN, COMPLETE WHEN OPENED. Five columns carry the answer to "what happened, to
   whom, on which channel, how did it end"; the eleven diagnostic facts are one keystroke away
   on EVERY row — including the accepted ones, which are the rows people actually argue about.
   The toggle is the When cell, so the control costs no column of its own. */
.delivery__toggle {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 0;
  font: inherit;
  font-size: var(--text-xs);
  color: var(--text);
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
}
.delivery__toggle:hover { color: var(--accent); }
.delivery__caret { color: var(--text-subtle); }

.delivery__detailrow > td {
  padding-top: 0;
  background: var(--surface-sunken);
}

.delivery__facts {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
}
.delivery__facts dt { color: var(--text-subtle); white-space: nowrap; }
.delivery__facts dd { margin: 0; word-break: break-word; }

/* ---------- the colour picker ----------
   A button the size of a button, showing the colour AND its name. What it replaces was a
   thin sliver of native <input type="color"> that said nothing about being clickable.
   Every colour arrives as --swatch through the CSSOM, never as a style attribute. */
.colorpick { position: relative; display: inline-block; }

.colorpick__trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  min-width: 10rem;
  text-align: left;
}
.colorpick__trigger:hover { border-color: var(--border-strong); }
.colorpick__trigger:disabled { opacity: 0.55; cursor: not-allowed; }

/* The swatch is the point. It is a real square, not an edge. */
.colorpick__swatch {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--radius-sm);
  background: var(--swatch, var(--border-strong));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18);
}
.colorpick__name { flex: 1; }
.colorpick__caret { color: var(--text-subtle); font-size: var(--text-xs); }

/* OVER THE PAGE, NOT INSIDE THE FIELD. The panel is attached to the body while it is open,
   so it escapes .modal__body — which scrolls, and which was both clipping the palette and
   growing to contain it, making the dialog jump the moment somebody opened a colour.
   Fixed, because the coordinates are measured against the viewport. The position arrives as
   custom properties through the CSSOM, so a panel placed at runtime still needs no style
   attribute. The fallbacks keep it usable anywhere the coordinates cannot be measured. */
.colorpick__panel {
  position: fixed;
  z-index: 1200;
  top: var(--pop-top, 50%);
  left: var(--pop-left, 50%);
  max-height: var(--pop-max, min(24rem, calc(100vh - 2rem)));
  overflow-y: auto;
  padding: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md, 0 8px 24px rgb(0 0 0 / 0.18));
}
.colorpick__panel[hidden] { display: none; }

/* THE FAMILY SELECTOR, above the swatches. A real <select>, so it is native on a phone and
   reachable by keyboard without this stylesheet having to reinvent a menu. */
.colorpick__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.colorpick__palettelabel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-subtle);
}
.colorpick__palette {
  padding: var(--space-1) var(--space-2);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
/* UNDERSTATED ON PURPOSE. Adopting a palette is a deliberate act, not the obvious next step
   in choosing one category's colour, so it reads as a link rather than competing with the
   swatches for attention. */
.colorpick__setdefault {
  padding: 0;
  font: inherit;
  font-size: var(--text-xs);
  color: var(--accent);
  background: none;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}
.colorpick__setdefault:disabled { opacity: 0.55; cursor: not-allowed; }

.colorpick__grid { display: grid; grid-template-columns: repeat(6, 1.75rem); gap: var(--space-2); }

/* THE COLOUR IN WORDS. A ring around a swatch is not a signal everybody can see, and
   "Selected: Soft Blue · Pastel" is the answer the control exists to give. */
.colorpick__selected {
  margin: var(--space-3) 0 0;
  font-size: var(--text-xs);
  color: var(--text-subtle);
}
.colorpick__option {
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 2px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--swatch);
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18);
}
.colorpick__option:hover { transform: scale(1.08); }
/* The chosen one is marked by a RING, not only by being slightly larger — a size
   difference is not a signal somebody can rely on. */
.colorpick__option--chosen { border-color: var(--text); outline: 2px solid var(--surface); outline-offset: -4px; }

.colorpick__customrow { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-3); }
.colorpick__customlabel { font-size: var(--text-xs); color: var(--text-subtle); }
.colorpick__custom {
  width: 7rem;
  padding: var(--space-1) var(--space-2);
  font: inherit;
  font-size: var(--text-sm);
  font-family: var(--font-mono, monospace);
  color: var(--text);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* ---------- category identity on a task card ----------
   ONE MEANING PER ELEMENT. The card's colour is its CATEGORY and nothing else — priority
   stays a separate chip, the assignee stays a separate badge, the title stays the loudest
   thing on the card. A card flood-filled with a saturated colour would take that meaning
   away from all three.

   RESTRAINT BY CONSTRUCTION: a 3px edge at full strength, and a wash of the same colour at
   ~7% behind the card. `color-mix` keeps the wash relative to the card's own surface, so it
   is a tint in a light theme and a shade in a dark one without a second set of colours. */
.card--categorized {
  border-left: 3px solid var(--category-color, var(--border-strong));
  /* THE WHOLE CARD IS ONE COLOUR.
     The wash is published as a custom property so everything INSIDE the card can derive from
     it. That is the defect this fixes: nested controls kept the global sunken surface, so at
     any real strength a coloured card had near-black boxes cut into it.
     The strength comes from the CATEGORY, per card. The 7% fallback is what this treatment
     has always used, so a category with none recorded looks exactly as it always did. */
  --card-wash: color-mix(in srgb, var(--category-color, transparent) var(--category-strength, 7%), var(--surface));
  background: var(--card-wash);
  /* Computed against the category's own luminance when the wash is strong enough for the
     category to be what the text sits on. Below that the theme's colour is already right. */
  color: var(--card-ink, var(--text));
}

.card__category {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
/* CATEGORY IS NEVER COLOUR ALONE. The dot is the colour; the name beside it is what makes
   the card readable to somebody who cannot tell the dots apart. */
.card__categorydot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--category-color, var(--border-strong));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.2);
}

/* ---------------------------------------------------------------------------
   THE PALETTE PREVIEW in My settings.
   A dropdown of family NAMES asks somebody to choose between "Soft" and "Muted" from two
   words. The colours are the whole decision, so they are on the page beside the control —
   and named underneath, because colour is never the only identity.
   --------------------------------------------------------------------------- */

.palettepreview { margin-top: var(--space-3); }
.palettepreview__swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.palettepreview__swatch {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--radius-sm);
  background: var(--swatch);
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18);
}
.palettepreview__names {
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
  color: var(--text-subtle);
}

/* ---------------------------------------------------------------------------
   A SELECT YOU CAN ADD TO.
   The offer to create one is the last OPTION in the list — where somebody who cannot find
   their project is already looking — rather than a button beside the field. Choosing it opens
   a dialog over the form, which is why nothing here needs to make room for an inline row.
   --------------------------------------------------------------------------- */

.addable__status {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  color: var(--text-subtle);
}

/* ---------------------------------------------------------------------------
   THE CATEGORY COLOUR STRENGTH CONTROL.
   The preview is the REAL card treatment with real content on it, because the question the
   cap exists for — is the title still readable — cannot be answered by a swatch.
   --------------------------------------------------------------------------- */

.strength__row { display: flex; align-items: center; gap: var(--space-3); }
.strength__slider { flex: 1; min-width: 0; }
.strength__value {
  min-width: 3rem;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  text-align: right;
}
.strength__preview { margin-top: var(--space-3); max-width: 22rem; }
.strength__card { pointer-events: none; }
.strength__caution {
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs);
  color: var(--warn-text, var(--text-muted));
}

/* ---------------------------------------------------------------------------
   IMPORTANCE.
   An OUTLINE, and deliberately not a background or a left edge: the card's colour already
   means its category and the chip already means its priority. A third fact needs a third
   device, or one of them loses its meaning.
   --------------------------------------------------------------------------- */

.card--important {
  outline: 2px solid var(--important);
  outline-offset: -1px;
}
.row--important > td:first-child { box-shadow: inset 3px 0 0 var(--important); }
.row--important > td { background: color-mix(in srgb, var(--important) 6%, transparent); }

/* NEVER COLOUR ALONE — the word, for anybody who cannot make out the outline. */
.chip--important {
  color: var(--important-ink, var(--important));
  background: color-mix(in srgb, var(--important) 14%, transparent);
  border-color: color-mix(in srgb, var(--important) 40%, transparent);
}

/* ---------------------------------------------------------------------------
   EVERYTHING INSIDE A COLOURED CARD IS THE SAME COLOUR AS THE CARD.
   Reported: "the fields in the cards on the task list show black background." The inline
   controls kept var(--surface-sunken) — nearly black in a dark theme — so a washed card had
   boxes cut into it. Each nested surface is a nudge away from the card's OWN wash instead, so
   the card reads as one object at any strength.
   --------------------------------------------------------------------------- */

.card--categorized .card__status {
  color: inherit;
  background: color-mix(in srgb, var(--card-wash) 88%, var(--border-strong));
  border-color: color-mix(in srgb, var(--card-wash) 60%, var(--border-strong));
}
.card--categorized .card__open { color: inherit; }
.card--categorized .card__category,
.card--categorized .card__meta { color: inherit; }
/* The chips keep their own meaning — priority is priority — but sit on the card's colour
   rather than on a surface that is no longer behind them. */
.card--categorized .chip {
  background: color-mix(in srgb, var(--card-wash) 82%, var(--border-strong));
  border-color: color-mix(in srgb, var(--card-wash) 55%, var(--border-strong));
}

/* ---------------------------------------------------------------------------
   THE CATEGORY SLIVER ON THE TASK LIST.
   The same fact the Kanban card's coloured edge reports, so it is the same device: a thin
   edge in the category's own colour, beside the task's NAME. No wash — a colour behind a
   dense table of text is a different judgement from a colour on a card, and what was asked
   for was the sliver.
   --------------------------------------------------------------------------- */

.table__titlecell { position: relative; }
.table__titlecell--categorized { box-shadow: inset 3px 0 0 var(--category-color, var(--border-strong)); padding-left: var(--space-3); }
.cell__category { color: var(--text-muted); }

/* ---------------------------------------------------------------------------
   ASSIGNEE AVATARS.
   Compact circles of initials beside the task title, because a card is not wide enough for a
   name and nowhere near wide enough for an address. Three faces then a count; every full name
   lives in the stack's accessible name, so the compression is lossless somewhere.
   --------------------------------------------------------------------------- */

.avatars { display: inline-flex; flex: none; align-items: center; }
/* Overlapped slightly, so four circles cost less than four circles' width. */
.avatars .avatar + .avatar, .avatars__more { margin-left: -0.35rem; }
.avatar, .avatars__more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.35rem;
  height: 1.35rem;
  border-radius: 50%;
  font-size: 0.62rem;
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
  color: var(--accent-contrast, #fff);
  background: var(--accent);
  /* The ring separates overlapping circles from each other and from the card beneath. */
  box-shadow: 0 0 0 2px var(--card-wash, var(--surface));
}
.avatars__more { color: var(--text); background: var(--surface-sunken); }

/* ---------------------------------------------------------------------------
   THE ACCOUNT CONTROL (RB-12).
   `.menu__trigger` is deliberately compact (1.75rem) everywhere else — a quiet per-row
   action that must not compete with the row's own content. Here it sits in the header beside
   the bell and theme buttons, which are `.btn`-sized (a 2.25rem touch target), so it is sized
   up to match its actual neighbours rather than by changing the shared rule six other callers
   depend on.
   --------------------------------------------------------------------------- */

.account__trigger { min-width: 2.25rem; height: 2.25rem; }
.avatar--account { width: 1.75rem; height: 1.75rem; font-size: 0.72rem; }

/* ---------------------------------------------------------------------------
   PRIORITY.
   COLOUR + SHAPE + LABEL, and the shape is the part that survives everything. The colouring
   used to be keyed on the literal ids `urgent`/`high`/`medium`, so a workspace that renamed
   its priorities got no colour at all and nobody found out. The level comes from the
   workspace's own weights now, and the stylesheet colours by level.
   --------------------------------------------------------------------------- */

.prio { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); }
.prio__bars { display: inline-flex; align-items: flex-end; gap: 1px; height: 0.7rem; }
.prio__bar {
  width: 3px;
  height: 100%;
  border: 1px solid var(--prio-color, var(--text-subtle));
  border-radius: 1px;
  background: transparent;
}
/* FILLED IS THE SIGNAL. Four bars always render, so "one" and "one of four" cannot look the
   same, and an empty bar is an outline rather than a gap. */
.prio__bar--on { background: var(--prio-color, var(--text-subtle)); }
.prio__label { color: var(--prio-color, var(--text-muted)); font-weight: var(--weight-medium); }

.prio[data-level="1"] { --prio-color: var(--text-subtle); }
.prio[data-level="2"] { --prio-color: var(--text-muted); }
.prio[data-level="3"] { --prio-color: var(--color-warning); }
.prio[data-level="4"] { --prio-color: var(--color-danger); }

/* PRIORITY STAYS READABLE ON A WASHED CARD.
   REPORTED: at stronger category strengths the card's own text adapts but the priority
   indicator washes out. The cause is that the four levels above are coloured from PAGE-level
   tokens — --text-subtle, --text-muted, --color-warning, --color-danger — which are chosen to
   read against the PAGE background. A categorized card is not the page: it is the page tinted
   by an arbitrary category colour at up to 80%, and --card-ink exists precisely because the
   right text colour there has to be computed.

   ONE MEANING PER MECHANISM IS PRESERVED. The card's colour is still its CATEGORY and the
   bars are still its PRIORITY. What changes is only how each level survives the wash:

     * the two QUIET levels are quiet TEXT, and quiet text on this card is --card-ink. They
       follow it, so they stay legible at any strength in any theme instead of fading into
       the colour;
     * the two LOUD levels KEEP THEIR HUE, because the hue is the meaning. They are given the
       same nudge-plate the chips beside them already use, so the hue never has to win a
       contrast fight with a colour nobody chose for it. */
.card--categorized .prio[data-level="1"] { --prio-color: color-mix(in srgb, var(--card-ink, currentColor) 72%, transparent); }
.card--categorized .prio[data-level="2"] { --prio-color: var(--card-ink, currentColor); }
.card--categorized .prio[data-level="3"],
.card--categorized .prio[data-level="4"] {
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--card-wash) 82%, var(--border-strong));
}
/* An empty bar is an outline, and it has to remain visible on the wash too. */
.card--categorized .prio__bar { border-color: color-mix(in srgb, var(--prio-color, var(--card-ink, currentColor)) 60%, transparent); }

@media (forced-colors: active) {
  /* Author colour is discarded here, which is exactly where a colour-only signal disappears.
     The bars are borders, so the rank is still countable. */
  .prio__bar { border-color: CanvasText; }
  .prio__bar--on { background: CanvasText; }
  .prio__label { color: CanvasText; }
}

/* ---------------------------------------------------------------------------
   AN EDIT CONFLICT IS A DECISION, NOT AN ERROR.
   Somebody else changed the thing you were editing. Nobody did anything wrong, so this does not
   look like a failure — it looks like a question, with the answers beside it and the form still
   holding everything that was typed.
   --------------------------------------------------------------------------- */

.modal__conflict {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-warning);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}
.modal__conflict[hidden] { display: none; }
.modal__conflictsay { margin: 0; font-size: var(--text-sm); }
.modal__conflictactions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }

/* ============================================================
   TASK FORM LAYOUTS — five arrangements of one form.

   THE BUG ALL THREE BROKEN LAYOUTS SHARED. `.modal__tabpanel` is a
   THREE-COLUMN GRID, and the field width modifiers are grid rules
   written for it: `field--grow` is `grid-column: 1 / -2`,
   `field--flag` is `grid-column: -2 / -1; align-self: end`, and
   `field--narrow` caps a field at 11rem. Reparenting those fields
   into a new container left both layout systems running at once —
   the two-column split became one cell of the old grid a third of
   the dialog wide, the guided rail rendered above the panel instead
   of beside it, and in every non-grid layout `align-self: end` threw
   Important to the far right on a row of its own.

   So each layout OWNS its arrangement, and the modifiers are reset
   wherever the grid they were written for no longer exists.
   ============================================================ */

/* THE RESET. Scoped to the layouts, so the tabbed form the modifiers were written for keeps
   them exactly as they were. Anything reparented takes the width of the column it is in. */
.tf-pop .field,
.tf-ledger .field,
.tf-step__panel .field,
.tf-strip .field,
.tf-sheet .field {
  grid-column: auto;
  max-width: none;
  align-self: stretch;
}

/* Help text is a whole sentence and must never set a column's width. */
.tf-ledger .field__help,
.tf-step__panel .field__help,
.tf-sheet .field__help { max-width: 44ch; }

/* ---- 01 · command bar ---------------------------------------
   THE TASK'S OWN WORDS, THEN WHAT IS TRUE ABOUT IT.

   Height is independent of field count, and — the part the first
   attempt missed — so is the amount somebody has to READ. The title
   leads, the description is one line until it is wanted, and the rest
   is two short rails of chips that show their VALUES. A chip that is
   set looks different from one that is not, so the rail doubles as a
   summary of the task rather than as a printout of its schema.

   Nothing here reserves space for something that is not there. */
.modal__body--command { display: flex; flex-direction: column; gap: var(--space-3); }
.modal__body--command > .field { grid-column: auto; max-width: none; }

/* THE TITLE IS THE THING BEING CREATED, not Field #1.
   Borderless with a single heavy rule under it — the focus ring is
   untouched, the label is still there for assistive technology, and
   `required` still drives the submit button exactly as before. */
.tf-cmd__title { display: block; }
.tf-cmd__title .field__label {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.tf-cmd__title .field__input {
  width: 100%;
  padding: var(--space-1) 0 var(--space-2);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  letter-spacing: -.02em;
  line-height: var(--leading-tight);
  color: var(--text);
  background: none;
  border: 0;
  border-bottom: 2px solid var(--text);
  border-radius: 0;
}
.tf-cmd__title .field__input::placeholder { color: var(--text-subtle); font-weight: var(--weight-normal); }
.tf-cmd__title .field__input:focus-visible { border-bottom-color: var(--accent); }

/* THE DESCRIPTION COSTS ONE LINE UNTIL IT HAS SOMETHING TO SAY. */
.tf-cmd__descblock { display: block; }
.tf-cmd__descopen {
  display: block; width: 100%; text-align: left;
  font: inherit; font-size: var(--text-sm);
  color: var(--text-subtle);
  background: none; border: 0; border-radius: var(--radius-sm);
  padding: var(--space-1) 0;
  cursor: text;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tf-cmd__descopen:hover { color: var(--text-muted); }
.tf-cmd__descopen--set { color: var(--text-muted); }
.tf-cmd__desc[hidden] { display: none; }
.tf-cmd__desc .field__label { font-size: var(--text-xs); color: var(--text-subtle); }

/* THE RAILS. Two rows, wrapping — state first, then what is most
   likely to be reached for next. */
.tf-cmd__rail { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.tf-cmd__rail:empty { display: none; }
.tf-cmd__rail--secondary { padding-top: var(--space-1); }

/* WHAT THE DOOR OPENS, in its own area rather than mixed back into the rails.
   Dropping five more chips into the middle of a rail somebody has already
   learned re-orders it under their cursor and leaves no way to tell what was
   always there from what has just arrived. The rule and the inset say plainly:
   this is a separate, less common set. */
.tf-cmd__rail--more {
  margin-top: var(--space-1);
  padding: var(--space-3) 0 var(--space-1) var(--space-3);
  border-top: 1px solid var(--border);
  border-left: 2px solid var(--border);
}
.tf-cmd__rail--more[hidden] { display: none; }
.tf-cmd__slot { position: relative; display: inline-flex; }
.tf-cmd__slot[hidden] { display: none; }
/* A toggle chip IS its checkbox. The field still exists and is still
   read; it simply has no panel, so it is never shown. */
.tf-cmd__silent { display: none; }

.tf-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font: inherit; font-size: var(--text-xs); font-weight: var(--weight-medium);
  color: var(--text-muted); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
  cursor: pointer; white-space: nowrap;
  max-width: 16rem;
}
.tf-chip__text { overflow: hidden; text-overflow: ellipsis; }
.tf-chip:hover { border-color: var(--border-strong); color: var(--text); }

/* SET LOOKS DIFFERENT FROM UNSET. That difference is the whole reason
   the rail can be read instead of parsed. */
.tf-chip--set {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: var(--weight-bold);
}
.tf-chip--set:hover { border-color: var(--accent-hover); color: var(--accent-hover); }

/* A FLAG IS NOT JUST ANOTHER VALUE. Important and Urgent use the
   importance colour the table rows and board cards already use. */
.tf-chip--flag {
  border-color: color-mix(in srgb, var(--important) 45%, transparent);
  background: color-mix(in srgb, var(--important) 14%, transparent);
  color: var(--important-ink, var(--important));
  font-weight: var(--weight-bold);
}
.tf-chip--flag:hover { border-color: var(--important); color: var(--important-ink, var(--important)); }

/* The door to the rest. Dashed, because it opens something rather
   than stating something. */
.tf-chip--add { border-style: dashed; }

.tf-chip__dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--dot, currentColor); flex: none; }
/* Inside a chip the avatars carry no card behind them, so the ring
   that separates them is the chip's own background. */
.tf-chip .avatar, .tf-chip .avatars__more {
  width: 1.15rem; height: 1.15rem; font-size: 0.55rem;
  box-shadow: 0 0 0 2px var(--accent-soft, var(--surface));
}

/* WHAT A CHIP OPENS.
   Fixed, so it is measured against the viewport and neither clipped by
   the dialog's scrolling panel nor added to its scroll height. It stays
   INSIDE the dialog in the DOM — a panel on the body would fall out of
   the modal's tab trap and out of its inert handling. */
.tf-pop {
  position: fixed; z-index: 1300;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  width: max-content; min-width: 15rem;
  max-width: min(26rem, calc(100vw - 1rem));
  max-height: var(--pop-max, min(22rem, calc(100vh - 2rem)));
  overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--space-4);
  padding: var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.tf-pop[hidden] { display: none; }
/* A SPANNING PANEL IS NOT POINTING AT ANYTHING. It is centred on its anchor and may cover it, so
   it reads as a panel over the form rather than as a dropdown from the chip — which is what it is
   at that size. */
.tf-pop[data-placement="span"] { box-shadow: var(--shadow-lg, var(--shadow)); }

/* RF-20260824-15 (EXTENDED) — THE ROOMY VARIANT, for a panel holding a whole workflow rather
   than a single control. The recurrence rule is seven fields and a live summary sentence; at
   the shared 26rem x 22rem it could not show the pattern and its end condition at once.

   BOTH LIMITS STILL DEFER TO THE VIEWPORT. `min()` against `100vw`/`100vh` means the panel can
   never exceed the screen — it takes the room a desktop has and gives it back on a phone, which
   is what keeps the header and the primary actions reachable rather than pushed off-screen.
   Deliberately not fixed pixel dimensions: those work on exactly one display. */
.tf-pop--roomy {
  min-width: min(30rem, calc(100vw - 2rem));
  max-width: min(40rem, calc(100vw - 1rem));
  /* RF-20260827-12 — THE FALLBACK IS DOUBLED, AND IT IS ONLY A FALLBACK.
     `--pop-max` is the real constraint and is computed from the room actually available (see
     `core/popover.js`); this value applies for the instant before the first measurement and
     anywhere there is no layout to measure. It was 34rem, which is the half-a-viewport figure the
     report is about, so the fallback matched a cap that no longer applies. Still `min()` against
     the viewport: a fixed height is right on exactly one display. */
  max-height: var(--pop-max, min(68rem, calc(100vh - 3rem)));
}

/* Two columns once there is genuinely room for two, so the rule reads as pairs — pattern with
   its interval, the two ways a series ends — instead of one tall column of seven rows. */
@media (min-width: 48rem) {
  .tf-pop--roomy { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
  .tf-pop--roomy > .field--full,
  .tf-pop--roomy > .tf-pop__summary { grid-column: 1 / -1; }
}
.tf-pop .field__help { max-width: 34ch; }
.tf-pop .field__input { width: 100%; }

/* ---- 02 · two columns ---------------------------------------
   The split OWNS the panel — the panel is a plain block here, so
   there is only one layout system arranging the fields. */
.modal__body--ledger .modal__tabpanel { display: block; }
.modal__body--ledger .modal__tabpanel--hidden { display: none; }

.tf-ledger {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  gap: var(--space-4) var(--space-5);
  align-items: start;
}
.tf-ledger__content, .tf-ledger__meta { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
/* The title still leads and Important still rides on its row — a deliberate pairing, not an
   accident of the grid it used to live in. */
.tf-ledger__content .field--flag { align-self: flex-start; }
/* The disclosure toggle and its fields span both columns, beneath them. */
.tf-ledger > .modal__more, .tf-ledger > .modal__advanced { grid-column: 1 / -1; }
.tf-ledger > .modal__advanced { display: flex; flex-direction: column; gap: var(--space-4); }
@media (max-width: 52rem) { .tf-ledger { grid-template-columns: 1fr; } }

/* ---- 03 · guided steps --------------------------------------
   THE RAIL SITS BESIDE THE FORM, NOT ABOVE IT. The dialog stacks
   its children, so a vertical rail rendered as a full-width strip
   above the panel — which is a tab strip wearing a rail's clothes.
   The form becomes a two-column grid: rail down the left for the
   whole height, everything else in the right column. */
.modal__form--rail { display: grid; grid-template-columns: 10rem minmax(0, 1fr); column-gap: var(--space-5); }
.modal__form--rail > * { grid-column: 2; }
.modal__form--rail > .tf-rail { grid-column: 1; grid-row: 1 / -1; }

.tf-rail { display: flex; flex-direction: column; gap: 2px; align-self: start; }
.tf-step {
  display: flex; align-items: baseline; gap: var(--space-2);
  font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer;
  color: var(--text-muted); background: none; border: 0;
  border-left: 2px solid transparent; padding: var(--space-2) var(--space-3);
}
.tf-step:hover { color: var(--text); }
.tf-step--active {
  color: var(--text); font-weight: var(--weight-bold);
  border-left-color: var(--color-info); background: var(--surface-sunken);
}
.tf-step__num { font-size: var(--text-xs); color: var(--text-subtle); font-variant-numeric: tabular-nums; }

/* Two even columns, so paired fields sit together instead of one per row. */
.tf-step__panel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); align-items: start; }
.tf-step__panel > .field--grow, .tf-step__panel > .field--full { grid-column: 1 / -1; }
.tf-step__panel > .field[data-field="description"] { grid-column: 1 / -1; }
.tf-step__panel > .field--flag, .tf-step__panel > .field--check { align-self: end; }
@media (max-width: 46rem) {
  .modal__form--rail { grid-template-columns: 1fr; }
  .modal__form--rail > *, .modal__form--rail > .tf-rail { grid-column: 1; grid-row: auto; }
  .tf-rail { flex-direction: row; flex-wrap: wrap; }
  .tf-step__panel { grid-template-columns: 1fr; }
}

/* ---- 04 · sentence ------------------------------------------
   Fields flow inline, each introduced by a connecting word. THE
   WORD AND ITS FIELD ARE ONE UNIT — they were separate flex items,
   so the line wrapped between them and left "for" stranded at the
   end of a row with its field on the next. And a field's help text
   is a sentence of its own: inside a sentence it is noise, so it is
   available on demand rather than printed inline. */
.tf-sentence {
  margin: 0 0 var(--space-4); display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--space-3); font-size: var(--text-base);
}
.tf-sentence__slot { display: flex; align-items: baseline; gap: var(--space-2); margin: 0; min-width: 0; }
.tf-sentence__lead { color: var(--text-muted); white-space: nowrap; align-self: flex-end; padding-bottom: var(--space-2); }
.tf-sentence__slot .field { display: flex; flex-direction: column; gap: var(--space-1); }
.tf-sentence__slot .field__label { font-size: var(--text-xs); color: var(--text-subtle); margin: 0; }
.tf-sentence__slot .field__input { min-width: 8rem; }
.tf-sentence__slot[data-slot="title"] { flex: 1 1 18rem; }
.tf-sentence__slot[data-slot="title"] .field__input { width: 100%; font-size: var(--text-lg); font-weight: var(--weight-bold); }
/* Help text belongs to the field, and inside a sentence it breaks the line it belongs to. */
.tf-sentence__slot .field__help { display: none; }

.tf-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--border);
}
.tf-strip .field__help { max-width: 40ch; }

/* ---- 06 · full sheet ----------------------------------------
   TWELVE COLUMNS OF GROUPS — not of fields.

   THE DEFECT THIS REPLACES: the spans used to sit on the field cells,
   where `.tf-sheet .field { grid-column: auto }` above (0,2,0) beat
   `.tf-sheet__cell--3` (0,1,0) and auto-placed every one of twenty-six
   fields into a single column of twelve. One row, unreadable, with a
   horizontal scrollbar.

   The reset above is CORRECT and is deliberately left alone: a field
   may not choose its own width, which is what stops the stair-step.
   What was wrong was asking the same element to also carry the span.
   A group carries it now, the reset does not match a group, and the
   two rules no longer describe the same thing — so this is resolved by
   structure rather than by a longer selector winning a fight.

   `minmax(0, 1fr)` on every column and `min-width: 0` on every group
   is what makes horizontal overflow impossible: a long word, a wide
   select or a dependency list shrinks its group instead of pushing the
   grid past the dialog. */
.tf-sheet { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--space-5) var(--space-4); align-items: start; }

.tf-sheet__group { min-width: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.tf-sheet__group--4 { grid-column: span 4; }
.tf-sheet__group--6 { grid-column: span 6; }
.tf-sheet__group--12 { grid-column: span 12; }

/* The group's name. Subordinate to a field label on purpose — it
   organises the form, it is not another thing to fill in. */
.tf-sheet__legend {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-subtle);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--border);
}

/* THE SIX THINGS A TASK IS, ON ONE SCREEN.
 *
 * A CELL IS THE GRID ITEM AND THE FIELD INSIDE IT IS NOT. That is the whole
 * trick: `.tf-sheet .field { grid-column: auto }` is more specific than any
 * per-cell span class and beat every one of them the first time spans were
 * attempted here. Spanning the WRAPPER means the two rules describe different
 * elements, so there is no contest to lose. */
.tf-sheet__cell { min-width: 0; }
.tf-sheet__cell--4 { grid-column: span 4; }
.tf-sheet__cell--6 { grid-column: span 6; }
.tf-sheet__cell--12 { grid-column: span 12; }

/* ONE DOOR, FULL WIDTH, UNDER EVERYTHING. The button sits on its own line so it
   reads as a change of altitude rather than as a seventh field. */
.tf-sheet__morewrap { display: flex; flex-direction: column; gap: var(--space-4); align-items: flex-start; }
.tf-sheet__more { flex: none; }
.tf-sheet__morebody {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--space-5) var(--space-4);
  align-items: start;
  width: 100%;
}
.tf-sheet__morebody[hidden] { display: none; }

/* FIELDS STACK INSIDE A GROUP. A grid in here would reintroduce exactly
   the raggedness the grouping exists to remove. */
.tf-sheet__fields { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }

/* (Help text is capped to 44ch by the shared reset above — a sentence
   must never set a column's width.) */

/* COLLAPSE, NEVER SCROLL. Thirds become halves, then full width. The
   twelve-column track is unchanged throughout, so groups stay aligned
   with each other at every size. */
@media (max-width: 60rem) {
  .tf-sheet__group--4 { grid-column: span 6; }
  .tf-sheet__cell--4 { grid-column: span 6; }
}
@media (max-width: 44rem) {
  .tf-sheet__group--4, .tf-sheet__group--6 { grid-column: span 12; }
  .tf-sheet__cell--4, .tf-sheet__cell--6 { grid-column: span 12; }
}

/* ---------------------------------------------------------------------------
   THE COMMAND PICKER — a search-first palette over a native <select>.

   THIS STYLESHEET WAS MISSING. `commandpicker.js` shipped at beta.43 emitting
   a dozen `cmdpick__*` class names, and not one of them existed in any
   stylesheet — so Status rendered as an unstyled trigger sitting NEXT TO the
   raw native select it was supposed to replace, in production. The JavaScript
   was tested and correct; nothing tested that the classes it emits mean
   anything, because the harness has no cascade.

   `.cmdpick__native` is the load-bearing one: the select stays in the DOCUMENT
   holding the value, and must be gone from SIGHT. It is hidden the accessible
   way — the trigger carries the accessible name and the palette is a real
   listbox — rather than with `display:none` on a control the form still reads.
   --------------------------------------------------------------------------- */
.cmdpick { position: relative; display: block; min-width: 0; }

/* Present to the form, absent from the page. Not `display:none`: the select is
   still the value holder and is still set programmatically. */
.cmdpick__native {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.cmdpick__trigger {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%;
  min-height: var(--control-height);
  padding: var(--control-padding-y) var(--control-padding-x);
  font: inherit; font-size: var(--control-font-size);
  text-align: left;
  color: var(--text);
  background: var(--control-surface);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  cursor: pointer;
}
.cmdpick__trigger:hover { border-color: var(--control-border-hover); }
.cmdpick__trigger[aria-expanded="true"] { border-color: var(--control-border-active); }
.cmdpick__trigger:disabled { cursor: not-allowed; opacity: var(--control-disabled-opacity); }

.cmdpick__value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdpick--empty .cmdpick__value { color: var(--text-subtle); }
.cmdpick__hint { flex: none; color: var(--text-subtle); font-size: var(--text-xs); }

/* THE COLOUR EDGE, for the one thing that has a canonical colour. A category is
   recognised by its colour everywhere else in the product; a status has none and
   inventing one would be colour that means nothing. */
.cmdpick__swatch { display: none; }
.cmdpick--swatched .cmdpick__swatch {
  display: inline-block; flex: none;
  width: 0.6rem; height: 0.6rem; border-radius: 50%;
  background: var(--swatch, var(--accent));
}
.cmdpick--swatched .cmdpick__trigger { border-left: 3px solid var(--swatch, var(--accent)); }

/* The palette. Fixed and placed against the viewport by core/popover.js — inside
   `.modal__body` it would be clipped and would add to that box's scroll height. */
.cmdpick__panel {
  position: fixed; z-index: 1300;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  width: min(22rem, calc(100vw - 1rem));
  display: flex; flex-direction: column;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.cmdpick__panel[hidden] { display: none; }

/* NO SEARCH ROW, so the panel is narrower and the list starts at the top. A short enum
   vocabulary — status, priority, effort — is pointed at, not typed into; see
   commandpicker.js.
   THE LIST TAKES THE KEYBOARD in the search box's place, so it keeps the global
   :focus-visible ring rather than suppressing it: a keyboard user who opens this panel must
   be able to see where they are, and it is the only thing focused. */
.cmdpick__panel--nosearch { width: min(15rem, calc(100vw - 1rem)); }
.cmdpick__panel--nosearch .cmdpick__list { padding-top: var(--space-2); }

.cmdpick__searchrow {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.cmdpick__icon { color: var(--text-subtle); flex: none; }
.cmdpick__search {
  flex: 1; min-width: 0;
  font: inherit; font-size: var(--text-sm);
  color: var(--text); background: none; border: 0; padding: var(--space-1) 0;
}
.cmdpick__search::placeholder { color: var(--text-subtle); }

.cmdpick__list { max-height: 15rem; overflow-y: auto; padding: var(--space-1); }
.cmdpick__none { margin: 0; padding: var(--space-3); font-size: var(--text-sm); color: var(--text-muted); }

.cmdpick__opt {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  font-size: var(--text-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.cmdpick__opt--active { background: var(--surface-sunken); }
.cmdpick__opt--on { font-weight: var(--weight-bold); }
.cmdpick__optlabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.cmdpick__tick { flex: none; color: var(--accent); }
.cmdpick__dot { flex: none; width: 0.6rem; height: 0.6rem; border-radius: 50%; background: var(--swatch, var(--accent)); }
.cmdpick__opt--swatched { border-left: 3px solid var(--swatch, var(--accent)); }

/* UNAVAILABLE, AND SAYING SO. Shown rather than hidden, because an option that
   is silently absent teaches nobody why it is not there. Not a pointer, not a
   keyboard target, and the reason travels in its own label. */
.cmdpick__opt--off { cursor: not-allowed; color: var(--text-subtle); }
.cmdpick__opt--off .cmdpick__optlabel { text-decoration: none; }
.cmdpick__opt--off:hover { background: none; }

.cmdpick__foot {
  display: flex; gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--border);
  font-size: var(--text-xs); color: var(--text-subtle);
}
.cmdpick__kbd {
  font-family: var(--font-mono); font-size: 0.65rem;
  padding: 0 var(--space-1);
  border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: var(--radius-sm);
  color: var(--text-muted);
}
.cmdpick__create {
  display: block; width: 100%; text-align: left;
  font: inherit; font-size: var(--text-sm);
  padding: var(--space-2); margin-top: var(--space-1);
  color: var(--accent); background: none;
  border: 0; border-top: 1px solid var(--border); border-radius: 0;
  cursor: pointer;
}
.cmdpick__create:hover { background: var(--surface-sunken); }

/* ---------------------------------------------------------------------------
   THE TASK PICKER — dependencies, searched by title.

   Written WITH the component this time. The command picker shipped emitting a
   dozen class names that existed in no stylesheet, and its own test file now
   asks the question that would have caught it; so does this one.
   --------------------------------------------------------------------------- */
.taskpick { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }

/* THE SAME LANGUAGE AS THE TRIGGERS BESIDE IT. It is a search box rather than a trigger, so
   it keeps its own behaviour — but a field that is two pixels taller than its neighbour is
   the drift this set of tokens exists to stop. */
.taskpick__input {
  width: 100%; min-width: 0;
  min-height: var(--control-height);
  padding: var(--control-padding-y) var(--control-padding-x);
  font-size: var(--control-font-size);
  background: var(--control-surface);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
}
.taskpick__input:hover { border-color: var(--control-border-hover); }
.taskpick__input:disabled { opacity: var(--control-disabled-opacity); cursor: not-allowed; }
.taskpick__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.taskpick__chips:empty { display: none; }
.taskpick__chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  max-width: 100%;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  color: var(--text);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
}
.taskpick__chiptitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A dependency whose task is not in the candidate list. Kept, and visibly unresolved. */
.taskpick__chip--unknown { border-style: dashed; color: var(--text-muted); }
.taskpick__note { font-size: 0.65rem; color: var(--text-subtle); white-space: nowrap; }

.taskpick__peek, .taskpick__remove {
  flex: none;
  font: inherit; font-size: var(--text-xs); line-height: 1;
  padding: 0 var(--space-1);
  color: var(--text-subtle);
  background: none; border: 0; border-radius: var(--radius-sm);
  cursor: pointer;
}
.taskpick__peek:hover, .taskpick__remove:hover { color: var(--text); }
.taskpick__peek:disabled, .taskpick__remove:disabled { cursor: not-allowed; opacity: 0.5; }

/* The results, fixed and placed against the viewport by core/popover.js — inside
   the dialog's scrolling body they would be clipped and would add to its height. */
.taskpick__results {
  list-style: none; margin: 0; padding: var(--space-1);
  position: fixed; z-index: 1300;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  min-width: var(--pop-width, 20rem);
  max-width: min(32rem, calc(100vw - 1rem));
  max-height: var(--pop-max, 16rem); overflow-y: auto;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.taskpick__results[hidden] { display: none; }

.taskpick__result {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.taskpick__result--active { background: var(--surface-sunken); }
.taskpick__title { font-size: var(--text-sm); color: var(--text); overflow: hidden; text-overflow: ellipsis; }
/* SECONDARY, and subordinate to the title on purpose — it is what tells two
   similar titles apart, not what somebody is scanning for. */
.taskpick__meta { grid-column: 1; font-size: var(--text-xs); color: var(--text-subtle); }
.taskpick__result .taskpick__peek { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
.taskpick__none { padding: var(--space-3); font-size: var(--text-sm); color: var(--text-muted); }

/* The read-only summary. A panel over the field — never a navigation, because
   the draft underneath is the whole reason somebody is here. */
.taskpick__preview {
  position: fixed; z-index: 1310;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  width: min(24rem, calc(100vw - 1rem));
  max-height: var(--pop-max, 20rem); overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.taskpick__preview[hidden] { display: none; }
.taskpick__previewtitle { margin: 0; font-size: var(--text-base); font-weight: var(--weight-bold); }
.taskpick__facts { display: grid; grid-template-columns: auto 1fr; gap: var(--space-1) var(--space-3); margin: 0; font-size: var(--text-sm); }
.taskpick__facts dt { color: var(--text-subtle); }
.taskpick__facts dd { margin: 0; color: var(--text); }
.taskpick__previewtext { margin: 0; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-normal); }
.taskpick__previewclose { align-self: flex-start; }
.taskpick__status { margin: 0; }

/* ---------------------------------------------------------------------------
   THE DATE PICKER. Calendar dates, never instants.
   --------------------------------------------------------------------------- */
.datepick { position: relative; display: block; min-width: 0; }

/* Present to the form, absent from the page — the input is still the value
   holder, so it is clipped rather than display:none'd. */
.datepick__native {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.datepick__trigger {
  display: flex; align-items: center; gap: var(--space-2);
  width: 100%;
  min-height: var(--control-height);
  padding: var(--control-padding-y) var(--control-padding-x);
  font: inherit; font-size: var(--control-font-size);
  text-align: left;
  color: var(--text);
  background: var(--control-surface);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  cursor: pointer;
}
.datepick__trigger:hover { border-color: var(--control-border-hover); }
.datepick__trigger[aria-expanded="true"] { border-color: var(--accent); }
.datepick__trigger:disabled { cursor: not-allowed; opacity: 0.6; }
.datepick__icon { flex: none; color: var(--text-subtle); font-size: var(--text-xs); }
.datepick__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.datepick--empty .datepick__value { color: var(--text-subtle); }

.datepick__panel {
  position: fixed; z-index: 1300;
  top: var(--pop-top, 50%); left: var(--pop-left, 50%);
  width: max-content;
  padding: var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.datepick__panel[hidden] { display: none; }

.datepick__head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.datepick__month { flex: 1; margin: 0; text-align: center; font-size: var(--text-sm); font-weight: var(--weight-bold); }
.datepick__nav {
  flex: none;
  font: inherit; line-height: 1;
  width: 1.75rem; height: 1.75rem;
  color: var(--text-muted);
  background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer;
}
.datepick__nav:hover { color: var(--text); border-color: var(--border-strong); }

.datepick__grid { display: flex; flex-direction: column; gap: 2px; }
.datepick__row { display: grid; grid-template-columns: repeat(7, 2rem); gap: 2px; }
/* The weekday header row. Slightly separated, because it labels the grid rather
   than being part of it. */
.datepick__row--head { margin-bottom: var(--space-1); }
.datepick__dayname { display: grid; place-items: center; font-size: 0.65rem; color: var(--text-subtle); text-transform: uppercase; letter-spacing: 0.04em; }
.datepick__pad { display: block; }

.datepick__day {
  display: grid; place-items: center;
  height: 2rem;
  font: inherit; font-size: var(--text-xs);
  color: var(--text);
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.datepick__day:hover { background: var(--surface-sunken); }
/* TODAY IS MARKED BY AN OUTLINE, the chosen day by a fill — so a day that is
   both still reads as chosen, and neither depends on colour alone. */
.datepick__day--today { border-color: var(--border-strong); font-weight: var(--weight-bold); }
.datepick__day--chosen { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast, #fff); font-weight: var(--weight-bold); }
.datepick__day--active { outline: 2px solid var(--accent); outline-offset: -2px; }

.datepick__foot { display: flex; gap: var(--space-2); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--border); }
.datepick__action {
  font: inherit; font-size: var(--text-xs);
  padding: var(--space-1) var(--space-3);
  color: var(--text-muted);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer;
}
.datepick__action:hover { color: var(--text); border-color: var(--border-strong); }

/* The layout picker in My settings. */
.tf-pick { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--space-3); }
.tf-pick__option {
  display: flex; flex-direction: column; gap: var(--space-1); text-align: left;
  font: inherit; cursor: pointer; padding: var(--space-3);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text);
}
.tf-pick__option:hover { border-color: var(--border-strong); }
.tf-pick__option[aria-pressed="true"] { border-color: var(--color-info); border-width: 2px; background: var(--surface-sunken); }
.tf-pick__name { font-weight: var(--weight-bold); font-size: var(--text-sm); }
.tf-pick__tag { font-size: var(--text-xs); color: var(--color-info); }
.tf-pick__detail { font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-normal); }

/* ============================================================
   THE SIGN-IN SCREEN
   A heading, a sentence and two stacked buttons said nothing
   about what is behind them. The left half makes the case; the
   right signs somebody in. The decoration is a miniature of the
   real board, in the real visual language — the honest way to
   show what TaskTracker looks like is to show it.
   ============================================================ */
.state--auth { max-width: none; text-align: left; }

.signin {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: center;
  max-width: 66rem;
  margin: 0 auto;
  padding: var(--space-6) var(--space-4);
}
@media (max-width: 62rem) {
  .signin { grid-template-columns: 1fr; gap: var(--space-5); }
  .signin__board { display: none; }
}

.signin__eyebrow {
  margin: 0 0 var(--space-3);
  font-size: var(--text-xs);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--color-info);
}
.signin__title {
  margin: 0 0 var(--space-4);
  font-size: clamp(1.9rem, 4.2vw, 3rem);
  line-height: 1.06;
  letter-spacing: -.03em;
  font-weight: var(--weight-bold);
  text-wrap: balance;
  max-width: 16ch;
}
.signin__lede {
  margin: 0 0 var(--space-5);
  max-width: 46ch;
  font-size: var(--text-base);
  color: var(--text-muted);
  line-height: var(--leading-normal);
}

.signin__points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.signin__point { display: flex; gap: var(--space-3); align-items: flex-start; max-width: 48ch; }
.signin__tick {
  flex: none; width: 1.35rem; height: 1.35rem; border-radius: 50%;
  display: grid; place-items: center; font-size: var(--text-xs);
  color: var(--color-info); background: var(--surface-sunken);
  border: 1px solid var(--border);
}
.signin__pointtitle { display: block; font-size: var(--text-sm); font-weight: var(--weight-bold); }
.signin__pointdetail { display: block; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-normal); }

.signin__panel { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
.signin__card {
  padding: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.signin__cardtitle { margin: 0 0 var(--space-2); font-size: var(--text-lg); letter-spacing: -.015em; }
.signin__cardhint { margin: 0 0 var(--space-4); font-size: var(--text-sm); color: var(--text-muted); }
.signin__actions { display: flex; flex-direction: column; gap: var(--space-2); margin: 0; }
.signin__note {
  margin: var(--space-4) 0 0; padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-normal);
}
.signin__detail { margin: var(--space-2) 0 0; font-size: var(--text-xs); }

/* Separates the ordinary sign-in buttons from the lower-emphasis "Request access" entry
   point beneath them — a subtle rule with centered text, not a heavy section break. */
.signin__divider {
  display: flex; align-items: center; gap: var(--space-3);
  margin: var(--space-4) 0; color: var(--text-muted); font-size: var(--text-xs);
}
.signin__divider::before, .signin__divider::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}

/* The miniature board. Decorative, inert, and built from the same language the real one
   uses — a category edge, a priority bar, an assignee initial. */
.signin__board { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); }
.signin__col {
  padding: var(--space-3);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: flex; flex-direction: column; gap: var(--space-2);
  min-width: 0;
}
.signin__colname {
  margin: 0; font-size: var(--text-xs); letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-subtle);
}
.signin__mini {
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--category-color, var(--border-strong));
  border-radius: var(--radius-sm);
  display: flex; flex-direction: column; gap: var(--space-2);
}
.signin__minitop { display: flex; gap: var(--space-2); align-items: flex-start; }
.signin__face {
  flex: none; width: 1.1rem; height: 1.1rem; border-radius: 50%;
  display: grid; place-items: center;
  font-size: .6rem; font-weight: var(--weight-bold);
  color: var(--surface); background: var(--category-color, var(--border-strong));
}
.signin__minititle { font-size: var(--text-xs); line-height: 1.3; color: var(--text); }
.signin__bars { display: flex; gap: 2px; align-items: flex-end; height: .5rem; }
.signin__bar { width: 3px; height: 3px; border-radius: 1px; background: var(--border-strong); }
.signin__bar:nth-child(2) { height: 5px; }
.signin__bar:nth-child(3) { height: 7px; }
.signin__bar:nth-child(4) { height: 9px; }
.signin__bar--on { background: var(--category-color, var(--color-info)); }

/* Preview sits beside the card it previews. */
.tf-pick__cell { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.tf-pick__cell .tf-pick__option { flex: 1; }
.tf-pick__preview { align-self: flex-start; }

/* What is behind the hidden board columns. Quiet — it is an accounting note, not a warning:
   nothing is wrong, those tasks simply are not drawn here. */
.board__hidden { margin: var(--space-3) 0 0; font-size: var(--text-sm); color: var(--text-muted); }

/* ---------- the project → task tree ----------
   Connectors are drawn with real layout: a left border on each row, inset by depth. Subtle by
   intent — they are a reading aid beside the indentation and the accessible names, never the
   only thing carrying the relationship. Every colour is a token, so the tree follows the
   palette and both modes without a second definition. */
/* RF-15 — the whole padded area is the target, not just the arrow glyph. Matches the same
   ~1.75rem compact-control footprint `.menu__trigger` already established elsewhere, so a
   pointer or a touch has a real target instead of a few pixels of glyph — the row itself
   still names what will receive a click via `aria-label`, unaffected by this. */
.projects__disclosure {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  min-height: 1.75rem;
  background: none;
  border: 0;
  border-radius: var(--radius-sm, 4px);
  color: var(--text-muted);
  font: inherit;
  cursor: pointer;
  padding: 0;
  line-height: 1;
}
.projects__disclosure:hover { color: var(--text); background: var(--surface-sunken); }
.projects__disclosure:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.projects__subtree { grid-column: 1 / -1; margin-top: var(--space-2); }
.projects__empty { margin: 0 0 0 var(--space-4); font-size: var(--text-sm); }

/* RF-08 — BACKLOG GROUPED BY PROJECT. Reuses `.projects__disclosure` (above) for the
   expand/collapse control rather than a second implementation. */
.backlog__groups { display: flex; flex-direction: column; gap: var(--space-3); }
.backlog__group {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.backlog__grouphead { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); }
.backlog__groupname { font-weight: var(--weight-medium); }

.tasktree { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.tasktree__row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  /* The inset IS the hierarchy, and it is bounded so deep nesting cannot push a title off a
     narrow screen. */
  margin-inline-start: calc(var(--space-3) + min(var(--depth, 0) * var(--space-4), 12rem));
  border-inline-start: 1px solid var(--border);
  padding: var(--space-1) 0 var(--space-1) var(--space-3);
}
.tasktree__open {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--text);
  text-align: start;
  cursor: pointer;
  /* Long titles wrap rather than truncating: a task nobody can read the name of is a task
     nobody can find. */
  overflow-wrap: anywhere;
}
.tasktree__open:hover { color: var(--accent); }
.tasktree__open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tasktree__meta { flex: none; font-size: var(--text-xs); }

@media (max-width: 40rem) {
  /* Less inset per level when there is less room, so the relationship still reads without
     the titles being squeezed into a column of two words. */
  .tasktree__row { margin-inline-start: calc(var(--space-2) + min(var(--depth, 0) * var(--space-3), 6rem)); }
}

/* A deferred task included by `Not Yet Shown`. A note, not a disabled state: the task is real
   and editable, it simply is not due to appear yet. Dressing it as unavailable would say
   something untrue. */
.cell__deferred { color: var(--text-muted); font-style: italic; }

/* Subtask indentation in the task table. The inset is on the TITLE cell only, so every other
   column stays aligned down the table — indenting the whole row would turn a table into a
   staircase. Bounded, so deep nesting cannot push the title out of its column, and reduced at
   narrow widths where the room is not there. */
.table__row[data-depth] .cell--title { padding-inline-start: calc(var(--space-2) + min(var(--depth, 0) * var(--space-4), 8rem)); }
.table__row[data-depth]:not([data-depth="0"]) .cell--title { border-inline-start: 1px solid var(--border); }

@media (max-width: 60rem) {
  .table__row[data-depth] .cell--title { padding-inline-start: calc(var(--space-1) + min(var(--depth, 0) * var(--space-2), 3rem)); }
}

/* ---------- the Projects page ----------

   WHICH ROW THE "+" BELONGS TO HAS TO BE UNMISTAKABLE. A column of identical buttons where
   nothing says which item receives the new task is worse than no buttons at all: somebody
   files work under the wrong project and does not notice.

   So the highlight is on the ROW, and it responds to `:focus-within` as well as `:hover` —
   otherwise a keyboard user tabbing onto the "+" gets no indication of the target at all,
   which is precisely when they most need it. */

/* PRIORITY 8 — COMPACT HEADER, SCOPED TO THIS PAGE ONLY. `.view__header` and `.toolbar` are
   shared chrome Tasks/Backlog/Kanban all still use at their normal spacing; `.view--projects`
   is the page's own existing root modifier (already on the `<section>`), so tightening lives
   here rather than in the shared rules everyone else depends on. The list itself — what
   somebody is actually here to work in — is what the saved space gives back to. */
.view--projects .view__header { margin-bottom: var(--space-3); }
.view--projects .toolbar { padding: var(--space-3); }

.projects { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }

.projects__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.projects__row:hover,
.projects__row:focus-within {
  border-color: var(--accent);
  background: var(--surface-raised, var(--surface));
}

.projects__name { font-weight: var(--weight-bold); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.projects__count { color: var(--text-subtle); font-size: var(--text-xs); white-space: nowrap; }
.projects__project--archived .projects__name { color: var(--text-muted); }

/* RF-60 — a second line beneath `.projects__row`: the project's own description (never
   allowed to overflow the row — one line, ellipsised, the FULL text still reaches a screen
   reader/hover via its own `title`) and the same "most important info" — a due date — the
   Tasks table itself leads with. */
.projects__facts {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: 0 var(--space-3) var(--space-2);
  font-size: var(--text-xs); color: var(--text-subtle);
}
/* RF-20260825-04 — THE DESCRIPTION ON THE PROJECT'S OWN ROW.
   TRUNCATION IS VISUAL ONLY. `text-overflow: ellipsis` shortens what is DRAWN; the complete
   text stays in the DOM as a real text node, so a screen reader reads all of it. That is the
   whole reason this can satisfy both "on the row" and "must not overflow" without a tooltip
   carrying the only copy. `min-width: 0` is what actually lets a flex item shrink below its
   content — without it the row would widen instead of the text ellipsising. */
.projects__desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.projects__desc--inline {
  /* Shrinks first and hardest: the name, the count and the controls all keep their size, and
     the description gives up whatever room is needed. */
  flex: 1 1 6rem;
  color: var(--ink-2);
  font-size: var(--text-sm);
}
/* AND IN FULL ONCE THE PROJECT IS OPEN — no truncation, no ellipsis, no hovering. */
.projects__desc--full {
  white-space: pre-wrap;
  overflow: visible;
  text-overflow: clip;
  margin: 0 0 var(--space-2);
  color: var(--ink-2);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}
@media (max-width: 44rem) {
  /* On a narrow screen the row has no room to give, and half a word of description is worse
     than none — it is still one disclosure away, in full. */
  .projects__desc--inline { display: none; }
}
.projects__stat { flex: 0 0 auto; white-space: nowrap; }
.projects__stat--overdue { color: var(--color-danger); font-weight: var(--weight-medium); }

/* The add action sits at the far end of its own row, so its position says what it belongs to
   as clearly as its accessible name does. */
.projects__add,
.tasktree__add {
  margin-left: auto;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  line-height: 1;
  font-size: var(--text-sm);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.projects__add:hover,
.projects__add:focus-visible,
.tasktree__add:hover,
.tasktree__add:focus-visible { border-color: var(--accent); color: var(--text); }

/* A TASK ROW IS ITS OWN TARGET TOO, for the same reason and by the same rule. */
.tasktree__row:hover,
.tasktree__row:focus-within { background: var(--surface-raised, var(--surface)); }

@media (max-width: 40rem) {
  /* The count is the first thing to go; the name and the action are what the row is for. */
  .projects__count { display: none; }
}

/* PROJECT ADMINISTRATION SITS APART FROM THE "+".
   The "+" is frequent and low-consequence and keeps the far right of the row where it has
   always been. Renaming, archiving and deleting are rarer and heavier, so they read as their
   own set — a rule of separation rather than four adjacent buttons any of which might add a
   task. `margin-left: auto` moves to this group; the "+" then follows it. */
.projects__admin {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
}
.projects__admin + .projects__add { margin-left: var(--space-3); }

/* THE ACTIONS ARE QUIET UNTIL THE ROW IS THE ONE BEING USED. A dozen rows each showing four
   buttons is a wall; the row already highlights on hover and focus-within, so the actions
   follow the same signal. They remain in the DOM and in the tab order either way — hiding a
   control from a keyboard user is not restraint, it is removal. */
@media (hover: hover) and (min-width: 48rem) {
  .projects__admin { opacity: 0; transition: opacity 120ms ease; }
  .projects__row:hover .projects__admin,
  .projects__row:focus-within .projects__admin { opacity: 1; }
}

/* Narrow: the admin group wraps under the name rather than squeezing it out. */
@media (max-width: 48rem) {
  .projects__row { flex-wrap: wrap; }
  .projects__admin { margin-left: 0; width: 100%; padding-top: var(--space-2); }
  .projects__admin + .projects__add { margin-left: auto; }
}

/* ---------- the task-form field arrangement ---------- */

/* TWO GROUPS SIDE BY SIDE WHERE THERE IS ROOM, because the whole question this screen answers
   is "which side is this field on", and an answer you have to scroll between is harder to
   read than one you can see at once. Below the width where two columns would squeeze the
   labels, they stack — the grid does that itself, with no breakpoint to keep in step. */
.fieldconf {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-4);
}

.fieldconf__group {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  padding: var(--space-3);
  min-width: 0;
}

.fieldconf__grouphead { margin-bottom: var(--space-2); }

.fieldconf__grouptitle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
}

.fieldconf__count {
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  padding: 0 var(--space-2);
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-muted);
}

.fieldconf__grouphint {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.fieldconf__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* An emptied group must still be a drop target with somewhere to drop. */
  min-height: 2.5rem;
}

/* ONE COMPACT ROW: handle, name, quiet state, one placement control. The previous version
   spent a full-width button on "Move to +N more" and two more on the arrows, on every row, so
   twenty fields drew sixty buttons of equal weight and none of them read as more or less
   important than the field name beside it. */
.fieldconf__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--surface-raised, var(--surface));
  min-width: 0;
}

.fieldconf__item:hover,
.fieldconf__item:focus-within {
  border-color: var(--border);
}

/* B4 — THE INSERTION LINE. A visible edge, not a subtle background tint, on the side the
   dragged row will actually land: `border-top` for before, `border-bottom` for after. Using a
   real border rather than a background colour keeps the indicator legible at the same
   thickness in both themes and against a row that already has hover/focus styling. */
.fieldconf__item--dropbefore { border-top: 2px solid var(--accent); }
.fieldconf__item--dropafter { border-bottom: 2px solid var(--accent); }

/* B4 — WHICH GROUP RECEIVES IT. A dashed outline around the whole list currently under the
   pointer, distinct from the per-row insertion line above so the two questions ("where in the
   list" and "which list") stay visually separate rather than competing for the same border. */
.fieldconf__list--dropzone {
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.fieldconf__handle {
  flex: 0 0 auto;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: grab;
  padding: var(--space-1);
  line-height: 1;
}

.fieldconf__handle:hover:not(:disabled) { color: var(--text); }
.fieldconf__handle:active:not(:disabled) { cursor: grabbing; }
.fieldconf__handle:disabled { cursor: default; opacity: 0.4; }

.fieldconf__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
}

.fieldconf__note {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* THE PLACEMENT CONTROL, and the reason it is an arrow rather than a sentence: the sentence
   is already written at the top of the group it would move the field into. */
.fieldconf__place {
  flex: 0 0 auto;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  padding: 0 var(--space-2);
  line-height: 1.6;
}

.fieldconf__place:hover:not(:disabled) { color: var(--text); border-color: var(--border-strong); }
.fieldconf__place:disabled { cursor: default; opacity: 0.4; }

/* LOCKED SAYS SO IN WORDS. A button that cannot do anything is worse than no button, and a
   greyed-out one still invites the click that will not work. */
.fieldconf__lock {
  flex: 0 0 auto;
  font-size: var(--text-xs);
  color: var(--text-muted);
  padding: 0 var(--space-2);
  border-radius: 999px;
  border: 1px dashed var(--border);
  white-space: nowrap;
}

.fieldconf__empty {
  font-size: var(--text-xs);
  color: var(--text-muted);
  padding: var(--space-2);
}

.fieldconf__keys { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; }

.fieldconf__keys kbd {
  font-family: var(--font-mono, monospace);
  font-size: var(--text-xs);
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  padding: 0 var(--space-1);
  color: var(--text);
}

/* ---------- RF-38 — task form defaults ----------
   One compact row per DEFAULTABLE field: its name, then whichever control its type needs
   (select/checkbox/number/text). Deliberately its own class family rather than reusing
   .fieldconf__* — that family carries drag-handle machinery (.fieldconf__handle,
   .fieldconf__place, the drop-indicator classes) this panel has no use for; sharing it would
   mean every row here dragging along markup and CSS for a capability it does not offer. */
.taskdefaults__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.taskdefaults__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--surface-raised, var(--surface));
}

.taskdefaults__item:hover,
.taskdefaults__item:focus-within {
  border-color: var(--border);
}

.taskdefaults__name {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: var(--weight-medium);
}

.taskdefaults__control { flex: 0 0 auto; min-width: 12rem; }

/* RF-20260827-02 — THE REQUIRED COLUMN, ON THE ROW ITS FIELD OWNS.
   A fixed width so the boxes line up down the list rather than tracking each control's width —
   a column of checkboxes that wanders is a column nobody can scan. */
.taskdefaults__required {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  /* NO GAP, BECAUSE THERE IS NOTHING LEFT TO SEPARATE — RF-20260903-07. The cell holds the checkbox
     and a label whose only content is `.sr-only` text, so the gap separated the box from something
     of zero width and pushed it 4px off the centre line the heading marks. Measured: cell centre
     658, box centre 654. The label stays exactly as it is — it is what names the box for a screen
     reader — it simply no longer reserves space it does not occupy. */
  gap: 0;
  min-width: 6.5rem;
  /* RF-20260903-07 — CENTRED, UNDER THE HEADING THAT WAS ALREADY CENTRED.
     CONTRACT CHANGED DELIBERATELY. RF-20260828-03 centred the "REQUIRED" heading and left the row
     checkboxes at `flex-start` on purpose, reasoning that a column of boxes should share one left
     edge. The result on screen is the thing Terry photographed: a centred heading with the boxes
     sitting off to its left, so no box is under the word that names it. Terry: *"Required column
     should be on the right side. and the checkbox aligned center."* The column already IS rightmost
     in both tables; the alignment is what was wrong. The boxes still share one edge — they now share
     the CENTRE line, which is the one the heading marks. */
  justify-content: center;
}
.taskdefaults__requiredlabel { font-size: var(--text-sm); color: var(--text-muted); cursor: pointer; }
/* A field with a Required box but no default still needs its Default column, or the Required
   column of that row would slide left and break the alignment of every row below it. */
.taskdefaults__control--none { min-width: 12rem; }
.taskdefaults__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-2) var(--space-1);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.taskdefaults__head .taskdefaults__name,
.taskdefaults__head .taskdefaults__control,
.taskdefaults__head .taskdefaults__required { font-weight: 600; }
/* RF-20260828-03 centred the heading; RF-20260903-07 centred the boxes beneath it, so this rule is
   now the same value the base rule already carries. Kept rather than deleted: it states that the
   heading's centring is intentional in its own right, so a later change to the row alignment cannot
   silently take the heading with it. */
.taskdefaults__head .taskdefaults__required { justify-content: center; }
/* NARROW SCREENS: the row wraps rather than scrolling sideways, and Required keeps its own line
   so the checkbox never ends up under the label of the field above it. */
@media (max-width: 44rem) {
  .taskdefaults__item { flex-wrap: wrap; }
  .taskdefaults__control, .taskdefaults__control--none { min-width: 0; flex: 1 1 10rem; }
  .taskdefaults__required { min-width: 0; }
  .taskdefaults__head { display: none; }
}

/* RF-20260828-03 — THE SAME WRAP, KEYED TO THE COLUMN'S OWN WIDTH, NOT THE VIEWPORT'S. The
   media query above only ever fires on a genuinely narrow BROWSER, which is correct for the
   list at full page width but wrong once it is split across `.matrixgrid` columns (see that
   class's own comment): a column can be exactly this narrow on a wide monitor. Same three
   properties, same effect, evaluated against whichever `.matrixgrid__section` this list is
   actually inside. Harmless where there is no such ancestor — `@container` with no named
   container simply never matches, exactly like an unmet `@media` query. */
@container (max-width: 27rem) {
  .taskdefaults__item { flex-wrap: wrap; }
  .taskdefaults__control, .taskdefaults__control--none { min-width: 0; flex: 1 1 10rem; }
  .taskdefaults__required { min-width: 0; }
  .taskdefaults__head { display: none; }
}

/* ---------- the overflow menu ---------- */

.menu { position: relative; display: inline-flex; }

/* COMPACT, AND QUIET UNTIL WANTED. It replaces a cluster of four or five labelled buttons per
   row, so it must not itself read as a fifth button competing with the row's content. */
.menu__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  line-height: 1;
  font-size: var(--text-base);
}

.menu__trigger:hover:not(:disabled),
.menu__trigger[aria-expanded="true"] {
  color: var(--text);
  border-color: var(--border);
  background: var(--surface-raised, var(--surface));
}

.menu__trigger:disabled { cursor: default; opacity: 0.4; }

/* MOUNTED ON THE BODY, so it cannot be clipped by the table's own horizontal scroll — see
   the note at the top of menu.js. Placement comes from the shared popover arithmetic. */
.menu__panel {
  position: fixed;
  top: var(--pop-top, 50%);
  left: var(--pop-left, 50%);
  /* ABOVE THE MODAL OVERLAY, and this is load-bearing rather than cosmetic.
     The panel is mounted on the BODY so a scroll container cannot clip it, which means that
     inside a dialog it and `.modal` are siblings and stacking order is the only thing deciding
     which one receives a click. At 60 against the modal's 100 it opened BEHIND a full-viewport
     overlay: present in the DOM, invisible, every click swallowed by the backdrop. It looked
     exactly like a menu that does nothing, because it was one.
     1300 is the value every other in-dialog floating panel here already uses (the date picker,
     the command pickers), so this joins that convention rather than inventing a new tier. */
  z-index: 1300;
  min-width: 12rem;
  max-width: 20rem;
  padding: var(--space-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg, 0 8px 24px rgb(0 0 0 / 0.18));
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.menu__panel[hidden] { display: none; }

/* THE COLLAPSED-RAIL TOOLTIP (RB-11). Body-mounted for the same reason `.menu__panel` is —
   see the note above `root`'s listeners in sidebar.js — and pinned to the SAME z-index tier
   every other body-mounted floating panel here uses, rather than inventing a new one. */
.tooltip {
  position: fixed;
  top: var(--tip-top, 50%);
  left: var(--tip-left, 50%);
  z-index: 1300;
  max-width: 16rem;
  padding: var(--space-1) var(--space-2);
  background: var(--surface-raised, var(--surface));
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-xs);
  white-space: nowrap;
  box-shadow: var(--shadow, 0 4px 16px rgb(0 0 0 / 0.12));
  pointer-events: none;
}

/* RF — Terry: smaller text in the waffle context menu, to shrink the panel itself. One step
   down the same token scale `.menu__detail`/`.tooltip` already use, not a bespoke size — this
   is the one shared `menu.js` component every waffle in the app renders through ("one
   implementation, six places," see its own header note), so the change is consistent
   everywhere a waffle opens, not just Tasks/Projects. */
.menu__item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: var(--space-2);
  color: var(--text);
  cursor: pointer;
  font-size: var(--text-xs);
}

.menu__item:hover:not(:disabled),
.menu__item:focus-visible { background: var(--surface-raised, var(--surface)); }
.menu__item:disabled { cursor: default; color: var(--text-muted); opacity: 0.6; }

/* DESTRUCTIVE, AND VISIBLY SO — but by colour AND by sitting under its own rule, because
   colour alone says nothing to a reader who cannot distinguish it. */
.menu__item--danger { color: var(--danger, #b42318); }
.menu__item--danger:hover:not(:disabled) { background: var(--danger-surface, rgb(180 35 24 / 0.08)); }

.menu__detail { font-size: var(--text-xs); color: var(--text-muted); }

.menu__group {
  margin: var(--space-1) var(--space-2) 0;
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.menu__rule { border: 0; border-top: 1px solid var(--border); margin: var(--space-1) 0; }

.menu__empty { margin: 0; padding: var(--space-2); font-size: var(--text-xs); color: var(--text-muted); }

/* RF-20260824-10 — THE PROGRESS SLIDER.
   A real <input type="range">, so the handle, the keyboard and the touch behaviour are the
   platform's. Only the track fill and the readout are ours. The percentage is always TEXT
   beside it — the colour of the track is decoration and is never the only way to read it. */
.progress-slider { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.progress-slider__label { flex: 0 0 100%; font-size: var(--text-sm); color: var(--ink-2); }
.progress-slider__input {
  flex: 1 1 5rem;
  min-width: 4rem;
  /* A 44px-tall hit area for touch, while the visible track stays slim. */
  height: 2.75rem;
  margin: 0;
  background: transparent;
  /* RF-20260826-04 — THE SELECTED THEME, NOT A FIXED COLOUR.
     `accent-color` is the one property that themes a native range input's fill AND its thumb
     together, so the control follows `--accent` exactly as every button, link and focus ring
     already does. It was pinned to `--color-success`, which is a semantic colour meaning "this
     went well" rather than "this is the accent" — so the slider stayed green in every palette
     and read as belonging to some other application. There is deliberately no per-palette rule
     here: the twenty-one palettes already define `--accent`, and a second colour system for one
     control is the thing that drifts. Changing theme re-evaluates the variable, so it updates
     with no reload. */
  accent-color: var(--accent);
  cursor: pointer;
}
/* DISABLED STAYS OBVIOUSLY DIFFERENT FROM EDITABLE, and not by opacity alone — a muted accent
   at 60% can still read as an ordinary control on a pale palette. */
.progress-slider__input:disabled {
  cursor: not-allowed;
  opacity: 0.6;
  accent-color: var(--text-subtle);
}
.progress-slider__value {
  flex: 0 0 auto;
  min-width: 3ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--ink-1);
}
.progress-slider__status { flex: 0 0 100%; font-size: var(--text-xs); color: var(--ink-2); min-height: 1em; }
.progress-slider__status[data-tone="ok"] { color: var(--color-success); }
.progress-slider__status[data-tone="error"] { color: var(--color-danger); }
.progress-slider__reason { flex: 0 0 100%; font-size: var(--text-xs); color: var(--ink-2); margin: 0; }

/* RF-20260824-11 — SIX COLUMNS IN THE PROJECTS TREE, WITHOUT LOSING THE TREE.
   The row stays a flex line so the depth inset above keeps working; the cells are given
   proportions rather than a grid, because a grid would have to fight the per-row inset that
   IS the hierarchy. */
.tasktree__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-inline-start: var(--space-3);
  padding: var(--space-1) 0 var(--space-1) var(--space-3);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  border-block-end: 1px solid var(--border);
}
.tasktree__cell { flex: 0 0 auto; min-width: 0; }
.tasktree__cell--title { flex: 1 1 12rem; display: flex; align-items: baseline; gap: var(--space-2); }
.tasktree__cell--status { flex: 0 0 7rem; }
.tasktree__cell--priority { flex: 0 0 6rem; }
.tasktree__cell--due { flex: 0 0 6.5rem; font-variant-numeric: tabular-nums; }
.tasktree__cell--owner { flex: 0 0 8rem; }
.tasktree__cell--progress { flex: 0 0 6rem; }
.tasktree__cell--actions { flex: 0 0 auto; margin-inline-start: auto; display: flex; gap: var(--space-1); align-items: center; }
.tasktree__cell--overdue { color: var(--color-danger); font-weight: 600; }
.tasktree__owner { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* DEGRADING, NOT OVERFLOWING. Narrow screens drop the least scannable columns rather than
   letting the row scroll sideways: the title, status and due date are what somebody is looking
   for in a project tree, and the rest are still on the task itself and in the Tasks view.
   The header hides with them, so a column heading never survives its column. */
@media (max-width: 60rem) {
  .tasktree__cell--owner,
  .tasktree__cell--progress { display: none; }
}
@media (max-width: 44rem) {
  .tasktree__head { display: none; }
  .tasktree__cell--priority { display: none; }
  .tasktree__cell--status,
  .tasktree__cell--due { flex: 0 0 auto; }
}

/* RF-20260825-05 — THE EXTENDED PROSE SUBSET.
   Colours are CLASSES, never inline styles: `npm run validate` fails the build on an inline
   style in app/js, and the CSP's style-src is strict. That is also what keeps a colour name a
   closed set rather than a value a user supplies. */
.prose__u { text-decoration: underline; }
.prose__c--red { color: var(--prose-red, #dc2626); }
.prose__c--orange { color: var(--prose-orange, #ea580c); }
.prose__c--amber { color: var(--prose-amber, #b45309); }
.prose__c--green { color: var(--prose-green, #16a34a); }
.prose__c--teal { color: var(--prose-teal, #0d9488); }
.prose__c--blue { color: var(--prose-blue, #2563eb); }
.prose__c--purple { color: var(--prose-purple, #7c3aed); }
.prose__c--pink { color: var(--prose-pink, #db2777); }
.prose__c--grey { color: var(--prose-grey, #6b7280); }

/* Nested lists indent by their own nesting, not by a hand-set depth variable. */
.prose__list .prose__list { margin-block: var(--space-1); padding-inline-start: var(--space-4); }

/* RF-20260827-10 — THE OVERFLOW PANEL. `hidden` does the hiding, so nothing inside is focusable
   while it is closed.

   IT IS OUT OF FLOW, AND THAT IS THE FIX rather than a restyle. It used to be an in-flow child of
   `.prose__toolbar` carrying `flex: 1 1 100%` — the idiom that forces a flex item onto its own
   line, which only works in a container that WRAPS. This one is `flex-wrap: nowrap` by design and
   always has been, and in a row that does not wrap `flex-basis: 100%` with `flex-shrink: 1` means
   "take whatever the other children left" — so the panel's width was the row's width minus the
   seven controls in front of it. The narrower the dialog, the narrower the panel. Measured in Edge
   at `96ae857`: Advanced at a 340px viewport gave the panel 38px, so twenty-one controls wrapped
   into twenty-one rows, 673px tall, clipped by the dialog; at 420px with a 24px root it was 42px
   and 1009px tall; Simple at 420px/24px gave four rows of one control each. That is the reported
   stranded lone control, and it also grew the field's own height by up to a thousand pixels —
   exactly the "a wrapping toolbar moves everything below it" harm the `nowrap` note above exists
   to prevent.

   `position: fixed` SO IT DOES NOT CHANGE THE FIELD'S SHAPE. Out of flow, it cannot squeeze the
   row, cannot make the toolbar taller and cannot push the page sideways — which is three of the
   four things the old in-flow panel did. It stays a DOM DESCENDANT of the toolbar — unlike
   `.menu__panel`, which is body-mounted — so `setDisabled`, the Undo/Redo state sync and the tab
   order all still find it by walking the toolbar, which is what they have always done.

   BUT `position: fixed` DOES NOT ESCAPE THE CLIPPING HERE, AND THAT IS MEASURED RATHER THAN
   ASSUMED. It is supposed to: a fixed element's containing block is the viewport, and no ancestor
   in this stylesheet declares `transform`, `filter`, `perspective`, `contain` or `will-change`.
   Blink clips it anyway at `.modal__panel { overflow: hidden }`. Measured in Edge at a 560px
   viewport: the panel's own rectangle read left 200 / right 552 against a dialog whose right edge
   is 545, and `elementFromPoint` returned the panel at x=503 and the modal BACKDROP at x=545 — the
   last seven pixels were not merely invisible, they were not pressable, and a real click there
   dismissed the dialog. So the fourth thing is fixed by CLAMPING: `placeMore()` intersects the
   viewport with every clipping ancestor and places the panel inside that region, publishing its
   width as `--pop-maxw`.

   SIZED BY ITS OWN CONTENT, capped to that region, so it wraps into a compact grid instead of a
   column one control wide. The `100vw` fallback is what applies before the first measurement and
   anywhere there is no layout engine at all. */
.prose__more {
  position: fixed;
  top: var(--pop-top, 50%);
  left: var(--pop-left, 50%);
  z-index: 1300;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  align-items: center;
  width: max-content;
  max-width: min(22rem, var(--pop-maxw, calc(100vw - 1.5rem)));
  max-height: var(--pop-max, none);
  overflow-y: auto;
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.prose__more[hidden] { display: none; }
.prose__swatch { font-size: 0.9em; }
.prose__swatch--red { color: var(--prose-red, #dc2626); }
.prose__swatch--orange { color: var(--prose-orange, #ea580c); }
.prose__swatch--amber { color: var(--prose-amber, #b45309); }
.prose__swatch--green { color: var(--prose-green, #16a34a); }
.prose__swatch--teal { color: var(--prose-teal, #0d9488); }
.prose__swatch--blue { color: var(--prose-blue, #2563eb); }
.prose__swatch--purple { color: var(--prose-purple, #7c3aed); }
.prose__swatch--pink { color: var(--prose-pink, #db2777); }
.prose__swatch--grey { color: var(--prose-grey, #6b7280); }

/* ---- RF-20260827-10 WAVE 1 — THE TIPTAP TOOLBAR: IT WRAPS, AND NOTHING IS EVER HIDDEN ---------

   TERRY'S INSTRUCTION, SUPERSEDING §J.47'S OVERFLOW MECHANISM, IN ONE DECLARATION. Controls WRAP
   onto additional rows when they do not fit; a control is never moved behind a "⋯", never hidden
   to satisfy a one-row layout, and never dropped. That is why there is no `ResizeObserver`, no
   width measurement, no priority list and no overflow panel in `editor/toolbar.js` at all — the
   whole mechanism is `flex-wrap: wrap` here.

   `.prose__toolbar` ABOVE IS DELIBERATELY LEFT ALONE. It is `flex-wrap: nowrap` because the custom
   engine's own row still measures itself and partitions into a panel, and changing that would break
   a mechanism four other surfaces still use. This class is added ALONGSIDE it, so the two engines'
   rows can differ without either one being edited into the other's shape.

   EACH GROUP IS ITS OWN NON-WRAPPING BOX, so related controls stay together and it is the GROUPS
   that wrap — the "keep related controls together" half of the instruction. `flex-wrap: wrap` on
   the group itself is the other half and it is not a contradiction: a group wider than the whole
   container (nine colour swatches on a phone) has to wrap INSIDE itself, because the alternative
   is clipping it, and a control somebody cannot reach is exactly what none of this may produce. */
/* THE WRAPPER TIPTAP MOUNTS INTO. It carries `--prose-rows` — a custom property, which inherits,
   and which must never become a `style` attribute the CSP would refuse — and contributes nothing
   else. The chrome belongs to `.prose__doc` on the editable child, which is the same class the
   custom engine's own region carries. */
.prose__docwrap { display: flex; flex-direction: column; min-width: 0; }

/* RF-20260827-10 WAVE 3 — THE EMPTY-BOX HINT, for a field that is always a visible empty box.
   The comment composer is the one prose surface with no read mode: Description and Goal open as
   rendered words behind an Edit control, so they never show an unlabelled empty box and never
   needed one of these.

   `position: relative` IS ON THE MODIFIER, NOT ON `.prose__docwrap`, so a field with no hint gains
   no new containing block for absolutely positioned descendants. `pointer-events: none` is what
   makes a press on the hint a press on the document underneath it — a prompt that swallowed the
   click that is trying to start typing would be worse than no prompt at all. The offsets are
   `.field__input`'s own padding plus its 1px border, which is where the first character sits. */
.prose__docwrap--hinted { position: relative; }
.prose__placeholder {
  position: absolute;
  top: calc(var(--space-3) + 1px);
  inset-inline-start: calc(var(--space-3) + 1px);
  max-width: calc(100% - 2 * (var(--space-3) + 1px));
  margin: 0;
  color: var(--text-subtle);
  pointer-events: none;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.prose__toolbar--wrap { flex-wrap: wrap; row-gap: var(--space-1); }
.prose__toolgroup {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  align-items: center;
  /* The group takes its natural width and is allowed to shrink only as far as wrapping its own
     children; `min-width: 0` is what lets that happen inside a flex parent instead of overflowing. */
  min-width: 0;
}
/* A GROUP IS SEPARATED FROM THE NEXT BY SPACE, NOT BY A LINE. A divider between groups that WRAP
   would sit at the end of a row pointing at nothing. */
.prose__toolbar--wrap > .prose__toolgroup + .prose__toolgroup { margin-inline-start: var(--space-2); }

/* THE PRESSED STATE IS DRIVEN BY `aria-pressed`, WHICH IS THE ONE THE ASSISTIVE TREE ALREADY HAS.
   Styling a separate `--active` class would be a second source of truth for "is Bold on", and the
   two would drift the first time a sync path missed one of them. */
.prose__toolbtn[aria-pressed="true"] {
  color: var(--text-inverse);
  background: var(--accent);
  border-color: var(--accent);
}
/* A PRESSED SWATCH KEEPS ITS OWN COLOUR AND GAINS A RING, rather than taking the accent fill: the
   swatch's colour IS its meaning, and painting the pressed one accent-blue would say the text is
   blue when it is red. */
.prose__swatch[aria-pressed="true"] {
  background: var(--surface-sunken);
  border-color: currentColor;
  box-shadow: inset 0 0 0 1px currentColor;
}

/* THE FONT/SIZE/LINK PANELS. The same fixed, clamped, `--pop-*`-driven placement `.prose__more`
   uses — see its own note above for why `position: fixed` still has to be clamped to the clipping
   ancestors rather than trusted to escape them. A column rather than a wrapped grid, because these
   hold rows of WORDS and a form, not a palette of glyphs. */
.prose__pop {
  position: fixed;
  top: var(--pop-top, 50%);
  left: var(--pop-left, 50%);
  z-index: 1300;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  width: max-content;
  min-width: 12rem;
  max-width: min(22rem, var(--pop-maxw, calc(100vw - 1.5rem)));
  max-height: var(--pop-max, none);
  overflow-y: auto;
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.prose__pop[hidden] { display: none; }
.prose__popitem {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* A FULL-WIDTH TARGET, and tall enough for a fingertip. A menu row that is only as wide as its
     word is a row people miss on a touch screen. */
  width: 100%;
  min-height: 2.25rem;
  padding: var(--space-1) var(--space-2);
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-sm);
  text-align: start;
  cursor: pointer;
}
.prose__popitem:hover:not(:disabled) { background: var(--surface-sunken); }
.prose__popitem[aria-pressed="true"] { border-color: var(--accent); font-weight: var(--weight-bold); }
.prose__popitem:disabled { cursor: default; opacity: 0.4; }
/* THE LINK BOX. It looks like `.field__input` and deliberately does not USE that class: that class
   is how this application's dialogs and their tests identify a field ON THE FORM, and a box inside
   a toolbar popover is not one. The declarations are duplicated rather than shared for exactly that
   reason — the visual is the same, the meaning is not. */
.prose__popinput {
  width: 100%;
  min-width: 0;
  padding: var(--space-2);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
.prose__popinput:focus-visible {
  outline: none;
  border-color: var(--color-info);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-info) 25%, transparent);
}
.prose__popinput:disabled { background: var(--surface-sunken); color: var(--text-subtle); cursor: not-allowed; }
.prose__poprow { display: flex; gap: var(--space-2); }
.prose__popnote:empty { display: none; }
.prose__popnote { color: var(--color-danger); }

/* RF-20260825-07 — THE COMMENTS TAB.
   An INDEPENDENT SCROLL REGION: a long thread scrolls within its own panel rather than growing
   the dialog until the compose box is off screen. The compose box stays put at the bottom, which
   is the whole reason somebody opened the tab. */
/* RF-20260827-11 — the panel fills the stable shell rather than capping itself at a fraction of
   the viewport. The cap was there because the panel's height used to decide the dialog's; it
   no longer does, and keeping it would leave a gap below the composer on a tall screen. */
.modal__tabpanel[data-tab="comments"] {
  display: flex;
  flex-direction: column;
  min-height: 0;
  width: 100%;
  /* MEASURED IN EDGE: the comments section came out 288px wide inside an 878px panel, and the
     composer 170px. The cause is inherited from the base `.modal__tabpanel` rule, which is a
     three-column GRID and sets `align-items: start` for it. Switching the panel to `flex` left
     that declaration in force, and under a flex COLUMN `align-items: start` sizes children to
     their own content — so the region was as wide as a default `<textarea>` and no wider.
     `stretch` is the flex default; it has to be said out loud only because the grid rule already
     said something else. */
  align-items: stretch;
  gap: var(--space-3);
}
.modal__tabpanel[data-tab="comments"] .comments__section { width: 100%; min-width: 0; }
.modal__tabpanel[data-tab="comments"] .comments__section {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1 1 auto;
}
.modal__tabpanel[data-tab="comments"] .comments {
  overflow-y: auto;
  min-height: 0;
  flex: 1 1 auto;
}
.modal__tabpanel[data-tab="comments"] .comments__compose { flex: none; }
.modal__tabpanel[data-tab="attachments"] .attachments__section { width: 100%; min-width: 0; }
.modal__tabpanel[data-tab="attachments"] .attachments {
  overflow-y: auto;
  min-height: 0;
  flex: 1 1 auto;
}

/* RF-20260825-07 (REOPENED) — THE OUTER SECTION STRIP: the task, then its conversation.
   A conversation is not a property of a task the way a due date is, so it is no longer a field
   carrying a tab name — it is a top-level SECTION, and this is the strip that separates the two.

   IT IS A DIFFERENT STRIP FROM THE FIELD TABS AND MUST LOOK LIKE ONE. Both can be on screen at
   once in the tabbed layouts ("Edit task | Comments" above "Task | Details | Recurrence"), and
   two identical rows of tabs stacked on top of each other would read as one confused strip. The
   outer one is the heavier of the two because it is the higher-level choice. */
.modal__tabs--sections {
  gap: var(--space-2);
  border-bottom-width: 2px;
  margin-bottom: var(--space-2);
}
.modal__tabs--sections > .modal__tab { font-weight: var(--weight-medium); }
.modal__tabs--sections > .modal__tab--active { font-weight: var(--weight-bold); }

/* HIDING THE FORM SIDE HAS TO OUT-SPECIFY THE NODES IT HIDES.
   The outer strip hides `body` and the inner strip/rail directly rather than wrapping them in a
   panel — wrapping them broke the guided-steps rail and laid the whole form out on the field
   panel's three-column grid. But `.modal__tabpanel--hidden` is a single class declared at line
   ~1476, and `.modal__body--command` (`display: flex`) is declared later, so plain source order
   would have left the form VISIBLE UNDERNEATH the comment thread in exactly the layout that is
   the default. Two classes beat one, whatever the order. */
.modal__tabpanel--hidden.modal__body,
.modal__tabpanel--hidden.modal__tabs,
.modal__tabpanel--hidden.tf-rail { display: none; }

/* AND THE SAME PROBLEM ON THE OTHER SIDE, found by measuring rather than by reading.
   `.modal__tabpanel[data-tab="comments"]` sets `display: flex` and is (0,2,0); plain
   `.modal__tabpanel--hidden` is (0,1,0). So the COMMENTS panel stayed displayed while the task
   was showing — a zero-height flex box sitting in the form's column, pushing the guide and the
   buttons below where they belonged. Measured in Edge: the footer at 767 with the task showing
   and 661 with the comments showing, from one panel that never actually went away.
   A class plus an attribute out-specifies a class plus an attribute, so this is stated as both. */
.modal__tabpanel--hidden[data-tab] { display: none; }

/* GUIDED STEPS KEEPS ITS RAIL BESIDE THE BODY when there is an outer strip above both.
   `.modal__form--rail` is a two-column grid whose every child sits in column 2 except the rail.
   The section strip is a choice ABOUT the whole form, so it spans both columns on its own row,
   and the rail starts below it instead of sharing row 1. */
.modal__form--sections.modal__form--rail > .modal__tabs--sections { grid-column: 1 / -1; grid-row: 1; }
/* `grid-row: 2`, NOT `2 / -1`, AND THE DIFFERENCE IS NOT COSMETIC.
   A negative row line counts back from the end edge of the EXPLICIT grid, and this grid declares
   only `grid-template-columns` — so there are no explicit rows and `-1` resolves to line 1.
   `2 / -1` therefore normalises to `1 / 2`: the rail landed in row ONE, on top of the section
   strip, and the body was pushed down to row two BELOW the rail instead of beside it. Measured in
   Edge: rail 89-246 and strip 89-238 occupying the same band, body starting at 258.
   A single line number places it in row 2 spanning one row, which is what was meant, and the
   body then auto-places into row 2 column 2 alongside it. */
.modal__form--sections.modal__form--rail > .tf-rail { grid-row: 2; }

/* AND IT STILL COLLAPSES ON A NARROW SCREEN. The rule above is more specific than the existing
   `@media (max-width: 46rem)` collapse AND comes after it, so without this the rail would be
   pinned to `grid-row: 2 / -1` in a single-column grid on a phone — spanning every row and
   sitting on top of the fields. Restated here at matching specificity inside the same breakpoint. */
@media (max-width: 46rem) {
  .modal__form--sections.modal__form--rail > .modal__tabs--sections,
  .modal__form--sections.modal__form--rail > .tf-rail { grid-column: 1; grid-row: auto; }
}

/* A custom field may carry no label when its own content already provides the heading — an
   empty one would otherwise leave a gap that reads as a missing label. */
.field__label:empty { display: none; }

/* RF-20260824-17 — a completed entry on the Calendar.
   The tick and the "due …" note are TEXT; the muted tone is decoration on top of them, never
   the only way to tell a finished task from an outstanding one. */
.cal__done { flex: none; font-size: var(--text-xs); color: var(--color-success); }
.cal__wasdue { flex: none; font-size: var(--text-xs); color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* RF-20260826-01 — MY SETTINGS: TWO INDEPENDENT COLUMNS.
   NOT A GRID. A grid row makes its cells the same height, which is exactly what was wrong:
   Landing View was stretched to match whatever sat beside it. Two flex columns side by side
   flow separately, so a card is only ever as tall as its own content and nothing in one column
   can pad out anything in the other.
   `align-items: start` on the container stops the COLUMNS themselves stretching to equal
   heights, which would put the same problem back one level up. */
.panel__columns {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
}
.panel__column {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Equal share of the width, and `min-width: 0` so a wide control inside cannot force the
     column past its share and introduce horizontal scrolling. */
  flex: 1 1 0;
  min-width: 0;
}
@media (max-width: 60rem) {
  /* ONE SENSIBLE COLUMN. Wrapping rather than reordering: the left column's cards stay
     together and in order, then the right column's, so nothing is lost, duplicated or
     shuffled into an order nobody chose. */
  .panel__columns { flex-direction: column; }
  .panel__column { width: 100%; }
}

/* RF-20260826-02 — THE REORDERABLE LIST, shared by View Tabs. */
.reorder { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.reorder__row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
/* WHERE IT WILL LAND. An insertion line rather than a moved row, so the list does not reflow
   under the pointer while somebody is still deciding. */
.reorder__row--above { box-shadow: inset 0 2px 0 0 var(--accent); }
.reorder__row--below { box-shadow: inset 0 -2px 0 0 var(--accent); }
.reorder__handle {
  flex: none;
  cursor: grab;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  /* A real 44px touch target: the handle is the control, so it has to be grabbable by a
     thumb as well as a pointer. */
  min-width: 2.75rem;
  min-height: 2.75rem;
}
.reorder__handle:hover:not(:disabled) { border-color: var(--border-strong); color: var(--text); }
.reorder__handle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.reorder__handle:disabled { cursor: not-allowed; opacity: 0.5; }
.reorder__label { flex: 1 1 auto; min-width: 0; }
.reorder__meta { flex: none; display: inline-flex; align-items: center; gap: var(--space-1); }

/* A TAB THAT STILL NEEDS AN ANSWER — RF-20260901-04.
 *
 * Save names what is missing, but a field on another tab is a refusal somebody cannot see. The
 * dot marks WHERE, and carries its own accessible name so it is not colour-only. */
.tabs__tab--attention { color: var(--color-danger); }
.tabs__attention { margin-left: var(--space-1); color: var(--color-danger); font-size: var(--text-base); line-height: 1; }

/* ---------------------------------------------------------------------------
   RF-20260901-03 — THE THREE ARRANGEMENTS FROM THE DESIGN GALLERY
   ---------------------------------------------------------------------------
   CAPTURE is the default for both creating and editing. The task's own words take
   the main column; everything else is a column of groups that stay closed and
   report what they hold. */
.tf-capture { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: var(--space-5) var(--space-6); align-items: start; }
.tf-capture__content { min-width: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.tf-capture__side { min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); }

/* PROJECT LEFT OF TITLE, ON ONE ROW, ALIGNED. Both are required and a task cannot
   exist without either, so they are answered together rather than in two places.
   `align-items: start` — corrected after measuring. The first attempt used `end`, reasoning that
   aligning bottoms would align the inputs; it does the opposite here. Project carries a help line
   on a new task ("Choose the project this task belongs to.") which hangs BELOW its input, so
   bottom-alignment pushed the project's input 38px above the title's. Both labels are one line, so
   aligning the tops aligns the labels AND the inputs, and the help text hangs below where it
   belongs. Measured: 231 vs 269 before, level after. */
.tf-capture__head { display: grid; grid-template-columns: minmax(9rem, 15rem) minmax(0, 1fr); gap: var(--space-3); align-items: start; }
/* THE SPAN CLASSES ARE RESET INSIDE THIS ROW, and this is the whole reason the row works.
   `.field--grow` is `grid-column: 1 / -2` and `.field--full` is `1 / -1` — written for the tab
   panel's own three-column grid, and both are more specific than any placement this row could
   ask for. Measured in a real browser: Project and Title were BOTH at x=299, stacked, in a grid
   that genuinely had two columns. The row places its own children; nothing inherited decides. */
.tf-capture__head > * { min-width: 0; margin: 0; grid-column: auto; }
@media (max-width: 44rem) { .tf-capture__head { grid-template-columns: 1fr; } }
@media (max-width: 60rem) { .tf-capture { grid-template-columns: 1fr; } }

/* RF-20260901-05 — A CANONICAL FIELD TAB HAS NO PROSE COLUMN, SO IT DOES NOT RESERVE ONE.
 * People & Relationships and Recurrence & Options carry no title, description or goal, so the
 * 1.55fr content column would be empty and the whole tab would render in the remaining 1fr. The
 * groups take the width instead, and lay out side by side when there is room for it. */
.tf-capture--groupsonly { grid-template-columns: 1fr; }
.tf-capture--groupsonly > .tf-capture__side {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
  gap: var(--space-3);
  align-items: start;
}

/* CARDS uses the width that is already there instead of stacking. */
.tf-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: var(--space-3); align-items: start; }
.tf-cards__lead { display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-4); }

/* STACK is one column, one reading order — the safe one at any width. */
.tf-stack { display: flex; flex-direction: column; gap: var(--space-2); }
.tf-stack__lead { display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-4); }

/* A GROUP THAT REPORTS ITSELF. Shared by all three: the closed state is the whole
   argument, so it carries a live summary rather than a label on a door. */
.tf-grp { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-raised); }
.tf-grp--open { border-color: var(--border-strong); }
.tf-grp__head {
  display: flex; align-items: baseline; gap: var(--space-2); width: 100%;
  padding: var(--space-3); background: none; border: none; cursor: pointer;
  text-align: left; font: inherit;
}
.tf-grp__head:hover { background: var(--surface); }
.tf-grp__head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.tf-grp__mark { flex: none; color: var(--text-subtle); }
.tf-grp__name { flex: none; font-size: var(--text-sm); font-weight: var(--weight-medium); }
/* TRUNCATED, NEVER WRAPPED. A summary that grew to three lines would undo the height
   the collapsing bought back. */
.tf-grp__summary {
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--text-xs); color: var(--text-subtle);
}
.tf-grp__summary--empty { font-style: italic; opacity: 0.7; }
/* RF-20260901-05 — A GROUP LAYS ITS FIELDS OUT, IT DOES NOT JUST STACK THEM.
 *
 * This was `flex-direction: column`, so every field in every group took a whole row whatever its
 * width. In the narrow Capture side column that is right and this grid produces exactly the same
 * single column. On the wide canonical tabs it was the measured complaint: "Every" and "Day of the
 * month" are two-digit boxes that each claimed a full dialog width, while Start date, Due date and
 * Progress sat capped at 11rem in the same panel — one group looking compressed and the next
 * looking stretched.
 *
 * `auto-fit` + `minmax` IS THE WHOLE MECHANISM. No breakpoint decides the count; the container
 * does, so the same declaration gives one column in a 20rem side panel and three across a wide
 * tab, and it keeps working at any dialog width including the ones nobody tested. A field that
 * genuinely needs the row asks for it with `.field--full`. */
.tf-grp__body {
  padding: 0 var(--space-3) var(--space-3);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-4);
  align-items: start;
}
.tf-grp__body > .field--full { grid-column: 1 / -1; }
/* RF-20260902-01 — A PAIR OF FIELDS THAT READS AS ONE CONTROL: `[ 7 ] [ Days ]`.
 *
 * MEASURED FIRST, THEN BUILT. The first attempt was a grid hint — `grid-column: 1` on the first of
 * the pair — and it could not work: a group in Capture is about 400px wide and `.tf-grp__body`'s
 * `minmax(13rem, 1fr)` resolves to ONE column at that width, so both halves sat at the same `left`,
 * 112px apart. An item cannot opt out of a track that does not exist.
 *
 * SO THE PAIR IS ITS OWN TWO-COLUMN BOX, occupying one cell of the group's grid and splitting
 * inside itself. It works at whatever width the group has, and collapses to one column only when
 * the window genuinely cannot hold two — which is the responsive behaviour asked for.
 *
 * `align-items: start` ALIGNS THE LABELS, WHICH IS WHAT ALIGNS THE INPUTS. Measured with `end`
 * instead: the two bottoms lined up and the two TOPS were 46px apart, because only one half
 * carries help text under it and the taller field therefore started higher. Aligning the tops puts
 * both labels on one line and both controls on the next; the help text hangs below where it
 * belongs, which is the same reasoning `.tf-capture__head` already records for Project and Title.
 *
 * AND THE TWO CONTROLS ARE GIVEN ONE HEIGHT. A native `<select>` and a `<input type=number>` do
 * not agree to the pixel — measured 39px against 40px — which is invisible on separate rows and
 * is exactly the kind of thing that reads as sloppy when they sit side by side. */
/* `auto-fit`, NOT `1fr 1fr`, AND THAT IS WHAT HANDLES A HALF THAT IS NOT THERE. Both halves are
 * always BUILT — `showWhen` hides a field, it does not remove it, which is what stops a hidden
 * control from dropping its value out of the payload — so "Does not repeat" leaves the pair
 * holding one visible field and one `display: none` one. With a fixed two-column track that
 * visible field would occupy half a row and leave a gap beside it. `auto-fit` collapses the track
 * nothing was placed in, so one visible half takes the whole width and two split it. */
.field-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: var(--space-3);
  align-items: start;
}
/* One height for both halves, whatever element each of them turns out to be. */
.field-pair .field__input,
.field-pair .cmdpick__trigger,
.field-pair .datepick__trigger { min-height: 2.5rem; }
.tf-grp__body > .field-pair,
.tf-sheet__fields > .field-pair { grid-column: 1 / -1; }
/* Neither half is capped, so each fills its own half and the two match. `.field--narrow` is
   already released inside a group; this says so for the pair wherever it appears, because "the
   same width as each other" is the whole of the visual claim. */
.field-pair > .field { max-width: none; min-width: 0; }
/* A phone cannot hold two, and two 90px boxes would be worse than one above the other. */
@media (max-width: 30rem) { .field-pair { grid-template-columns: 1fr; } }
/* `.field--grow` is `1 / -2` and `.field--flag` is `-2 / -1` on the tab panel's own three-column
   grid — placements written for a grid this is not. Inside a group they simply take a cell. */
.tf-grp__body > .field--grow,
.tf-grp__body > .field--flag { grid-column: auto; }
/* AND THE 11rem CAP IS RELEASED INSIDE A GROUP. The grid already decides how wide a cell is; a
   max-width on top of it only ever leaves a short control in a wide cell. */
.tf-grp__body > .field--narrow { max-width: none; }
.tf-grp__body[hidden] { display: none; }

/* The tab panel is a three-column grid by default; these three arrangements own the
   whole panel and do their own placing, exactly as the ledger already does. */
/* ONE SCROLLBAR IN THE DIALOG, NOT FIVE.
 *
 * REPORTED BY TERRY, and the word he used is the right one: *"you are trying to embed that new
 * /edit task form in an iframe GROSSSSSS ... get it out of there and put it on the flat form!"*
 * There is no iframe anywhere in this application — what he was looking at was WORSE, because it
 * was accidental: the panel holding the property groups had its own scrollbar, and so did the
 * Comments panel, and so did Attachments. Three little scrolling boxes stacked inside a dialog
 * that was itself scrolling. Measured: the body was 212px of a 919px form, and the section panels
 * were 63px and 50px.
 *
 * These panels are `overflow: auto` because the tabbed form used them as independently scrolling
 * tabs. Stacked as SECTIONS, that turns each one into a porthole. The body scrolls; nothing
 * inside it does. */
/* SCOPED TO THE FIELD PANEL, NOT TO THE SECTIONS. Comments and Attachments are `modal__section`
   panels and they rely on their own box to establish a height; taking `overflow` off them made
   them collapse into each other. Only the panel holding the arrangement is flattened. */
/* `:not(.modal__tabpanel--hidden)` IS THE WHOLE FIX, AND LEAVING IT OUT WAS THE DEFECT.
 *
 * `.modal__tabpanel--hidden { display: none }` is one class — specificity 0,1,0. The rule below is
 * two, so setting `display: block` on every panel BEAT the hiding and rendered the Details and
 * Recurrence tabs stacked underneath the Task tab. Measured: 633 + 164 + 110 = 907px of content in
 * a 357px body, in a dialog that was also showing Comments and Attachments at the same time.
 *
 * That is what Terry was looking at when he said it had been embedded in an iframe. There is no
 * iframe in this application; there were three tabs painting on top of each other inside a
 * scrolling box, which looks like one and is worse, because nothing about it was intended.
 *
 * NEVER OVERRIDE `display` ON SOMETHING THAT IS HIDDEN. The arrangement only needs the VISIBLE
 * panel to stop being a three-column grid. */
.modal__form--capture .modal__tabpanel:not(.modal__section):not(.modal__tabpanel--hidden),
.modal__form--cards .modal__tabpanel:not(.modal__section):not(.modal__tabpanel--hidden),
.modal__form--stack .modal__tabpanel:not(.modal__section):not(.modal__tabpanel--hidden) {
  display: block; overflow: visible; min-height: 0;
}

/* And the arrangement's own columns never become scrollers either — the right-hand column having
   its own scrollbar is exactly what read as an embedded frame. */
.tf-capture, .tf-capture__content, .tf-capture__side,
.tf-cards, .tf-stack { overflow: visible; min-height: 0; }

/* RF-20260824-01 — RENAMING A VIEW TAB, in the row that names it.
 *
 * The name box and the Shown control share one group so they stay together at the row's trailing
 * edge, and the box is given a real but modest width: wide enough to read a 24-character name,
 * narrow enough that it does not push the drag handle and the view's own name off a phone. */
.reorder__metagroup { flex: none; display: inline-flex; align-items: center; gap: var(--space-3); }
.reorder__rename { width: 12rem; max-width: 40vw; }
@media (max-width: 40rem) {
  .reorder__metagroup { flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
  .reorder__rename { width: 100%; max-width: none; }
}
.reorder__metalabel { font-size: var(--text-sm); color: var(--ink-2); }

/* RF-20260826-11 — THE DEFAULT VOCABULARY ROW'S TRAILING CONTENT.
   The site vocabulary was a table until this RF; its other two columns — the state chips or the
   weight/points figure, and the row's own Edit/Delete — now ride on the reorder row as `meta`.
   They sit at the END of the row and keep their reading order, so the label still leads and the
   destructive control is still last. */
.reorder__trail { flex: none; display: inline-flex; align-items: center; gap: var(--space-3); margin-left: auto; }
/* Tabular figures, so a column of weights or points lines up the way it did in the table. */
.reorder__meta--num { font-variant-numeric: tabular-nums; color: var(--ink-2); min-width: 2ch; justify-content: flex-end; }
.reorder__actions { gap: var(--space-2); }

/* THE THREE CARDS SIT IN A NARROW SPLIT, so a row that runs out of width wraps its trailing
   content under the label rather than squeezing the label to nothing. */
@media (max-width: 64rem) {
  .vocabreorder .reorder__row { flex-wrap: wrap; }
  .vocabreorder .reorder__trail { margin-left: 0; }
}

/* RF-20260827-27 — THE ASSIGNED TO DISCLOSURE. See `assigneelist.js` for why this exists: the
   compact "First +N" label replaces a `title` attribute as the way the full list is reached. */
.assigneelist { display: inline-flex; max-width: 100%; }
.assigneelist__trigger {
  display: inline-block;
  max-width: 100%;
  padding: 0;
  font: inherit;
  color: inherit;
  background: transparent;
  border: none;
  border-bottom: 1px dotted var(--text-subtle);
  cursor: pointer;
  text-align: left;
  /* THE BUTTON TRUNCATES ITSELF. `.cell--assignees`'s own `text-overflow: ellipsis` only
     applies to the cell's own inline content — a flex-child button inside it would otherwise
     be silently clipped with no ellipsis at all, which is worse than the unbounded column this
     RF removes: a name cut off mid-letter with no indication anything is missing. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.assigneelist__trigger:hover { border-bottom-color: var(--text); }
.assigneelist__trigger:focus-visible { outline: 2px solid var(--color-info); outline-offset: 2px; }

/* OVER THE PAGE, NOT INSIDE THE CELL — the same reason `.colorpick__panel` is body-mounted:
   `.table__wrap { overflow-x: auto }` clips anything positioned inside it. Position arrives as
   custom properties through the CSSOM, so this needs no style attribute anywhere. */
.assigneelist__panel {
  position: fixed;
  z-index: 1200;
  top: var(--pop-top, 50%);
  left: var(--pop-left, 50%);
  max-height: var(--pop-max, min(16rem, calc(100vh - 2rem)));
  overflow-y: auto;
  margin: 0;
  padding: var(--space-2);
  list-style: none;
  min-width: 12rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md, 0 8px 24px rgb(0 0 0 / 0.18));
}
.assigneelist__panel[hidden] { display: none; }
.assigneelist__panel:focus { outline: none; }
.assigneelist__person { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }

/* RF-20260827-10 WAVE 5b — RICH TEXT EDITOR SETTINGS.
   Both screens (Workspace Settings and Site Administration) are ONE COLUMN, FULL WIDTH, natural
   height. No `.panel__columns`, no `.panel__pair`, no `.matrixgrid`, no `.cards--split`: nothing
   here is a genuine side-by-side pair — each surface is a disclosure holding a radio group, two
   reorder lists and a toolbar preview, all of which want the whole measure. The only thing narrowed
   is prose, to a comfortable reading width. */
.rte__measure { max-width: 45ch; }

/* THE RICH TEXT EDITOR SETTINGS INTRODUCTION, spanning the whole section.
 *
 * `rte__measure`'s 45ch is right for the help line inside ONE setting card and wrong for the
 * paragraph that introduces four of them: applied there it made a narrow column of text down the
 * left of a wide panel, reading as a note about the first setting rather than as the description
 * of the section.
 *
 * AND NO CAP OF ITS OWN, which corrects a second version of the same mistake. `100ch` was chosen
 * as a "generous" maximum on the reasoning that unbounded prose on a very wide display is a line
 * of text a metre long. The reasoning is sound and the number was not: `.card__hint` is set in
 * `--text-xs`, so its `ch` is small and `100ch` MEASURED 647px inside a 1248px panel — barely half
 * the section, which is the narrow block the requirement names, arrived at by a different route.
 *
 * THE MEASURE IS ALREADY BOUNDED, one level up. `.app__main` caps the whole application at 1280px,
 * so the widest this paragraph can be is the width of the settings panel — a normal reading
 * measure, not a metre. A second cap here can only ever be a narrower one. */
/* RF-20260903-16 — THE SITE EDITOR SETTINGS AS A MATRIX: one row per surface, defaults on the
   left, enforcement on the right.

   ROW COUPLING IS DELIBERATE HERE. `RF-20260903-14` replaced a grid with independent columns
   precisely to STOP two unrelated cards sharing a row height; this does the opposite on purpose,
   because the two cells of a row are the two halves of one decision about one surface and must
   stay level with each other as either disclosure opens.

   THE HEADINGS ARE GRID ITEMS, not a separate header row, so they sit in the same two tracks the
   cells do and cannot drift out of alignment with them. */
.rte__matrix {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2) var(--space-4);
  align-items: start;
}
.rte__matrixhead {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--text);
}
@media (max-width: 60rem) {
  /* ONE COLUMN, AND THE HEADINGS GO. Each disclosure's own summary already reads "Task description
     — site default" / "— site enforcement", so the column headings would be a second, now-wrong
     answer to a question the row already answers for itself. Nothing is lost by removing them. */
  .rte__matrix { grid-template-columns: 1fr; }
  .rte__matrixhead { display: none; }
}
.rte__intro { display: flex; flex-direction: column; gap: var(--space-2); }
.rte__intro .card__hint { max-width: none; }

/* THE PER-SURFACE / PER-TIER DISCLOSURE. A native <details>, so it is keyboard-operable, announced
   as expandable and reachable by find-in-page without a line of JavaScript. */
.rte__disclosure { padding: 0; }
.rte__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  cursor: pointer;
  border-radius: var(--radius);
}
.rte__summary:hover { background: var(--surface-sunken); }
.rte__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.rte__summarytitle { font-weight: 600; margin-right: auto; }
.rte__summarynote { font-size: var(--text-sm); color: var(--text-muted); }
.rte__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  border-top: 1px solid var(--border);
}
.rte__previewtitle { margin: 0; font-size: var(--text-sm); font-weight: 600; color: var(--text-muted); }

/* THE PREVIEW IS A PICTURE OF A TOOLBAR, and it is allowed to scroll rather than force the page to.
   Every control inside it is already disabled by `setDisabled(true)`, so nothing here is reachable
   by pointer or keyboard. */
.rte__preview { display: flex; flex-direction: column; gap: var(--space-2); }
.rte__previewrow {
  padding: var(--space-2);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  overflow-x: auto;
}
.rte__custom { display: flex; flex-direction: column; gap: var(--space-3); }
.rte__list { display: block; }
.rte__showlabel { font-size: var(--text-sm); color: var(--text-muted); }
.rte__restore { gap: var(--space-2); flex-wrap: wrap; }
.rte__enforcerow { display: flex; align-items: center; gap: var(--space-2); }

/* A ROW THAT CANNOT BE MOVED, and looks it. The row itself stays fully usable — its visibility
   checkbox is the reason it is listed at all — so only the handle is dimmed, never the content. */
.reorder__row--locked .reorder__label { color: var(--text-muted); }

/* NARROW SCREENS. A reorder row is a handle, a name, a chip and a checkbox; below roughly 30rem the
   trailing content wraps under the name rather than squeezing the name to nothing, which is the
   same answer `.vocabreorder` already gives for the same shape. */
@media (max-width: 30rem) {
  .rte__list .reorder__row { flex-wrap: wrap; }
  .rte__list .reorder__trail { margin-left: 0; }
  .rte__summarytitle { margin-right: 0; flex: 1 1 100%; }
}

/* ---------- RF-20260827-18: a chart, and the table that is its real answer ---------- */

/* THE TABLE IS THE CONTENT; THE CHART IS A SHAPE. They sit side by side where there is room and
   stack where there is not, and the chart is capped so it cannot grow into the page — a pie the
   size of a monitor tells nobody anything a small one does not. */
.chart {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
  margin-top: var(--space-3);
}

@media (max-width: 44rem) {
  .chart { grid-template-columns: minmax(0, 1fr); justify-items: center; }
}

.chart__pie { width: 100%; max-width: 12rem; height: auto; }

/* A HAIRLINE BETWEEN SLICES, in the card's own background rather than a fixed colour, so two
   adjacent slices stay separable in both modes and at any palette. */
.chart__slice { stroke: var(--surface); stroke-width: 1; }

/* The swatch that ties a table row to its slice. Square rather than round, because a round one
   at this size reads as a bullet and disappears into the text beside it. */
.chart__key {
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-right: var(--space-2);
  border-radius: 2px;
  vertical-align: baseline;
}

.chart__slice--1 { fill: var(--chart-1); }
.chart__slice--2 { fill: var(--chart-2); }
.chart__slice--3 { fill: var(--chart-3); }
.chart__slice--4 { fill: var(--chart-4); }
.chart__slice--5 { fill: var(--chart-5); }
.chart__slice--6 { fill: var(--chart-6); }
.chart__slice--7 { fill: var(--chart-7); }
.chart__slice--8 { fill: var(--chart-8); }

.chart__key--1 { background: var(--chart-1); }
.chart__key--2 { background: var(--chart-2); }
.chart__key--3 { background: var(--chart-3); }
.chart__key--4 { background: var(--chart-4); }
.chart__key--5 { background: var(--chart-5); }
.chart__key--6 { background: var(--chart-6); }
.chart__key--7 { background: var(--chart-7); }
.chart__key--8 { background: var(--chart-8); }

/* RF-20260902-01 — A FIELD NOTICE: true, worth knowing, and not a refusal.
 *
 * Distinct from `.field__reason`, which explains why a control is switched OFF, and from
 * `.field__help`, which is standing explanation. This is a statement about the value somebody has
 * just entered — a task due after its project's deadline — and the save goes through. Warning
 * colour rather than danger, because nothing has gone wrong. */
.field__notice {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  color: var(--color-warning);
}
.field__notice[hidden] { display: none; }

/* RF-20260902-01 — the Urgent flag row. Same shape as MSR and Add to backlog: the row comes from
   `.field--check`, and this rule only says it fills the cell it is given. See `.tf-backlog`. */
.tf-urgent { display: flex; width: 100%; }
.tf-urgent__why[hidden] { display: none; }

/* RF-20260902-01 — THE PREVIEW SURFACE. A read of the task as it currently stands, including
   values nobody has saved yet. Presentable rather than "the form with everything disabled": a
   disabled form is still a form, and reading one is the thing this exists to avoid. */
.tf-preview { display: flex; flex-direction: column; gap: var(--space-4); }
.tf-preview__flags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; }
.tf-preview__block { display: flex; flex-direction: column; gap: var(--space-2); }
.tf-preview__heading {
  margin: 0;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-subtle);
}
/* The facts read as a list of answers, two columns on anything but a phone. */
.tf-preview__facts { margin: 0; }

/* RF-20260903-01 — A LEADING CONTROL IN THE LABEL ROW, and a state badge beside the label.
 *
 * THE ROW'S HEIGHT IS STILL THE LABEL'S, which is the constraint everything here answers to.
 * `beta.346` established that Project and Title line up because their label rows are the same
 * height as a bare label; a button or a badge that made this row taller would put Project's
 * selector back below Title's input, which is the defect that fix exists to prevent. Both are
 * sized under the row's line box and neither adds block spacing. */
.field__lead {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A square, rounded, icon-only control — small enough to sit under a 22px label row, large
     enough to press on a phone. */
  width: 1.375rem;
  height: 1.375rem;
  min-height: 0;
  padding: 0;
  font-size: var(--text-xs);
  line-height: 1;
  border-radius: var(--radius-sm, 4px);
}
/* NEITHER OF THEM MAY GROW THE ROW, and negative block margins are what guarantees it rather
 * than hoping their box happens to be smaller than the label's line.
 *
 * MEASURED WITHOUT THIS: Project's control sat 1px below Title's, and the ↻ appearing pushed the
 * Title input down 2px. Both are small and both are the exact defect `beta.346` fixed — a label
 * row that is taller than its label puts the two controls out of line, which is the thing the eye
 * notices on a row where two fields sit side by side.
 *
 * `align-items: center` keeps them optically centred on the label; the negative margins mean the
 * row's height is the LABEL's height, whatever is riding beside it. */
.field__labelrow > .field__lead,
.field__labelrow > .field__badge {
  align-self: center;
  margin-block: -0.5rem;
}
/* RF-20260903-04 — THE FLAG CHECKBOXES PUT THEIR HELP ICONS IN ONE COLUMN.

   Terry: *"The Urgent checkbox, label, and tooltip icon should be aligned with the Important
   checkbox, label, and tooltip icon."* The box and the word already lined up — `.field--check`
   guarantees that. The ICON did not, and could not, because it follows the label TEXT and
   "Important" is three characters longer than "Urgent": measured at x 958 against x 938.

   PUSHED TO THE END OF THE ROW rather than given a magic label width. A fixed `min-width` in
   characters would line these two up and break on the next flag somebody adds, or on a longer
   word in another language. An auto inline-start margin puts every flag's icon on the same edge
   whatever the labels say — the alignment that was asked for, and the one that survives a new
   field.

   THE ROW HAS TO FILL THE FIELD FIRST, which is the half the first attempt missed and the reason
   this is worth writing down. `.field__labelrow` was already `display: flex`, but it sits inside
   `.field__check-body`, a column flex item that shrinks to its content — so the row was 92px wide
   for Important and 72px for Urgent, and an `auto` margin inside a box exactly as wide as its
   contents has nothing to distribute. The field itself was 300px the whole time. Both the body
   and the row are told to take the width they are offered, and only then does the icon have
   somewhere to be pushed to.

   SCOPED TO `field--flag`, which is the width these compact flags declare — Important, Urgent,
   MSR, Archived. Every other checkbox in the application keeps its icon beside its word. */
.field--flag.field--check .field__check-body { flex: 1 1 auto; width: 100%; }
.field--flag .field__labelrow { flex-wrap: nowrap; width: 100%; }
.field--flag .field__labelrow > .field__label { flex: 0 1 auto; }
.field--flag .field__labelrow > .helpicon { margin-inline-start: auto; flex: none; }
.field__leadglyph { pointer-events: none; }
/* A disabled leading control is still visible and still explains itself — see the `title` set in
   `taskform.js`. Muted rather than removed, because a control that vanishes when it cannot act
   moves the row underneath somebody's cursor the moment it can. */
.field__lead:disabled { opacity: 0.45; cursor: not-allowed; }

/* THE STATE BADGE. It reports; it is not pressable and takes no tab stop. `chip--muted` is the
   existing vocabulary for exactly this — the Tasks grid already marks a repeating task with the
   same glyph in the same chip. */
.field__badge { flex: none; line-height: 1; }
.field__badge[hidden] { display: none; }

/* RF-20260904-08 — THE ANNOUNCEMENT'S CHARACTER COUNT.
   Ordinary hint text until it matters, then it says so plainly. The colour is the danger token
   rather than a new one, because "you cannot save this" is the same fact every other refusal on
   this screen reports, and it is paired with words rather than carried by colour alone. */
.announce__count { font-variant-numeric: tabular-nums; }
.announce__count--over { color: var(--color-danger); font-weight: 600; }

/* RF-20260904-11 — THE SECOND TAB ROW, INDENTED UNDER THE SURFACE IT BELONGS TO.
   Two rows of equally-placed tabs would read as six siblings; the indent is what makes the
   nesting legible without a border or a box drawing it. */
.tabs--nested { margin-left: var(--space-4); margin-top: calc(var(--space-2) * -1); }

/* RF-20260904-12 — A PANE AND THE LIMITS THAT GOVERN IT, SIDE BY SIDE.
   The limits used to be a tab of their own, which meant leaving the editor to change the number
   that constrained it. They belong beside the thing they bound, narrow enough to read as a
   secondary control rather than as a second subject. */
.announce__split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 15rem;
  gap: var(--space-4);
  align-items: start;
}
.announce__limits {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-2);
}
.announce__limits-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* THE SAME WIDTH, WHICH TAKES `border-box` TO ACTUALLY MEAN. A text input and a select carry
   different intrinsic padding and different default sizing, so two controls both set to
   `width: 100%` still landed on different edges — visible immediately as a ragged right in a
   column only fifteen rems wide. */
.announce__limits .field__input,
.field--compact .field__input {
  width: 100%;
  box-sizing: border-box;
  /* AND THE `max-width` THAT WAS ACTUALLY CAUSING IT. `.field__input[type="number"]` caps every
     number field at 11rem product-wide, which is right for a number sitting in a wide form and
     wrong inside a 15rem column beside a full-width select — the two controls landed on different
     right edges. Setting `width: 100%` alone did not fix it and could not: `max-width` is a
     different property and was still clamping. */
  max-width: none;
}
.field__hint { margin: var(--space-1) 0 0; font-size: var(--text-xs); color: var(--text-muted); }

/* THE NESTED TAB ROW, indented under the surface it belongs to so two levels read as a
   hierarchy rather than as four equal tabs. */
.tabs--nested { margin-left: var(--space-5); }

/* NARROW SCREENS PUT THE LIMITS UNDERNEATH rather than squeezing both. */
@media (max-width: 900px) {
  .announce__split { grid-template-columns: minmax(0, 1fr); }
  .tabs--nested { margin-left: var(--space-3); }
}

/* RF-20260904-14 — A COLOUR CIRCLE BESIDE A PALETTE'S NAME.
   Twenty-one themes is more than anybody reads word by word; the circle is what makes the list
   scannable. A ring rather than a bare disc so a pale swatch still has an edge against the menu. */
.menu__swatch {
  flex: 0 0 auto;
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 50%;
  background: var(--menu-swatch, var(--accent));
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.25);
}
.menu__tick {
  margin-left: auto;
  padding-left: var(--space-2);
  color: var(--accent);
  font-weight: 700;
}
.menu__item--checked .menu__label { font-weight: 600; }
.menu__item--choice {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
}

/* RF-20260904-14 — THE THEME DROPDOWN.
   Twenty-one palettes as twenty-one menu rows pushed Sign out below the fold. One row that opens
   on demand, and the list scrolls rather than growing the menu without limit. */
.themepick { display: flex; flex-direction: column; gap: var(--space-1); width: 100%; }
.themepick__toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
  text-align: left;
}
.themepick__toggle:hover,
.themepick__toggle:focus-visible { background: var(--surface-raised, var(--surface)); }
.themepick__name { flex: 1 1 auto; }
.themepick__caret { flex: 0 0 auto; color: var(--text-muted); }
.themepick__list {
  display: flex;
  flex-direction: column;
  max-height: 14rem;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.themepick__list[hidden] { display: none; }
.themepick__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2);
  background: none;
  border: 0;
  color: var(--text);
  cursor: pointer;
  text-align: left;
}
.themepick__option:hover,
.themepick__option:focus-visible { background: var(--surface-raised, var(--surface)); }
.themepick__option--current { font-weight: 600; }
.menu__custom { padding: var(--space-1) var(--space-2); }

/* RF-20260904-16 — A GRID CELL THAT READS AS TEXT AND EDITS IN PLACE.
   Terry: *"the column/field values when not edited should show as text (and their approp colors)
   but outlines for the field so the user knows they can click them."* So the OUTLINE is the
   affordance and the CONTENT is unchanged — the same priority bars, effort chip and red overdue
   date the board and the matrix draw. Replacing them with dropdowns would have turned a scannable
   table into a wall of form controls and thrown away the colour that makes it readable. */
.celledit {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  /* RF-20260904-17 — SIZED TO ITS CONTENT, NEVER STRETCHED.
     Terry: *"it shoves content to the right... as its hiding the actions column"*. `width: 100%`
     made every one of these boxes claim the full column, and three columns of boxes plus their
     padding widened the table until Actions was pushed off the right edge. A cell that reads as
     text must occupy what the text occupies. */
  width: auto;
  max-width: 100%;
  min-height: 1.75rem;
  padding: 2px var(--space-1);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* A RESTING OUTLINE THAT DOES NOT SHOUT. Twenty rows times three columns is sixty boxes; at full
   border strength the grid becomes a spreadsheet and the data stops being the thing you see. */
.celledit { border-color: color-mix(in srgb, var(--border) 55%, transparent); }
.celledit:hover { border-color: var(--control-border-hover, var(--border)); background: var(--surface-raised, var(--surface)); }
.celledit:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* THE CELL INHERITS THE COLUMN'S COLOUR — an overdue date stays red inside its outline, because
   the outline says "editable" and says nothing about the value. */
.cell--overdue .celledit__text { color: inherit; }
.celledit__text { display: inline-block; }

/* RF-20260904-17 — AN OPEN EDITOR MUST NOT RESIZE ITS COLUMN.
   Swapping a compact chip for a `<select>` re-laid-out the whole table mid-interaction: the row
   jumped sideways under the pointer, and every other column shifted with it. Bounded here so the
   editor occupies the space the value already did. */
.table td .field__input--inline {
  max-width: 100%;
  min-width: 0;
}
.cell--due .field__input--inline { max-width: 100%; }
