/* Shared components — controls and surfaces every app page shares:
   preference switches, the configurable indicator and its popover, keyboard hints,
   comment rows, avatars, badges, modal chrome and card decorations.

   LAYER ORDER:  tokens.css -> base.css -> THIS FILE -> the page's own <style>

   THE RULE FOR WHAT MAY LIVE HERE: a rule is movable only if the SUBJECT of
   its selector — the last compound — carries a class. Not merely "contains a
   class somewhere": `.auth-field input`'s subject is a bare `input` at
   (0,1,1), which ties with the `input[type=text], …` group that stays in the
   page, and ties break by source order.

   One deliberate exception: `.modal-title-row h3`, a bare h3 at (0,1,1). It
   came over with the modal shell because splitting the shell across two
   files is worse than the risk, and the risk was checked — no other rule
   anywhere matches an h3 inside a `.modal-title-row`.

   Also not here: element selectors whose cascade position is part of their
   behaviour (html, body, ::selection, the input focus group, select option,
   the scrollbar pseudo-elements); :focus-visible/::placeholder/the disabled-
   control rule, decided by position; and classes shared with only some pages.

   Loaded by every app page — tasks, team_board, okrs, admin and mobile.

   Verified by css-resolved-check.py (computed styles, real browser, both
   themes) and css-tokens-check.py --rules. */

/* The base rule, carrying only the MODIFIERS of a component whose base rule
   lives in a page's own <style> — the same shape `.import-status` below is,
   and still live. login.html does not load this file; it mirrors these
   token names in CSS/login.css. */
/* THE DESTRUCTIVE BUTTON — sys_design-system.md §3: `--red-soft` fill,
   `--red` border, `--red-text` label.

   It lives here because the "Delete my account" dialog (JS/features/account.js)
   mounts into whichever page carries `#accountOverlay`, and only some pages
   declared `.btn.danger` in their own <style> — on the rest the most
   destructive control in the product rendered as an ordinary grey button.

   Nothing that already declared it changes: a page <style> comes after the
   <link>, so a page with its own copy keeps it, and mobile.html keeps its
   deliberately OUTLINED variant — a filled red block is too heavy on a phone
   sheet. `.alt-app .btn.danger` in CSS/okr-suite.css outranks this and is
   likewise untouched. */
.btn.danger{background:var(--red-soft); border-color:var(--red); color:var(--red-text);}
.btn.danger:hover:not(:disabled){border-color:var(--red); color:var(--red-text);}

/* ---------- Preference row + toggle switch ---------- */
  .pref-row{
    display:flex; align-items:flex-start; gap:14px;
    padding:12px 0; border-bottom:1px solid var(--border-soft);
  }

.pref-row:last-child{border-bottom:none;}

.pref-copy{flex:1; min-width:0;}

.pref-title{font-size:13px; font-weight:600; color:var(--text);}

.pref-desc{font-size:12px; color:var(--text-faint); line-height:1.5; margin-top:3px;}

/* A real button that reads as a link, inside a `.pref-row`'s copy. Same
   formula as `.source-note-ref-link` (CSS/task-editor-panel.css): transparent,
   no border, `--accent-text`, underlined so colour is never the only signal
   (sys_design-system.md §18/§21c). Not that class itself — it lives in a
   sheet this page has no reason to load — the same idea, homed here instead. */
.pref-link{
  display:block; background:transparent; border:none; color:var(--accent-text);
  font-size:12px; font-weight:600; cursor:pointer; padding:0; margin-top:6px;
  text-decoration:underline; text-align:left;
}
.pref-link:hover{color:var(--text);}

/* The on/off switch moved to CSS/switch.css — sys_design-system.md §14a. A
   standard that cannot reach the sign-in page is not a standard; a file of
   its own is loaded by every page instead. */


/* `.import-status` is now one of the two names of the canonical `.cfg-status`
   component further down this file — see the note there for why the alias exists. */

/* ---------- Account panel (shared across hosts — keep in step) ----------
   One modal, identical markup and rules everywhere. Uses the app's own
   tokens, so it inherits each theme without a second palette. */
  .account-overlay{
    position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:var(--z-alert);
    display:none; align-items:center; justify-content:center; padding:20px;
  }

.account-overlay.show{display:flex;}

.account-card{
    background:var(--panel); border:1px solid var(--border); border-radius:var(--radius);
    width:100%; max-width:420px; max-height:88vh; overflow-y:auto; padding:20px;
  }

.account-title-row{display:flex; align-items:center; justify-content:space-between; margin-bottom:4px;}

.account-sub{font-size:12px; color:var(--text-faint); margin-bottom:14px;}

.account-field{display:flex; flex-direction:column; gap:4px; margin-bottom:11px;}

.account-field .hint{font-size:11px; color:var(--text-faint);}

.account-row-2{display:flex; gap:10px;}

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

.account-actions{display:flex; justify-content:flex-end; gap:8px; margin-top:16px;}

.account-status{font-size:12px; min-height:1em; margin-top:8px;}

.account-status.ok{color:var(--green-text);}

.account-status.err{color:var(--red-text);}

/* FOLLOW-UP EMAIL: the button goes UNDER its label, at the label's left edge.
   `align-items:flex-start` is the whole fix: it stops the label stretching,
   so the button starts where the label starts rather than the far right edge
   of a `.form-row`'s stretch. */
.followup-field{
  display:flex; flex-direction:column; gap:5px; align-items:flex-start;
}
/* Qualified to (0,3,1) on purpose: a global `button.btn.small{align-self:
   flex-end}` elsewhere is (0,2,1) and a plain `.followup-field .btn` at
   (0,2,0) would lose to it. Same lesson as `brand-title-input` above. */
.followup-field button.btn.small{ align-self:flex-start; }

/* ---------- What the shared people picker draws with ----------

   Shared because JS/components/people-picker.js is: note sharing opens it
   and so does a key result's contributors, on different pages. The class
   names keep their note-share- prefix — renaming would touch every call
   site to say nothing new, and where they came from is worth seeing. */
.note-share-chip{
  display:inline-flex; align-items:center; gap:4px; max-width:100%;
  font-size:11px; color:var(--text); background:var(--panel-2);
  border:1px solid var(--border-soft); border-radius:999px; padding:2px 4px 2px 9px;
}
.note-share-chip-name{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.note-share-chip-x{
  border:0; background:transparent; color:var(--text-faint); cursor:pointer;
  font-size:10px; line-height:1; padding:2px 4px; border-radius:50%; flex:0 0 auto;
}
.note-share-chip-x:hover{ color:var(--red-text); background:var(--panel); }
.note-share-chip-x:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; }
.note-share-label{
  font-size:11px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--text-faint); font-weight:700;
}
.note-share-search{ width:100%; max-width:100%; }
.note-share-selected{ display:none; align-items:center; flex-wrap:wrap; gap:6px; }
.note-share-selected-label{ font-size:11px; color:var(--text-dim); font-weight:600; }

/* ---------- The task action group ----------

   Related task actions sit together, on one row, under the title — things
   you do about the TASK, not beside the paperclip. No labels: the button
   text says what it is, and a heading above one button is chrome that
   pushes the description down a panel where vertical space is scarce.

   Responsiveness is `.control-row`'s, not a second implementation. The whole
   group carries `data-ai-only`, not its button: with no assistant configured
   the group is absent rather than an empty row holding a gap. */
.task-action-group{ display:flex; flex-direction:column; gap:6px; }
/* Qualified past a global `button.btn.small{align-self:flex-end}` that would
   otherwise drop these to the bottom of the row's cross axis. Same lesson
   and fix as `.followup-field button.btn.small` above. */
.task-action-group > .control-row > button.btn.small{ align-self:center; }

/* ---------- The standard dialog shell ----------

   Shared, because a dialog that several pages open cannot keep its chrome in
   one of them — a component whose chrome stays in one host is the failure
   that once rendered a picker `position:static`, unpositioned and behind
   everything, on a page that shares the dialog but not the stylesheet. The
   same lesson CSS/okr.css exists for (core rule 4). */
.settings-modal-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:var(--z-dialog);
  display:flex; align-items:center; justify-content:center; padding:24px;
}
.settings-modal{
  background:var(--panel); border:1px solid var(--border); border-radius:var(--radius);
  /* THE STYLESHEET'S WIDTH IS THE FLOOR, NOT THE SIZE. The size is set from
     the window at open time and on resize (§12: a dialog is sized from the
     window it has) — sizeModalToViewport() or sizeSettingsDialog(), both of
     which set an explicit width/height so a dialog does not resize as the
     reader moves between panes. What is left here is the fallback for a
     dialog opened before its sizer runs: `min(440px, 100%)` rather than a
     hard 440px, since the floor must not exceed the room a narrow phone has. */
  width:auto; min-width:min(440px, 100%); max-width:100%;
  box-shadow:0 12px 32px rgba(0,0,0,.5);
  display:flex; flex-direction:column; overflow:hidden;
}
.settings-modal-header{
  display:flex; align-items:center; justify-content:space-between;
  padding:16px 18px; border-bottom:1px solid var(--border); flex:0 0 auto;
}
.settings-modal-title{ font-size:14px; font-weight:600; }
.settings-modal-body{
  padding:18px; overflow-y:auto; display:flex; flex-direction:column; gap:18px;
  flex:1; min-height:0;
}
/* A tabbed settings dialog pins its tab strip and scrolls the pane — §12,
   the same rule `.modal-card.panes` states for the other modal shell.
   `panes` makes the body a frame instead of a scroller: the strip keeps its
   height, the active pane takes what is left and scrolls inside it. Opt-in,
   since the other dialogs sharing this chrome are single panes right to
   scroll as a whole. */
.settings-modal.panes > .settings-modal-body{ overflow:hidden; }
.settings-modal.panes > .settings-modal-body > .modal-tabs{ flex:0 0 auto; }
.settings-modal.panes > .settings-modal-body > .modal-pane.active{
  display:flex; flex-direction:column; flex:1 1 auto; min-height:0; overflow-y:auto;
}

/* ---------- "How this works" — one class, wherever a concept needs explaining ----------

   Closes the gap where a reader has not been told something they cannot
   infer from a button (a task on a board AND your list without being
   copied; a note private until named people are added). The class stays
   dormant until a canonical destination exists again — it is a ROUTE to an
   explanation, never a second copy of one.

   Faint, small and last, deliberately: an experienced reader's eye passes
   over it every time, a new one finds it exactly when stuck. A new tab, so
   nobody loses the dialog they were filling in. */
.learn-link{
  font-size:11px; color:var(--text-faint); text-decoration:none; white-space:nowrap;
  border-bottom:1px dotted var(--border-strong); padding-bottom:1px;
}
.learn-link:hover{ color:var(--accent-text); border-bottom-color:var(--accent); }
.learn-link:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

/* Standard modal action row — sys_design-system.md §12. Cancel on the left
   of the primary so the committing button is never under the cursor by accident. */
.settings-modal-footer{
  display:flex; align-items:center; justify-content:flex-end; gap:8px;
  padding:12px 18px; border-top:1px solid var(--border); flex:0 0 auto;
}

/* The class is live; the feature it was named for is not.
   JS/features/task-editor-markup.js reuses it for `#attachSection` — the
   attachments dropzone, status line and file list — which still needs the
   stacked column this rule gives. Renaming it would touch a call site to say
   nothing new, so the name stays. Shared for the reason above: a component
   whose chrome stays in one host is the failure CSS/okr.css exists to avoid. */
.task-email-section{ margin-top:8px; display:flex; flex-direction:column; gap:8px; }
.email-status{ font-size:12px; color:var(--text-dim); max-width:220px; }

/* Rides the right-hand end of the Analytics header row; the click guard is
   in JS, so reading it cannot collapse the panel. */
  .app-updated-note{
    margin-left:auto;white-space:nowrap;cursor:default;
    font-size:11px;color:var(--text-faint);
    /* nowrap keeps the date on one line, but also means the box never gives
       way — ellipsis loses the tail instead of clipping it mid-date silently,
       and the full text is on the element's own title. */
    min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;
  }

/* Without a label the three buttons read as unrelated actions rather than a choice
     between views. */
  .switcher-label{
    font-size:11px;color:var(--text-faint);text-transform:uppercase;
    letter-spacing:.5px;font-weight:600;margin-right:2px;white-space:nowrap;
  }

/* `a.switcher-label-link` and `.kbd-hint` were here, deleted rather than
   commented out: nothing emits either class, grepped repo-wide before
   removal. The Suite states a key the same way with `.a-kbd`
   (CSS/alt/alt-components.css), so the pattern survives the rule. */

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

.card.dragging{opacity:.4;}

/* ---------- Configurable indicator — sys_design-system.md §19 ----------
     Sized and coloured like the app's other icon buttons so it reads as chrome. */
  .cfg-dot{
    flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
    width:18px; height:18px; padding:0; margin-left:2px;
    background:transparent; border:none; border-radius:5px;
    color:var(--text-faint); cursor:pointer;
  }

.cfg-dot:hover{color:var(--accent-text); background:var(--panel);}

.cfg-dot:focus-visible{color:var(--accent-text);}

/* Same surface, border, radius and shadow as .context-menu, which is this app's
     existing small-popover pattern — this is that component with a different body. */
  .cfg-popover{
    position:fixed; z-index:var(--z-panel);
    background:var(--panel-2); border:1px solid var(--border);
    border-radius:var(--radius-sm); box-shadow:0 8px 24px rgba(0,0,0,.4);
    padding:10px 12px; width:236px;
  }

.cfg-pop-title{
    font-size:11px; text-transform:uppercase; letter-spacing:.4px;
    color:var(--text-faint); font-weight:700; margin-bottom:6px;
  }

.cfg-pop-list{margin:0 0 8px; padding-left:16px;}

.cfg-pop-note{font-size:11px; color:var(--text-faint); line-height:1.45; margin-bottom:8px;}

/* The popover carries its own button base, because its two hosts disagree
   about `.btn`: one carries a base rule, the other has none anywhere, so the
   same shared component rendered as raw OS chrome on one host.

   Scoped to the popover, above `html .btn` on purpose — `.cfg-popover .btn`
   at (0,2,0) wins on both hosts, so the control is byte-identical on each
   rather than one fixed host and one still broken. Not fixed by loading the
   other host's whole editor sheet for one button — a shared component
   styling itself is the smaller, more honest answer. */
.cfg-popover .btn{
  background:var(--panel-2);
  border:1px solid var(--border-strong);
  color:var(--text);
  padding:7px 12px;
  border-radius:var(--radius-sm);
  font-size:13px;
  font-weight:600;
  display:flex; align-items:center; gap:6px;
  white-space:nowrap;
}
.cfg-popover .btn:hover{ border-color:var(--accent); color:var(--accent-text); }
.cfg-popover .btn.small{ padding:5px 9px; font-size:12px; }
.cfg-pop-go{width:100%; justify-content:center;}

/* ---------- Link field with an "open" button — removed ----------
   `.link-field`/`.link-open-btn` styled the External Link row's input-plus-
   open-button pair. Neither class is emitted anywhere; the live answer is
   the Suite's link row (altDrawerLinks(), JS/alt/alt-drawer.js). Three other
   stylesheets still carry `.link-open-btn` rules just as unreachable —
   reported rather than removed here, since they are not this lane's files. */

/* Only rendered when the count is above zero. */
  .card-comments{
    display:inline-flex; align-items:center; gap:3px; font-size:11px; font-weight:600;
    color:var(--text-faint); background:var(--panel-2); border-radius:999px; padding:1px 7px;
  }

/* `.card-okr` was here — a board card's "supports a key result" pill.
   Removed: emitted by nothing; the Suite says the same thing with its own
   chip, and okrs.html says it from the objective's side. */

/* `.orientation-toggle-btn` was here — removed, not orphaned by the
   deletion: the Suite's panel arrangement is a preference under
   `settings.altUx`, reached from Settings rather than a pinned button. */

/* ---------- The modal shell ----------
   Lifted out to a shared file so every host opens the SAME dialog rather
   than a look-alike — a page keeps only its own overrides (#adminModal
   sizing, #tempPwOverlay z-index). */
/* The scrollbar moved to CSS/base.css — sys_design-system.md §2 — which is
   the one shared file every page with this palette loads, unlike this one. */

.modal-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.5); display:none;
  align-items:center; justify-content:center; z-index:var(--z-dialog); padding:20px;
}
.modal-overlay.show{display:flex;}
/* Every dialog arrives — and leaves — the same way, from one rule rather
   than nine call sites: attaching the primitive to the shared shell means a
   dialog cannot opt out by forgetting.

   Done with `transition`/`allow-discrete` rather than a JS-timed animation,
   so there is no window where a stale overlay is left on screen if a
   handler throws or the reader reopens mid-fade — the browser owns the
   whole sequence, and degrades to instant open/close on an older browser.

   Pointer events go with `.show`, not the fade, so the scrim fading out
   cannot swallow a click meant for the page underneath. The card moves and
   the scrim only fades — both travelling reads as the whole window lurching. */
