/* ============================================================
   PALETTE TOKENS — one block per (palette × theme).
   Components read only these tokens, never raw hex.
   Values copied verbatim from mockup/palettes.html — already compared and
   approved there (see PALETTES.md), not re-derived here.
   ============================================================ */

/* fallback defaults = Paper & Ink, light */
:root,
:root[data-palette="paper"][data-theme="light"] {
  --paper: #FAF9F6; --surface: #FFFFFF; --surface-2: #F3F2ED; --line: #E6E4DE; --line-soft: #EEEDE8;
  --ink: #23221F; --ink-2: #57544E; --ink-3: #8C887F;
  --accent: #35526E; --accent-ink: #2B4157; --accent-weak: rgba(53,82,110,0.10);
  --done: #5E8C61; --done-weak: rgba(94,140,97,0.12); --flag: #B5763E;
  --shadow: 0 1px 2px rgba(30,28,24,0.05), 0 8px 24px rgba(30,28,24,0.06);
}
:root[data-palette="paper"][data-theme="dark"] {
  --paper: #1A1B1E; --surface: #212327; --surface-2: #2A2D32; --line: #33363B; --line-soft: #2A2D31;
  --ink: #ECEAE4; --ink-2: #A8A49B; --ink-3: #6F6C65;
  --accent: #7FA6CE; --accent-ink: #A9C6E4; --accent-weak: rgba(127,166,206,0.15);
  --done: #7CB07F; --done-weak: rgba(124,176,127,0.16); --flag: #D19A63;
  --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 10px 30px rgba(0,0,0,0.35);
}

/* --- Cobalt: crisp white + sage, saturated blue, green done --- */
:root[data-palette="cobalt"][data-theme="light"] {
  --paper: #F5F6F2; --surface: #FFFFFF; --surface-2: #EAEDE4; --line: #DBDFD3; --line-soft: #E6E9DF;
  --ink: #16283A; --ink-2: #4A5A6B; --ink-3: #8592A0;
  --accent: #2F7FD1; --accent-ink: #256BB4; --accent-weak: rgba(67,143,223,0.12);
  --done: #4CA357; --done-weak: rgba(103,197,105,0.16); --flag: #C97A3C;
  --shadow: 0 1px 2px rgba(16,40,58,0.06), 0 8px 24px rgba(16,40,58,0.07);
}
:root[data-palette="cobalt"][data-theme="dark"] {
  --paper: #0B1B2B; --surface: #10263B; --surface-2: #163049; --line: #22405A; --line-soft: #142B41;
  --ink: #E4ECF5; --ink-2: #9DB0C4; --ink-3: #6A7E92;
  --accent: #5FA4E8; --accent-ink: #8BBEF0; --accent-weak: rgba(95,164,232,0.18);
  --done: #67C569; --done-weak: rgba(103,197,105,0.18); --flag: #E0A05E;
  --shadow: 0 1px 2px rgba(0,0,0,0.35), 0 10px 30px rgba(0,0,0,0.45);
}

/* --- Meadow: green as the star accent, sage neutrals --- */
:root[data-palette="meadow"][data-theme="light"] {
  --paper: #F4F6F0; --surface: #FFFFFF; --surface-2: #E8ECE0; --line: #D8DECB; --line-soft: #E4E8DA;
  --ink: #1F2A20; --ink-2: #4E5A4E; --ink-3: #86917F;
  --accent: #3F9E55; --accent-ink: #348247; --accent-weak: rgba(103,197,105,0.16);
  --done: #2E7D5B; --done-weak: rgba(46,125,91,0.15); --flag: #C67F3A;
  --shadow: 0 1px 2px rgba(31,42,32,0.06), 0 8px 24px rgba(31,42,32,0.07);
}
:root[data-palette="meadow"][data-theme="dark"] {
  --paper: #14211A; --surface: #1B2C22; --surface-2: #24382C; --line: #2F4638; --line-soft: #1E3226;
  --ink: #E6EEE4; --ink-2: #A3B2A2; --ink-3: #6C7A6B;
  --accent: #67C569; --accent-ink: #8FD98F; --accent-weak: rgba(103,197,105,0.18);
  --done: #57B98F; --done-weak: rgba(87,185,143,0.16); --flag: #DDA05E;
  --shadow: 0 1px 2px rgba(0,0,0,0.4), 0 10px 30px rgba(0,0,0,0.5);
}

/* Which scheme the browser's own widgets should use.
   Without this the root computes `color-scheme: normal`, so Chromium renders
   native controls — the <select> dropdown, the date and time pickers,
   scrollbars — in its light chrome regardless of the theme, and falls back to
   an "author-styled" path for the select popup that paints an option's hover
   highlight only as wide as the option's own intrinsic box. Our selects are
   stretched to the pane by `width: 100%` (560px against a ~146px intrinsic
   width), so that highlight visibly stopped short of the row.
   Declaring the scheme puts those controls back on the native path, where the
   highlight spans the row — and gets the pickers and scrollbars right as a
   side effect. Keyed off data-theme, so it holds for every palette. */
:root { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* ============================================================
   BASE LAYOUT — structural styles shared by every palette/font combo.
   Mirrors mockup/index.html's proven markup shape: .app > .topbar + .board
   (three .col sections), rebuilt here as the real app's CSS.
   ============================================================ */

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ff-ui);
  -webkit-font-smoothing: antialiased;
}

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

.topbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.logout-form { margin: 0; }
.topbar .brand {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  font-size: var(--wordmark-size);
  margin-right: 1rem;
}
.space-tab {
  /* These render as <button> on the board and as <a> on the Overview — same
     pill, different navigation (see space_tabs.html). The next three
     declarations are what make the two identical, and none is decoration:

     * text-decoration — an <a> is underlined by default.
     * font-size — a <button> does NOT inherit the page's font size; the UA
       stylesheet gives it its own (13.33px in Chromium), while the <a>
       inherited 16px. That alone made the Overview's pills 10px wider and 3px
       taller, which is the visible bug this fixes. 0.85rem is the nearest
       value on the house scale to what the board has always rendered, so the
       board is unchanged to the eye and no longer depends on a UA default.
     * display — an <a> is inline, so its vertical padding does not contribute
       to the line box and the pill's height is not its own to control.

     walkthrough:space-overview step 5b.10 measures both screens and would catch
     any of the three being dropped. */
  text-decoration: none;
  font-size: 0.85rem;
  display: inline-block;
  font-family: var(--ff-ui);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  cursor: pointer;
}
/* The strip's one management control (BACKLOG.md #27). Deliberately quieter
   than a Space: it is a way *out* of the board, not another context to be in,
   and at equal weight it read as a Space named "+". Squared off and narrower
   so the row's rhythm says "this one is different" before you read it. */
.space-tab--add {
  padding-left: 0.55rem;
  padding-right: 0.55rem;
  color: var(--ink-3);
  border-style: dashed;
  /* .space-tab sets these for the button/link parity described above; the
     add pill is always an <a>, but it must still match the pills' height. */
  line-height: 1;
}
.space-tab--add:hover { color: var(--accent); border-color: var(--accent); }

.space-tab[aria-current="true"] {
  border-color: var(--accent);
  color: var(--accent-ink);
  background: var(--accent-weak);
}

/* Three columns and, between them, two zero-width tracks that hold the
   dividers (BACKLOG.md #40). The widths are variables so base.html can set them
   when a divider is dragged. It sets the variables, never grid-template-columns
   itself, so the phone layout's one-column rule below still wins with nothing
   to undo. They are fr units, set from the columns' pixel widths, so a saved
   layout keeps its proportions as the window changes size. The minimums stop a
   column being dragged to nothing, a column you could not drag back. The
   defaults are the split the board always had. */
.board {
  --w-projects: 1fr;
  --w-tasks: 1fr;
  --w-detail: 1.4fr;
  --min-projects: 11rem;
  --min-tasks: 15rem;
  --min-detail: 20rem;
  display: grid;
  grid-template-columns:
    minmax(var(--min-projects), var(--w-projects)) 0
    minmax(var(--min-tasks), var(--w-tasks)) 0
    minmax(var(--min-detail), var(--w-detail));
  flex: 1;
  min-height: 0;
}

/* The divider itself has no width. It sits on the columns' shared border, and
   what the pointer grabs is a 10px band either side of it, **only as tall as
   the column headings** (PROJECTS / TASKS / DETAIL). The first build made the
   whole border grabbable, and the owner found the accent line flashing as the
   pointer crossed the board on its way anywhere else (2026-09-19). A faint
   grip marks the spot at all times, since a smaller target has to be seen to
   be found; hover and keyboard focus turn it accent. Only while dragging does
   the line run the full height, to show where the column edge will land. */
.col-divider {
  position: relative;
  z-index: 4;
  width: 0;
  outline: none;
}
.col-divider::before {
  content: "";
  position: absolute;
  top: 0;
  height: var(--col-head-h);
  left: -5px;
  width: 10px;
  cursor: col-resize;
}
.col-divider::after {
  content: "";
  position: absolute;
  top: calc(var(--col-head-h) / 2 - 0.5rem);
  height: 1rem;
  left: -2px;
  width: 4px;
  border-radius: 2px;
  /* The muted text colour, softened, rather than --line: in the dark themes
     --line is barely distinguishable from the surface, and a grip nobody can
     see is no better than no grip. */
  background: var(--ink-3);
  opacity: 0.6;
  transition: background-color 0.12s ease, opacity 0.12s ease;
}
.col-divider:hover::after,
.col-divider:focus-visible::after { background: var(--accent); opacity: 1; }
.col-divider.is-dragging::after {
  top: 0;
  height: 100%;
  left: -1px;
  width: 3px;
  border-radius: 0;
  background: var(--accent);
  opacity: 1;
}
/* While dragging, the whole page shows the resize cursor and selects no text,
   or sweeping across a column would highlight its contents. */
body.is-resizing { cursor: col-resize; user-select: none; }
@media (prefers-reduced-motion: reduce) {
  .col-divider::after { transition: none; }
}
.col {
  border-right: 1px solid var(--line);
  overflow-y: auto;
  min-width: 0;
}
.col:last-child { border-right: none; }
.board { --col-head-h: 2.6rem; }
/* A fixed height, not padding around the label, so .col-title below can stick
   exactly underneath it (BACKLOG.md #38). The same height the padding gave. */
.col-head {
  height: var(--col-head-h);
  display: flex;
  align-items: center;
  padding: 0 1rem;
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 3;
}

/* --- Which task is showing (BACKLOG.md #38) ------------------------------
   A name that sticks just under a column's heading. Only the Detail pane uses
   it: the owner tried the same over the Tasks column, naming its project, and
   found the marked project row enough. The Detail pane's (--task) has no
   height of its own and hangs its name below, visible only once the title has
   scrolled away; task_detail.html says why it must not take up space. It wraps
   rather than truncating (the owner's call). The base .col-title rule is kept
   whole so a heading over the Tasks column can come back as one element if
   the Projects column ever becomes collapsible. */
.col-title,
.col-title__inner {
  padding: 0.5rem 1rem 0.55rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  font-size: 1.05rem;
  line-height: 1.3;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.col-title {
  position: sticky;
  top: var(--col-head-h);
  z-index: 2;
  margin: -0.5rem -0.5rem 0.5rem;
}
.col-title--task {
  height: 0;
  padding: 0;
  margin: 0 -0.5rem;
  border: 0;
  background: none;
}
.col-title__inner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  box-shadow: 0 2px 6px -4px rgba(0, 0, 0, 0.25);
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.12s ease, visibility 0.12s;
}
.col-title--task.is-shown .col-title__inner { visibility: visible; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .col-title__inner { transition: none; }
}
.col-head .label {
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.col-body { padding: 0.5rem; }

.empty-state {
  padding: 2rem 1rem;
  color: var(--ink-3);
  text-align: center;
}

.row {
  display: block;
  width: 100%;
  text-align: left;
  /* An unbroken name (a URL, a long code) breaks rather than running off the
     edge of its column (BACKLOG.md #39). */
  overflow-wrap: anywhere;
  padding: 0.6rem 0.75rem;
  border: none;
  background: transparent;
  border-radius: 8px;
  color: var(--ink);
  cursor: pointer;
  font-family: var(--ff-ui);
}
.row:hover { background: var(--surface-2); }
/* The selected project and task (BACKLOG.md #38). The fill alone was a pale
   grey in Paper & Ink, easy to miss, and the ask was to know "at a glance".
   The bar gives it an edge that reads from across the board. Set by base.html's
   syncSelection(), never by hand. */
.row[aria-current="true"] {
  background: var(--accent-weak);
  color: var(--accent-ink);
  box-shadow: inset 3px 0 0 var(--accent);
}

.t-title { font-family: var(--ff-ui); }
.notes { font-family: var(--ff-ui); color: var(--ink-2); white-space: pre-wrap; }
/* The drill-down breadcrumb (ISSUES.md #2) exists only below 860px. Hidden by
   default so that adding it changed nothing about the desktop board — the
   markup ships on every screen, the narrow-width block below reveals it. */
.crumbs { display: none; }

.d-breadcrumb {
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.d-title {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  font-size: var(--title-size);
  margin: 0.25rem 0 1rem;
  /* Also the Task edit form's title <input> (tasks/forms.py TaskForm) --
     override input chrome so it still reads as a heading. */
  border: none;
  background: transparent;
  outline: none;
  color: var(--ink);
  width: 100%;
}
.t-title.done, .d-title.done { text-decoration: line-through; color: var(--ink-3); }
/* The title since BACKLOG.md #39: a one-row textarea that grows, so a long title
   wraps instead of scrolling invisibly inside an <input>. base.html sizes it.

   **This keeps the look the title actually had, not the one the rule above
   describes.** That rule was written to make the input "read as a heading", but
   the shared field rule's input[type="text"] outranks a lone .d-title, so it
   never did. The title shipped, and was used for weeks, as a bordered field in
   the UI face. A bare textarea is weaker than .d-title, so without this
   the switch would have silently restyled the title into a heading nobody
   asked for. Whether it *should* read as a heading is a separate, open call. */
textarea.d-title {
  display: block;
  font-family: var(--ff-ui);
  font-weight: normal;
  font-size: 1rem;
  line-height: 1.4;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
  background: var(--surface);
  resize: none;
  overflow: hidden;
}
.field-label {
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.btn-primary {
  font-family: var(--ff-ui);
  background: var(--accent);
  color: var(--surface);
  border: none;
  border-radius: 6px;
  padding: 0.5rem 1rem;
  cursor: pointer;
}
/* The outlined partner to .btn-primary, for the second of two actions.
   The outline is an inset shadow rather than a border so the two buttons are
   exactly the same height side by side; a 1px border would make this one 2px
   taller. First used by Flow's Won't do (BACKLOG.md #33). */
.btn-secondary {
  font-family: var(--ff-ui);
  background: transparent;
  color: var(--ink);
  border: none;
  box-shadow: inset 0 0 0 1px var(--line);
  border-radius: 6px;
  padding: 0.5rem 1rem;
  cursor: pointer;
}
.btn-secondary:hover { box-shadow: inset 0 0 0 1px var(--ink-3); background: var(--surface-2); }

/* The small round pill the top bar's glyph controls share — the theme toggle,
   the search link and the app menu's button.

   It was called .theme-toggle until 2026-09-09, when adding the menu button
   made "generic pill style" and "the theme toggle" two different things that
   had been sharing one name. `button.theme-toggle` had been a unique locator
   only by accident (the search control is an <a>), and the menu button broke
   it. Renaming the shared half fixes that at the root: .theme-toggle now means
   the theme toggle and nothing else. */
.icon-btn {
  border: 1px solid var(--line);
  background: var(--surface);
  border-radius: 999px;
  padding: 0.4rem 0.7rem;
  cursor: pointer;
  color: var(--ink-2);
}

/* Shared form styling — used by the board's own forms later and by
   two_factor's login/setup pages today (see templates/two_factor/_base.html). */
.two-factor-page {
  max-width: 24rem;
  margin: 4rem auto;
  padding: 0 1rem;
}

/* The sign-in card (templates/two_factor/). two_factor's own templates assume
   Bootstrap, which this app never loads, so until 2026-09-24 the login page
   was a bare table with a greyed-out Back button. These give it the app's
   wordmark, a panel, labels above fields and the app's own buttons. */
.auth-brand {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  font-size: 1.35rem;
  text-align: center;
  margin: 0 0 1.25rem;
  color: var(--ink);
}
.auth-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 1.6rem 1.5rem 1.4rem;
  box-shadow: var(--shadow);
}
.auth-title {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  font-size: 1.6rem;
  margin: 0 0 0.3rem;
}
.auth-sub { font-family: var(--ff-ui); color: var(--ink-3); font-size: 0.88rem; margin: 0 0 1.2rem; }
.auth-field { margin: 0 0 1rem; }
.auth-field label {
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-2);
  margin: 0 0 0.35rem;
}
.auth-field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.auth-field-error, .auth-error p { font-family: var(--ff-ui); color: var(--flag); font-size: 0.82rem; margin: 0.35rem 0 0; }
.auth-error {
  border: 1px solid var(--flag);
  border-radius: 6px;
  padding: 0.1rem 0.75rem 0.45rem;
  margin: 0 0 1rem;
}
.auth-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1.4rem;
}
.auth-buttons { display: flex; flex: 1; justify-content: flex-end; gap: 0.5rem; }
/* The first step has only Next: it takes the whole row, like a sign-in button. */
.auth-buttons > .btn-primary:only-child { flex: 1; }
.auth-actions .btn-primary, .auth-actions .btn-secondary, .auth-wide { font-size: 0.95rem; padding: 0.6rem 1.1rem; }
.auth-actions .btn-primary:only-child { padding: 0.65rem 1rem; }
.auth-wide { width: 100%; margin: 0 0 0.5rem; }
.auth-link {
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-3);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: none;
}
.auth-link:hover { color: var(--ink); text-decoration: underline; }
.auth-backup { margin: 1rem 0 0; text-align: center; }
/* The app's own sign-in (templates/allauth/, templates/account/), which uses
   the same card. Its forms stack one full-width button per row rather than
   two_factor's Back/Next pair: "Sign in with a passkey", "or", then the
   email form and its "Email me a code". */
.auth-actions--stack { flex-direction: column; align-items: stretch; gap: 0.6rem; margin-top: 0.4rem; }
.auth-actions--stack .auth-wide, .auth-card > .auth-wide { margin: 0; }
.auth-actions--stack .auth-link { align-self: center; }
a.auth-button-link { display: block; box-sizing: border-box; text-align: center; text-decoration: none; }
.auth-button-group { display: flex; flex-direction: column; gap: 0.6rem; }
.auth-or {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.1rem 0;
  font-family: var(--ff-ui);
  font-size: 0.8rem;
  color: var(--ink-3);
}
.auth-or::before, .auth-or::after { content: ""; flex: 1; border-top: 1px solid var(--line); }
.auth-row { display: flex; justify-content: center; gap: 1.25rem; margin: 1rem 0 0; }
.auth-below { font-family: var(--ff-ui); font-size: 0.88rem; color: var(--ink-3); text-align: center; margin: 1.1rem 0 0; }
.auth-below a { color: var(--accent); }
.auth-sub strong { color: var(--ink-2); font-weight: 600; word-break: break-all; }
.auth-heading { font-family: var(--ff-ui); font-size: 1rem; margin: 1.2rem 0 0.4rem; }
.auth-messages { margin: 0 0 1rem; }
.auth-message {
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-2);
  background: var(--surface-2);
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
  margin: 0 0 0.4rem;
}
.auth-message--error { color: var(--flag); border: 1px solid var(--flag); background: none; }
.auth-details { margin: 1rem 0 0; }
.auth-details summary { display: block; text-align: center; list-style: none; }
.auth-details summary::-webkit-details-marker { display: none; }
.auth-details[open] summary { margin-bottom: 0.8rem; }
/* templates/404.html: the sign-in card with one way out. Its action is a link,
   not a button, so it needs a button's box and no underline. 500.html cannot
   use any of this; it carries its own copy inline. */
.error-page .auth-sub { line-height: 1.45; }
a.error-page-action { display: block; box-sizing: border-box; text-align: center; text-decoration: none; margin: 0; padding: 0.65rem 1rem; }
input[type="text"], input[type="password"], input[type="email"],
input[type="date"], input[type="time"], select, textarea {
  font-family: var(--ff-ui);
  font-size: 1rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
  background: var(--surface);
  color: var(--ink);
  width: 100%;
}
label { font-family: var(--ff-ui); color: var(--ink-2); }

/* ============================================================
   PHASE 3 — CRUD controls: quick-add, checkboxes, cancel/delete,
   task grouping, the detail-pane form, and the undo toast.
   ============================================================ */

.quick-add { padding: 0.4rem 0.75rem; }
.quick-add input {
  border-style: dashed;
  background: transparent;
}

.row-wrap { display: flex; align-items: center; gap: 0.25rem; }
.row-wrap .row { flex: 1; min-width: 0; }

.checkbox, .d-check {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface);
  color: var(--done);
  cursor: pointer;
  line-height: 1;
  padding: 0;
}
.checkbox { width: 1.5rem; height: 1.5rem; font-size: 0.9rem; }
.d-check { width: 2rem; height: 2rem; font-size: 1.1rem; margin-top: 0.15rem; }
.checkbox.is-done, .d-check.is-done { background: var(--done-weak); border-color: var(--done); }

.cancel-btn {
  flex: 0 0 auto;
  border: none;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.1s ease;
  padding: 0.25rem 0.5rem;
}
.row-wrap:hover .cancel-btn { opacity: 1; }
.cancel-btn:hover { color: var(--flag); }

/* --- Due dates in the Tasks column (BACKLOG.md #28) ---------------------
   The list is ordered by due date, so the date has to be visible: a column
   sorted on a value it doesn't show reads as arbitrary.

   Colour is never the only cue. Task.due_label spells out "Overdue" in words,
   because these palettes are validated for colour-vision deficiency and a chip
   that means "late" only by being orange fails that test.

   The chip sits inside .row (a flex button) and is pushed right with
   margin-left: auto. A long title *wraps* (BACKLOG.md #39, the owner's call;
   until 2026-09-18 it truncated with an ellipsis), and because the row aligns
   by baseline the chip stays level with the title's first line: the date stays
   top-right however many lines the title takes. overflow-wrap: anywhere breaks
   an unbroken string (a URL, a long code) that would otherwise run off the edge,
   which wrapping alone does not. */
.row-wrap .row { display: flex; align-items: baseline; gap: 0.5rem; }
.row-wrap .row .t-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* The checkbox, handle and ✕ are centred on the row, however many lines its
   title wraps onto: the owner's call on testing #39, matching the Detail
   pane's checkbox. First built level with the title's first line instead. The
   due date is inside .row and still sits top-right, level with the first line. */
#taskList .row-wrap { align-items: center; }

.due-chip {
  flex: 0 0 auto;
  font-family: var(--ff-meta);
  font-size: 0.7rem;
  letter-spacing: 0.02em;
  color: var(--ink-3);
  white-space: nowrap;
}
.due-overdue { color: var(--flag); font-weight: 600; }
.due-today { color: var(--accent-ink); font-weight: 600; }
/* "No date" stays quiet on the row — the heading above the group is where the
   nudge is made. Repeating it loudly on every row would turn a prompt into
   nagging, and these rows are already at the bottom under their own label. */
.due-none { color: var(--ink-3); font-style: italic; }

.undated-head {
  margin: 1rem 0 0.25rem;
  padding: 0 0.75rem;
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-top: 1px solid var(--line-soft);
  padding-top: 0.75rem;
}
/* The nudge itself: lower-case, not shouted, and on its own line so the
   heading still scans as a group label. */
.undated-hint {
  display: block;
  margin-top: 0.15rem;
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--ff-ui);
  font-size: 0.75rem;
  color: var(--ink-3);
}