.modal-overlay, .account-overlay{
  opacity:0; pointer-events:none;
  transition:opacity var(--dur-exit) var(--ease-in),
             display  var(--dur-exit) allow-discrete;
}
.modal-overlay.show, .account-overlay.show{
  opacity:1; pointer-events:auto;
  transition:opacity var(--dur-surface) var(--ease-out),
             display  var(--dur-surface) allow-discrete;
}
@starting-style{
  .modal-overlay.show, .account-overlay.show{ opacity:0; }
}
/* The same 8px rise and .985 as `.surface-enter`, repeated here only because
   this shell cannot wear a utility class it never had in its markup. Change
   one, change both — motion-system.test.js checks they agree. */
.modal-overlay > .modal-card,
.account-overlay > .modal-card{
  transform:translateY(8px) scale(.985);
  transition:transform var(--dur-exit) var(--ease-in);
}
.modal-overlay.show > .modal-card,
.account-overlay.show > .modal-card{
  transform:none;
  transition:transform var(--dur-surface) var(--ease-out);
}
@starting-style{
  .modal-overlay.show > .modal-card,
  .account-overlay.show > .modal-card{ transform:translateY(8px) scale(.985); }
}
@media (prefers-reduced-motion: reduce){
  .modal-overlay, .account-overlay,
  .modal-overlay.show, .account-overlay.show,
  .modal-overlay > .modal-card, .account-overlay > .modal-card,
  .modal-overlay.show > .modal-card, .account-overlay.show > .modal-card{ transition-duration:1ms !important; }
}

.modal-card{
  background:var(--panel); border:1px solid var(--border); border-radius:var(--radius);
  width:100%; max-width:520px; max-height:88vh; overflow-y:auto; padding:20px;
}
.modal-card.wide{max-width:640px;}

/* A tall modal pins its chrome and scrolls its pane — sys_design-system.md
   §12. `.modal-card` scrolls as a whole, right for a short dialog and wrong
   for a tall one, where scrolling to the description scrolls away the
   control used to leave. Add `panes` to a card whose content is a set of
   `.modal-pane` children. */
.modal-card.panes{ display:flex; flex-direction:column; overflow:hidden; }
.modal-card.panes > .modal-title-row,
.modal-card.panes > .modal-tabs{ flex:0 0 auto; }
.modal-card.panes > .modal-pane.active{
  display:flex; flex-direction:column; flex:1 1 auto; min-height:0; overflow-y:auto;
}

.modal-title-row{display:flex; align-items:center; justify-content:space-between; margin-bottom:14px;}

.modal-title-row h3{margin:0; font-size:16px;}

.modal-footer{display:flex; justify-content:flex-end; gap:8px; margin-top:18px; padding-top:14px; border-top:1px solid var(--border);}

/* A tab strip is one line tall — a wrapped label doubles the strip's height
   and reads as two rows of tabs. `nowrap` fixes the height; the strip
   scrolls instead if a window is genuinely too narrow for the labels. */
.modal-tabs{display:flex; gap:4px; margin-bottom:14px; background:var(--panel-2); border-radius:var(--radius-sm); padding:3px; overflow-x:auto;}

.modal-tab{flex:1; background:transparent; border:none; padding:7px; border-radius:6px; color:var(--text-dim); font-weight:600; font-size:12px; white-space:nowrap;}

.modal-tab.active{background:var(--accent-fill); color:#fff;}

/* ================= `.tabs-underline` — A TAB STRIP THAT IS NOT A SEGMENTED CONTROL =================

   The default above is a tray of pills: `flex:1` stretches every tab equally
   and `.active` fills it solid, which reads badly in a wide editor where the
   default-selected tab becomes a huge blue slab carrying no news. This is
   the other treatment: natural-width labels, a rule under the strip, the
   active one sitting ON that rule — the same vocabulary as `.a-drawer-tabs`/
   `.a-tab` in the suite's own shell.

   A class, not a restyle of `.modal-tab`, since fourteen surfaces render
   that class and only the editors needed the new treatment — opt-in, and
   `task-editor-tab-parity.test.js` asserts both halves hold.

   Lives here, in the shared sheet, rather than in CSS/alt/, because it must
   reach a host that cannot load CSS/alt/ — components.css is the only
   stylesheet both hosts load.

   THE `#panelModalTabs` HALF OF EVERY SELECTOR IS A TEMPORARY ALIAS, NOT A
   SECOND HOST — matched by id only until `tabs-underline` reaches that host's
   own markup directly. When it does, delete every `:root.embed-task
   #panelModalTabs` line in this block; a component identified by a host id
   is the thing this block exists to stop being. The parity suite fails if a
   rule here names one host and not the other, so the alias cannot be
   half-removed.

   AND WHOEVER DOES THAT MUST COUNT THREE HOSTS, NOT TWO: a third host
   renders this same strip byte-identical but carries no `.alt-app`, so
   dropping the alias would silently fall it back to the pill tray unless the
   `.alt-app` prefix is dropped in the same change or a parallel selector is
   added. The suite cannot catch this: losing a host reads as a selector
   list that got shorter, not wrong.

   Scoped to `.alt-app` because the values are the suite's `--a-*` roles,
   which other hosts do not define — roles, not a second palette.

   The underline is the tab's own bottom border, not an absolute bar under
   the strip: `.modal-tabs` carries `overflow-x:auto`, and 1px of overflowing
   decoration would put a vertical scrollbar down the end of the strip. */
.alt-app .modal-tabs.tabs-underline,
:root.embed-task #panelModalTabs{
  gap:2px; padding:0; background:none; border-radius:0;
  border-bottom:1px solid var(--a-line);
}
/* `0 1 auto`, not `0 0 auto`: never grow, since growing is the pill tray
   this replaces, but shrink rather than scroll when the strip genuinely
   cannot hold the row — a no-shrink row put a horizontal scrollbar under a
   tab strip that silently became two lines tall (sys_design-system.md §9).
   Ellipsis instead, one line at every width; `min-width:0` is what lets a
   flex item shrink below its content at all. */
.alt-app .modal-tabs.tabs-underline .modal-tab,
:root.embed-task #panelModalTabs .modal-tab{
  flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis;
  height:38px; padding:0 14px; border-radius:0; background:none;
  border-bottom:2px solid transparent;
  color:var(--a-faint); font-family:var(--a-body); font-size:13px; font-weight:600;
  transition:color var(--a-quick) var(--a-ease), border-color var(--a-quick) var(--a-ease);
}
.alt-app .modal-tabs.tabs-underline .modal-tab:hover,
:root.embed-task #panelModalTabs .modal-tab:hover{ color:var(--a-dim); }

/* No longer shaded, per the product owner's request: the COUNT alone says a
   task has comments now — `Comments (3)` needs nothing added to it.

   The three rules below still have to exist, though they no longer paint
   anything new: `.modal-tab.has-unread` elsewhere is the Message Center's
   bare pill treatment, and this strip's higher-specificity selector is what
   has always kept that pill look off the underline tabs. Deleting these
   outright would remove the only thing cancelling the pill rule and let its
   fill and glow leak back in, so each property is restated at the SAME
   values the un-lit tab already carries — a documented no-op.

   The unread fact now survives only in the tab's `aria-label`/`title`; a
   sighted reader scanning the strip will not see it. That trade was the
   owner's explicit call, not a quiet side effect. */
.alt-app .modal-tabs.tabs-underline .modal-tab.has-unread,
:root.embed-task #panelModalTabs .modal-tab.has-unread{
  background:none; color:var(--a-faint); box-shadow:none;
}
.alt-app .modal-tabs.tabs-underline .modal-tab.has-unread:hover,
:root.embed-task #panelModalTabs .modal-tab.has-unread:hover{
  background:none; color:var(--a-dim); box-shadow:none;
}
.alt-app .modal-tabs.tabs-underline .modal-tab.active,
:root.embed-task #panelModalTabs .modal-tab.active{
  background:none; color:var(--a-blue-text); border-bottom-color:var(--a-blue);
}
/* Active-and-unread still needs its own line even though the values match
   `.active` exactly: that rule and `.has-unread` tie on specificity, so
   restating the values at six classes removes the tie rather than relying
   on source order to settle it. */
.alt-app .modal-tabs.tabs-underline .modal-tab.has-unread.active,
:root.embed-task #panelModalTabs .modal-tab.has-unread.active{
  background:none; color:var(--a-blue-text); border-bottom-color:var(--a-blue); box-shadow:none;
}
.alt-app .modal-tabs.tabs-underline .modal-tab:focus-visible,
:root.embed-task #panelModalTabs .modal-tab:focus-visible{
  outline:2px solid var(--a-ring); outline-offset:-2px; border-radius:var(--a-r-sm);
}

.modal-pane{display:none;}

.modal-pane.active{display:block;}

.comments-list{max-height:280px; overflow-y:auto; display:flex; flex-direction:column; gap:10px; margin-bottom:12px;}
/* The list grows to admit an open edit, then returns to its own 280px the
   moment it closes — companion to `.comment-item-body .rt-input{max-height:
   none}` in CSS/alt/alt-rich-text.css, which lets the edit box itself grow.
   Left alone, this rule's fixed 280px would just move the clip down one
   level, onto a still-cramped list inside an ancestor pane with height
   nobody was using. `:has(.rt-composer)` scopes the relief to the moment a
   comment is actually being edited; ordinary browsing keeps today's
   pinned-composer, list-scrolls-internally behaviour untouched. */
.comments-list:has(.rt-composer){ max-height:none; }

.comment-item{background:var(--panel-2); border:1px solid var(--border-soft); border-radius:var(--radius-sm); padding:8px 10px; display:flex; gap:8px;}

/* ================= THE HISTORY TAB'S ROWS =================

   Here rather than in CSS/task-editor-panel.css because two surfaces render
   `taskAuditRowHtml()`'s output, and one of the two hosts does not load that
   sheet — components.css is the only stylesheet both load.

   Deliberately does not look like a comment: `.comment-item` above is a card
   with an avatar, because a comment is something a person said. An audit row
   is a FACT the system recorded, with no author to put a face to, only an
   actor label — a quiet stacked list rather than cards, cheaper to scan and
   less inviting to reply to.

   The list has no empty state of its own: when there is nothing to show,
   task-audit.js writes the sentence into `#panelAuditStatus` and leaves the
   list empty, so `.audit-list` must collapse to nothing rather than reserve a gap. */
/* The rhythm is `.comments-list`'s, deliberately, since the two are tabs of
   one panel: same gap, padding, radius and edge as `.comment-item` above —
   otherwise the panel visibly changes texture when the tab changes. */
.audit-list{display:flex; flex-direction:column; gap:10px;}
.audit-event{
  background:var(--panel-2); border:1px solid var(--border-soft);
  border-radius:var(--radius-sm); padding:8px 10px;
}
/* Not a comment: an audit row has an actor LABEL, no account behind it, and
   nothing to reply to, so it takes the card and leaves the avatar and action
   row out. The change itself at body size, 13px — the sentence the reader
   opened the tab for. */
.audit-event-line{font-size:13px; line-height:1.45; color:var(--text);}
/* Who and when, on the secondary tier, the pairing every byline uses. The
   middot is written by taskAuditRowHtml(), not a ::before, since the "when"
   half is omitted for a row with no timestamp and a css separator would dangle. */
.audit-event-meta{font-size:11px; line-height:1.5; color:var(--text-faint); margin-top:3px;}
/* Every value above is a token, which is what makes the custom themes free. */