.task-group { margin-top: 0.5rem; }
.task-group-head {
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0.4rem 0.75rem;
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.task-group--muted .row { color: var(--ink-3); }
.row--cancelled { text-decoration: line-through; }

/* Centred on the title box, not pinned to its top: once the title wraps
   (#39) the box grows, and a top-aligned checkbox sat above the first line of
   text, inside the box's padding (the owner's test, 2026-09-18). */
.d-title-row { display: flex; align-items: center; gap: 0.5rem; margin: 0.25rem 0 1rem; }
/* The title's own margins (uneven, top and bottom) and the checkbox's nudge
   are moved onto the row, or centring would include them and sit ~7px high. */
.d-title-row .d-title { margin: 0; }
.d-title-row .d-check { margin-top: 0; }
.d-form { display: flex; flex-direction: column; gap: 0.35rem; margin-top: 0.5rem; }
.d-form label:not(:first-child) { margin-top: 0.5rem; }

.d-actions { display: flex; gap: 1rem; margin-top: 1rem; }
/* Move to… (BACKLOG.md #37). Under the actions it sits beside in purpose: a
   deliberate, occasional thing, not an everyday field. */
.d-move { margin-top: 1rem; }
.d-move-row { display: flex; align-items: center; gap: 0.75rem; margin-top: 0.35rem; }
.d-move-row select { flex: 1; min-width: 0; }
.link-btn {
  border: none;
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  padding: 0;
}
.link-btn:hover { color: var(--ink); text-decoration: underline; }
.link-btn--danger:hover { color: var(--flag); }

/* ---------- Drag to reorder (BACKLOG.md #24) ---------- */
.drag-handle {
  /* A <button> since BACKLOG.md #18 made it the keyboard reorder control, so
     the UA's own button styling has to be undone — it arrives with a border,
     a background and its own font size, none of which a ⋮⋮ glyph wants. */
  border: none;
  background: transparent;
  font-family: inherit;
  flex: 0 0 auto;
  color: var(--ink-3);
  cursor: grab;
  opacity: 0;
  transition: opacity 0.1s ease;
  padding: 0 0.15rem;
  font-size: 0.85rem;
  letter-spacing: -0.15em;
  user-select: none;
}
.row-wrap:hover .drag-handle,
.subtask-list li:hover .drag-handle { opacity: 1; }
/* The width-holder on dated task rows (BACKLOG.md #28). visibility, not
   opacity: the hover rule above sets opacity on every .drag-handle, and this
   one must stay invisible through that while still taking up its space. */
.drag-handle--placeholder { visibility: hidden; }
/* A dated row's handle (BACKLOG.md #37): it only drags the row out onto a
   project, so the cursor says "grab" and never implies a reorder. */
.drag-handle--move-only { cursor: grab; }
/* The project a dragged task will land in, highlighted as the pointer moves
   over it. Dashed, so it reads as "drop here" rather than as a selection. */
/* While a task is dragged, every project it could go to is outlined faintly,
   so the Projects column reads as the place to drop it before the pointer
   gets there. Not the project it is already in: dropping there moves nothing. */
.board.is-moving-task #projList .row-wrap[data-id] .row:not([aria-current="true"]) {
  outline: 1px dashed var(--accent);
  outline-offset: -2px;
}
/* The #projList form outranks the faint outline above, which would otherwise
   win on specificity and make the target look like every other project. */
.row-wrap.is-drop-target .row,
.board.is-moving-task #projList .row-wrap[data-id].is-drop-target .row {
  background: var(--accent-weak);
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}
/* The Tasks column heading says what a drop will do, for the length of the
   drag only (board.html). */
.col-head .drag-hint {
  display: none;
  margin-left: auto;
  padding-left: 0.75rem;
  min-width: 0;
  /* Two lines fit the heading's fixed height, so the hint wraps rather than
     losing its second half ("stay in date order" was the half that mattered)
     at the default width. A narrowed column (#40) clips the second line. */
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  text-wrap: balance;
  line-height: 1.2;
  text-align: right;
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--accent-ink);
}
.board.is-moving-task:not(.is-moving-dated) .drag-hint--undated,
.board.is-moving-task.is-moving-dated .drag-hint--dated { display: -webkit-box; }

/* --- The collapsible Projects column (2026-10-02) --------------------------
   Collapsed, the column becomes a narrow rail that expands it again, and the
   Tasks and Detail columns share the rest by their own proportions. Driven by
   data-projects-collapsed on <html> (base.html sets it before paint). Every
   rule that reads it is inside the min-width query, so a phone, which shows
   one pane at a time anyway, never sees any of this. */
.projects-rail,
.col-title--project,
.empty-state__collapsed,
.drag-hint--undated-collapsed,
.drag-hint--dated-collapsed { display: none; }
.col-collapse {
  margin-left: auto;
  width: 1.75rem;
  height: 1.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--ink-3);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
}
.col-collapse:hover { background: var(--surface-2); color: var(--ink); }
.col-collapse:focus-visible,
.projects-rail:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.projects-rail {
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  background: var(--surface);
  color: var(--ink-3);
  cursor: pointer;
}
.projects-rail:hover { background: var(--surface-2); color: var(--ink); }
/* The chevron sits where the heading's button was, so the control does not
   jump when the column changes state. */
.projects-rail__chevron {
  display: flex;
  align-items: center;
  height: var(--col-head-h);
  font-size: 1.15rem;
}
.projects-rail__label {
  writing-mode: vertical-rl;
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
@media (min-width: 861px) {
  html[data-projects-collapsed] .board {
    grid-template-columns:
      2.5rem 0
      minmax(var(--min-tasks), var(--w-tasks)) 0
      minmax(var(--min-detail), var(--w-detail));
  }
  /* Hidden, not removed: the grid places its children in order, and without
     this divider the Tasks column would slide into its zero-width track.
     visibility also takes it out of the tab order. */
  html[data-projects-collapsed] .col-divider[data-divider="0"] { visibility: hidden; }
  html[data-projects-collapsed] .col--projects { overflow: hidden; }
  html[data-projects-collapsed] .col--projects > .col-head,
  html[data-projects-collapsed] .col--projects > .col-body { display: none; }
  html[data-projects-collapsed] .projects-rail { display: flex; }
  /* BACKLOG.md #38's heading, back now that the marked row is out of sight. */
  html[data-projects-collapsed] .col-title--project { display: block; }
  /* A reload or a Space switch leaves no project listed, and the column that
     would choose one is shut: say how to open it. */
  html[data-projects-collapsed] .empty-state__collapsed { display: inline; }
  /* Nothing to drop a task on: say how to get it back instead. */
  html[data-projects-collapsed] .board.is-moving-task .drag-hint--undated,
  html[data-projects-collapsed] .board.is-moving-task .drag-hint--dated { display: none; }
  html[data-projects-collapsed] .board.is-moving-task:not(.is-moving-dated) .drag-hint--undated-collapsed,
  html[data-projects-collapsed] .board.is-moving-task.is-moving-dated .drag-hint--dated-collapsed { display: -webkit-box; }
}
/* The handles are invisible until the row is hovered, which is fine for a
   mouse and useless for a keyboard: tabbing to a control you cannot see is
   how a focus indicator gets lost. Anything focused shows itself. */
.drag-handle:focus-visible,
.row-wrap:focus-within .drag-handle { opacity: 1; }
.drag-handle:active { cursor: grabbing; }
/* Sortable's ghostClass — the placeholder showing where the row will land. */
.is-dragging { opacity: 0.4; background: var(--accent-weak); }

.subtasks { margin-top: 1.5rem; }
.subtask-list { list-style: none; padding: 0; margin: 0.5rem 0; }
/* Centred, like the task rows (BACKLOG.md #39): a subtask long enough to wrap
   keeps its checkbox, pencil and ✕ halfway down its title. */
.subtask-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; }
/* The pencil and ✕ hide until hover, exactly like the project and task rows.
   Those rules key off .row-wrap, which a subtask <li> isn't — same behaviour,
   different container. */
.subtask-list li:hover .edit-btn,
.subtask-list li:hover .cancel-btn { opacity: 1; }
.subtask-list li .subtask-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.subtask-rename { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.25rem; }
.subtask-rename input { flex: 1; min-width: 0; padding: 0.4rem 0.5rem; }
.subtask-rename .edit-btn { opacity: 1; color: var(--done); }

.row-wrap .project-rename { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.25rem; }
.project-rename input { padding: 0.6rem 0.75rem; }
.project-rename .edit-btn { opacity: 1; color: var(--done); }

.edit-btn {
  flex: 0 0 auto;
  border: none;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.1s ease;
  padding: 0.25rem 0.5rem;
}
.row-wrap:hover .edit-btn { opacity: 1; }
.edit-btn:hover { color: var(--accent); }

.toast-container {
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
}
.toast {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--ink);
  color: var(--paper);
  border-radius: 8px;
  padding: 0.6rem 1rem;
  box-shadow: var(--shadow);
  font-family: var(--ff-ui);
}
/* .toast-link is the move toast's View (BACKLOG.md #37): a link, styled as the
   Undo beside it so the two read as a pair, and with the same contrast. */
.toast button,
.toast .toast-link {
  border: none;
  background: transparent;
  /* --paper, not --accent-ink. The toast is a deliberately *inverted* surface
     (--ink background), and --accent-ink is an accent designed to sit on the
     normal one — so it was a dark-background colour on a dark background in
     light mode, and the light-background version on a light one in dark mode.
     It failed WCAG AA in all six palette/theme combinations, worst at 1.42:1.
     --paper is the palette's designed contrast partner to --ink, which puts
     every combination above 13:1. */
  color: var(--paper);
  /* The accent used to be what marked this as clickable, so with the colour
     gone the affordance has to come from somewhere — underline plus weight,
     which does not depend on a hue reading correctly against the surface. */
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
  font-weight: 600;
}
.toast button:hover,
.toast button:focus-visible,
.toast .toast-link:hover,
.toast .toast-link:focus-visible {
  /* A visible change on hover without introducing a second colour: the label
     stays --paper and the surface behind it lifts slightly. */
  background: rgba(127, 127, 127, 0.25);
  border-radius: 4px;
}


/* --- Repeating tasks (Phase 4) ------------------------------------------- */

/* Alpine sets x-cloak on elements whose x-show hasn't been evaluated yet.
   Without this rule the weekday checkboxes and the custom-rule box flash
   into view on every Detail-pane swap before Alpine hides them again. */
[x-cloak] { display: none !important; }

.recurrence {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--line);
}

/* Indented so the conditional controls read as belonging to the Repeats
   dropdown above them rather than as more top-level task fields. */
.recurrence-detail {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding-left: 0.75rem;
  border-left: 2px solid var(--line);
  margin-left: 0.15rem;
}

.recurrence-interval { display: flex; align-items: center; gap: 0.5rem; }
.recurrence-interval input { width: 5rem; }
.recurrence-unit { font-size: 0.85rem; color: var(--ink-2); }

/* Django renders CheckboxSelectMultiple as a <ul>; make it a wrapping row of
   day chips rather than a seven-item vertical list. */
.recurrence ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
}
.recurrence ul li { display: flex; align-items: center; }
.recurrence ul label {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.85rem;
  cursor: pointer;
}

.recurrence-summary {
  margin: 0.15rem 0 0;
  font-size: 0.8rem;
  color: var(--ink-2);
  font-style: italic;
}
/* The "rule set but no due date" case — it isn't an error, but it does mean
   the task silently won't repeat, so it shouldn't read as ordinary help text. */
.recurrence-summary--warn { color: var(--ink); font-style: normal; }

.field-hint {
  margin: 0.15rem 0 0;
  font-size: 0.75rem;
  color: var(--ink-3);
}
.field-hint code {
  font-family: var(--ff-mono, monospace);
  font-size: 0.95em;
}


/* ============================================================
   NARROW SCREENS — the drill-down (PLAN.md Phase 5)
   ============================================================

   The first @media rule this stylesheet has ever carried. Until now the board
   was a hard three-column grid at every width, so a phone rendered three
   squeezed columns side by side rather than anything usable.

   Ported from mockup/index.html's proven ≤860px block rather than designed
   fresh — same breakpoint, same "collapse the grid and show one .col at a
   time off a data-pane attribute" mechanism. What is deliberately *not* ported
   is that mockup's Space treatment: it hid the Space name and left only a
   colour dot, which fights ISSUES.md #1's requirement that Spaces stay visible
   and tactile, and the real app's tabs have no dot to fall back on. The strip
   scrolls instead.
   ============================================================ */

@media (max-width: 860px) {
  /* One column, and only the pane named by .board[data-pane] is in it. */
  .board { grid-template-columns: 1fr; }
  .col { display: none; border-right: 0; }
  /* One pane at a time, so there is nothing beside it to resize against. */
  .col-divider { display: none; }
  .board[data-pane="projects"] .col--projects,
  .board[data-pane="tasks"] .col--tasks,
  .board[data-pane="detail"] .col--detail { display: flex; }

  /* BACKLOG.md #38 at phone width: the drill-down breadcrumb already names the
     project and task, so it does the job the .col-title blocks do on desktop.
     Showing both would say the same thing twice. It sticks instead, so a long
     subtask list cannot scroll it away either. */
  .col-title { display: none; }
  /* The pane is a flex row here, which stretches .col-body to exactly the
     pane's height and lets the content overflow it. A sticky element cannot
     outlive its parent's box, so the breadcrumb below would stick for one
     screen and then scroll away with it. Content height, not the pane's. */
  .col > .col-body { flex: 1 1 auto; align-self: flex-start; min-width: 0; }

  /* The column headings label three visible columns on desktop. With one pane
     on screen at a time the breadcrumb says where you are, and a heading
     reading "Tasks" above it is a wasted line on the shortest screen. */
  .col-head { display: none; }

  /* The Space strip stays on screen at every level — it is what makes the
     breadcrumb able to stop at Project > Task instead of carrying a root
     crumb, since tapping the current Space is the way back to its projects
     (ISSUES.md #1 and #2, and the synergy between them). */
  .topbar {
    flex-wrap: wrap;
    padding: 0.6rem 0.75rem;
    gap: 0.4rem 0.5rem;
  }
  .topbar .brand { margin-right: auto; }
  /* Board/Overview drops onto the Space strip's row rather than the first one:
     on the top line it pushed "Log out" onto a line of its own at 390px, which
     read as broken chrome. It keeps the desktop's spatial rule even so —
     Spaces on the left, the view toggle on the right — so the two kinds of
     navigation stay told apart at every width. The strip already scrolls, so
     it can give up the width without losing anything. */
  #boardNav {
    order: 4;
    flex: 0 0 auto;
    /* The strip beside it already takes the free space on this row. */
    margin-left: 0;
  }
  .nav { margin-left: 0.5rem; }
  #spaceTabs {
    order: 3;
    /* min-width is what pushes this onto its own row on a phone: the first row
       is already ~260px of brand, theme toggle and Log out, so a strip that
       refuses to be narrower than 9rem cannot fit beside them and wraps —
       taking the view toggle (order 4) with it. Without the floor the strip
       squeezed into whatever was left of the top row (114px at 390px) while
       the toggle sat alone below, which is both ugly and the compression
       ISSUES.md #1 exists to prevent. On a wider screen it simply stays up
       top, which is the desktop layout and equally fine. */
    /* flex-basis 0 with a min-width, and both halves matter.
       The basis is what lets the strip *share* row two with the view toggle:
       flex line-breaking uses an item's hypothetical size, so a strip sized by
       its pills (237px) plus the toggle (163px) exceeds 390px and they split
       across two rows. A basis of 0 makes the pair fit, and flex-grow then
       gives the strip whatever the toggle does not use.
       The min-width is what pushes the pair onto row two in the first place:
       the top row has ~128px left after the brand, theme toggle and Log out,
       and a strip that refuses to go below 9rem cannot fit there. Without it
       the strip squeezed into that 128px while the toggle sat alone below —
       both ugly and the compression ISSUES.md #1 exists to prevent. */
    flex: 1 1 0;
    min-width: 9rem;
    display: flex;
    gap: 0.4rem;
    /* Horizontal scroll rather than a dropdown, per ISSUES.md #1: Spaces stay
       visible and tappable instead of hiding behind a menu. Snapping makes a
       swipe land on a whole pill rather than halfway across one. */
    overflow-x: auto;
    scroll-snap-type: x proximity;
    /* The scrollbar would eat a line of height it does not deserve on a phone;
       the pills being cut off at the edge is the affordance. */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  #spaceTabs::-webkit-scrollbar { display: none; }
  .space-tab {
    scroll-snap-align: start;
    /* Stops the flex row from compressing pills toward illegibility, which is
       the failure ISSUES.md #1 described. Past three or four Spaces the strip
       scrolls instead of shrinking. */
    flex: 0 0 auto;
  }

  .crumbs {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    /* Sticky since BACKLOG.md #38: at this width it is what names the project
       and task, so it must not scroll away with them. The negative margins span
       .col-body's padding so it reads as a bar rather than a floating strip. */
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--paper);
    border-bottom: 1px solid var(--line-soft);
    margin: -0.5rem -0.5rem 0.25rem;
    padding: 0.5rem 1.25rem 0.35rem;
    /* One line, scrolled rather than wrapped: a wrapping trail changes the
       height of the pane header as you move around, which is more disorienting
       than a crumb running off the edge. */
    overflow-x: auto;
    white-space: nowrap;
    scrollbar-width: none;
  }
  .crumbs::-webkit-scrollbar { display: none; }
  .crumb {
    font-family: var(--ff-meta);
    font-size: 0.8rem;
    max-width: 60vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .crumb--link {
    border: none;
    background: transparent;
    padding: 0;
    color: var(--accent-ink);
    cursor: pointer;
    /* Tap target: the text is small, so the padding above gives the row height
       rather than the glyph doing it. */
    min-height: 2rem;
  }
  .crumb--up {
    border: none;
    background: transparent;
    color: var(--accent-ink);
    /* Wider than the glyph so the tap target is a thumb's worth, without the
       chevron itself getting large enough to compete with the trail. */
    padding: 0 0.35rem 0 0;
    font-size: 1.35rem;
    line-height: 1;
    cursor: pointer;
    min-width: 1.75rem;
    min-height: 2.25rem;
  }
  .crumb--current { color: var(--ink-3); }
  .crumb-sep { color: var(--ink-3); font-size: 0.7rem; }

  /* The detail pane's desktop breadcrumb (Space / Project) would sit directly
     above the drill-down trail saying nearly the same thing twice. */
  .d-breadcrumb { display: none; }

  /* Hover-revealed controls are a mouse idea. Without a pointer there is no
     hover, so the pencil, archive and ✕ would be permanently invisible —
     show them outright rather than leaving them unreachable. */
  .drag-handle,
  .edit-btn,
  .cancel-btn { opacity: 1; }
}

/* Touch devices at any width, not just narrow ones — a tablet is wide enough
   for the three-column board and still has no hover. Same reasoning as the
   block above, applied on the right axis. */
@media (hover: none) {
  .drag-handle,
  .edit-btn,
  .cancel-btn { opacity: 1; }
}

/* ============================================================
   OVERVIEW SCREEN (BACKLOG.md #29)
   Ported from mockup/insights.html — the same relationship the board's CSS
   has to mockup/index.html. Values converted to the token scale rather than
   copied as raw pixels, so a palette or type change reaches this screen too.

   Nothing here depends on heat. The mockup's forecast, stacked workload and
   runway timeline are all heat-derived and deliberately not ported; this is
   VIEWS.md's Tier 1, which needs only due date, priority and effort.
   ============================================================ */
/* #boardNav, not .nav, carries the auto margin — the wrapper is the flex item
   of .topbar, and a margin on the inner element does nothing. It is now the
   first of the right-hand controls, so it is what pushes that whole group away
   from the Space pills; the theme toggle used to do this. */
#boardNav { margin-left: auto; }
.nav {
  display: flex;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 3px;
  margin-right: 0.5rem;
}
.nav a,
.nav span {
  font-family: var(--ff-ui);
  font-size: 0.8rem;
  padding: 0.3rem 0.75rem;
  border-radius: 6px;
  color: var(--ink-2);
  text-decoration: none;
}
.nav a:hover { color: var(--ink); background: var(--surface); }
/* The current screen, which is a <span> rather than a link — there is nothing
   to click that would do nothing. */
.nav [aria-selected="true"] {
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
  box-shadow: var(--shadow);
}

/* The single column of every non-board screen (Search, Spaces, Export, Overview).

   **`width: 100%` is load-bearing (BACKLOG.md #31b).** `.wrap` is a child of
   `.app`, a flex column, and in a flex container `margin: 0 auto` does not just
   centre an item — auto margins opt it out of stretching, so it shrinks to fit
   its content. Without this line each screen, and each *state* of a screen, was
   its own width: Search measured 595px empty, 648px with results and 979px with
   none, and because the column is centred its left edge — title, search box and
   all — jumped sideways by nearly 200px between them. With it, every one of
   these screens shares one column, capped at max-width and centred as intended. */
.wrap { width: 100%; max-width: 1120px; margin: 0 auto; padding: 1.6rem 1.4rem 4rem; }

/* The exit from a utility screen (Spaces, Search, Export). Sits above the page
   title, where a breadcrumb would, because that is where people look for a way
   back — not in the top-right beside a control that switches views.

   **It is accent-coloured rather than muted, and that is the point.** The first
   version used --ink-3 and read as caption text sitting above a heading —
   which is the same failure it was added to fix: present, but not obviously a
   way out. The colour is what says "this is a control"; position and wording
   alone were not enough. It is still small and still above the title, so it
   does not compete with the heading for attention.

   The chevron is the same glyph the phone drill-down's "up a level" control
   uses, so "‹" means one thing throughout the app. */
.back-link {
  display: inline-block;
  margin-bottom: 0.5rem;
  color: var(--accent-ink);
  text-decoration: none;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
}
.back-link:hover { text-decoration: underline; }
.page-title {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-tracking);
  font-size: 1.9rem;
  margin: 0 0 0.15rem;
}
.page-sub { color: var(--ink-3); font-size: 0.8rem; margin: 0 0 1.5rem; font-family: var(--ff-ui); }

.section { margin-top: 1.9rem; }
.section-h { display: flex; align-items: baseline; gap: 0.6rem; margin: 0 0 0.25rem; }
.section-h h2 {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  font-size: 1.2rem;
  margin: 0;
}
.section-note { color: var(--ink-3); font-size: 0.78rem; margin: 0 0 0.85rem; font-family: var(--ff-ui); }

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 0.6rem 0.75rem;
}

.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.85rem; }
.tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 1rem 1.1rem;
}
.tile .t-num {
  font-size: 2.1rem;
  font-weight: 700;
  line-height: 1;
  /* Tabular figures so four tiles in a row read as a set rather than as four
     differently-spaced numbers. */
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
}
.tile .t-lab { margin-top: 0.45rem; font-size: 0.78rem; color: var(--ink-2); font-family: var(--ff-ui); }
/* Only tinted when the count is non-zero — a zero painted orange says
   "overdue" to someone glancing, which is the opposite of the truth. The
   label underneath always names the number, so the colour is never the only
   thing carrying it. */
.tile--flag .t-num { color: var(--flag); }
.tile--accent .t-num { color: var(--accent-ink); }

.group-head {
  margin: 0.6rem 0 0.15rem;
  padding: 0 0.5rem;
  font-family: var(--ff-meta);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
.group-head:first-child { margin-top: 0.15rem; }

.ov-row {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.45rem 0.5rem;
  border-radius: 8px;
  color: var(--ink);
  text-decoration: none;
  font-family: var(--ff-ui);
}
.ov-row:hover { background: var(--surface-2); }
/* Wraps like the board's rows (BACKLOG.md #39), on the Overview and in Search
   alike. .ov-row already aligns by baseline, so the project and date columns
   stay level with the title's first line. */
.ov-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* Which project a task belongs to. On the board this is implied by the column
   you are looking at; on a Space-wide list it has to be said. */
/* Both right-hand columns get a fixed width and are right-aligned, so they
   read as columns down the panel rather than as ragged text pushed around by
   whatever the neighbouring chip happens to say. "Overdue · Sep 5" and
   "Sep 15" differ enough in width to make that obvious otherwise. */
.ov-project {
  flex: 0 0 11rem;
  text-align: right;
  font-size: 0.75rem;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ov-row .due-chip { flex: 0 0 7.5rem; text-align: right; }
/* On the All-Spaces Overview (BACKLOG.md #46) the column names the Space
   first, with its colour if it has one, so it needs more room. The Space is
   a shade darker than the project: it is the bigger of the two answers to
   "where does this live". */
.ov-project:has(.ov-space) { flex-basis: 15rem; }
.ov-space { color: var(--ink-2); }
.ov-space .space-dot { display: inline-block; margin-right: 0.3rem; }
/* An Overview row with its checkbox (BACKLOG.md #30): the button beside the
   link, since it cannot sit inside one. Search and Insights use .ov-row bare. */
.ov-row-wrap { display: flex; align-items: center; gap: 0.35rem; }
.ov-row-wrap .ov-row { flex: 1; min-width: 0; }
.ov-check { margin-left: 0.3rem; }
.ov-check:hover { border-color: var(--done); }

/* ---------- The app menu ---------- */

/* position: relative is what the panel below anchors to, so the menu opens
   under its own button rather than at the corner of the page. */
.app-menu { position: relative; display: inline-flex; }

.app-menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 0.4rem);
  z-index: 30;
  min-width: 11rem;
  padding: 0.3rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow);
  /* Right-aligned to its button, which is the last thing in the top bar — so
     the panel grows leftward into the page instead of off the right edge.
     Matters most on a phone, where there is no room to the right at all. */
}