.comment-avatar{width:26px; height:26px; border-radius:50%; color:#fff; font-size:10px; font-weight:700; display:flex; align-items:center; justify-content:center; flex-shrink:0;}

.comment-body-col{flex:1; min-width:0;}

/* Author, when, and the actions — one line, since it had room going spare
   (a row holding only Reply/Edit/Delete, on a panel whose height is scarce).
   `flex-wrap` keeps that honest at a narrow width: the actions drop to their
   own line only when the name and time actually need the room. */
.comment-item-meta{display:flex; align-items:baseline; flex-wrap:wrap; font-size:11px; color:var(--text-faint); margin-bottom:4px; gap:4px 8px;}
/* The time sits with the name it belongs to; the actions are pushed to the far edge. */
.comment-item-when{margin-right:auto;}

.comment-item-author{font-weight:700; color:var(--text-dim); font-size:13px;}

.comment-item-body{font-size:13px; color:var(--text); white-space:pre-wrap; word-break:break-word;}

/* No margin-top: it is a member of the meta line now, not a row under the body. */
.comment-actions-row{display:flex; gap:12px;}

.comment-action-btn{background:none; border:none; color:var(--text-faint); font-size:11px; cursor:pointer; padding:0;}

.comment-action-btn:hover{color:var(--accent-text);}

.comment-action-btn.danger:hover{color:var(--red-text);}

.comment-edited-tag{color:var(--text-faint); font-size:11px; font-style:italic;}

/* One definition of the comment box — two former private copies had already
   drifted (padding, line-height, margin-top). The contract is one definition
   each: the composer is `.comment-textarea`, the inline editor is `.rt-input`,
   owned by CSS/alt/alt-rich-text.css, built by JS/components/comment-list.js. */
.comment-textarea{
  width:100%; resize:vertical; font-family:inherit;
  background:var(--panel-2); border:1px solid var(--border-strong); border-radius:var(--radius-sm);
  color:var(--text); padding:8px 10px; font-size:13px; line-height:1.45;
}

/* The composer is sized to be written in, not a single line that grows a
   scrollbar — `min-height` rather than `rows=` alone, since a `rows`
   attribute is the INITIAL size and a script setting `.value` does not
   restore it. Still user-resizable; this is the floor, not the cap. */
.comment-add-row .comment-textarea{ min-height:132px; }
/* The same floor for the shared rich composer that replaced it — `.rt-input`'s
   own default (64px) is a sensible answer elsewhere and the wrong one here. */
.comment-add-row .rt-input{ min-height:132px; }
/* Five rules once stood here, scoped to `.note-comments-body` — a row-laid
   composer with Post beside the box — measured against a real defect,
   "the post button is missing", where a floor in a narrow rail walked Post
   below the fold, and correct for the surface they were written for.

   THAT SURFACE IS GONE. `.note-comments-body` was a retired page's wrapper,
   and nothing in this repository has emitted the class since.

   The contract did not move here — it was deliberately replaced, which is
   why there is nothing to repoint. The Suite's note composer, `.a-composer`
   in CSS/alt/alt-notes.css, is a flex COLUMN with Post BELOW the box: as a
   sibling of a growing textarea in a row on a narrow panel, the button ended
   up sitting on top of the words being typed. Same defect, opposite answer,
   decided later with the newer surface in front of the person deciding.
   Re-adding a row-shaped composer for notes would be reopening that. */

.comment-textarea:focus{border-color:var(--accent);}

/* Post is pushed right on this row, so it reads as part of the comment composer rather
     than sitting next to the modal's Save button. */
  .comment-suggest-row{display:flex; align-items:center; gap:6px; flex-wrap:wrap;}

.comment-suggest-row > .btn{margin-left:auto;}

.comment-suggest-chip{background:var(--panel); border:1px solid var(--border); border-radius:999px; padding:4px 10px; font-size:12px; color:var(--text-dim); cursor:pointer;}

.comment-suggest-chip:hover{border-color:var(--accent); color:var(--accent-text);}

/* The Mention button. The same chip as its neighbours, deliberately — it
   belongs to that row and a different control would read as something else.
   It carries an icon and a slightly stronger edge, the one control in the
   row that opens something rather than types something. */

.comment-add-row{display:flex; flex-direction:column; gap:8px;}
/* team_board.html pairs "Tidy formatting with AI" with "From email…" on one
   row — scoped to this one host via its own id, not a change to the base
   rule above (tasks.html and notes.html keep their own column/row behaviour
   untouched). The two controls must stay DOM siblings (never nested) because
   commentAiAttach() writes one element's whole innerHTML on first attach and
   would erase the other if nested — so this file makes two siblings of a
   COLUMN flex container share one line without touching the DOM.

   The technique: switch this `.comment-add-row` to a ROW that wraps, force
   every child back onto its own full-width line with `flex-basis:100%`,
   except the paired two, which land on the same wrapped line together — the
   same "wraps before it clips" behaviour `.control-row` gives tasks.html's
   pairing. `> *` defaults every child to a full line so a future child added
   here is full-width until deliberately exempted. */
.comment-add-row:has(> #taskCommentAi){
  display:flex; flex-direction:row; flex-wrap:wrap; align-items:center; gap:8px;
}
.comment-add-row:has(> #taskCommentAi) > *{ flex-basis:100%; }
.comment-add-row:has(> #taskCommentAi) > #altTaskCommentTools,
.comment-add-row:has(> #taskCommentAi) > #taskCommentAi{ flex:0 1 auto; margin-top:0; }
/* ---------- AND POST COMMENT JOINS THAT ROW, HARD RIGHT ----------

   Per the product owner: "move the post comment up to be aligned with the
   'from email' button on the left but keep it on the right aligned with the
   right edge of the multi-line text box."

   The same technique, moved one line up, not a new one: `flex:0 0 auto`
   holds the button at its own width, and `margin-left:auto` absorbs the
   flex line's free space, measured against `.comment-add-row`'s own content
   box — the same box `.rt-composer` (the multi-line box he named) fills at
   the inherited `flex-basis:100%`, so neither edge is written down twice.
   Still wraps before it clips, the behaviour the `> *` rule above was built for.

   The DOM move is taskCommentPostPlace() (JS/features/board-task-modal.js),
   moving team_board.html's own static button rather than building a second
   one — it must stay a SIBLING of `#taskCommentAi`, never nested. */
.comment-add-row:has(> #taskCommentAi) > #taskAddCommentBtn{ flex:0 0 auto; margin-left:auto; }
/* ---------- THE SAME PLACEMENT ON tasks.html's FRAMED EDITOR (?embed=task) ----------

   "Do the same for individual tasks" — the owner, in the message after the one above.

   Needed no technique at all: this host's pairing row is a real
   `.control-row`, so Post is simply its last child and one auto margin does
   the identical job. At ≤640px there is nothing for it to do — the button
   already takes the whole width at that breakpoint, so the margin has no
   slack to absorb and the row stacks like every other `.control-row`. */
#commentActionsRow > #panelAddCommentBtn{ margin-left:auto; }

.comments-empty{color:var(--text-faint); font-size:12px; text-align:center; padding:16px 0;}
/* In a note it is a line, not a block: a third of the height, kept rather
   than removed because "no comments yet" and "the comments failed to load"
   must not look alike. The rule that did this for the note panel went with
   the rest of that scope; `.a-empty` on the Suite's own panel keeps the
   distinction now — a product rule, not a cosmetic one. */

/* ================= THE TASK EDITOR'S HEADER =================

   One header, two editors. Per the product owner: "place the buttons and
   header to look exactly the same way it is on the tasks view." Unscoped
   and shared, which is what a component drawn by two pages has to be — the
   first build of the team header stacked its three icons vertically with an
   invisible dot when these rules stayed scoped to one host's class.

   `.panel-head` itself is not here: it carries the panel's own padding and
   border, which the modal card already supplies — the shared part is the
   row's contents. */
/* The save state is on the tab row now, hard right. `.modal-tabs` is the
   class both hosts already carry, so one rule reaches both — still right-
   aligned, not centred, since the text changes length (idle -> Saving… ->
   All changes saved · 2:15 PM) and a centred element would slide under the eye.

   It yields first, and more eagerly than the tabs: a higher shrink factor
   (`flex:0 3 auto` against the tabs' `1`) narrows the status line before it
   ever touches a tab label, truncated rather than wrapped — there is no
   second line in this row to drop it to.

   `align-self:center` is new here: `.modal-tab` sets its own height under
   `.tabs-underline` and this element has none of its own, so without it the
   row's default stretch would pull the text to the top. `.cfg-status`
   supplies the colours; this only re-lays it out. */
.modal-tabs > .save-status{
  flex:0 3 auto; min-width:0; margin-left:auto; text-align:right; align-self:center;
  font-size:12px; line-height:1.3; color:var(--text-dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* The component writes into a child span when it is showing dots, so the ellipsis has to
   apply to whatever is inside it too. */
.modal-tabs > .save-status .proc{ overflow:hidden; text-overflow:ellipsis; max-width:100%; }

.ph-left{display:flex; align-items:center; gap:10px;}
.ph-right{display:flex; align-items:center; gap:2px;}
.status-dot{width:8px; height:8px; border-radius:50%; background:var(--text-faint); flex:0 0 auto;}
.ph-divider{width:1px; height:18px; background:var(--border-soft); margin:0 4px;}
/* The two task actions are the only coloured icons in the app's chrome —
   the two that change something you cannot undo with a click. */
.icon-btn.complete-btn{color:var(--green-text);}
.icon-btn.complete-btn:hover{background:var(--green-soft);}
.icon-btn.danger{color:var(--red-text);}
.icon-btn.danger:hover{background:var(--red-soft);}

/* ================= THE DUE DATE FIELD, AND "No date" =================

   One field, two editors, which were not the same control: one had a
   calendar glyph, a full-width hit area and the No date switch; the other
   was a bare `<input type=date>`. One set of rules both pages read now,
   rather than a copy in each.

   The glyph is ours, the picker is the browser's: the native indicator is
   made transparent and stretched across the right 32px, so the whole area
   opens the picker while the reader sees a glyph matching every other icon. */
.date-field-wrap{position:relative;}
/* Full width, since the readout is inset from the WRAP and the wrap is a
   block — every other field in that modal already fills its row, and a
   narrow date field read as unfinished beside them. */
.date-field-wrap input[type=date]{padding-right:32px; width:100%;}
.date-field-wrap input[type=date]::-webkit-calendar-picker-indicator{
  opacity:0; cursor:pointer;
  position:absolute; right:0; top:0; width:32px; height:100%;
}
.date-field-icon{
  position:absolute; right:10px; top:50%; transform:translateY(-50%);
  color:var(--text-dim); pointer-events:none;
}

/* A switch that belongs to one field sits inside that field, under its
   control — the placement `.switch-check` uses for "Show in my tasks" and
   "Mark as an OKR". Two adjustments on the default: a tighter gap, and
   `flex-wrap` so the word drops below the toggle in a narrow column. */
.switch-check.no-due{
  display:flex; flex-wrap:wrap; gap:6px; margin-top:7px;
}

/* The field says "No date" — it does not just go empty. A cleared
   `<input type=date>` shows the browser's own `mm/dd/yyyy`, which reads as a
   date not yet filled in — the opposite of "deliberately unscheduled". So
   the words are painted over the box.

   THE INPUT STAYS IN PLACE, disabled and transparent, rather than being
   display:none, keeping the control's exact height so toggling the switch
   changes what the field SAYS without moving anything around it. */
.date-field-wrap.is-no-date input[type="date"]{ color:transparent; }
.date-field-wrap.is-no-date .date-field-icon{ opacity:.45; }
.date-none{ display:none; }
.date-field-wrap.is-no-date .date-none{
  /* Inset by the input's 1px border so the box's own outline still frames it. */
  position:absolute; inset:1px 32px 1px 1px;
  display:flex; align-items:center; padding:0 11px;
  border-radius:var(--radius-sm);
  background:var(--panel-2); color:var(--text-dim);
  /* "No date" broke onto two lines in a 71px box and sat clipped by the input's height. */
  font-size:14px; white-space:nowrap; pointer-events:none;
}

/* ---------- Always-visible failure banner — removed ----------
   The class is emitted by nothing now. The contract has a live owner, which
   is why this is a deletion and not a loss: JS/core/app-errors.js is the
   application's failure-visibility layer and renders `.a-err`, guarded by
   outage-visibility.test.js — Definition of Done 4f, stated once. */

/* ---------- Settings pane ----------
   The rows a settings modal is built from — one layout primitive shared
   across pages rather than a copy in each (core rule 4). */
.settings-row{display:flex;flex-direction:column;gap:8px;}
.settings-row + .settings-row{padding-top:16px;border-top:1px solid var(--border-soft);}
.settings-row-label{
  font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px;font-weight:600;
}
/* `.settings-row-desc` was here. Removed: no page and no module writes the
   class. `.pane-hint` immediately below is the live description in these
   panes, and the Suite's is `.a-row-desc` (CSS/alt/alt-components.css). */
.pane-hint{font-size:12px; color:var(--text-faint); line-height:1.5; margin-bottom:8px;}
/* An add-a-thing row: one or more inputs that grow, and a button that does not. Named for
   the weather list it started in; the shape is the same wherever a settings pane lets you
   append to a list. */
.weather-add-row{display:flex; gap:7px; margin-top:10px; flex-wrap:wrap;}
.weather-add-row input,.weather-add-row select{flex:1 1 130px; min-width:0;}
.weather-add-row .btn{flex:0 0 auto;}
.weather-empty{font-size:12px; color:var(--text-faint); padding:8px 2px;}
/* One status component, two names — `.import-status` carried only state
   modifiers (`.err`/`.ok`) and no base rule, a component that does not exist
   until a state is set: no size, no colour, no reserved height, which went
   unnoticed while a page's own <style> supplied them and broke the moment a
   host loaded neither.

   The previous fix changed the WRITERS, not the stylesheet — which fixed the
   lines those writers touch and left every other element carrying the class
   still unstyled. Aliasing the name onto the component that already exists
   fixes it once, for every writer, and does not create a second component:
   one set of declarations, both names resolve to it.

   Line-height is part of the component now: the one message in this
   application routinely three lines long had none, and read as unformatted
   text beside prose that did. */
.cfg-status,
.import-status{font-size:12px; line-height:1.5; color:var(--text-dim); min-height:16px; margin-top:8px;}
.cfg-status.err,
.import-status.err{color:var(--red-text);}
/* And the success colour, which this file never carried — `.err` was here
   from the start and `.ok` was not, so a success message rendered in the
   neutral dim grey and looked merely unstyled rather than wrong. */
.cfg-status.ok,
.import-status.ok{color:var(--green-text);}

/* ---------- CSV import: preview and column mapping ----------
   Every colour is a token, so the panel follows the palette into light mode
   with nothing to keep in step. */
.import-preview{
  border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--panel-2); padding:11px 12px; margin-top:12px;
}
.import-preview-head{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; margin-bottom:8px;}
.import-preview-head .import-file{font-size:13px; color:var(--text); word-break:break-all;}
.import-preview-head .import-shape{font-size:12px; color:var(--text-dim);}
/* The table scrolls inside its own box. A 12-column CSV must not widen the settings
   modal, and a modal that grows with the file is one that no longer fits the screen. */
.import-table-scroll{overflow-x:auto; max-width:100%;}
.import-table{border-collapse:collapse; font-size:12px; width:max-content; min-width:100%;}
.import-table th,.import-table td{
  border:1px solid var(--border-soft); padding:4px 8px; text-align:left;
  max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.import-table th{background:var(--panel); color:var(--text-dim); font-weight:600;}
.import-table td{color:var(--text);}
.import-table .import-more{color:var(--text-faint); text-align:center;}
.import-preview-foot{font-size:12px; color:var(--text-faint); margin-top:7px;}

.import-mapping{
  border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--panel-2); padding:11px 12px; margin-top:10px;
}
.import-map-head{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; margin-bottom:9px;}
.import-map-head b{font-size:13px;}
.import-map-note{font-size:12px; color:var(--text-dim); flex:1 1 220px;}
.import-map-row{
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.1fr); gap:10px;
  align-items:center; padding:4px 0;
}
/* `.import-map-field` was here, removed — not emitted anywhere. The column
   name cell it was written for is now the first grid child of
   `.import-map-row`, which sizes it. */
.import-map-row select{font-size:12px; padding:4px 7px; min-width:0;}
/* Where an answer came from. "matched by name" is a lookup; the rest are a model's
   judgement, and the difference is worth showing before somebody trusts it. */
.import-conf{
  font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  padding:1px 5px; border-radius:4px; border:1px solid transparent; white-space:nowrap;
}
.import-conf.exact{background:var(--green-soft); color:var(--green-text); border-color:var(--green);}
.import-conf.high{background:var(--accent-soft); color:var(--accent-text); border-color:var(--accent);}
.import-conf.med{background:var(--gray-soft); color:var(--text-dim); border-color:var(--border-strong);}
.import-conf.low{background:var(--yellow-soft); color:var(--warn-text); border-color:var(--yellow);}
.import-conf.none{background:var(--gray-soft); color:var(--text-faint); border-color:var(--border);}
/* The review table reads left to right like the spreadsheet: column, what it becomes, how
   sure, what is in it. The old field-oriented grid rows stay for anything still using them. */
.import-map-table th,.import-map-table td{vertical-align:top;}
.import-map-table .import-col-name b{display:block; font-size:12px; color:var(--text);}
.import-map-table .import-col-why{display:block; font-size:11px; color:var(--text-faint); margin-top:2px; max-width:26ch;}
.import-map-table .import-col-sample{color:var(--text-faint); max-width:30ch;}
.import-map-table select{font-size:12px; padding:3px 6px; max-width:100%;}
.import-map-table tr.is-ignored .import-col-name b{color:var(--text-faint);}

.import-review,.import-results{
  border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--panel-2); padding:11px 12px; margin-top:10px;
}
.import-summary{display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
.import-summary-total{font-size:13px; color:var(--text); font-weight:600;}
/* Counts, not prose: "238 ready · 4 need attention · 3 cannot import" is the one line that
   has to be readable at a glance before someone presses Import. */
.import-chip{
  font-size:11px; padding:2px 8px; border-radius:999px; border:1px solid transparent;
  white-space:nowrap;
}
.import-chip b{font-weight:700;}
.import-chip.ready{background:var(--green-soft); color:var(--green-text); border-color:var(--green);}
.import-chip.attention{background:var(--yellow-soft); color:var(--warn-text); border-color:var(--yellow);}
.import-chip.invalid{background:var(--red-soft); color:var(--red-text); border-color:var(--red);}
.import-warn{
  margin-top:9px; padding:7px 9px; border-radius:var(--radius-sm);
  background:var(--yellow-soft); border:1px solid var(--yellow);
  color:var(--warn-text); font-size:12px; line-height:1.5;
}
.import-sub{
  font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.06em;
  margin:12px 0 6px;
}
.import-problems{margin:0; padding-left:18px;}
.import-problem{font-size:12px; line-height:1.55; color:var(--text-dim);}
.import-problem b{color:var(--text);}
.import-problem.invalid b{color:var(--red-text);}
.import-more-note{font-size:12px; color:var(--text-faint); margin-top:5px;}
.import-modes{
  border:1px solid var(--border); border-radius:var(--radius-sm);
  padding:9px 11px; margin-top:10px;
}
.import-modes legend{font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.06em; padding:0 5px;}
.import-modes label{display:flex; align-items:flex-start; gap:7px; font-size:13px; color:var(--text); padding:3px 0; cursor:pointer;}
.import-modes input{margin-top:2px; flex:0 0 auto;}

.import-unmapped{margin-top:10px; padding-top:9px; border-top:1px solid var(--border-soft);}
.import-unmapped b{font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.06em;}
.import-unmapped ul{margin:6px 0 0; padding-left:18px;}
.import-unmapped li{font-size:12px; color:var(--text-faint); line-height:1.5;}

/* ---------- Message centre ----------
   One dialog, opened from the task app and the team board over the same rows. It is the
   app's modal — .modal-overlay / .modal-card / .modal-tabs above — and everything here is
   the LIST inside it. The chrome was hand-rolled once and looked like a different product. */

.mc-card{
  /* Taller than a form modal because it is a reading surface, and a flex column so the list
     scrolls inside the card while the title, tabs and footer stay put. */
  display:flex; flex-direction:column; overflow:hidden;
  max-width:560px; height:min(620px, 88vh);
}
.mc-card .modal-title-row,.mc-card .modal-tabs,.mc-card .modal-footer{ flex:0 0 auto; }
/* min-height:0 is what lets a flex child shrink below its content and actually scroll. */
.mc-card .modal-pane.active{ flex:1 1 auto; min-height:0; overflow-y:auto; margin:0 -4px; padding:0 4px; }
/* No `justify-content` here — the footer takes `.modal-footer`'s `flex-end`
   like every other dialog, now that the close button moved up into
   `.modal-title-row` and left this footer with a single child.

   If a left-hand control ever comes back, it is `.control-spacer`, never
   `space-between`: the canonical action row is `.settings-modal-footer` +
   `.control-row` + `.control-row-end`, and a spacer keeps the right-hand
   group together and right-aligned when the row wraps. */

.mc-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:7px; }
.mc-empty{ padding:34px 18px; color:var(--text-dim); font-size:13px; text-align:center; }
.mc-empty.err{ color:var(--red-text); }
.mc-empty .btn{ margin-top:10px; }

.mc-item{
  display:flex; gap:9px; align-items:flex-start;
  /* Wraps only so the reply composer can claim a line of its own — the dot,
     avatar, text and buttons stay on the first line regardless (see .mc-body). */
  flex-wrap:wrap;
  padding:11px 12px; cursor:pointer;
  border:1px solid var(--border); border-radius:var(--radius-sm);
  background:var(--panel-2);
}
.mc-item:hover,.mc-item:focus-visible{ border-color:var(--accent); }
/* Unread is the one thing this list must make obvious — three signals, none
   colour alone: the accent edge, the dot, and a raised surface. The edge
   transitions, so marking one read de-emphasises it rather than snapping. */
.mc-item.unread{ border-left:3px solid var(--accent); background:var(--panel-2); }
.mc-item{ transition:border-color var(--dur-quick) var(--ease-out),
                     background-color var(--dur-quick) var(--ease-out),
                     transform var(--dur-micro) var(--ease-move); }
/* Rows arrive in order — the same capped stagger the progress bars use, set per row in JS. */
.mc-list .mc-item{ animation:mcRowIn var(--dur-quick) var(--ease-out) both; }
@keyframes mcRowIn{ from{ opacity:0; transform:translateY(4px); } }

/* The actor, as two letters. Quiet by construction: orientation, not
   decoration, must not out-shout the message. */
.mc-avatar{
  flex:0 0 auto; width:28px; height:28px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  font-size:11px; font-weight:700; letter-spacing:.02em;
  color:var(--text-dim); background:var(--panel-2); border:1px solid var(--border);
}
.mc-item.unread .mc-avatar{ color:var(--accent-text); border-color:var(--accent); }
/* What was commented on, in front of the thing it names. */
.mc-src{ flex:0 0 auto; color:var(--text-faint); vertical-align:-2px; margin-right:5px; }

/* CAUGHT UP READS AS FINISHED, not as empty. */
.mc-empty-done{ display:flex; flex-direction:column; align-items:center; gap:6px; }
.mc-empty-mark{ color:var(--green); opacity:.85; margin-bottom:2px; }
.mc-empty-head{ font-size:14px; font-weight:600; color:var(--text); }
.mc-empty-sub{ font-size:12px; color:var(--text-faint); }

@media (prefers-reduced-motion: reduce){
  .mc-list .mc-item{ animation-duration:1ms !important; animation-delay:0ms !important; }
  .mc-item{ transition-duration:1ms !important; }
}
/* The dot, wherever unread is signalled — the inbox row and the comments pane both. Named
   for what it means rather than for the Message Center it started in, because it is now
   used outside it. */
.unread-dot{
  flex:0 0 auto; width:7px; height:7px;
  border-radius:50%; background:var(--accent);
}
.mc-item .unread-dot{ margin-top:6px; }
/* Two columns, always: the message takes the room, the buttons keep their
   own. `flex:1 1 0%`, not `1 1 auto` — a wrapping flex line is laid out from
   each item's BASE size, and it wraps before it shrinks, so a base of `auto`
   let a long message push the buttons onto a second line. `min-width:0`
   does not help, since wrapping pre-empts shrinking entirely. A base of
   zero means this item never contributes to the overflow that wraps the line. */
.mc-body{ flex:1 1 0%; min-width:0; }
.mc-author{ font-size:13px; font-weight:600; color:var(--text); }
/* The name carries the weight; the verb is quiet — a bold "mentioned you"
   beside a bold name would read as two subjects. */
.mc-author .mc-verb{ font-weight:400; color:var(--text-dim); }
.mc-excerpt{
  margin:3px 0 0; padding:0; font-size:13px; line-height:1.5; color:var(--text-dim);
  /* Somebody else's paragraph, clamped so one long comment cannot own the dialog. */
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
}
/* The context line stays inside the column it was given, or it lands on top
   of the buttons beside it. */
.mc-context{
  margin-top:6px; font-size:12px; color:var(--text-faint);
  min-width:0; overflow-wrap:anywhere;
}
.mc-context b{ color:var(--text-dim); font-weight:600; }
/* The TEAM reads a step stronger than the board and the time: with one inbox spanning
   several teams it is the first thing that tells a reader where a message came from. */
.mc-context .mc-team:not(:empty){ color:var(--text-dim); font-weight:600; }
.mc-context span:not(:empty)::before,.mc-context time::before{ content:' · '; }
.mc-actions{ display:flex; flex-direction:column; gap:5px; flex:0 0 auto; }

/* A mention the reader is named on but cannot open — mention_context()'s
   access gate can hand back a row with no subject at all. It keeps its
   place, timestamp and unread state, but there is nothing to open. Quiet
   rather than an error: Definition of Done 4f, an ordinary state, not a fault. */
.mc-item-unavailable{ cursor:default; }
.mc-item-unavailable:hover,.mc-item-unavailable:focus-visible{ border-color:var(--border); }
.mc-item-unavailable .mc-excerpt{ color:var(--text-faint); font-style:italic; }
/* This row's context line holds only a timestamp — no icon, no task, no team and no board
   came before it — so the leading " · " every other row's context line draws ahead of
   `.mc-when` would dangle with nothing to its left. */
.mc-context.mc-context-solo time::before{ content:''; }

/* ---------- replying to a comment ----------
   One line above the box, not a second editor — says who is being answered
   and offers a way out; the composer itself is untouched. */
.comment-reply-banner{
  display:flex; align-items:center; gap:6px;
  padding:5px 8px; border-radius:var(--radius-sm);
  background:var(--accent-soft); border:1px solid var(--accent);
  font-size:12px; color:var(--text-dim); line-height:1.4;
}
.comment-reply-banner .crb-label{
  text-transform:uppercase; letter-spacing:.4px; font-size:10px; font-weight:700;
  color:var(--accent-text); flex:0 0 auto;
}
.comment-reply-banner .crb-who{ color:var(--text); flex:0 0 auto; }
/* The quote is the part that must give way — a long comment cannot push the cancel off the
   row, and the reader already knows which comment they pressed Reply on. */
.comment-reply-banner .crb-quote{
  min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.comment-reply-banner .crb-cancel{
  margin-left:auto; flex:0 0 auto; background:none; border:none; cursor:pointer;
  color:var(--text-dim); font-size:15px; line-height:1; padding:2px 4px;
  border-radius:var(--radius-sm);
}
.comment-reply-banner .crb-cancel:hover{ color:var(--text); background:var(--panel-2); }

/* Whom a reply answers, flat and one line: indentation buys nothing here
   and costs the width the words need. A left rail is the whole treatment. */
.comment-item.is-reply{ border-left:2px solid var(--border-soft); padding-left:8px; }
.comment-reply-marker{
  display:flex; align-items:center; gap:4px;
  font-size:11px; color:var(--text-faint); margin:1px 0 3px;
}
.comment-reply-marker b{ color:var(--text-dim); font-weight:600; }

/* ---------- Threads ----------
   The same card, saying something else: a thread row reuses `.mc-item`
   entirely since it is the same inbox; only the three lines inside are new. */

/* Who replied, then how much is new. The count is quiet: the row's job is the name and the
   words, and the number is the thing you check second. */
.mc-thread-count{ font-weight:400; color:var(--text-faint); }
/* Which conversation, under the reply rather than above it — the new fact
   leads. One line, clipped, so a long root cannot push the card open. */
.mc-thread-root{
  margin-top:5px; font-size:12px; color:var(--text-faint); line-height:1.45;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.mc-thread-root-label{ text-transform:uppercase; letter-spacing:.4px; font-size:10px; font-weight:700; }
.mc-thread-root-author{ color:var(--text-dim); font-weight:600; }

/* The expanded conversation, flat deliberately: indenting a reply-to-a-reply
   would spend width this panel has none of. The left rail says "one thread" instead. */
.mc-thread-convo{
  margin-top:9px; padding-left:10px; border-left:2px solid var(--border-soft);
  display:flex; flex-direction:column; gap:8px;
}
.mc-convo-line{ font-size:12px; line-height:1.5; }
.mc-convo-line b{ color:var(--text); font-weight:600; }
.mc-convo-line time{ color:var(--text-faint); margin-left:6px; }
.mc-convo-line time::before{ content:''; }   /* .mc-context adds a separator; this is not that row */
.mc-convo-text{ display:block; color:var(--text-dim); margin-top:2px; }
/* ---------- Replying from the Message Center ----------

   Spans the row, under everything else in it — a composer squeezed beside
   the actions column is one nobody can type a sentence in. A 100% flex
   basis claims a line of its own, the same mechanism the topbar identity uses. */
.mc-reply{
  flex:1 0 100%;
  display:flex; flex-direction:column; gap:6px;
  margin-top:8px; padding-top:8px;
  border-top:1px solid var(--border-soft);
}
.mc-reply-input{
  width:100%; resize:vertical; min-height:52px;
  font:inherit; font-size:13px; line-height:1.45;
}
/* The committing action sits at the trailing edge, like every other action
   row: status, Cancel, Post reply, never the primary under the cursor's
   resting place. A mixed row, so the alignment belongs to the GROUP — the
   canonical control row's shape 2, with Cancel/Post inside a
   `.control-row-end` so they cannot come apart onto separate wrapped lines. */
.mc-reply-actions{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.mc-reply-actions > .control-row-end{ margin-left:auto; }
/* Says what happened, next to the button that did it. Never a colour on its own — the failure
   case adds .err, and the ordinary case is the same faint grey every status line uses. */
.mc-reply-status{ font-size:11px; color:var(--text-faint); flex:1 1 auto; min-width:0; }
/* An empty span would still claim a gap beside the buttons. */
.mc-reply-status:empty{ display:none; }
.mc-reply-status.err{ color:var(--red-text); }
/* The comment posted and stands; only a notification did not happen. Not
   `.err` — that invites a re-post of something that already succeeded.
   `--warn-text` is this codebase's worth-reading-not-alarming tone elsewhere. */
.mc-reply-status.notice{ color:var(--warn-text); }

/* The comment the conversation started from, marked without a second colour: it is the one
   line here that is not a reply. */
.mc-convo-line.root b::after{
  content:'started this'; margin-left:6px; font-size:10px; font-weight:700;
  text-transform:uppercase; letter-spacing:.4px; color:var(--text-faint);
}

/* The per-tab counts. Same pill as the opener's badge — one badge shape in this panel, not
   two — sized down because it sits inside a tab rather than beside an icon. */
.mc-tab-count{
  display:inline-block; min-width:15px; margin-left:6px; padding:0 4px;
  border-radius:999px; background:var(--accent-fill); color:#fff;
  font-size:10px; font-weight:700; line-height:16px; text-align:center;
  font-variant-numeric:tabular-nums;
}
/* On the selected tab the accent fill is already the background, so the pill inverts rather
   than disappearing into it. */
.modal-tab.active .mc-tab-count{ background:rgba(255,255,255,.28); color:#fff; }

/* The opener, wherever a page puts it. */
/* The count sits in the button, not its corner. A floating pip is the
   conventional shape and the wrong one on a wrapping flex bar, where the
   corner is exactly where a neighbour crops it. Inline, it cannot be cut off. */
[data-mc-open]{ position:relative; }

[data-mc-badge]{
  display:inline-block; min-width:17px; padding:0 5px; margin-left:1px;
  border-radius:999px; font-size:11px; line-height:17px; font-weight:700;
  text-align:center; background:var(--red); color:#fff;
}
/* The badge stays inline on the icon button too, for the same reason — a
   corner pip would crop on a wrapping flex bar. */
[data-mc-open] svg{ flex:0 0 auto; }

/* The button itself lights up — the count alone is easy to walk past on a
   busy bar; the fill is what the eye catches from across the screen. Steady,
   never animated. One treatment, every control carrying pending attention —
   the selector names its hosts rather than a bare `.has-unread`, since
   `.modal-tab` sets its own background at the same specificity. */
[data-mc-open].has-unread,
.modal-tab.has-unread,
.btn.has-unread{
  background:var(--accent-soft); border-color:var(--accent); color:var(--accent-text);
  box-shadow:0 0 0 1px var(--accent), 0 0 12px -3px var(--accent);
}

[data-mc-open].has-unread:hover,
.modal-tab.has-unread:hover,
.btn.has-unread:hover{ background:var(--accent-fill); color:#fff; }
/* The active tab is already filled with the accent, so the glow would be a second statement
   of the same thing on top of it — the pane it points at is open and being read. */
.modal-tab.has-unread.active{
  background:var(--accent-fill); color:#fff; box-shadow:none;
}

/* On the lit button the red pip would be a second alarm for one fact. It becomes the
   accent's own solid fill — still a count, no longer a warning. */
[data-mc-open].has-unread [data-mc-badge]{ background:var(--accent-fill); }

/* ---------- Paste an email (the shared dialog) ----------
   Injected and appended to <body>, so its chrome cannot live in one host's
   <style> — the same mistake that already cost the modal shell and CSS/okr.css. */

/* A column, so the textarea takes every pixel the dialog's height leaves it. An email thread
   is long, and the popover this shape replaced gave it five rows. */
.email-paste-modal .settings-modal-body{ display:flex; flex-direction:column; gap:10px; min-height:0; }
.email-paste-drop{
  border:1.5px dashed var(--border); border-radius:var(--radius-sm);
  padding:12px; text-align:center; font-size:13px; color:var(--text-dim);
  flex:0 0 auto;
}
.email-paste-drop.dragover{ border-color:var(--accent); color:var(--accent-text); background:var(--accent-soft); }
.email-paste-note{ font-size:11px; }
.email-paste-text{
  flex:1 1 auto; min-height:180px; resize:none; width:100%;
  background:var(--panel-2); border:1px solid var(--border-strong); border-radius:var(--radius-sm);
  color:var(--text); padding:9px 11px; font-size:13px; line-height:1.5;
}
.email-paste-text:focus{ border-color:var(--accent); }
/* The status line sits under the box here rather than beside a button, so the 220px cap the
   in-panel version needs would truncate a sentence that has room. */
.email-paste-modal .email-status{ max-width:none; flex:0 0 auto; }

/* ---------- Comments waiting to be read ----------
   The row above a task's comment list. Two buttons, never one toggle: a control whose
   meaning flips with state makes you read the list before you know what pressing it does. */
.comment-read-row{ margin:0 0 10px; align-items:center; }
/* An author `display` beats the UA's [hidden] rule — the answer now lives
   once, in CSS/base.css (sys_design-system.md §2), not per component. */
.comment-read-note{
  display:inline-flex; align-items:center; gap:6px;
  font-size:12px; color:var(--text-dim); margin-right:auto;
}
.comment-read-note.unread{ color:var(--accent-text); font-weight:600; }
/* The card badge carries the same signal as the tab, so noticing does not require opening
   every task. Border and fill rather than the button glow — a 11px chip with a box-shadow
   halo reads as a rendering fault next to the other card chips. */
.card-comments.has-unread{
  color:var(--accent-text); background:var(--accent-soft);
  box-shadow:inset 0 0 0 1px var(--accent);
}

/* ---------- @mention picker and chips ---------- */

/* Fixed, and placed by the component: the width and coordinates come from
   the textarea's own rect, since the old absolute positioning needed a
   positioned ancestor `.comment-add-row` did not have. --z-float is the
   floor, raised further by mention-picker.js above whatever dialog it opened in. */
/* ---------- you are here ----------

   The current page is highlighted, never disabled — the same accent
   treatment the active View chip has always had. `disabled` takes the
   element out of the tab order and reads as "you cannot", when the state
   meant is position, not permission; `aria-current="page"` says "you are
   here" instead and stays reachable. Declared once, so a page cannot opt
   out by forgetting. */
.btn[aria-current="page"]{
  border-color:var(--accent); color:var(--accent-text); background:var(--accent-soft);
  cursor:default;
}

.mention-picker{
  position:fixed; z-index:var(--z-float);
  max-height:220px; overflow-y:auto; padding:4px;
  background:var(--panel-2); border:1px solid var(--border);
  border-radius:var(--radius-sm); box-shadow:0 -8px 24px rgba(0,0,0,.26);
}
/* Sized down from the board's roster avatars: this is a list row, not a header. */
.mention-avatar{ width:20px; height:20px; font-size:9px; flex:0 0 auto; font-style:normal; }
.mention-option-name{ flex:1 1 auto; min-width:0; }
/* Only filled when two people share a name — see the picker's draw(). */
.mention-option-hint:not(:empty){ font-size:11px; color:var(--text-faint); flex:0 0 auto; }
.mention-option{
  display:flex; align-items:center; gap:8px; width:100%; text-align:left; padding:6px 9px;
  background:none; border:0; border-radius:5px; cursor:pointer;
  font-size:13px; color:var(--text);
}
/* Highlighted by keyboard as well as hover — the list is driven with arrow keys. */
.mention-option.on,.mention-option:hover{ background:var(--accent-soft); color:var(--accent-text); }
/* The picker explaining why it has nothing to offer. Faint and unhighlightable, so it does not
   read as a row that can be picked — see drawNote() in mention-picker.js. */
.mention-empty{
  padding:7px 9px; font-size:12px; line-height:1.45; color:var(--text-faint); cursor:default;
}

/* A mention inside a posted comment. Reads as a reference, not a link to nowhere. */
.mention{
  padding:0 3px; border-radius:3px; font-weight:600;
  background:var(--accent-soft); color:var(--accent-text);
}

/* The one link treatment, for every URL the app turns into an anchor. A
   class rather than a bare `a` rule, since `a` is deliberately unstyled
   globally. The underline is what makes it read as a link without relying
   on colour alone — WCAG 1.4.1. */
.rich-link{
  color:var(--accent-text);
  text-decoration:underline;
  text-underline-offset:2px;
  text-decoration-thickness:1px;
  border-radius:3px;
}
.rich-link:hover{ text-decoration-thickness:2px; }
/* :focus-visible, not :focus, so a mouse click does not leave a ring sitting there. */
.rich-link:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:2px;
  text-decoration-thickness:2px;
}

/* The comment a Message Center item pointed at, so arriving from a mention lands you on the
   right paragraph rather than the right page. */
.comment-focused{
  outline:2px solid var(--accent); outline-offset:2px; border-radius:var(--radius-sm);
}

/* ================= THE RICH-TEXT VOCABULARY =================

   The closed set of class names JS/core/sanitize.js keeps, the only way
   formatting may be expressed in user-authored text — one definition so a
   note formatted in one editor reads the same in the other.

   Classes rather than colours: `execCommand`'s inline `<font>`/`style=`
   output is not an allowed tag or attribute, so both were unwrapped on the
   way to storage. sanitize.js writes the list by hand; this file gives each
   name a value — adding one means adding it in BOTH places.

   A class survives a theme where a stored hex does not: every colour below
   resolves through CSS/tokens.css, re-answered per theme rather than baked
   into the document. Every ratio is measured against --panel-2, the worst
   surface in both themes. Do not change a value without re-measuring.

   ---------- size ----------
   In `em`, not pixels, since this text nests inside surfaces at several
   sizes and a multiple of the surrounding text means the same thing
   everywhere. `line-height` comes with the size, so a mixed paragraph sets evenly. */
.rt-sm{ font-size:.85em; line-height:1.7; }
.rt-lg{ font-size:1.25em; line-height:1.45; }
.rt-xl{ font-size:1.6em; line-height:1.3; font-weight:600; }

/* ---------- text colour ----------
   The `-text` variant of each role, never the fill — CSS/tokens.css's rule
   that a colour used as a fill is not the same colour used as text.

   Measured on --panel-2 (dark / light):
     red 4.95 / 5.40 · orange 5.19 / 5.45 · yellow 10.13 / 6.00 · green 5.43 / 5.31
     blue 5.93 / 5.29 · purple 5.46 / 5.52 · gray 7.27 / 7.05

   Yellow is --warn-text rather than --yellow, since --yellow measures
   3.1:1 as light-mode text. Grey is --text-dim, not a grey of its own —
   "quieter than the rest" is a tier this application already has. */
.rt-c-red{ color:var(--red-text); }
.rt-c-orange{ color:var(--orange-text); }
.rt-c-yellow{ color:var(--warn-text); }
.rt-c-green{ color:var(--green-text); }
.rt-c-blue{ color:var(--accent-text); }
.rt-c-purple{ color:var(--purple-text); }
.rt-c-gray{ color:var(--text-dim); }

/* ---------- highlight ----------
   Four washes, their own values rather than the `--*-soft` set: those are
   tuned for a chip or a selected row, a tint that says "this one" without
   competing with the text on it, and a highlighter needs the opposite — seen
   from across the room. The two themes are not one value at two alphas:
   dark mode washes a saturated hue over navy, light mode puts pale ink
   under dark text. Pink is the one genuinely new pair — nothing in the
   palette means pink as a STATE, and a highlighter is not a state.

   Body text on each, measured (dark / light):
     yellow 6.44 / 9.74 · green 8.32 / 8.99 · blue 8.56 / 8.10 · pink 9.49 / 8.11

   The mechanism is the one tokens.css uses: dark values on :root, light
   ones under `:root.theme-light, body.theme-light` — both selectors, since
   the boot script stamps <html> before <body> exists. */
:root{
  --rt-h-yellow:rgba(255,185,0,.34);
  --rt-h-green:rgba(23,179,94,.30);
  --rt-h-blue:rgba(59,130,246,.34);
  --rt-h-pink:rgba(236,72,153,.32);
}
:root.theme-light, body.theme-light{
  --rt-h-yellow:rgba(255,214,64,.60);
  --rt-h-green:rgba(120,224,166,.60);
  --rt-h-blue:rgba(133,190,255,.60);
  --rt-h-pink:rgba(255,158,201,.60);
}
/* `color:var(--text)` is not decoration: the UA's own `<mark>` rule is
   `background:mark; color:marktext`, system black prose in a dark paragraph
   otherwise. The background below overrides the first half, this the second. */
.rt-h-yellow,.rt-h-green,.rt-h-blue,.rt-h-pink{
  color:var(--text);
  border-radius:2px;
  padding:0 2px;
  /* Negative side margins so the padding keeps ink off the glyphs without
     pushing the words either side of it apart. */
  margin:0 -2px;
  box-decoration-break:clone;
  -webkit-box-decoration-break:clone;
}
.rt-h-yellow{ background:var(--rt-h-yellow); }
.rt-h-green{ background:var(--rt-h-green); }
.rt-h-blue{ background:var(--rt-h-blue); }
.rt-h-pink{ background:var(--rt-h-pink); }

/* ---------- font family ----------
   Three intentions, not a font picker: a dropdown of whatever is installed
   on the author's machine is a promise a shared document cannot keep. Not
   themed, unlike the colours above — a typeface is not a contrast decision.
   `--a-mono` is not reused here: that token is the application's own chrome,
   and a note opting into monospace is asking for code, not the clock's font. */
.rt-f-sans { font-family: Inter, "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif; }
.rt-f-serif{ font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif; }
.rt-f-mono { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; }

/* ---------- alignment ----------
   Three, and left is deliberately not among them: left is the absence of a class, so "back to
   normal" is removing one rather than adding a fourth the sanitiser would also have to know. */
.rt-al-c{ text-align:center; }
.rt-al-r{ text-align:right; }
.rt-al-j{ text-align:justify; }

/* ---------- a note page's heading ----------
   One note, many dated pages, in one `description` column. The whole point
   of real markup rather than a serialisation envelope is what any reader
   sees: an ordinary document of headed sections — so it is styled as a
   heading, not as chrome, and the rule under it is the quiet divider tier,
   a break in a document rather than the edge of a control. */
.rt-page-title{
  font-size:1.15em;
  font-weight:600;
  line-height:1.35;
  color:var(--text);
  margin:0 0 8px;
  padding-bottom:6px;
  border-bottom:1px solid var(--border-soft);
}

/* The page wrapper is structure, and structure is invisible: no border, no
   background, no padding, only the space between one page and the next —
   `:first-child` removes even that so a note does not open with a gap.
   A `div` selector, not a bare attribute one, matches the shape
   sanitize.js actually validates, so a marker on the wrong element gets no
   styling to make it look legitimate. */
div[data-note-page]{
  display:block;
  margin:0 0 20px;
  padding:0;
  border:0;
  background:none;
}
div[data-note-page]:first-child{ margin-top:0; }
div[data-note-page]:last-child{ margin-bottom:0; }

/* ---------- Attachments ----------
   One renderer, one set of element ids, one set of rules — two copies of a
   file list is two things to keep in step for no benefit (core rule 4). */
  .attach-dropzone{
    border:1.5px dashed var(--border);border-radius:var(--radius-sm);
    padding:12px;text-align:center;font-size:13px;color:var(--text-dim);
    display:flex;flex-direction:column;align-items:center;gap:8px;
  }
  .attach-dropzone.dragover{border-color:var(--accent);color:var(--accent-text);background:var(--accent-soft);}
  .attach-count-badge{
    position:absolute;top:-5px;right:-5px;background:var(--accent-fill);color:#fff;font-size:10px;font-weight:700;
    border-radius:999px;min-width:15px;height:15px;padding:0 3px;display:flex;align-items:center;justify-content:center;
    line-height:1;box-shadow:0 0 0 1.5px var(--panel);
  }
  .attach-status{font-size:12px;color:var(--text-dim);margin-top:6px;}
  /* 11px is the floor for --text-faint (sys_design-system.md): small and muted compound. */
  .attach-hint{font-size:11px;color:var(--text-faint);margin-top:6px;}
  .attach-status.err{color:var(--red-text);}
  .attach-status.ok{color:var(--green-text);}
  .attach-list{display:flex;flex-direction:column;gap:6px;margin-top:8px;}
  .attach-row{
    display:flex;align-items:center;gap:8px;background:var(--panel-2);border:1px solid var(--border-soft);
    border-radius:var(--radius-sm);padding:7px 9px;
  }
  .attach-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;}
  .attach-row .attach-name{
    font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    text-decoration:none;display:block;
  }
  .attach-row .attach-name:hover{color:var(--accent-text);text-decoration:underline;}
  .attach-row .attach-remove{
    flex:0 0 auto;background:none;border:none;color:var(--text-faint);cursor:pointer;font-size:14px;padding:0 2px;
  }
  .attach-row .attach-remove:hover{color:var(--red-text);}
/* ---------- AI comment formatting ----------
   Two actions and an accept/cancel row. Deliberately small: the comment box is the thing
   being used, and this sits under it rather than around it. */
.comment-ai{ margin-top:6px; }
/* Paired with "Tidy formatting with AI" in one `.control-row`. `.comment-
   ai`'s own top margin above is right as the only thing on its line and
   wrong as a flex item beside that toolbar, where it would break the row's
   `align-items:center` — `.control-row` already carries the gap needed. */
.control-row > .comment-ai{ margin-top:0; }
.comment-ai-row{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.comment-ai-status{ font-size:12px; color:var(--text-dim); }
.comment-ai-status.err{ color:var(--red-text); }
/* Present only while a proposal is on screen — the author's original is held until they
   answer, and this row is the question. */
.comment-ai-accept{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-top:6px; padding:7px 9px;
  border:1px solid var(--accent); border-radius:var(--radius-sm);
  background:var(--accent-soft);
}
.comment-ai-accept span{ flex:1 1 180px; font-size:12px; color:var(--accent-text); }

/* Export options, now a group of preference rows in Team Settings rather than a pill of
   checkboxes in the board ribbon. A fieldset for the grouping semantics only — the rows
   inside are the app's ordinary .pref-row/.pref-switch. */
.export-opts{ border:none; margin:0; padding:0; }
.export-opts > legend{ padding:0; margin-bottom:2px; }

/* ---------- "possible action item" ----------
   Under the comment box, after posting. Small and dismissible: a suggestion that competes
   with the thread is one people learn to close without reading. */
.ctt-card{
  margin-top:8px; padding:9px 11px;
  border:1px solid var(--yellow); border-radius:var(--radius-sm);
  background:var(--yellow-soft);
}
/* Clearly labelled as a suggestion rather than a decision the app has made. */
.ctt-head{
  font-size:10px; text-transform:uppercase; letter-spacing:.06em;
  color:var(--warn-text); font-weight:700;
}
.ctt-title{ margin-top:3px; font-size:13px; color:var(--text); }
.ctt-meta{ margin-top:3px; font-size:12px; color:var(--text-dim); }
.ctt-actions{ display:flex; gap:6px; margin-top:8px; }

/* ---------- Task editor sections ----------

   Collapsible groups in both task editors, shared because a compact editor
   on one page and a long form on the other would be two products. The
   header is a real <button> with aria-expanded; the chevron is decoration,
   never the only signal — sys_design-system.md §6. Deliberately quiet
   chrome: eight boxed panels would cost more room than the fields they group. */

.tsec{ border-top:1px solid var(--border-soft); }

.tsec:first-of-type{ border-top:none; }

.tsec-head{
  display:flex; align-items:center; gap:7px; width:100%;
  padding:7px 0; background:none; border:0; cursor:pointer;
  color:var(--text-dim); font-family:inherit; text-align:left;
}

.tsec-head:hover{ color:var(--accent-text); }

.tsec-title{
  font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  flex:0 0 auto;
}

/* The whole point of collapsing without hiding: this line is what the section becomes. */
.tsec-summary{
  flex:1 1 auto; min-width:0; font-size:12px; color:var(--text-faint);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

.tsec-chev{ flex:0 0 auto; }

.tsec.collapsed .tsec-chev{ transform:rotate(-90deg); }

/* The gap the grouping took away: wrapping rows in a section made the
   wrapper the child of the flex column, so the rows inside lost the
   parent's gap. Restated here, a little smaller since the header now
   provides some separation itself. */
.tsec-body{
  display:flex; flex-direction:column; gap:13px;
  padding:2px 0 10px;
}

/* The board's rows carry their own margin, so a gap here would double it.
   Gap only — an added `display:block` once outranked the collapse rule
   below, so no section body on the team board ever actually hid. A flex
   column with no gap lays rows out identically to block either way. */
#taskDetailsPane .tsec-body{ gap:0; }

/* Not the disclosure primitive — same reason as the OKR objective body: the
   0fr->1fr row trick needs exactly ONE child, and a section body holds
   every field in the section, so the first one was crushed. `display:none`
   until there is a wrapper to animate — sys_design-system.md §2b. */
.tsec.collapsed > .tsec-body{ display:none; }
/* The chevron turns on the same clock as the section it belongs to. That clock — and its
   reduced-motion answer — is the disclosure primitive's, further down: one list, every page. */

/* ---------- the description gets what the metadata gave back ----------

   The board's editor only — its description was a fixed size whatever the
   window was, so collapsing the metadata above it reclaimed space that
   went nowhere. It grows with the viewport now, floored and ceilinged.
   The task app's #f-desc is deliberately NOT here: it is flex:1 inside the
   panel's column and already takes what the sections above it give back. */
#taskDescInput{
  min-height:clamp(160px, 38vh, 520px);
  max-height:clamp(160px, 46vh, 640px);
}

/* An AI-only control with no assistant configured. Hidden rather than disabled: see
   JS/components/ai-availability.js. !important because these controls are inline, flex and
   grid items with their own display rules, and this must win against all of them. */
.ai-unavailable{ display:none !important; }

/* The inline checkbox — a box and label on one line, inside a row of other
   controls. Not the settings-pane `.pref-switch`, far too tall for a filter
   row. `.ncl-inline-check` is what the notes checklist called it first and
   is kept so that markup keeps working — one rule, not two implementations. */
.inline-check, .ncl-inline-check{ display:flex; align-items:center; gap:8px; cursor:pointer; }
.inline-check input, .ncl-inline-check input{ width:auto; margin:0; accent-color:var(--accent); cursor:pointer; }

/* ---------- Choosing people ----------

   The one treatment for "which of these people": a labelled checkbox row
   per person, used by more than one editor, which is why it lives here
   rather than in either page. Deliberately not a chip/token field with a
   search box — a team roster is tens of people, not thousands, so every
   option is visible and one tick is the whole interaction; keyboard-
   operable and screen-reader-labelled for free. */
.group-pick{
  display:flex; align-items:baseline; gap:8px; padding:4px 0; font-size:13px;
  cursor:pointer;
}
.group-pick input{width:auto; margin:0; accent-color:var(--accent); cursor:pointer;}
.group-pick .nm{flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.group-pick .sub{font-size:11px; color:var(--text-faint);}
/* Somebody who cannot be picked — no account yet, in the team board's case — is dimmed
   AND disabled AND labelled, rather than a tick that silently fails. */
.group-pick.off{cursor:default;}
.group-pick.off .nm{color:var(--text-faint);}
/* A scrollable well when the roster is longer than the dialog.

   `flex-shrink:0` IS LOAD-BEARING AND WAS MEASURED, NOT GUESSED. This well is a flex item
   inside `.settings-modal-body`, which is itself a flex column. A flex item with
   `overflow-y:auto` gets an automatic minimum size of ZERO, so when the body's content does
   not fit, the browser shrinks THIS — the one item that can scroll — before anything else.
   That is backwards: the well is the dialog's actual content and the controls around it are
   chrome, so the well must be the LAST thing to give, not the first.

   Measured on tasks.html's bulk-comment dialog, five rows, before the fix: 125px at 1440px,
   then 38.7px at 600px and 24px at 390px — under one row, with the rows still in the DOM and
   positioned, simply crushed into a sliver nobody can see or tick. After: a steady 168px at
   every width from 1440 down to 390, with `.settings-modal-body` scrolling instead
   (scrollHeight 671 against a 530px box) — which is what its own `overflow-y:auto` is for.

   SAFE FOR ALL SEVEN CONSUMERS, checked rather than assumed: every container that holds one of
   these wells scrolls — `.settings-modal-body` and `.cfg-popover` both declare `overflow-y:auto`,
   and `.okr-bulk-list` layers onto this same rule. `flex-shrink:0` only changes behaviour when
   the parent is overflowing, and in every one of those the parent absorbs it. */
.people-pick-list{
  display:flex; flex-direction:column; max-height:168px; overflow-y:auto; flex-shrink:0;
  border:1px solid var(--border-strong); border-radius:var(--radius-sm);
  padding:6px 10px; background:var(--panel-2);
}

/* ---------- A vertical rule between groups in one row ----------

   One hairline, wherever a row holds two groups of controls that answer
   different questions — without it, a strip of controls read as one group
   when the last one answers something else. `align-self:stretch` takes the
   row's own height rather than a guessed one, so a button's padding
   changing cannot stop it matching. */
.vrule, .cfg-row .cfg-divider{
  width:1px; align-self:stretch; margin:3px 2px; flex:0 0 auto;
  background:var(--border-soft);
}
/* ---------- Person field: type a name, pick a person ----------

   Reads as the <select> it replaced — same field chrome, same radius, same
   type — since it answers the same question; only now you can type in it.
   The list is the application's existing small panel (`.cfg-popover`),
   positioned by popover.js. No fourth surface, no bespoke scroll container. */
.person-select{ position:relative; display:flex; align-items:stretch; min-width:0; }
.person-select-input{
  flex:1 1 auto; min-width:0;
  /* Room for the caret, which overlays the field's right edge the way a select's arrow does. */
  padding-right:26px !important;
}
.person-select-caret{
  position:absolute; right:1px; top:1px; bottom:1px; width:24px;
  display:flex; align-items:center; justify-content:center;
  background:none; border:none; padding:0; cursor:pointer;
  font-size:10px; color:var(--text-faint); border-radius:0 var(--radius-sm) var(--radius-sm) 0;
}
.person-select-caret:hover{ color:var(--text-dim); }
.person-select-list{ width:260px; max-height:230px; overflow-y:auto; padding:5px; }
.person-select-opt{
  display:flex; align-items:baseline; gap:8px; padding:5px 8px; font-size:13px;
  border-radius:var(--radius-sm); cursor:pointer; min-width:0;
}
.person-select-opt .nm{ flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.person-select-opt .sub{ font-size:11px; color:var(--text-faint); margin-left:auto; }
/* OPT-IN: `personSelectField({ columns:true })` — see the component's own header for who asks
   for this and why it is not every caller's layout. "The names and email line up" means every
   ROW disagrees about nothing, which flex's `margin-left:auto` cannot give: each row pushes its
   own hint to ITS OWN right edge, so the hint's start position follows that row's name length,
   never its neighbour's. A fixed `grid-template-columns`, repeated on every row, fixes that
   without a subgrid — the rows never negotiate a shared width because they are never asked to;
   they are simply given the same one.

   THE NAME NEVER TRUNCATES — no ellipsis, wraps onto a second line instead — because a name
   the reader cannot read in full is the defect this exists to fix; a two-line row is a cosmetic
   cost, a misread identity is not. The hint keeps its ellipsis: a clipped email tail costs less
   than a clipped name, and this is the column with the outlier lengths (a real address in the
   fixture list runs past 30 characters). */
.person-select-list.is-columns{ width:340px; }
.person-select-list.is-columns .person-select-opt{
  display:grid; grid-template-columns:140px 1fr; column-gap:8px; align-items:baseline;
}
.person-select-list.is-columns .person-select-opt .nm{
  overflow:visible; text-overflow:clip; white-space:normal; word-break:break-word;
}
.person-select-list.is-columns .person-select-opt .sub{
  margin-left:0; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* TWO DIFFERENT MARKS, because they answer two different questions. `is-current` is who the
   record holds — it survives typing. `is-active` is where the keyboard is right now, and it
   moves with every arrow press. Showing one treatment for both is how a reader loses track of
   which name Enter is about to take. */
.person-select-opt.is-current .nm{ font-weight:700; }
.person-select-opt.is-active{ background:var(--accent-soft); }
.person-select-opt:hover{ background:var(--panel); }
/* Listed and not pickable — a roster row with no account. Dimmed AND labelled, and the
   keyboard steps over it: the same treatment `.group-pick.off` gives the multi-select. */
.person-select-opt.is-off{ cursor:default; }
.person-select-opt.is-off .nm{ color:var(--text-faint); }
.person-select-opt.is-off:hover{ background:none; }
.person-select-none{ padding:6px 8px; font-size:12px; color:var(--text-faint); }

/* ---------- Reorder by dragging ----------

   See JS/components/reorder-list.js for why this replaced a pair of ↑ / ↓ buttons per row,
   and why the grip is a real button rather than a decorative handle.

   THE GRIP IS THE ONLY DRAG AFFORDANCE THE ROW GETS. The row itself is draggable — dragging
   from anywhere on it works — but the cursor is only shown as a grab on the handle, because
   the row is full of text inputs and a `cursor:move` over one of those says "you cannot type
   here", which is false. */
.reorder-row{ position:relative; }
.reorder-grip{
  flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
  width:22px; padding:0; cursor:grab;
  background:none; border:none; color:var(--text-faint); border-radius:var(--radius-sm);
}
.reorder-grip:hover{ color:var(--text-dim); background:var(--panel-2); }
.reorder-grip:active{ cursor:grabbing; }
.reorder-grip:focus-visible{ outline:2px solid var(--focus-ring); outline-offset:1px; }
/* The row being carried, and the one it would land on. Opacity for the first because it is
   still under the pointer; an accent EDGE for the second, so the target is a position rather
   than a highlighted row — you are dropping between things, not onto one. */
.reorder-row.is-dragging{ opacity:.45; }
.reorder-row.is-drop-target{ box-shadow:inset 0 2px 0 0 var(--accent); }
/* NOTHING HERE ANIMATES, deliberately, so there is no reduced-motion block. A drag is
   already the reader moving something at their own speed; a transition on the row would be
   the interface disagreeing with the pointer about where it is. */

/* ---------- Visually hidden, but read ----------

   ONE RULE, EVERY NAME THAT MEANS IT. There were two identical copies under two names —
   `.sr-only-okr` in okr.css and `.sr-only-loading` in processing.css — which is a rule
   duplicated rather than a rule shared, and the second one only existed because nobody
   looked for the first. They are the same declaration and they are listed together here.

   `clip` rather than `display:none` or `visibility:hidden`: both of those remove the text
   from the accessibility tree as well as from the page, which is the opposite of the point.

   processing.css keeps its own copy of `.sr-only-loading`, and that is deliberate rather
   than an oversight — mobile.html loads processing.css and does NOT load this file, so a
   shared rule here cannot reach it. Every other host gets it from here. */
.sr-only, .sr-only-okr, .sr-only-loading{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* ---------- Multi-select field ----------

   "Which of these" as one control — see JS/components/multi-select.js for why this shape
   rather than a row of chips or a <select multiple>. The closed field states its own answer;
   the open panel is the `.group-pick` rows above, in the `.people-pick-list` well above,
   inside the `.cfg-popover` surface this application already uses for every small panel
   hanging off a button. Three existing pieces, no fourth surface. */
.ms-field{ position:relative; min-width:0; }
/* Reads as the sibling of a <select>, because that is what it stands in for: same field
   chrome, same radius, same type size. The caret is the only thing that says it opens. */
.ms-trigger{
  display:flex; align-items:center; gap:8px; width:100%; min-width:0;
  background:var(--panel-2); color:var(--text);
  border:1px solid var(--border-strong); border-radius:var(--radius-sm);
  padding:6px 9px; font-size:13px; font-family:inherit; cursor:pointer; text-align:left;
}
.ms-trigger:hover:not(:disabled){ border-color:var(--accent); }
.ms-trigger:disabled{ cursor:default; opacity:.55; }
/* Nothing chosen is a real state and is said quietly — "No boards" in the dim tier reads as
   an empty field rather than as a value somebody picked. */
.ms-trigger.is-empty .ms-value{ color:var(--text-faint); }
.ms-value{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ms-caret{ flex:0 0 auto; font-size:10px; color:var(--text-faint); }
/* SELECT ALL / DESELECT ALL above a multi-select panel's rows. Opt-in per caller
   (multiSelectField's `selectAll` option, default off), so the People pane, the board task
   editor's Boards and Contributors fields and account email routing are untouched unless they
   ask for it. Reuses `.btn.small` — the same pair board-bulk-assign.js and okr-bulk.js already
   draw — rather than inventing a third treatment for one control. */
.ms-bulk-row{ display:flex; gap:8px; margin:0 0 7px; }
/* TYPE-TO-FILTER, OPT-IN per multiSelectField's `search` option (see that file's own "TYPE TO
   FILTER" header note). `.a-input` (CSS/alt/alt-components.css) is the primitive; this rule
   only has to make it fill the 260px panel and sit above the rows, the same job `.ms-bulk-row`
   already does for its own pair of buttons. */
.ms-search-row{ margin:0 0 7px; }
.ms-search-row .a-input{ width:100%; }
.ms-pop{ width:260px; padding:8px; }
.ms-list{ max-height:210px; }
.ms-pop-note{ font-size:11px; color:var(--text-faint); line-height:1.45; margin-top:7px; }
/* The row's second answer: board admin, offered only once somebody is on the board. Pushed
   to the row's end so it never sits between the tick and the name. */
.ms-star{
  margin-left:auto; flex:0 0 auto; background:none; border:none; padding:0 2px;
  font-size:13px; line-height:1; color:var(--text-faint); cursor:pointer;
}
.ms-star.on{ color:var(--yellow); }
.ms-star:disabled{ cursor:default; opacity:.5; }

/* ---------- Canonical control row ----------

   THE RULE: when a row of controls no longer fits, it STACKS. It does not clip, shrink
   its type to stay on one line, or scroll sideways. Add `.control-row` to any container
   holding a row of buttons/selects and the behaviour comes with it.

   Two stages, because they solve different problems. Above the breakpoint the row wraps,
   so a couple of extra controls flow onto a second line at their natural width. Below it
   every control goes full width and the row becomes a stack — at that point the container
   is narrower than a comfortable touch target, and half-width buttons side by side are
   harder to hit than full-width ones above each other.

   640px matches the breakpoint the OKR list head and the help pages already use. Typography
   is deliberately untouched: shrinking text to keep one row is the failure this replaces. */
.control-row{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:8px;
}
/* A spacer keeps whatever follows it pushed right on a wide row, and collapses when the
   row stacks so it cannot open a gap in the middle of the stack. */
.control-row > .control-spacer{ flex:1 1 auto; }

/* ---------- Right-aligned controls keep their alignment when they wrap ----------

   THE RULE: when a right-aligned control group no longer fits on one row, the continuation
   row stays against the SAME right edge. It does not restart from the left.

       wide        [ A ] [ B ] [ C ] [ D ]
       narrow                    [ A ] [ B ]
                                 [ C ] [ D ]

   WHY IT MATTERS. A flex line is packed first and aligned second, so `flex-wrap:wrap` with
   no justify-content leaves every line after the first flush LEFT — even when the group as
   a whole is pinned right. On classic_tasks.html that put the account and sign-out buttons alone on
   a second row at the far left of a 1900px topbar, a full screen-width away from the
   cluster they belong to, with nothing but empty space to their right.

   Two shapes: a standalone right-aligned cluster gets `.control-row-end` as
   the whole component; a mixed row (something on the left, a spacer,
   controls on the right) puts the right-hand controls INSIDE one instead,
   so the alignment belongs to the group, not the row.

   Not `justify-content:flex-end` on the whole row: tried and reverted — a
   wrapping flex container with no spacer on the first line would
   right-align every line, including one with nothing to absorb the slack,
   floating the brand in the middle of its own row. */
.control-row-end{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:8px;
  justify-content:flex-end;
}
/* `.control-row.align-end` is gone for good, not merely unused today. It was a
   SECOND SPELLING of `.control-row-end` — same declaration, same job — and `align-end` was
   emitted by nothing anywhere in the application, where `.control-row-end` is emitted by
   nineteen files. One component, one name: a modifier nobody can reach is how one component
   grows two. */
@media (max-width:640px){
  .control-row{ align-items:stretch; }
  .control-row > .btn,
  .control-row > select,
  .control-row > input[type="search"],
  .control-row > input[type="text"]{
    flex:1 1 100%;
    width:100%;
    min-width:0;
  }
  /* Icon-only buttons stay square — stretching a 32px icon across the viewport is not a
     bigger touch target, it is a stripe. They sit together on their own line instead. */
  .control-row > .btn.icon-only{ flex:0 0 auto; width:auto; }
  .control-row > .control-spacer{ display:none; }
}

/* ---------- Signed in as ----------------------------------------------------------
   The identity element in every authenticated topbar. Gives way in order,
   and never by shrinking its type: below 1100px the address goes, below
   820px the "Signed in as" label goes, and the name — the part that
   answers the question — is the last thing standing. The topbar's right-
   hand group wraps (`.control-row-end`) rather than clipping the name
   beside a full set of icon buttons; the max-widths below still cap a
   genuinely long name so one account cannot push the line on its own. */
.signed-in-as{
  display:flex; align-items:baseline; gap:6px; flex:0 0 auto;
}
/* Gets its own line, above the buttons — identity, not one more control. A
   flex-basis of 100% claims a whole line inside `.control-row-end`'s own
   wrap, and must be the FIRST child or the icons before it keep the top
   line. An empty slot is `display:none` so no blank line is reserved while
   the profile read is in flight.

   THE LINE IS THE WRAPPER NOW, because the byline shares it — see
   JS/components/powered-by.js. The 100% basis and the right-alignment moved out
   here from the identity element itself; classic_team_board.html's own parent was
   `.topbar-trail`, a column, where no basis was needed because this was simply its first
   row. That page retired 2026-09-08 along with the one host this exception applied to — the
   rule below is scoped to `.control-row-end > .topbar-identity` rather than to
   `.topbar-identity` itself for that now-historical reason, and every current host goes
   through `.control-row-end`. */
.topbar-identity{
  display:flex; align-items:baseline; justify-content:flex-end;
  gap:10px; min-width:0; max-width:100%;
}
/* THE RULE BETWEEN THE BYLINE AND THE IDENTITY, and it goes when the byline does — a divider
   with nothing on one side of it is a stray mark. `align-self:stretch` on a baseline row would
   take the whole line height, so this one is sized to the text it separates. */
.topbar-identity .vrule{ align-self:center; height:12px; margin:0; }
@media (max-width:820px){
  .topbar-identity .vrule{ display:none; }
}
.control-row-end > .topbar-identity{
  flex:1 0 100%;
}
/* The line hides until the identity has landed — a company name alone on
   an otherwise unresolved topbar reads as a splash screen. poweredByRender() sets .ready. */
.topbar-identity:not(.ready){ display:none; }
.powered-by{
  font-size:11px; color:var(--text-faint); white-space:nowrap;
  letter-spacing:.2px; user-select:none;
}
/* It goes only on a genuinely narrow window — 820px, the width this app's
   topbars already drop the "Signed in as" label at. It was 1100px, and that
   was wrong for a reason worth keeping: 1100 is an ordinary laptop window, so the
   byline was absent on the machines most likely to see it — a byline nobody
   sees is not subtle, it is missing. */
@media (max-width:820px){
  .powered-by{ display:none; }
}
.sia-label{
  font-size:11px; color:var(--text-faint); text-transform:uppercase;
  letter-spacing:.5px; font-weight:600; white-space:nowrap;
}
/* A button that reads as text: this is identity, not an action, and a bordered control
   here competes with the account controls beside it. It gets a real focus ring from the
   global rule and an underline on hover so it is discoverably clickable. */
button.sia-name{
  appearance:none; background:none; border:none; padding:0; margin:0;
  font-family:inherit; cursor:pointer;
}
.sia-name{
  font-size:13px; font-weight:600; color:var(--text);
  max-width:22ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
button.sia-name:hover{ color:var(--accent-text); text-decoration:underline; }
.sia-email{
  font-size:12px; color:var(--text-faint);
  max-width:26ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
@media (max-width:1100px){
  .sia-email{ display:none; }
}
@media (max-width:820px){
  .sia-label{ display:none; }
}

/* ---------- The follow-up email dialog ----------

   Shared, because JS/features/followup-email.js is: both task editors open
   it. A shared dialog keeping its chrome in one host is the same defect
   that already cost the modal shell, the OKR dialog and the people picker. */
.followup-modal{ width:560px; }
.followup-modal-btn-row{ display:flex; gap:8px; margin-top:4px; }
.followup-intro{ font-size:12px; color:var(--text-dim); line-height:1.45; }
.followup-output{ display:flex; flex-direction:column; gap:8px; margin-top:2px; }
/* The wizard sits on this row, so the gap belongs here rather than on the label — without
   it the icon button crowds the textarea directly beneath it. */
.followup-label-row{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; }
.followup-format-btn{ padding:2px 8px; font-size:13px; line-height:1.4; }
.followup-field label{
  display:block; font-size:11px; color:var(--text-dim); text-transform:uppercase;
  letter-spacing:.4px; font-weight:600; margin-bottom:4px;
}
/* Real form controls, not read-only divs — same treatment as every other
   input (sys_design-system.md §2) so an editable draft looks editable. */
.followup-subject, .followup-to{
  font-size:13px; color:var(--text); width:100%; font-family:inherit;
  background:var(--panel-2); border:1px solid var(--border-strong); border-radius:var(--radius-sm);
  padding:8px 10px;
}
.followup-subject{ font-weight:600; }
.followup-subject:focus, .followup-to:focus, .followup-body:focus{ border-color:var(--accent); }
.followup-body{
  font-size:13px; color:var(--text); line-height:1.5; font-family:inherit;
  background:var(--panel-2); border:1px solid var(--border); border-radius:var(--radius-sm);
  padding:8px 10px; min-height:220px; resize:vertical; width:100%;
}
/* The status line's two outcomes — a failure in --text-dim reads as a mere caption. */
.email-status.err{ color:var(--red-text); }
.email-status.ok{ color:var(--green-text); }
/* .email-status's cap is for the task panel's narrow column, the wrong one
   in a wide dialog — same override the paste dialog needs. */
.followup-modal .email-status{ max-width:none; }
/* `.proc` is inline-flex — in a narrow box its dots and words fight for the
   row. Wrapping lets the dots keep their place and the sentence run on underneath. */
.email-status .proc{ flex-wrap:wrap; }
/* ---------- The quick input that opens the dialog ----------

   Lives here, inside the follow-up block, rather than in a stylesheet of
   its own: the dialog is injected into whatever page opened it, and a new
   sheet would need a <link> added to every host to exist at all.

   Every element the panel emits has a rule, checked rather than believed
   (Definition of Done 4d/4h). The label reuses `.followup-field label`
   above, so its caption matches To, Subject and Body. */
.followup-ask{ display:flex; flex-direction:column; gap:10px; margin-top:2px; }
/* The same control as .followup-body, at a quick input's height rather
   than a draft's — 220px is right for the email and absurd for a sentence. */
.followup-ask-input{
  font-size:13px; color:var(--text); line-height:1.5; font-family:inherit;
  background:var(--panel-2); border:1px solid var(--border); border-radius:var(--radius-sm);
  padding:8px 10px; min-height:64px; resize:vertical; width:100%;
}
.followup-ask-input:focus{ border-color:var(--accent); }
/* --text-faint, the quietest thing in the dialog, says the field above is optional. */
.followup-ask-note{ font-size:12px; color:var(--text-faint); line-height:1.45; margin:0; }
.followup-ask-row{ display:flex; gap:8px; align-items:center; }
/* What the draft was scoped to, echoed back above it. Hidden by the
   `hidden` attribute rather than a class — CSS/base.css's rule outranks
   this one's own `display`. */
.followup-scope{
  font-size:12px; color:var(--text-dim); line-height:1.45; margin:0;
  padding:6px 10px; background:var(--panel-2); border:1px solid var(--border);
  border-radius:var(--radius-sm); overflow-wrap:anywhere;
}

/* ---------- ELEVATION, AND IT IS NOT ONE VALUE ----------

   A shadow is a colour, so it has two themes: a dark-authored shadow under
   a near-white card is not a lift, it is a stain, and the toast is the one
   surface that appears unannounced over whatever the reader was looking at.

   The mechanism is the one this file uses for the highlight pens: the dark
   value on `:root`, the light one under `:root.theme-light, body.theme-
   light` — both selectors, since the boot script can only stamp <html>.
   The light value is navy, not black, derived from the suite's own
   elevation pair so both presentations lift a surface the same way.

   One token, one consumer, today. Three older shadows in this file
   (`.modal`, `.cfg-popover`, `.mention-picker`) are still dark-only and not
   fixed here — each sits over a dimmed backdrop, and changing the modal
   chrome's elevation is a visible design decision, not a colour correction. */
:root{ --shadow-float:0 14px 40px rgba(0,0,0,.45); }
:root.theme-light, body.theme-light{ --shadow-float:0 14px 40px rgba(19,32,54,.18); }

/* ---------- THE UNDO TOAST ----------

   Changing a task's due date from the list re-buckets the card, correct
   behaviour that feels like losing the task — the toast says what happened
   and offers to put it back. Not an alert: a failure gets
   `appReportError()`; this confirms something that worked. Bottom right,
   above everything with `--z-alert`, a token never a number. */
.undo-toast{
  position:fixed; right:20px; bottom:20px; z-index:var(--z-alert);
  display:flex; align-items:center; gap:14px;
  max-width:min(420px, calc(100vw - 40px));
  padding:12px 14px 12px 16px;
  background:var(--panel); color:var(--text);
  border:1px solid var(--border-strong); border-radius:var(--radius);
  box-shadow:var(--shadow-float);
  font-size:13px;
}
.undo-toast-text{flex:1 1 auto; min-width:0;}
/* A real button — the whole point of the toast, reachable by keyboard. */
.undo-toast .undo-toast-btn{flex:0 0 auto;}
/* Arrives from just below, quickly, once — anything longer competes with the list. */
@keyframes undoToastIn{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
.undo-toast{animation:undoToastIn var(--dur-quick) var(--ease-out);}
@media (prefers-reduced-motion: reduce){
  .undo-toast{animation:none !important;}
}
@media (max-width:560px){
  /* Full width on a phone rather than a card pinned to one corner, and clear of the
     bottom edge where a browser chrome bar sits. */
  .undo-toast{right:12px; left:12px; bottom:12px; max-width:none;}
}

/* ============================================================================
   THE MOTION PRIMITIVES

   Four of them, the whole vocabulary — a new detail surface, panel,
   disclosure or progress bar uses one rather than bringing its own.

   Transform and opacity only, everywhere possible: both are compositor
   properties, so a screen of them costs one paint rather than one reflow
   each. A width that genuinely has to move is one property on a small
   element, never a tree.

   Reduced motion is at the bottom, once, for all of them — it removes the
   travel, never the feedback; the state still changes visibly and instantly.
   ============================================================================ */

/* ---------- A · SURFACE LAUNCH ----------
   A detail surface arriving: slightly below and slightly small, settling.
   Not a zoom — 0.985 reads as "grew out of the page" without theatre.

   Two forms, one geometry, decided by the surface: a TOGGLED shell
   (`.modal-overlay`) animates as a state transition on `.show`, the only
   form that can also close without a scheduled class removal; a pane BUILT
   AND DESTROYED per open gets the keyframe instead, since it cannot
   transition from a previous state. Both use the same 8px and .985;
   motion-system.test.js checks they have not drifted apart.

   There is no `.surface-exit` class — it had no consumer, and nine call
   sites each scheduling a removal is the stale-overlay failure mode. A
   pane that destroys its own markup on close has nothing left to animate out. */
.surface-enter,
.okr-modal-pane{ animation:surfaceIn var(--dur-surface) var(--ease-out) both; }
@keyframes surfaceIn { from{ opacity:0; transform:translateY(8px) scale(.985); } }

/* ---------- B2 · REGION REVEAL ----------
   A page's main regions arriving after the boot curtain has lifted — only
   worth doing because the curtain waits for data, so this is one composited
   animation over finished content, not an empty board filling in.

   6px, not the surface primitive's 8px and scale: a whole region scaling
   reads as the window moving, a small rise reads as the content settling.
   Armed with the same capped stagger through motionRevealGroup(), so a page
   with more regions costs no more than one with few. `both` fill so a
   region is at opacity 0 during its delay and stays where it lands — it is
   hit-testable throughout, hidden rather than removed. */
.reveal-in{ animation:regionIn var(--dur-quick) var(--ease-out) both; }
@keyframes regionIn{ from{ opacity:0; transform:translateY(6px); } }

/* ---------- C · DISCLOSURE ----------
   A section, group, lane or band opening. `grid-template-rows: 0fr -> 1fr` animates to the
   content's own height without anyone measuring it — no scrollHeight read, no inline pixel
   value left behind when the content changes underneath. The child needs `min-height:0` or
   it refuses to be smaller than its contents. */
.disclosure{ display:grid; grid-template-rows:1fr; transition:grid-template-rows var(--dur-quick) var(--ease-out); }
.disclosure > *{ min-height:0; overflow:hidden; }
.disclosure.is-collapsed{ grid-template-rows:0fr; }
/* The wrapper shape, for a section whose contents are more than one box.
   The primitive's "exactly ONE child" cannot be relaxed — the explicit
   `1fr` defines a single row and everything after lands in an implicit
   `auto` one, crushing the first child. `.panel-fold-inner` is the one
   child the fold is allowed; whatever the section holds goes inside that. */
.panel-fold-inner{ min-width:0; }
/* The chevron turns with it, one rule, so a disclosure cannot animate while
   its own arrow snaps.

   EVERY CHEVRON IN THE APPLICATION IS NAMED HERE, the only place any of
   them is given a clock. Naming the hosts in one selector list is the
   pattern `.has-unread` already uses; a selector that matches nothing on a
   given page costs nothing. The ROTATION stays with the page, since the
   state class genuinely differs — the TIMING does not.

   Adding a chevron? Add it to this list. Do not give it a transition of its own.

   `.disclosure-chevron` stays, the one name here with no host of its own:
   it is the PRIMITIVE, the way `.surface-enter` is, named by sys_design-
   system.md's motion table, with the rest as its consumers. A primitive
   with consumers is not a dead selector; one with none is.

   The claim above is narrower than it reads: a few hosts turn their own
   chevrons on clocks of their own (canonical tokens, their own reduced-
   motion answer), and mobile.html's snap-with-no-transition chevron is a
   gap this list cannot close, since that page loads none of these rules. */
.disclosure-chevron,                          /* the primitive's own name — see below */
.tsec-chev,                                   /* the shared task-editor sections */
.okr-caret,                                   /* OKR groups and portfolio areas */
.chev,                                        /* team board + admin + mobile disclosures */
.note-sec-chev,
.ncl-chev,
.wx-chev,
.tcl-cat-h .cv,                               /* the task checklist's three categories */
.row .expand{ transition:transform var(--dur-quick) var(--ease-out); }
.is-collapsed > .disclosure-chevron,
.collapsed > .disclosure-chevron{ transform:rotate(-90deg); }

/* ---------- D · PROGRESS REVEAL — DECLARED HERE, IMPLEMENTED IN CSS/okr.css ----------
   A bar filling to the value it actually holds: starts at zero, arrives at
   the truth, which is what makes the number legible rather than a static block.

   The rule left this file because it had no host — a primitive with no
   consumer is not a primitive. The contract did not go with it: the live
   bar is `.okr-progress i` in CSS/okr.css, the same duration and curve,
   zeroed the same way under `prefers-reduced-motion`, with
   `.okr-progress-reveal` holding every fill at zero until
   okrRevealProgress() releases them with the capped stagger this section
   describes. sys_design-system.md's motion table names both halves; only
   the second was ever real.

   The value is the information: width comes from canonical data once, so a
   bar cannot animate through a number the record never held. */

/* ---------- E · MICRO ----------
   Hover, selection, a badge changing state. Fast enough that it reads as response rather
   than animation. */
.micro{ transition:transform var(--dur-micro) var(--ease-move),
                   background-color var(--dur-micro) linear,
                   border-color var(--dur-micro) linear,
                   opacity var(--dur-micro) linear; }

/* ---------- REDUCED MOTION ----------
   Every primitive, once. The travel goes; the state change does not — a
   reader who asked for less motion still needs to see that something happened.

   `!important`, and it is the right use of it. A media query adds NO specificity,
   and every page's own <style> is loaded AFTER this file, so an authored
   transition beat this block on source order alone. A user preference is
   exactly the case `!important` exists for: it must win over authored
   style regardless of where the author put it. */
@media (prefers-reduced-motion: reduce){
  .surface-enter, .okr-modal-pane{ animation-duration:1ms !important; }
  .disclosure, .micro{ transition-duration:1ms !important; }
  /* The region reveal goes away entirely rather than running fast: a page arriving is the one
     piece of motion a reader who asked for none has no reason to see at all. */
  .reveal-in{ animation:none !important; }
  /* Every chevron, by the same list that gave them the clock — so a new one cannot be
     added to the timing list and forgotten here. */
  .disclosure-chevron, .tsec-chev, .okr-caret, .chev,
  .note-sec-chev, .ncl-chev, .wx-chev, .tcl-cat-h .cv,
  .row .expand{ transition-duration:1ms !important; }
  /* The disclosure bodies and side rails that animate a BOX rather than a
     transform — they predate the primitives and still animate max-height/
     width since their contents are multi-child, but the clock and this
     answer are shared. The Suite answers the same question for its own
     boxes in CSS/alt/alt-tokens.css, zeroing the duration tokens under the
     preference rather than naming each box. What is left here is the two
     that still have a host: `.panel` and `.wx-detail`. */
  .panel, .wx-detail{ transition-duration:1ms !important; }
  /* Not the switch — moved to CSS/switch.css, which every page with a
     switch loads; putting it back here is the second copy. */
  /* The bar arrives at its value rather than travelling to it, still read.
     `.progress-reveal` went with its rule above; the bar it stood for
     answers the preference in CSS/okr.css, beside the rule that gives it the clock. */
  .okr-krbar span{ transition:none !important; }
  .undo-toast, .mc-list .mc-item{ animation-duration:1ms !important; }
}

/* ---------- A CARD ARRIVING BECAUSE THE READER NARROWED THE LIST ----------
   The same restrained vocabulary as every other entrance here: a 4px rise
   and a fade, on the canonical quick clock. Runs on a filter change only. */
.card.card-enter{ animation:cardIn var(--dur-quick) var(--ease-out) both; }
@keyframes cardIn{ from{ opacity:0; transform:translateY(4px); } }
@media (prefers-reduced-motion: reduce){
  .card.card-enter{ animation-duration:1ms !important; animation-delay:0ms !important; }
}


/* ---------- The refresh button, while it is working ----------
   Most reloads do not start painting the moment they are asked for, so the
   icon turns until the new document paints over it, telling the reader the
   press registered. The icon turns, not the button, so rotating it does not
   take the border and focus ring along. `.a-btn` is named here rather than
   given its own keyframe, so one animation keeps one definition. */
@keyframes refresh-spin { to { transform: rotate(360deg); } }
.btn.is-refreshing svg,
.a-btn.is-refreshing svg{
  /* --dur-progress, not a number of its own: it is 800ms and it is already the token for "a
     value filling to where it actually is", which is what a refresh is. */
  animation: refresh-spin var(--dur-progress) linear infinite;
  transform-origin: 50% 50%;
}
.btn.is-refreshing,
.a-btn.is-refreshing{ opacity:.75; }
/* The dimmed, disabled button says "working" without the rotation — the
   state is carried by opacity, which was already there. */
@media (prefers-reduced-motion: reduce){
  /* !important, since a media query adds no specificity and every page's
     own <style> loads after this file. */
  .btn.is-refreshing svg,
  .a-btn.is-refreshing svg{ animation:none !important; }
}


/* ---------- QUICK LINKS — a compact column beside a panel, and the editor behind it ----------

   Two surfaces draw this: the team board puts it beside the analytics
   rows, the Studio beside Task Actions — the same reasoning that moved the
   scrollbar rules and the modal shell here. The editor rows (`.ql-*`) are
   shared for the same reason: both are the same form over the same
   {description, url, enabled}. */
.quick-links{
  flex:0 0 auto; width:clamp(150px, 18%, 230px);
  display:flex; flex-direction:column; gap:4px;
  padding:8px 10px;
  background:var(--panel-2); border:1px solid var(--border-soft);
  border-radius:var(--radius-sm);
}
/* ===== THE EDITOR, in Team settings ===== */
.quick-links-config{align-items:flex-start;}
.ql-slot{margin-top:8px;}
.ql-row{
  display:flex; align-items:center; gap:8px;
}
.ql-row .ql-desc{flex:1 1 45%; min-width:0;}
.ql-row .ql-url{flex:1 1 55%; min-width:0;}
.ql-row .pref-switch{flex:0 0 auto;}
/* The row's own answer to "why is this not showing?" — see quickLinkRowProblem(). */
.ql-note{
  font-size:11px; color:var(--text-faint); line-height:1.4;
  margin:2px 0 0 48px;
}
.ql-note:empty{display:none;}
.ql-note.err{color:var(--red-text);}
@media (max-width:820px){
  .ql-row{flex-wrap:wrap;}
  .ql-row .ql-desc, .ql-row .ql-url{flex:1 1 100%;}
  .ql-note{margin-left:0;}
}
/* 11px, not 10: the faint floor is absolute, enforced per page by
   responsive-layout.test.js. */
.quick-links-head{
  font-size:11px; text-transform:uppercase; letter-spacing:.4px;
  color:var(--text-faint); font-weight:700;
}
/* Compact on purpose: 12px, one line each, ellipsised. A description long enough to wrap
   would push the panel's height around as somebody edits it. */
.quick-link{
  font-size:12px; color:var(--accent-text); text-decoration:none;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  padding:2px 0; border-radius:4px;
}
.quick-link:hover{text-decoration:underline;}
.quick-link:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}

/* Where the column sits is each page's business, not this file's — no
   page-specific layout is declared here. */

/* ---------- The compact task row — one definition, four surfaces ----------
   Shared across several lists that already carried this class name and had
   drifted underneath it: different sizes, gaps, padding and borders. See
   JS/components/task-row.js.

       TITLE                 meta | meta | meta          [ action ] [ action ]  */
.completed-row{
  display:flex; align-items:center; gap:12px;
  padding:9px 12px; margin-bottom:7px;
  background:var(--panel); border:1px solid var(--border-soft);
  border-radius:var(--radius-sm);
}
/* The title takes the slack and truncates. Everything else keeps its
   width, so a long title can never push the buttons off the right edge —
   a row you cannot act on.

   This is not provable from the declaration alone: `flex-shrink:10000` only
   has to dominate `.cmeta`'s factor of 1, so a smaller number looks like
   plenty and was tried first — but `.cmeta`'s spans are nowrap flex items
   whose automatic minimum size is their whole phrase, so even a small
   remaining share fires an ellipsis on a sub-pixel deficit (scrollWidth/
   clientWidth are integers, so this reads as "no overflow" and looks worse
   than no truncation at all). The size of the number is load-bearing — at
   10000 the meta's share falls below the layout unit and rounds to nothing.

   Not width-monotonic: the centre column is the window MINUS both panels,
   so a wider window with both panels open can be narrower than a smaller
   one with them closed. Vary the PANELS when checking this, not just the window. */
.completed-row .ctitle{
  flex:1 10000 auto; min-width:0; font-size:13px; font-weight:600; color:var(--text);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.completed-row .ctitle.is-clickable{cursor:pointer;}
.completed-row .ctitle.is-clickable:hover{color:var(--accent-text);}
/* Its flex-shrink stays 1, a safety valve rather than an oversight: 0 would
   make the meta read in full, but a task shared to several boards gets one
   button per place, each carrying the board name, so the actions alone can
   be wide — and a meta refusing to give any of it back pushes the buttons
   off the row and grows a horizontal scrollbar. */
.completed-row .cmeta{
  flex:0 1 auto; min-width:0; display:flex; align-items:center;
  font-size:12px; color:var(--text-faint); white-space:nowrap; overflow:hidden;
}
/* Vertical rules between the facts, not middle dots: team, board and
   status are different KINDS of thing, and a dot reads as a list of one
   kind while a rule reads as separate columns.

   When the title has nothing left to give, the facts truncate honestly
   rather than being cut: `min-width:0` overrides the automatic minimum
   size that made these spans incompressible, and `text-overflow` gives
   that shrink an ellipsis to show rather than a guillotine at the edge. */
.completed-row .cmeta > span{padding:0 10px; min-width:0; overflow:hidden; text-overflow:ellipsis;}
.completed-row .cmeta > span:first-child{padding-left:0;}
.completed-row .cmeta > span:last-child{padding-right:0;}
.completed-row .cmeta > span + span{border-left:1px solid var(--border);}
/* `button.btn.small` carries `align-self:flex-end` for the admin panes'
   column layout; inside this flex ROW that would drop it to the bottom edge. */
/* The row's action buttons, whichever vocabulary they carry — `.btn.small`
   is the classic name kept so an older host cannot lose the layout;
   `.a-btn.sm` is what JS/components/task-row.js emits now. Only the box
   model is set here; the Suite's own sheet owns how the button looks. */
.completed-row button.btn.small,
.completed-row button.a-btn.sm{align-self:center; flex:0 0 auto;}
/* The assignee chip, requested verbatim: "add the 'assignee chip' with the
   hover to see their full name in the 'On Team Boards' tab in tasks.html."
   Drawn between `.cmeta` and the action buttons when a caller passes
   `avatar: { name }`.

   Not `.a-row > .a-av` from CSS/alt/alt-tasks.css — that rule widens the
   avatar's track to align with a fixed-column grid `.completed-row` does
   not have. This rule is the same explicit insurance the action-button
   rule above takes, rather than leaning on the parent's alignment alone. */
.completed-row .a-av{align-self:center; flex:0 0 auto;}

/* Narrow: the row stacks rather than squeezing facts and buttons into a
   column of one word each. The rules become horizontal separators. */
@media (max-width: 720px){
  .completed-row{flex-wrap:wrap; gap:6px 10px;}
  .completed-row .ctitle{flex:1 1 100%; white-space:normal;}
  .completed-row .cmeta{flex:1 1 100%; white-space:normal; flex-wrap:wrap;}
}

/* ============================================================================
   EMAIL EVENT ROWS — one component, three panes
   ============================================================================
   A checkbox, the event, and one line saying what it actually costs you.
   The question is the same shape wherever it is asked, so the rows are the
   same rows across every surface that picks these events (core rule 4). */
.email-events-head{
  margin:16px 0 8px; font-size:11px; text-transform:uppercase; letter-spacing:.4px;
  color:var(--text-faint); font-weight:700;
}
.email-events{display:flex; flex-direction:column; gap:2px;}
.email-event{
  display:grid; grid-template-columns:auto minmax(0,1fr); gap:4px 10px;
  align-items:start; padding:8px 4px; border-bottom:1px solid var(--border-soft);
}
.email-event:last-child{border-bottom:none;}
.email-event input{margin:2px 0 0; accent-color:var(--accent);}
.email-event .ee-name{font-size:13px; color:var(--text); font-weight:600; cursor:pointer;}
.email-event .ee-why{grid-column:2; font-size:12px; color:var(--text-dim); line-height:1.45;}
/* The quiet ones read as quiet: off by default is a statement, not an oversight. */
.email-event.is-default-off .ee-name{font-weight:500; color:var(--text-dim);}
.email-event input:disabled + .ee-name{opacity:.5; cursor:default;}
/* Said, not warned about — a chatty event is somebody's own choice; what
   they are owed is knowing the cost before the inbox tells them. */
.email-event .ee-more{color:var(--text-faint); font-style:italic;}

/* ---------- Tasks → Settings → Email: the personal profile ----------
   The state in words first — which of the two modes somebody is on is a
   sentence at the top of the pane; the tint is only confirmation. */
.uep-status{
  display:flex; flex-direction:column; gap:3px; margin:2px 0 12px;
  padding:9px 11px; background:var(--panel-2);
  border:1px solid var(--border-soft); border-left:3px solid var(--border);
  border-radius:var(--radius-sm);
}
.uep-status b{font-size:13px; color:var(--text);}
.uep-status span{font-size:12px; color:var(--text-dim); line-height:1.5;}
.uep-status.is-custom{border-left-color:var(--accent);}

/* One line per team, never a matrix: teams do not agree with each other,
   so a single global set of ticks would be a picture of a configuration
   nobody has. `.uep-teams` (the column wrapper) was here, removed as
   unemitted — the rows are laid out by their own container now. */
.uep-team{
  display:grid; grid-template-columns:minmax(90px, 0.4fr) minmax(0,1fr); gap:2px 12px;
  align-items:baseline; padding:7px 4px; border-bottom:1px solid var(--border-soft);
}
.uep-team:last-child{border-bottom:none;}
.uep-team b{font-size:13px; color:var(--text); min-width:0; overflow-wrap:anywhere;}
.uep-team span{font-size:12px; color:var(--text-dim); line-height:1.45;}
.uep-team.is-off b{color:var(--text-dim); font-weight:500;}
.uep-team .uep-team-off{font-style:italic; color:var(--text-faint);}
.uep-team-empty{font-size:12px; color:var(--text-dim); line-height:1.5; padding:7px 4px;}

/* Narrow: the team name stops being a column and becomes a heading. */
@media (max-width: 620px){
  .uep-team{grid-template-columns:minmax(0,1fr); gap:2px;}
}

/* ============================================================================
   PASSWORD REQUIREMENTS — the live checklist
   ============================================================================
   Neutral until met, never red for merely unfinished — a half-typed
   password is not a mistake, and a wall of red trains a reader to ignore
   the colour that still has to mean something at the button. Rendered from
   the SAME rule array the validator reads, so a line cannot show a tick
   for a rule the button is refusing. */
.pw-reqs{list-style:none; margin:6px 0 0; padding:0; display:flex; flex-direction:column; gap:3px;}
.pw-req{
  display:grid; grid-template-columns:auto minmax(0,1fr); gap:8px; align-items:baseline;
  font-size:12px; color:var(--text-dim); line-height:1.45;
}
.pw-req-mark{
  font-size:12px; line-height:1.45; color:var(--text-faint);
  /* Fixed width so the labels do not shuffle sideways as ○ becomes ✓. */
  width:1em; text-align:center;
}
/* Met is a colour and a different glyph, and also a word for a screen
   reader — see the .sr-only span each row carries. Never colour alone. */
.pw-req.is-met{color:var(--text);}
.pw-req.is-met .pw-req-mark{color:var(--accent-text); font-weight:700;}

/* ---------- Account → Security ----------
   Compact on purpose: a permanent password form in the Account panel is a form somebody fills
   in by accident. The change is a deliberate act behind a button. */
.acct-security-row{
  display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;
  padding:9px 11px; background:var(--panel-2);
  border:1px solid var(--border-soft); border-radius:var(--radius-sm);
}
.acct-security-copy{display:flex; flex-direction:column; gap:2px; min-width:0;}
.acct-security-copy b{font-size:13px; color:var(--text);}
.acct-security-copy span{font-size:12px; color:var(--text-faint); letter-spacing:.12em;}

/* ---------- Change password dialog ---------- */
.chpw-card{max-width:440px;}
.chpw-step{display:flex; flex-direction:column; gap:6px; margin-bottom:12px;}
.chpw-lead{margin:0; font-size:13px; color:var(--text-dim); line-height:1.55;}
.chpw-note{margin:2px 0 6px; font-size:12px; color:var(--text-faint); line-height:1.5;}
/* The address is the point of the first step, so it is the one thing that is not body text. */
.chpw-dest{
  font-size:14px; font-weight:600; color:var(--text); word-break:break-all;
  padding:8px 11px; background:var(--panel-2);
  border:1px solid var(--border-soft); border-radius:var(--radius-sm);
}
.chpw-dest-inline{font-weight:600; color:var(--text); word-break:break-all;}
.chpw-label{
  font-size:11px; color:var(--text-dim); text-transform:uppercase;
  letter-spacing:.4px; font-weight:600; margin-top:4px;
}
/* A code is read back character by character, so it is spaced and tabular. */
.chpw-code{
  font-size:18px; letter-spacing:.34em; text-align:center;
  font-variant-numeric:tabular-nums; padding:9px 8px;
}
.chpw-hint{font-size:12px; color:var(--text-faint); line-height:1.5;}
.chpw-req-head{
  margin:12px 0 2px; font-size:11px; text-transform:uppercase; letter-spacing:.4px;
  color:var(--text-faint); font-weight:700;
}
/* Empty until there is something to mismatch, and takes no height until then. */
.chpw-match{font-size:12px; line-height:1.45; color:var(--text-dim); min-height:0;}
.chpw-match.err{color:var(--red-text);}
/* .btn is display:flex, so inside a column it would stretch to the card's width. Both the
   send and the resend button are ordinary-width controls, not full-width bars. */
#chpwSendBtn, #chpwResendBtn{align-self:flex-start;}

/* Narrow: the security row stacks rather than squeezing a button against a label. */
@media (max-width: 520px){
  .acct-security-row{flex-direction:column; align-items:stretch;}
  .acct-security-row .btn{align-self:flex-start;}
  .chpw-code{letter-spacing:.22em; font-size:16px;}
}

/* ---------- Account → Delete your account ----------

   The row in the panel reuses `.acct-security-row` rather than restating
   it — the same object, only red. Mounted by JS, not by markup: the account
   panel is authored across several pages and this feature is authored
   once, using the shared dialog classes so the animation, scrim and exit
   ramp all come from the shell above rather than a second dialog system. */
.acct-danger-row{border-color:var(--red);}
.acct-danger-copy b{color:var(--red-text);}
/* Not `letter-spacing:.12em` — that is for spacing out a row of password
   dots. This is a sentence and reads as one. */
.acct-danger-copy span{letter-spacing:normal; line-height:1.45;}

.acct-delete-card{max-width:460px;}
/* The warning is the dialog, so it is not body-text grey — a reader who
   skims this and presses the red button loses everything in the product. */
.acct-delete-warn{
  font-size:13px; line-height:1.55; color:var(--text);
  padding:10px 12px; background:var(--red-soft);
  border:1px solid var(--red); border-radius:var(--radius-sm);
}
.acct-delete-warn p{margin:0 0 8px;}
.acct-delete-warn p:last-child{margin-bottom:0;}
.acct-delete-warn ul{margin:0 0 8px; padding-left:18px;}
.acct-delete-warn li{margin:2px 0;}
.acct-delete-warn b{color:var(--red-text);}
.acct-delete-label{
  display:block; font-size:11px; color:var(--text-dim); text-transform:uppercase;
  letter-spacing:.4px; font-weight:600; margin:14px 0 5px;
}
/* The phrase is shown the way it must be typed — lower case, one space, no
   full stop — and `text-transform:none` is what keeps that true, since this
   span sits inside `.acct-delete-label`'s `uppercase` and text-transform
   inherits. Belt and braces: acctDeletePhraseOk() also case-folds now, so a
   reader who types it in capitals is accepted either way — the instruction
   has to be honest whether or not the parser is forgiving. */
.acct-delete-phrase{
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:13px; color:var(--red-text); background:var(--panel-2);
  border:1px solid var(--border-soft); border-radius:4px; padding:1px 5px;
  white-space:nowrap;
  text-transform:none;
}
/* Empty until there is something to say, same reasoning as `.chpw-match`.
   role="status", so it is announced when it changes, not read out on open. */
.acct-delete-hint{font-size:12px; line-height:1.45; color:var(--text-faint); margin-top:6px;}
.acct-delete-hint.err{color:var(--red-text);}

/* Narrow: the footer stacks for a full-size hit target. `column-reverse`,
   not `column` — the DOM keeps Cancel before Delete for reading/tab order,
   and the reverse puts Cancel nearest the thumb, the same §12 rule about
   the committing action never being where a cursor lands by accident. */
@media (max-width: 520px){
  .acct-delete-card .modal-footer{flex-direction:column-reverse; align-items:stretch;}
  .acct-delete-card .modal-footer .btn{justify-content:center;}
}

/* The section a link asked for, said once — a frame that scrolls
   somewhere without saying why leaves the reader looking for what moved. */
.tsec.tsec-landed{
  outline:2px solid var(--accent); outline-offset:2px; border-radius:8px;
  transition:outline-color var(--dur-surface, .3s) var(--ease-out, ease);
}

/* ===== THE BOARD PICKER DIALOG =====
   JS/features/board-picker.js builds this dialog on demand on any host
   that loads it, so its styling cannot stay inline on one page. Sits
   beside `.settings-modal-*`, which it uses for its shell. */
.board-picker-summary{font-size:11px; color:var(--text-faint); min-width:0; flex:1 1 auto;}
.board-picker-modal{width:520px;}
.board-picker-intro{font-size:12px; color:var(--text-dim); line-height:1.45; margin-bottom:10px;}
.board-picker-list{display:flex; flex-direction:column; gap:7px;}
.board-picker-row{
  display:flex; align-items:center; gap:10px; padding:8px 10px;
  background:var(--panel-2); border:1px solid var(--border-soft); border-radius:var(--radius-sm);
}
.board-picker-row input[type=checkbox]{width:auto; margin:0; accent-color:var(--accent); flex:0 0 auto;}
.board-picker-row .bp-name{flex:1; min-width:0; font-size:13px; font-weight:600;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.board-picker-row select{flex:0 0 auto; max-width:none;}
.board-picker-row.off select{opacity:.45; pointer-events:none;}
/* The home board's row — ticked and disabled rather than tickable, since
   it is the board the task is already open on. Dimmed like `.off` above,
   but for a different reason: that says "not on this board", this says
   "can't be changed here". */
.board-picker-row.pinned{opacity:.65;}
.board-picker-row.pinned input[type=checkbox],
.board-picker-row.pinned select{cursor:not-allowed;}
.board-picker-same{display:block; margin-top:6px; color:var(--text-dim);}
/* The board section's own row shape, reused on another task editor —
   changes nothing where the higher-specificity original already applies. */
.board-row{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}

/* ===== appConfirm()/appNotice() — JS/components/app-confirm.js =====
   The canonical replacement for window.confirm()/alert(). The shell is
   `.settings-modal-*` above, unchanged; these two rules are the only parts of this dialog
   that genuinely have nowhere else to come from.

   `.app-confirm-modal` caps the width. `.settings-modal` itself is `width:auto`, sized from
   whichever host set an explicit size at open time (sys_design-system.md §12) — nothing here
   does that, since a one- or two-sentence confirm has no content to size against, so without
   this the box would shrink-wrap to the width of its title and render oddly narrow. Matches
   `.board-picker-modal` just above: a fixed cap is the established way a dialog using this
   shell states its own size instead of calling the viewport sizer. `max-width:100%` keeps the
   overlay's own 24px side padding in control at 390px, the same floor `.settings-modal`'s own
   `min-width:min(440px,100%)` already protects. */
.app-confirm-modal{width:100%; max-width:420px;}

/* `.app-confirm-body` is plain body copy, not a hint. `.pane-hint` (above) is the nearest
   existing "text above a list" class and was the first thing tried, but it is deliberately
   `--text-faint` at 12px — right for a caption over a list of rows, wrong here because this
   paragraph IS the dialog's entire message, not a secondary note beside its real content. A
   confirm the reader has to squint at to read is the wrong default, so this is `--text` at the
   shell's own 13px body size, with the same `p` margin convention `.okrc-warn p` already uses
   (CSS/team-delete-flow.css) for a stack of short paragraphs. */
.app-confirm-body p{margin:0 0 10px; font-size:13px; line-height:1.55; color:var(--text);}
.app-confirm-body p:last-child{margin-bottom:0;}