/* Both <a> and <button> live in this panel — one navigates, one opens the
   shortcut panel in place — so the UA's button styling has to be undone for
   the two to read as one list. width/text-align matter as much as the border:
   a <button> shrink-wraps and centres its text, so without them the two kinds
   of row would not even line up. */
.app-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0.45rem 0.6rem;
  border-radius: 7px;
  color: var(--ink);
  text-decoration: none;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  white-space: nowrap;
}
/* Label left, key right — the row teaches the shortcut, which is the whole
   reason this item exists. */
.app-menu-item--key { display: flex; align-items: baseline; justify-content: space-between; gap: 1.2rem; }
.app-menu-key {
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 0 0.3rem;
}
.app-menu-item:hover { background: var(--surface-2); }
/* The running build (BACKLOG.md #31d). Separated by a rule and set in the meta
   face at caption size, so it reads as a label under the menu rather than as a
   fourth thing to click. user-select: all — it exists to be compared, and a
   SHA is easier to compare pasted than squinted at. */
.app-menu-version {
  margin-top: 0.3rem;
  padding: 0.45rem 0.6rem 0.25rem;
  border-top: 1px solid var(--line);
  font-family: var(--ff-meta);
  font-size: 0.7rem;
  color: var(--ink-3);
  white-space: nowrap;
  user-select: all;
}
.app-menu-item[aria-current="page"] { color: var(--accent-ink); background: var(--accent-weak); }

/* ---------- Data export (PRIVACY.md §1) ---------- */

.export-row {
  display: flex;
  align-items: baseline;
  gap: 0.8rem;
  padding: 0.55rem 0.75rem;
  margin: 0;
  font-family: var(--ff-ui);
}
.export-name { flex: 1; min-width: 0; }
.export-link {
  color: var(--accent-ink);
  text-decoration: none;
  font-size: 0.85rem;
  white-space: nowrap;
}
.export-link:hover { text-decoration: underline; }

.export-facts {
  margin: 0;
  padding: 0.4rem 0.75rem 0.6rem 1.6rem;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-2);
}
.export-facts li { margin-bottom: 0.35rem; }
.export-facts code {
  font-family: var(--ff-meta);
  font-size: 0.78rem;
}

/* ---------- Keyboard & screen readers (BACKLOG.md #18) ---------- */

/* Off-screen but still read aloud. Not display:none and not
   visibility:hidden — both remove the element from the accessibility tree,
   which is exactly what a live region must not be. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* First thing in the tab order, out of sight until it is focused. Without it,
   reaching the board means tabbing past the whole top bar on every page load. */
.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -3rem;
  z-index: 50;
  padding: 0.5rem 0.9rem;
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 8px;
  font-family: var(--ff-ui);
  text-decoration: none;
  transition: top 0.12s ease;
}
.skip-link:focus { top: 0.5rem; }

/* A visible focus ring everywhere. The board is built from buttons styled to
   look like plain rows, so the UA default outline is the only thing telling a
   keyboard user where they are — and several of these rows set their own
   background on hover, which is not the same signal. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-radius: 6px;
}

/* The "?" panel. A corner card rather than a modal: it is a reference you read
   *while* using the app, so it deliberately does not take focus or trap it —
   aria-modal is false and nothing behind it is inert. Escape closes it. */
.help-panel {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 40;
  width: min(22rem, calc(100vw - 2rem));
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--shadow);
  font-family: var(--ff-ui);
}
.help-panel h2 {
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  font-size: 1rem;
  margin: 0 0 0.6rem;
  /* Clear of the close button, so a long heading can never run under it. */
  padding-right: 2rem;
}
/* The panel's only clickable way out (BACKLOG.md #31c). Quiet until hovered —
   it is the exit, not the content — but a full 2rem target, since a small ✕ in
   a corner is exactly the kind of control a touch screen misses. */
.help-close {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  width: 2rem;
  height: 2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--ink-3);
  font-size: 0.85rem;
  cursor: pointer;
}
.help-close:hover { color: var(--ink); background: var(--surface-2); }
.help-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.help-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin-bottom: 0.3rem;
  font-size: 0.8rem;
}
.help-row kbd {
  /* Wide enough for the longest chord on one line — a wrapped key name reads
     as two shortcuts rather than one. */
  flex: 0 0 8.5rem;
  font-family: var(--ff-meta);
  font-size: 0.72rem;
  color: var(--ink-2);
}
.help-row span { color: var(--ink-3); }
.help-note {
  margin: 0.7rem 0 0;
  font-size: 0.72rem;
  color: var(--ink-3);
}

/* ---------- Search (BACKLOG.md #16) ---------- */

.search-form {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 1.4rem;
}
.search-form input {
  flex: 1;
  max-width: 34rem;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--ff-ui);
  font-size: 0.95rem;
}
.search-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* How many results this Space contributed, beside its name. Quiet, because the
   count is context for the heading rather than the thing you came to read. */
.section-count {
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
}

/* Finished work in a result list. Search deliberately includes completed and
   cancelled tasks (PRIVACY.md §1's "keep a record"), so the chip has to say so
   in words — the same colour-is-never-alone rule .due-overdue follows. */
.due-done { color: var(--ink-3); font-style: italic; }

/* Some of these pills are <button> and some are <a>. Same pill either way, so
   the anchor needs the underline removed and the box pinned — the .space-tab
   comment above explains why the two element types do not size alike. */
a.icon-btn {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ---------- The Spaces screen (BACKLOG.md #27) ---------- */

/* Narrower than the Overview's full width. These rows hold one short name and
   two small buttons; run out to 1120px and the archive control ends up a hand's
   width from the name it belongs to. */
.spaces-body { max-width: 34rem; }

/* .row is a <button> on the board and an <a> here, the same split .space-tab
   already has — and with the same consequence, that the anchor arrives
   underlined unless this says otherwise. */
.spaces-body a.row { text-decoration: none; }

.archived { margin-top: 2.2rem; }
.archived .page-sub { margin-bottom: 0.6rem; }

/* Archived rows are records, not controls: nothing about them is clickable
   except Restore, so they must not offer a row hover that leads nowhere. */
.row--muted {
  color: var(--ink-3);
  cursor: default;
}
.row--muted:hover { background: transparent; }

/* Says which Space an archived project came from, and marks an archived Space
   as a Space — without it the two lists read as one flat list of names. */
.row-note {
  font-size: 0.72rem;
  color: var(--ink-3);
  font-family: var(--ff-ui);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
}

/* The restore and rename controls only appear on hover elsewhere (see
   .row-wrap:hover .edit-btn). In the archived list that would hide the only
   action a row has, so they stay visible here. */
.archived .edit-btn { opacity: 1; }

/* The narrow-screen treatment, matching the drill-down's breakpoint. The tiles
   go two-up (the mockup's own answer) and the project name drops below the
   title rather than competing with it for a phone's width. */
@media (max-width: 860px) {
  .wrap { padding: 1rem 0.85rem 3rem; }
  .tiles { grid-template-columns: repeat(2, 1fr); }
  .ov-row { flex-wrap: wrap; gap: 0.15rem 0.6rem; }
  .ov-title { flex: 1 0 100%; }
  /* Below the title rather than beside it, and back to the left edge — the
     fixed columns above only make sense when there is width for them. */
  .ov-project { flex: 1 1 auto; text-align: left; }
  .ov-row .due-chip { flex: 0 0 auto; }
}

/* ---------- Enforcement mode (BACKLOG.md #36) ---------- */

/* The reminder banner. A strip under the top bar rather than a card floating
   over the board: it is dismissible, it moves nothing it covers, and it is
   gone for three days once answered. Accent-tinted so it reads as the app
   talking to you, never flag-coloured — nothing is wrong. */
.nudge {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.55rem 1rem;
  background: var(--accent-weak);
  border-bottom: 1px solid var(--line);
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink);
}
.nudge-text { margin: 0; flex: 1 1 18rem; }
.nudge-go { text-decoration: none; font-size: 0.8rem; padding: 0.35rem 0.8rem; }

.app-menu-heading {
  margin: 0.2rem 0 0;
  padding: 0.3rem 0.6rem 0.1rem;
  font-family: var(--ff-meta);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
.app-menu-rule { border: 0; border-top: 1px solid var(--line); margin: 0.3rem 0; }

/* The undated heading's way in. Sits inside the hint so it reads as the answer
   to "these tend not to get done". */
.undated-go { margin-left: 0.3rem; color: var(--accent-ink); white-space: nowrap; }

/* The Overview tile, when it is a door into the mode. */
a.tile--link { display: block; color: inherit; text-decoration: none; }
a.tile--link:hover { border-color: var(--accent); }
.tile-go { display: block; margin-top: 0.3rem; color: var(--accent-ink); font-size: 0.75rem; }

/* The Detail pane's "No due date on purpose" toggle. */
.d-excuse {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.2rem 0.45rem;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-2);
}
.d-excuse-hint { flex-basis: 100%; padding-left: 1.4rem; font-size: 0.75rem; color: var(--ink-3); }

/* The mode's card. A reading measure rather than the full column: one task at
   a time is the design, and a card stretched to 1120px would read as a form. */
.enf { max-width: 36rem; }
.enf-progress {
  margin: 0 0 0.4rem;
  font-family: var(--ff-meta);
  font-size: 0.75rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.enf-card { padding: 1.2rem 1.3rem; }
.enf-title {
  margin: 0;
  overflow-wrap: anywhere;
  font-family: var(--ff-display);
  font-weight: var(--display-weight);
  font-size: 1.35rem;
  outline: none;
}
.enf-project { margin: 0.2rem 0 0; font-family: var(--ff-ui); font-size: 0.8rem; color: var(--ink-3); }
.enf-notes {
  margin: 0.7rem 0 0;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-2);
  white-space: pre-line;
}
.enf-form { margin-top: 1rem; }
.enf-group { border: 0; margin: 0 0 1rem; padding: 0; }
.enf-group legend { padding: 0; margin-bottom: 0.4rem; }
.enf-optional { text-transform: none; letter-spacing: 0; margin-left: 0.3rem; }

.enf-dates { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.45rem; }
.enf-date {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--ff-ui);
  font-size: 0.9rem;
  cursor: pointer;
  text-align: left;
}
.enf-date:hover { border-color: var(--accent); background: var(--accent-weak); }
.enf-date-day { font-family: var(--ff-meta); font-size: 0.72rem; color: var(--ink-3); }
.enf-pick { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.55rem; }
.enf-pick input {
  flex: 1;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--ff-ui);
}
.enf-save { text-decoration: none; display: inline-block; }

/* Pills for the optional fields. The radio stays in the DOM, visually hidden,
   so the group keeps native arrow-key behaviour and its screen-reader role. */
.enf-choices { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.enf-choice input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.enf-choice span {
  display: inline-block;
  min-width: 2.4rem;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  text-align: center;
  font-family: var(--ff-ui);
  font-size: 0.82rem;
  color: var(--ink-2);
  cursor: pointer;
}
.enf-choice input:checked + span { background: var(--accent); border-color: var(--accent); color: var(--surface); }
.enf-choice input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

.enf-error { margin: 0 0 0.8rem; color: var(--flag); font-family: var(--ff-ui); font-size: 0.85rem; }
.enf-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--line);
}
.enf-done .btn-primary { margin-top: 1rem; }

/* Heat onboarding's option buttons (HEAT.md §11b): full-width and stacked,
   unlike .enf-date's two-column grid, because each label is a sentence and a
   preview line, not a short date. */
.hob-options { display: flex; flex-direction: column; gap: 0.6rem; }
.hob-option {
  display: block;
  width: 100%;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--ff-ui);
  font-size: 0.95rem;
  text-align: left;
  cursor: pointer;
}
.hob-option:hover, .hob-option:focus-visible { border-color: var(--accent); background: var(--accent-weak); }
.hob-option-preview {
  display: block;
  margin-top: 0.25rem;
  font-family: var(--ff-meta);
  font-size: 0.78rem;
  color: var(--ink-3);
}
/* The display card's three options carry a mini preview of their own — a
   couple of rows drawn the way that choice would draw the whole board. */
.hob-options--display { flex-direction: row; flex-wrap: wrap; }
.hob-options--display .hob-option { flex: 1 1 14rem; }
.hob-option-rows { margin-top: 0.6rem; max-width: none; pointer-events: none; }

@media (max-width: 860px) {
  .enf-card { padding: 1rem; }
  /* One column of date buttons on a phone: two squeezed side by side wrap
     their day labels and read as four separate controls. */
  .enf-dates { grid-template-columns: 1fr; }
}

/* ---------- Flow mode (BACKLOG.md #33) ---------- */
/* Shares Enforcement's card shape (.enf, .enf-card, .enf-title) on purpose:
   both Modes are "one task in front of you", and two different cards for the
   same idea would make each look like a different app. */

.flow-pager {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.4rem;
  min-height: 1.4rem;
}
.flow-pager .enf-progress { margin: 0; }
.flow-page {
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--accent-ink);
  text-decoration: none;
  padding: 0.15rem 0.3rem;
  border-radius: 6px;
}
.flow-page:hover { background: var(--accent-weak); }
.flow-page--off { color: var(--ink-3); opacity: 0.5; }

.flow-note {
  margin: 0.7rem 0 0;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-2);
}
.flow-note .flow-page { padding-left: 0; }

.flow-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.8rem;
  margin: 0.7rem 0 0;
}
/* On this card the date is the reason the task is here, so it reads at body
   size rather than as the board's small meta chip. */
.flow-meta .due-chip { font-size: 0.85rem; }
.flow-tag { font-family: var(--ff-ui); font-size: 0.8rem; color: var(--ink-2); }

.flow-subtasks {
  list-style: none;
  margin: 0.9rem 0 0;
  padding: 0.6rem 0 0;
  border-top: 1px solid var(--line-soft);
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  color: var(--ink-2);
}
.flow-subtasks li { display: flex; gap: 0.5rem; padding: 0.15rem 0; }
.flow-subtasks .done { text-decoration: line-through; color: var(--ink-3); }

.flow-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
}
/* On the project line under the title: quiet, and plainly a way to go
   somewhere rather than a thing to do to the task. */
.flow-open {
  color: var(--accent-ink);
  text-decoration: none;
  white-space: nowrap;
}
.flow-open:hover { text-decoration: underline; }

/* ============================================================
   HEAT (HEAT.md §6, §10) and the SETTINGS screen (BACKLOG.md #5)
   ============================================================ */

/* The Ember ramp, SETTINGS.md §1's approved values. A fixed scale layered on
   every palette rather than part of any one, so heat means the same thing
   whichever palette is chosen. Dusk and Slate join it below, as a
   data-heatpalette swap of these same names (2026-09-21, with heat on the
   board — until something drew the ramp there was nothing to judge one
   against). Ember needs no [data-heatpalette] rule of its own: it is the
   default, and these are its values. */
:root {
  --heat-cold: #A8A59E; --heat-cool: #C6A24E; --heat-warm: #C68A1E; --heat-hot: #E0661A; --heat-critical: #BC1C1C;
  --heat-alpha: 16%;
}
:root[data-theme="dark"] {
  --heat-cold: #5E5B54; --heat-cool: #C6A867; --heat-warm: #D9A757; --heat-hot: #E37F46; --heat-critical: #E75E50;
  --heat-alpha: 22%;
}

/* **Cold is deliberately absent from all four rules below**, and inherits
   Ember's neutral in every one of them: the absence of heat is the normal calm
   UI (HEAT.md §6), so only cool/warm/hot/critical carry a palette's
   personality.

   **Each dark rule names both attributes.** [data-theme="dark"] above and
   [data-heatpalette="dusk"] here have identical specificity, so with only the
   one attribute the later rule would win and dusk would paint its *light*
   values in the dark. Two attributes outrank one, whatever the order. */
:root[data-heatpalette="dusk"] {
  --heat-cool: #C9A66B; --heat-warm: #C3823E; --heat-hot: #B85C3E; --heat-critical: #7A3550;
}
:root[data-heatpalette="dusk"][data-theme="dark"] {
  --heat-cool: #D8BD8B; --heat-warm: #D69A5E; --heat-hot: #CC7A5A; --heat-critical: #A85770;
}
/* Slate shifts no hue at all — heat is saturation and lightness only, so
   nothing ever "turns red". It leans hardest of the three on the redundant
   text the UI already carries (the band in every row's accessible name, the
   words chip, the counts), which SETTINGS.md §1's validation run makes
   explicit: it fails a chroma floor built for categorical colour on purpose. */
:root[data-heatpalette="slate"] {
  --heat-cool: #B7AEA0; --heat-warm: #A98F72; --heat-hot: #8F6B4A; --heat-critical: #6B4A30;
}
:root[data-heatpalette="slate"][data-theme="dark"] {
  --heat-cool: #8A8378; --heat-warm: #A68F73; --heat-hot: #C2A17E; --heat-critical: #D9B78F;
}
/* Spectrum (2026-09-21, requested by the owner: "make the difference even
   more obvious") is the one ramp here that is not a shade of Ember. Where the
   other three stay in one warm hue family and escalate mostly by darkening —
   which is why they all fail the dataviz skill's own adjacent-step-ΔE check,
   same as noted above for Slate — Spectrum spans blue → teal-green → orange,
   so adjacent bands differ in *hue*, not only depth. Validated with
   scripts/validate_palette.js (SETTINGS.md §1): both CVD separation and the
   normal-vision floor PASS here, which none of Ember/Dusk/Slate manage.

   Cool is a real exception to the sentence above the Dusk rule: everywhere
   else "cool" is a paler version of the same warm hue that "warm" and "hot"
   are, so this is the one ramp where it is not. That is the whole point —
   see the HeatPalette docstring in tasks/models.py.

   Two collisions were checked and are worth naming rather than pretending
   they don't exist: cool's blue sits closer than ideal to the Cobalt and
   Paper & Ink palettes' own `--accent` (used for selection and "due today"),
   and warm's green sits closer than ideal to the Meadow palette's `--accent`
   and to `--done`. Neither can be fully separated at once — pushing either
   hue further away reduces its separation from Spectrum's own neighbouring
   band, which is the check that matters more, since it is what makes the
   ramp itself readable. The mitigations that already exist for every heat
   palette carry the rest: the heat bar sits on the row's bottom edge, never
   the left edge selection uses (HEAT.md §11e), and the band is always in the
   row's text and tooltip too, colour never being the only cue (HEAT.md §6). */
:root[data-heatpalette="spectrum"] {
  --heat-cool: #1D6FB8; --heat-warm: #159E7A; --heat-hot: #D9770D; --heat-critical: #B23A1D;
}
:root[data-heatpalette="spectrum"][data-theme="dark"] {
  --heat-cool: #4E94D1; --heat-warm: #1FAE86; --heat-hot: #C8791F; --heat-critical: #A82F1E;
}

/* ---------- Heat on the board (HEAT.md §11h step 4) ----------

   The accent bar runs across the *bottom* of a row, never its left edge:
   since BACKLOG.md #38 the left edge is how a row says it is selected, and two
   meanings on one edge is one too many (§11e).

   It is an ::after rather than padding and a box-shadow, so a row with heat
   and a row without are exactly the same height. The alternative shifts every
   hot row 3px taller than its neighbours, which reads as a rendering fault
   rather than as a signal. */
.row-wrap { position: relative; }
.row-wrap--heat::after {
  content: "";
  position: absolute;
  left: 0.25rem;
  right: 0.25rem;
  bottom: 0;
  height: 3px;
  border-radius: 2px;
  background: var(--heat-band);
}
.heat--cold { --heat-band: var(--heat-cold); }
.heat--cool { --heat-band: var(--heat-cool); }
.heat--warm { --heat-band: var(--heat-warm); }
.heat--hot { --heat-band: var(--heat-hot); }
.heat--critical { --heat-band: var(--heat-critical); }

/* The words display's chip inside a board row. It is written for the Settings
   preview's wide rows, where it is pushed right and never wraps; a Tasks
   column is a third of that. :has() rather than a blanket rule, so a row
   without a chip keeps exactly the layout it had.

   **The fix is a floor under the title, not a width on the chip.** The title
   is flex: 1 with min-width: 0 (BACKLOG.md #39, so an unbroken URL can break
   anywhere), and beside a chip that does not shrink it absorbed the entire
   shortfall — "Book flights" rendered one letter per line. Two other fixes
   were tried and both failed on their own terms: flex-basis: 100% on the chip
   forces the break but stretches the chip across the column, so it reads as a
   second, fatter bar; adding max-width: max-content to hold it in then lets it
   fit beside the title again and the title collapses exactly as before.

   With a floor, the wrap does the work: title and date keep the line they have
   always had, and the chip drops below at its own width. min(), so the floor
   can never exceed the column it is in. */
#taskList .row:has(.heat-chip), #projList .row:has(.heat-chip) { flex-wrap: wrap; }
#taskList .row:has(.heat-chip) .t-title { min-width: min(8rem, 100%); }
#taskList .heat-chip, #projList .heat-chip {
  margin-left: 0;
  margin-top: 0.2rem;
  white-space: normal;
}

/* A project's Hot/Critical count (§4). A number beside the name, so peak
   (the bar's colour) and volume (this) both read — a peak alone cannot tell
   one critical task from nine. Tabular figures so a column of them does not
   jitter as the counts change. */
.heat-count-badge {
  flex: 0 0 auto;
  font-family: var(--ff-meta);
  font-variant-numeric: tabular-nums;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--ink-2);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
  background: color-mix(in srgb, var(--heat-hot) var(--heat-alpha), transparent);
}
/* The Space pill's ember badge (SETTINGS.md §2). Same number, one level up,
   and quieter than the project count because the strip is navigation first:
   it says "something is going on in there", and the board says what. */
.space-heat {
  display: inline-block;
  margin-left: 0.35rem;
  font-family: var(--ff-meta);
  font-variant-numeric: tabular-nums;
  font-size: 0.65rem;
  font-weight: 700;
  line-height: 1.5;
  min-width: 1.35em;
  text-align: center;
  border-radius: 999px;
  padding: 0 0.3em;
  color: var(--ink);
  background: color-mix(in srgb, var(--heat-hot) 38%, transparent);
}

/* ---------- Space colours (SETTINGS.md §2, 2026-09-24) ----------
   A Space's colour is a name (Space.Color); each name maps to one of Insights'
   categorical tokens, which carry their own light and dark shades (see
   "Insights" below). Set once here as --space-color on anything that carries
   data-space-color: the tabs, the Spaces screen's dots and its swatches. */
[data-space-color="blue"] { --space-color: var(--cat-1); }
[data-space-color="orange"] { --space-color: var(--cat-2); }
[data-space-color="teal"] { --space-color: var(--cat-3); }
[data-space-color="amber"] { --space-color: var(--cat-4); }
[data-space-color="pink"] { --space-color: var(--cat-5); }
[data-space-color="green"] { --space-color: var(--cat-6); }
[data-space-color="violet"] { --space-color: var(--cat-7); }

/* The selected Space's underline, in both indicator modes (HEAT.md §10: the
   identity mark on the active tab). Inset from the pill's rounded ends so it
   reads as an underline, not as a bottom border. */
.space-tab { position: relative; }
.space-tab[aria-current="true"][data-space-color]::after {
  content: "";
  position: absolute;
  left: 0.9rem;
  right: 0.9rem;
  bottom: 0.2rem;
  height: 2px;
  border-radius: 2px;
  background: var(--space-color);
}

/* Workload count (the default) or Identity dot: every pill carries both, and
   the attribute on <html> picks one (UserPreferences.space_indicator). Hiding
   the count's wrapper takes its screen-reader text with it, so Identity mode is
   as quiet to a screen reader as it is to the eye. */
.space-dot {
  display: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  margin-right: 0.4rem;
  vertical-align: 0.05em;
  background: var(--space-color);
}
:root[data-space-indicator="identity"] .space-count { display: none; }
/* The dot is for the tabs you are not on; the one you are on has its underline. */
:root[data-space-indicator="identity"] .space-tab:not([aria-current="true"]) .space-dot { display: inline-block; }

/* The Spaces screen's picker: a dot per row that opens its swatches on a line
   of their own beneath the row. An empty ring means no colour. */
.space-rows .row-wrap { flex-wrap: wrap; }
.space-color-btn, .space-swatch {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  border: 1.5px dashed var(--ink-3);
  background: transparent;
  padding: 0;
  cursor: pointer;
}
.space-color-btn { margin: 0 0.35rem 0 0.1rem; }
.space-color-btn[data-space-color], .space-swatch[data-space-color] {
  border: none;
  background: var(--space-color);
}
.space-swatches {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.35rem 0 0.6rem 2.6rem;
}
.space-swatch { width: 1.5rem; height: 1.5rem; }
.space-swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--ink); }
.space-swatch:focus-visible, .space-color-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.settings-status { margin-left: 0.4rem; color: var(--done); font-weight: 600; }
.settings-status.is-error { color: var(--heat-critical); }
.settings-panel { padding: 0.9rem 1rem; }
.settings-field { border: 0; margin: 1.1rem 0 0; padding: 0; min-width: 0; }
.settings-label {
  display: block;
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.45rem;
  padding: 0;
}
.settings-hint { display: block; font-family: var(--ff-ui); font-size: 0.78rem; color: var(--ink-3); margin: 0.1rem 0 0; }
/* The Time zone section (tasks/timezones.py): one select, then the device
   shortcut beneath it. The field is the panel's first, so no top margin. */
.settings-timezone { display: block; margin-top: 0; }
.settings-timezone select { max-width: 22rem; margin-top: 0.55rem; }
.settings-timezone-device { margin-top: 0.6rem; font-size: 0.85rem; }

.settings-switch,
.settings-choice {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  font-family: var(--ff-ui);
  font-size: 0.88rem;
  padding: 0.3rem 0;
  cursor: pointer;
}
.settings-switch input,
.settings-choice input { margin-top: 0.2rem; accent-color: var(--accent); flex: none; }
.settings-switch-text strong,
.settings-choice strong { font-weight: 600; }

.settings-range { width: 100%; max-width: 28rem; accent-color: var(--accent); display: block; }
.settings-range-ends {
  display: flex;
  justify-content: space-between;
  max-width: 28rem;
  font-family: var(--ff-ui);
  font-size: 0.75rem;
  color: var(--ink-3);
}
.settings-range-value { color: var(--ink-2); font-weight: 600; max-width: 28rem; text-align: center; }

.settings-options { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.settings-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0.45rem 0.7rem;
  font-family: var(--ff-ui);
  font-size: 0.88rem;
  cursor: pointer;
  background: var(--surface);
}
.settings-option:has(input:checked) { border-color: var(--accent); background: var(--accent-weak); }
.settings-option input { accent-color: var(--accent); margin: 0; }

/* Palettes are :root-bound, so a swatch cannot borrow another palette's
   tokens: each carries its own paper, ink and accent, from tokens above. */
.swatch { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.swatch i { width: 0.8rem; height: 1rem; display: block; }
.swatch--paper i:nth-child(1) { background: #FAF9F6; } .swatch--paper i:nth-child(2) { background: #23221F; } .swatch--paper i:nth-child(3) { background: #35526E; }
.swatch--cobalt i:nth-child(1) { background: #F5F6F2; } .swatch--cobalt i:nth-child(2) { background: #16283A; } .swatch--cobalt i:nth-child(3) { background: #2F7FD1; }
.swatch--meadow i:nth-child(1) { background: #F4F6F0; } .swatch--meadow i:nth-child(2) { background: #1F2A20; } .swatch--meadow i:nth-child(3) { background: #3F9E55; }
:root[data-theme="dark"] .swatch--paper i:nth-child(1) { background: #1A1B1E; } :root[data-theme="dark"] .swatch--paper i:nth-child(2) { background: #ECEAE4; } :root[data-theme="dark"] .swatch--paper i:nth-child(3) { background: #7FA6CE; }
:root[data-theme="dark"] .swatch--cobalt i:nth-child(1) { background: #0B1B2B; } :root[data-theme="dark"] .swatch--cobalt i:nth-child(2) { background: #E4ECF5; } :root[data-theme="dark"] .swatch--cobalt i:nth-child(3) { background: #5FA4E8; }
:root[data-theme="dark"] .swatch--meadow i:nth-child(1) { background: #14211A; } :root[data-theme="dark"] .swatch--meadow i:nth-child(2) { background: #E6EEE4; } :root[data-theme="dark"] .swatch--meadow i:nth-child(3) { background: #67C569; }

/* The heat ramps, as warm/hot/critical — the three bands that actually differ
   between them (cool is close enough to be indistinguishable at 0.8rem, and
   cold is shared by all three). Hard-coded like the colour swatches above and
   for the same reason: a ramp is bound to :root, so a swatch cannot borrow the
   tokens of a palette that is not currently selected. These must stay in step
   with the :root[data-heatpalette] rules further up and with SETTINGS.md §1.

   **Spectrum breaks the "cool is indistinguishable" premise** — that is the
   feature — so its swatch gets a fourth dot rather than dropping cool like
   the other three. settings.html renders that fourth `<i>` only for this
   value; see the swatch markup there. */
.swatch--heat-ember i:nth-child(1) { background: #C68A1E; } .swatch--heat-ember i:nth-child(2) { background: #E0661A; } .swatch--heat-ember i:nth-child(3) { background: #BC1C1C; }
.swatch--heat-dusk i:nth-child(1) { background: #C3823E; } .swatch--heat-dusk i:nth-child(2) { background: #B85C3E; } .swatch--heat-dusk i:nth-child(3) { background: #7A3550; }
.swatch--heat-slate i:nth-child(1) { background: #A98F72; } .swatch--heat-slate i:nth-child(2) { background: #8F6B4A; } .swatch--heat-slate i:nth-child(3) { background: #6B4A30; }
.swatch--heat-spectrum i:nth-child(1) { background: #1D6FB8; } .swatch--heat-spectrum i:nth-child(2) { background: #159E7A; } .swatch--heat-spectrum i:nth-child(3) { background: #D9770D; } .swatch--heat-spectrum i:nth-child(4) { background: #B23A1D; }
:root[data-theme="dark"] .swatch--heat-ember i:nth-child(1) { background: #D9A757; } :root[data-theme="dark"] .swatch--heat-ember i:nth-child(2) { background: #E37F46; } :root[data-theme="dark"] .swatch--heat-ember i:nth-child(3) { background: #E75E50; }
:root[data-theme="dark"] .swatch--heat-dusk i:nth-child(1) { background: #D69A5E; } :root[data-theme="dark"] .swatch--heat-dusk i:nth-child(2) { background: #CC7A5A; } :root[data-theme="dark"] .swatch--heat-dusk i:nth-child(3) { background: #A85770; }
:root[data-theme="dark"] .swatch--heat-slate i:nth-child(1) { background: #A68F73; } :root[data-theme="dark"] .swatch--heat-slate i:nth-child(2) { background: #C2A17E; } :root[data-theme="dark"] .swatch--heat-slate i:nth-child(3) { background: #D9B78F; }
:root[data-theme="dark"] .swatch--heat-spectrum i:nth-child(1) { background: #4E94D1; } :root[data-theme="dark"] .swatch--heat-spectrum i:nth-child(2) { background: #1FAE86; } :root[data-theme="dark"] .swatch--heat-spectrum i:nth-child(3) { background: #C8791F; } :root[data-theme="dark"] .swatch--heat-spectrum i:nth-child(4) { background: #A82F1E; }

/* Each typeface's name set in its own display face (fonts.css). */
.type-sample { font-size: 1rem; }
.type-sample--editorial { font-family: "Newsreader", Georgia, serif; }
.type-sample--classic { font-family: "Fraunces", Georgia, serif; font-weight: 600; }
.type-sample--refined { font-family: "Cormorant Garamond", Georgia, serif; font-weight: 600; font-size: 1.1rem; }
.type-sample--terminal { font-family: "Space Mono", ui-monospace, monospace; font-size: 0.85rem; }

.settings-live { margin-top: 1.2rem; padding-top: 0.9rem; border-top: 1px solid var(--line-soft); }
.heat-counts { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 0.4rem; padding: 0; }
.heat-count {
  font-family: var(--ff-ui);
  font-size: 0.8rem;
  color: var(--ink-2);
  border-radius: 999px;
  padding: 0.15rem 0.6rem 0.15rem 0.5rem;
  border-left: 4px solid var(--heat-band, var(--line));
  background: color-mix(in srgb, var(--heat-band, var(--line)) var(--heat-alpha), transparent);
}
.heat-count-n { font-weight: 700; color: var(--ink); }
.heat-count--cold, .heat-row--cold, .heat-chip--cold { --heat-band: var(--heat-cold); }
.heat-count--cool, .heat-row--cool, .heat-chip--cool { --heat-band: var(--heat-cool); }
.heat-count--warm, .heat-row--warm, .heat-chip--warm { --heat-band: var(--heat-warm); }
.heat-count--hot, .heat-row--hot, .heat-chip--hot { --heat-band: var(--heat-hot); }
.heat-count--critical, .heat-row--critical, .heat-chip--critical { --heat-band: var(--heat-critical); }
.heat-count--none { border-left-style: dashed; }

.heat-preview-rows-h { margin-top: 0.9rem; }
.heat-rows { list-style: none; margin: 0; padding: 0; max-width: 34rem; }
.heat-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.6rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  margin-bottom: 0.35rem;
  background: var(--surface);
  font-family: var(--ff-ui);
}
/* §11e: along the bottom, because the left edge marks the selected row (#38). */
.heat-row--bar { box-shadow: inset 0 -3px 0 var(--heat-band); }
.heat-row-title { color: var(--ink); font-size: 0.9rem; }
.heat-row-where { color: var(--ink-3); font-size: 0.75rem; }
.heat-chip {
  margin-left: auto;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--ink);
  border-radius: 999px;
  padding: 0.1rem 0.55rem 0.1rem 0.45rem;
  border-left: 4px solid var(--heat-band);
  background: color-mix(in srgb, var(--heat-band) var(--heat-alpha), transparent);
  white-space: nowrap;
}
.heat-preview-note { margin-top: 0.7rem; }
.heat-preview-off { margin: 0; }

/* --- Retention (PRIVACY.md §2b) -------------------------------------------
   The one control on Settings that deletes something, so the block that says
   what it would delete is styled to be read rather than skimmed past: a warm
   left edge, the same device the overdue chip uses, and never colour alone —
   the count and the word "deleted" carry it in text. */
.settings-retention { border-top: 1px solid var(--line-soft); margin-top: 1.1rem; padding-top: 1rem; }
/* This one's explanation runs to three lines, and a range input has no
   margin of its own, so without this the thumb sits on the last line of text. */
.settings-retention > .settings-hint { max-width: 46rem; margin-bottom: 0.7rem; }
.retention-live { margin-top: 0.7rem; }
.retention-warning {
  font-family: var(--ff-ui);
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink-1);
  background: var(--surface-2);
  border-left: 4px solid var(--heat-hot);
  border-radius: 0 6px 6px 0;
  margin: 0 0 0.5rem;
  padding: 0.55rem 0.7rem;
}
.retention-examples { list-style: none; margin: 0 0 0.5rem; padding: 0; display: grid; gap: 0.2rem; }
.retention-examples li {
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline;
  font-family: var(--ff-ui); font-size: 0.8rem;
}
.retention-example-title { color: var(--ink-1); }
.retention-example-where, .retention-example-more { color: var(--ink-3); font-size: 0.75rem; }

/* ---------- Insights (BACKLOG.md #8) ---------- */

/* Project colours for the heat-off chart: the dataviz skill's validated
   categorical order, seven slots. Checked with its validate_palette.js on
   2026-09-23 against every palette's surface (#FFFFFF light; #212327, #10263B,
   #1B2C22 dark): all hard checks pass, worst adjacent colour-blind ΔE 9.1
   light and 8.4 dark. Slots 3–5 sit under 3:1 on white, and slot 6 on
   Meadow's dark surface, which is why every chart carries a legend and a table.
   Projects past the seventh share "Other", in ink rather than an eighth hue. */
:root {
  --cat-1: #2a78d6; --cat-2: #eb6834; --cat-3: #1baf7a; --cat-4: #eda100;
  --cat-5: #e87ba4; --cat-6: #008300; --cat-7: #4a3aa7;
}
:root[data-theme="dark"] {
  --cat-1: #3987e5; --cat-2: #d95926; --cat-3: #199e70; --cat-4: #c98500;
  --cat-5: #d55181; --cat-6: #008300; --cat-7: #9085e9;
}

.tile .t-num--name { font-size: 1.15rem; line-height: 1.25; overflow-wrap: anywhere; }
.ins-chip { margin-left: 0; }
.ov-row .ins-chip { flex: 0 0 auto; }
.tile .ins-chip { display: inline-block; margin-top: 0.3rem; }

.viz-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.viz { display: block; width: 100%; height: auto; overflow: visible; font-family: var(--ff-ui); }
/* A wide chart keeps a readable size on a phone and scrolls inside its own
   card instead: shrunk to fit 390px its 10px labels drew at about 4px. The
   page itself still never scrolls sideways. */
.viz-scroll { overflow-x: auto; }
.viz-card--wide .viz { min-width: 560px; }
.viz-today-label { fill: var(--ink-2); font-size: 10px; font-weight: 600; }
.viz-multiples { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.85rem; }
.viz-card { padding: 0.75rem 0.85rem 0.5rem; margin: 0; }
.viz-card-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin-bottom: 0.35rem; font-family: var(--ff-ui); }
.viz-card-name { font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.viz-card-meta { margin-left: auto; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.viz-card-count { font-size: 0.75rem; color: var(--ink-3); }

/* Recessive chrome: solid hairlines one step off the surface, muted text. */
.viz-axis { stroke: var(--ink-3); stroke-width: 1; }
.viz-guide { stroke: var(--line); stroke-width: 1; }
.viz-today { stroke: var(--ink-3); stroke-width: 1; }
.viz-tick, .viz-label { fill: var(--ink-3); font-size: 10px; font-variant-numeric: tabular-nums; }
.viz-tick--today { fill: var(--ink-2); font-weight: 600; }
.viz-hatch { fill: url(#viz-hatch); }
.viz-hatch-line { stroke: var(--line); stroke-width: 2; }

/* Lines: 2px, round, coloured by band. Cold and Cool share the neutral, as
   on the board, where neither draws a cue. */
.viz-seg { stroke-width: 2; stroke-linecap: round; fill: none; }
.viz-seg--cold, .viz-seg--cool { stroke: var(--heat-cold); fill: var(--heat-cold); }
.viz-seg--warm { stroke: var(--heat-warm); fill: var(--heat-warm); }
.viz-seg--hot { stroke: var(--heat-hot); fill: var(--heat-hot); }
.viz-seg--critical { stroke: var(--heat-critical); fill: var(--heat-critical); }
.viz-seg--forecast { stroke-dasharray: 4 3; }
line.viz-seg--cold, line.viz-seg--cool, line.viz-seg--warm, line.viz-seg--hot, line.viz-seg--critical { fill: none; }
.viz-dot { stroke: var(--surface); stroke-width: 2; }

.viz-mark--critical { fill: var(--heat-critical); }
.viz-mark--hot { fill: var(--heat-hot); }
.viz-mark--warm { fill: var(--heat-warm); }
.viz-mark--calm { fill: var(--heat-cold); }
.viz-mark--p1 { fill: var(--cat-1); } .viz-mark--p2 { fill: var(--cat-2); } .viz-mark--p3 { fill: var(--cat-3); }
.viz-mark--p4 { fill: var(--cat-4); } .viz-mark--p5 { fill: var(--cat-5); } .viz-mark--p6 { fill: var(--cat-6); }
.viz-mark--p7 { fill: var(--cat-7); } .viz-mark--other { fill: var(--ink-3); }
.viz-mark--forecast { opacity: 0.45; }

/* The hit area per day: invisible until pointed at, then a faint wash so the
   reader sees which day the tooltip is about. */
.viz-hit { fill: transparent; }
.viz-hit:hover { fill: color-mix(in srgb, var(--ink) 6%, transparent); }

.viz-legend { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; margin: 0 0 0.6rem; padding: 0; font-family: var(--ff-ui); font-size: 0.75rem; color: var(--ink-2); }
.viz-legend li { display: inline-flex; align-items: center; gap: 0.35rem; }
.viz-key { display: inline-block; width: 12px; height: 12px; border-radius: 3px; background: currentColor; }
.viz-key.viz-mark--critical { background: var(--heat-critical); } .viz-key.viz-mark--hot { background: var(--heat-hot); }
.viz-key.viz-mark--warm { background: var(--heat-warm); } .viz-key.viz-mark--calm { background: var(--heat-cold); }
.viz-key.viz-mark--p1 { background: var(--cat-1); } .viz-key.viz-mark--p2 { background: var(--cat-2); } .viz-key.viz-mark--p3 { background: var(--cat-3); }
.viz-key.viz-mark--p4 { background: var(--cat-4); } .viz-key.viz-mark--p5 { background: var(--cat-5); } .viz-key.viz-mark--p6 { background: var(--cat-6); }
.viz-key.viz-mark--p7 { background: var(--cat-7); } .viz-key.viz-mark--other { background: var(--ink-3); }
/* Line keys for the line charts, rects for the columns: the key mirrors the mark. */
.viz-key--line { width: 16px; height: 0; border-radius: 0; background: none; border-top: 2px solid var(--ink-3); }
.viz-key--line.viz-seg--cold { border-top-color: var(--heat-cold); } .viz-key--line.viz-seg--warm { border-top-color: var(--heat-warm); }
.viz-key--line.viz-seg--hot { border-top-color: var(--heat-hot); } .viz-key--line.viz-seg--critical { border-top-color: var(--heat-critical); }
.viz-key--dashed { border-top-style: dashed; }
.viz-key--hatch { background: repeating-linear-gradient(45deg, var(--line) 0 2px, transparent 2px 5px); border: 1px solid var(--line); }

.viz-table { margin-top: 0.6rem; font-family: var(--ff-ui); font-size: 0.78rem; }
.viz-table summary { cursor: pointer; color: var(--ink-2); }
.viz-table-scroll { overflow-x: auto; margin-top: 0.4rem; max-height: 22rem; overflow-y: auto; }
.viz-table table { border-collapse: collapse; font-variant-numeric: tabular-nums; }
.viz-table th, .viz-table td { padding: 0.2rem 0.6rem; border-bottom: 1px solid var(--line-soft); text-align: left; white-space: nowrap; color: var(--ink-2); }
.viz-table thead th { color: var(--ink); position: sticky; top: 0; background: var(--paper); }

.viz-tip {
  position: fixed;
  z-index: 50;
  pointer-events: none;
  max-width: 18rem;
  padding: 0.4rem 0.6rem;
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18), inset 0 0 0 1px var(--line);
  font-family: var(--ff-ui);
  font-size: 0.78rem;
}
.viz-tip-head { color: var(--ink-3); font-size: 0.72rem; }
.viz-tip-line { color: var(--ink); font-weight: 600; }

/* The project × day grid. A cell is its band; "none" is a day with data and
   nothing with heat, outlined so it reads as present-but-calm, not missing. */
.viz-cell--cold, .viz-cell--cool { fill: var(--heat-cold); }
.viz-cell--warm { fill: var(--heat-warm); }
.viz-cell--hot { fill: var(--heat-hot); }
.viz-cell--critical { fill: var(--heat-critical); }
.viz-cell--none { fill: transparent; stroke: var(--line); stroke-width: 1; }
.viz-cell--missing { fill: url(#viz-hatch); }
.viz-cell:hover { stroke: var(--ink); stroke-width: 1.5; }
.viz-row-label { fill: var(--ink-2); font-size: 11px; }
.viz-lane { fill: var(--ink); font-size: 11px; font-weight: 600; }
/* The runway's bars: 8px thick, round-ended, a wash of the band so the due
   date's dot, in the full band colour, is the loudest mark on the row. */
.viz-bar { opacity: 0.55; }
.viz-marker { stroke: var(--ink-3); stroke-width: 1; }
.viz-marker-label { fill: var(--ink-2); font-size: 10px; }
.viz-key--none { background: none; border: 1px solid var(--line); }
.viz-key--dot { width: 10px; height: 10px; border-radius: 50%; background: var(--ink-3); }

.ins-picker { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0 0 0.7rem; font-family: var(--ff-ui); font-size: 0.8rem; color: var(--ink-2); }
.ins-picker select { max-width: 100%; min-width: 0; font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 0.3rem 0.5rem; }
.ins-task { display: grid; grid-template-columns: minmax(0, 1fr) 15rem; gap: 0.85rem; align-items: start; }
.ins-task-facts { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 0.3rem 0.7rem; font-family: var(--ff-ui); font-size: 0.8rem; }
.ins-task-facts dt { color: var(--ink-3); }
.ins-task-facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.ins-task-why { grid-column: 1 / -1; margin: 0; }

@media (max-width: 860px) {
  .viz-multiples { grid-template-columns: minmax(0, 1fr); }
  .ins-task { grid-template-columns: minmax(0, 1fr); }
}

/* The account page (templates/core/account.html, BACKLOG.md #48): Settings'
   sections and panels, with a row per thing you can change. */
.account-panel { padding: 0.2rem 1rem; }
.account-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 0;
  border-top: 1px solid var(--line-soft);
}
.account-row:first-child { border-top: none; }
.account-row--add { justify-content: flex-start; flex-wrap: wrap; }
.account-value { font-family: var(--ff-ui); font-weight: 600; color: var(--ink); margin: 0; word-break: break-all; }
.account-hint { font-family: var(--ff-ui); font-size: 0.85rem; color: var(--ink-3); margin: 0.2rem 0 0; }
.account-empty { font-family: var(--ff-ui); font-size: 0.88rem; color: var(--ink-3); margin: 0; padding: 0.85rem 0; }
.account-actions { display: flex; gap: 0.9rem; flex-shrink: 0; }
.account-row > .btn-secondary, .account-row > .btn-primary { flex-shrink: 0; text-decoration: none; }
.account-delete { color: var(--flag); }
.account-hint a, .auth-list a, .auth-sub a { color: var(--accent); }
@media (max-width: 560px) {
  .account-row { flex-direction: column; align-items: flex-start; }
}
/* The one destructive button: deleting the account, removing a passkey. */
.btn-danger {
  font-family: var(--ff-ui);
  background: var(--flag);
  color: var(--surface);
  border: none;
  border-radius: 6px;
  padding: 0.6rem 1.1rem;
  font-size: 0.95rem;
  cursor: pointer;
}
.auth-list { font-family: var(--ff-ui); font-size: 0.88rem; color: var(--ink-2); padding-left: 1.1rem; margin: 0 0 1.1rem; line-height: 1.45; }
.auth-list li { margin: 0 0 0.4rem; }
.auth-check { display: flex; gap: 0.55rem; align-items: flex-start; font-family: var(--ff-ui); font-size: 0.9rem; color: var(--ink); margin: 0 0 0.6rem; }
.auth-check input { margin-top: 0.2rem; }
