/* Workroom design tokens. See docs/design.md. */
:root {
  /* Emoji are the platform's own (Apple's on iOS and macOS, Segoe on
     Windows, Noto on Android and Linux): never a bundled set, so a reaction
     looks like the same emoji in the person's keyboard (user, 2026-09-19).
     The picker takes this stack through its own --emoji-font-family. */
  --font-emoji:
    "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
    "Noto Color Emoji", sans-serif;
  --font-sans:
    -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI",
    Roboto, Helvetica, Arial, sans-serif;
  --font-mono:
    ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --paper: #fafaf9;
  --surface: #ffffff;
  --surface-2: #f3f3f1;
  /* The open row in the sidebar: a fill that is visible on a phone, which
     --surface-2 is not (1.11:1 against the sidebar's white; this is 1.23:1).
     The proposal: docs/work/2026-09-20-sidebar-selected-vs-unread.md. */
  --surface-3: #e8e8e5;
  --ink: #1b1d22;
  --ink-2: #5b5f68;
  --ink-3: #9a9ea6;
  /* A read sidebar row: between --ink and --ink-2, so bold stays the main
     unread/read cue but a read row does not read as flatly grey as --ink-2
     on its own (user, 2026-09-29: "too gray in both dark and light modes").
     Sidebar-specific rather than repointing --ink-2, which other secondary
     text still uses unchanged. See the comment on the read-row rule below
     for the contrast numbers this was tuned against. */
  --ink-sidebar-read: #41454c;
  /* A muted sidebar row, one step quieter than --ink-sidebar-read (a read
     row, above): see the comment on .nav-list button.muted .label for the
     numbers this was tuned against. */
  --ink-muted: #7b7f87;
  --line: #e6e7ea;
  --line-2: #d6d8dd;
  /* Retired 2026-09-30: every use outside think mode moved to ink and the
     neutrals (Jean, docs/work/2026-09-30-all-ink.md). Only --think* remains
     blue, so a var(--accent) left behind by accident resolves to nothing
     rather than quietly painting blue again. */
  --think: #2456c6;
  --think-bg: #eef3fc;
  --think-line: #c3d2f0;
  --think-ring: rgba(36, 86, 198, 0.12);
  --danger: #b3261e;
  /* A coding task marked done: its check, the one green in the app
     (docs/work/2026-09-29-coding-task-link-enrichment.md, decision 10). */
  --done: #1e7a3c;
  /* A coding task an agent was seen working on: the pill's amber dot, GitHub's in-progress colour (user, 2026-10-01). */
  --task-working: #9a6700;
  /* A coding task whose pull request merged and is not live yet: GitHub's merged purple (user, 2026-10-01). */
  --task-merged: #8250df;
  /* The two vendors' colours, for a coding task's tool disc only (design v3
     of docs/work/2026-09-29-coding-task-link-enrichment.md): Anthropic's
     clay behind Claude's mark, near-black behind Codex's, which alone
     inverts in dark mode. */
  --vendor-claude: #d97757;
  --vendor-codex: #0f0f0f;
  /* A pull request's preview only: the top bars' colour, and the browser's
     (docs/work/2026-10-07-pr-previews.md); production never shows it.
     markPreview in src/client.tsx repeats both values for theme-color. */
  --preview-bar: #dccdf9;
  /* Viewing the app as a member (docs/work/2026-10-13-view-as.md): amber,
     apart from the preview's purple and Think's blue. */
  --view-as: #9a6700;
  --view-as-bg: #fdf3dc;
  --view-as-line: #efd9a4;
  /* The tint behind a removed line in a file's diff, as --surface-2 is the
     tint behind an added one: --danger is a text colour and too strong to
     sit under 12.5 px mono. */
  --danger-bg: #fbecea;
  /* A table's select options: text on a tint, 5.3:1 or better, no blue.
     The one coloured exception to the neutrals (the owner's decision 3a,
     2026-10-13, docs/work/2026-10-13-database-no-brainers.md). */
  --opt-green: #1e6b37;
  --opt-green-bg: #e3f2e7;
  --opt-yellow: #7a5c00;
  --opt-yellow-bg: #fbf1cf;
  --opt-orange: #9a4a12;
  --opt-orange-bg: #fbeadc;
  --opt-red: #a3261e;
  --opt-red-bg: #fbe9e7;
  --opt-purple: #6b3fb8;
  --opt-purple-bg: #efe8fa;
  --opt-pink: #9b2c63;
  --opt-pink-bg: #fbe7f0;
  --opt-brown: #7a5230;
  --opt-brown-bg: #f4ece4;
  --opt-gray: #5b5f68;
  --opt-gray-bg: #efefed;
  /* What an option means (done, in progress, warning, danger): its colour,
     set here once, so a state reads the same on every table and Jean
     retunes it in one place (the owner's "states, not colours",
     2026-10-13, docs/work/2026-10-14-select-option-states.md). */
  --state-done: var(--opt-green);
  --state-done-bg: var(--opt-green-bg);
  --state-in-progress: var(--opt-yellow);
  --state-in-progress-bg: var(--opt-yellow-bg);
  --state-warning: var(--opt-orange);
  --state-warning-bg: var(--opt-orange-bg);
  --state-danger: var(--opt-red);
  --state-danger-bg: var(--opt-red-bg);
  --user-bubble: #f0f0ee;
  --radius: 10px;
  --radius-lg: 16px;
  /* The composer's mode switch and docking share one quick gesture
     (docs/work/2026-09-25-composer-mode-motion.md): fast enough to never
     feel like a wait, an ease-out so it settles rather than snapping.
     `prefers-reduced-motion` turns every transition and animation off below,
     which zeroes these durations along with everything else. */
  --motion-fast: 180ms;
  --motion-ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* The lit middle of a placeholder's sweep: a step above --surface-2 that
     is visible in both schemes (docs/work/2026-09-19-topic-skeleton.md). */
  --skeleton-sweep: #e4e4e0;
  /* One speed for every shimmer: the busy text and the placeholders'
     sweep share it and the `shimmer-sweep` keyframes
     (docs/work/2026-10-02-text-shimmer.md). */
  --shimmer-speed: 1.6s;
  /* The colour of the shadow a raised surface (the sidebar, the pinned
     to-do card) casts onto the conversation beside or below it: short,
     soft and low-opacity, reading as height rather than a drop-shadowed
     card (user rule, 2026-10-01). Composed into a directional box-shadow
     at each site, as --think-ring is. */
  --surface-shadow: rgba(17, 17, 20, 0.12);
  /* The Pages screen (docs/work/2026-10-16-pages-grid-title-first.md): a warm grey
     ground a white card sits on, and the inset panel inside the card that holds
     the page preview, a step darker than the card. Both are used by that screen only. */
  --pages-ground: #efeeea;
  --pages-inset: #f6f5f2;
  --sidebar: 268px;
  --drawer: min(86vw, 320px);
  --column: 720px;
  --bar: 52px;
  --safe-top: env(safe-area-inset-top);
  --safe-bottom: env(safe-area-inset-bottom);
  color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
  :root {
    --paper: #131416;
    --surface: #1b1c1f;
    --surface-2: #232428;
    --surface-3: #2e2f34;
    --ink: #ececec;
    --ink-2: #a5a8b0;
    --ink-3: #6f737b;
    --ink-sidebar-read: #c1c3c8;
    --ink-muted: #8a8e96;
    --skeleton-sweep: #34363b;
    /* Tuned darker than light mode's so the shadow stays perceptible against
       a dark card and dark stream alike without the blur reading as mud. */
    --surface-shadow: rgba(0, 0, 0, 0.45);
    --pages-ground: #0e0f11;
    --pages-inset: #141517;
    --line: #2a2b2f;
    --line-2: #37393e;
    --think: #7ea2f0;
    --think-bg: #1c2740;
    --think-line: #2f4270;
    --think-ring: rgba(126, 162, 240, 0.18);
    --danger: #f28b82;
    --done: #81c995;
    --task-working: #d29922;
    --task-merged: #ab7df8;
    --vendor-codex: #ececec;
    --preview-bar: #33255a;
    --view-as: #d29922;
    --view-as-bg: #33280f;
    --view-as-line: #5a4416;
    --danger-bg: #3a2320;
    --opt-green: #8fd3a2;
    --opt-green-bg: #1d3526;
    --opt-yellow: #e8c860;
    --opt-yellow-bg: #3a3116;
    --opt-orange: #f2a671;
    --opt-orange-bg: #42291a;
    --opt-red: #f29c94;
    --opt-red-bg: #43211e;
    --opt-purple: #c3a6f5;
    --opt-purple-bg: #2f2547;
    --opt-pink: #f0a3c6;
    --opt-pink-bg: #43212f;
    --opt-brown: #dcb995;
    --opt-brown-bg: #3a2d22;
    --opt-gray: #c1c3c8;
    --opt-gray-bg: #2e2f34;
    --user-bubble: #26272b;
  }
}
* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
}
/* A touch pans the page and never pinches it: iOS Safari ignores the
   viewport's user-scalable, and honours this only in part, so
   src/client/viewport.ts (stopPagePinch) also cancels its pinch events
   (docs/work/2026-10-22-no-zoom-out.md). Touch screens only: a desktop's
   pinch and Cmd +/- stay. It also turns off the pinch-zoom some boxes
   below still allow (the video, the bottom sheet's stream). */
@media (pointer: coarse) {
  html {
    touch-action: pan-x pan-y;
  }
}
/* The document itself never scrolls: only the feed, the drawer's list and a
   dialog do, each inside its own box. A fixed body is what stops iOS Safari
   from panning the whole page after the keyboard went up under the composer
   (seen on a phone on 2026-09-19: the composer slid up and the drawer's foot
   showed under it); the app inside keeps the dynamic viewport's height, so
   its bottom edge stays above the browser's toolbar. */
body {
  position: fixed;
  inset: 0;
  width: 100%;
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-synthesis: none;
  overscroll-behavior: none;
}
#root {
  height: 100dvh;
}
/* Under a phone's keyboard the app frame is the visible part of the screen,
   not the whole page: iOS keeps the page full height and pans it up, which
   took the top bar out of view. src/client/viewport.ts sets both variables
   only while a keyboard is up; otherwise they fall back to the full height.
   Only the app frame follows: the sign-in page keeps scrolling as it did. */
#root:has(.app) {
  position: fixed;
  top: var(--app-top, 0px);
  left: 0;
  width: 100%;
  height: var(--app-height, 100dvh);
}
h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
h2 {
  font-size: 17px;
  line-height: 1.3;
}
h3 {
  font-size: 15px;
}
p {
  margin: 0;
  overflow-wrap: anywhere;
}
a {
  color: var(--ink);
  text-decoration: underline;
}
a:hover {
  color: var(--ink-2);
}
button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: pointer;
  border-radius: 8px;
  overflow-wrap: anywhere;
}
button:disabled {
  cursor: default;
  opacity: 0.5;
}
/* Focus rings (docs/work/2026-10-22-focus-rings.md): keyboard focus only, in
   --ink-2 (no accent token exists since 2026-09-30, so the nearest neutral).
   A mouse click or a script moving focus on open draws nothing; a container
   focused by script (tabindex="-1") is never reached by Tab and never rings. */
:focus-visible {
  outline: 2px solid var(--ink-2);
  outline-offset: 2px;
}
:focus:not(:focus-visible),
[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible {
  outline: none;
}
/* After a tap or a click (src/client/input-modality.ts), a ring the browser
   still matches for script focus is hidden; the next key brings it back. */
html[data-input="pointer"] :focus-visible {
  outline: none;
}
input,
select,
textarea {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 10px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  font: inherit;
  font-size: 16px;
}
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px var(--surface-2);
}
textarea {
  resize: vertical;
  max-height: 40dvh;
}
label,
.field-label {
  display: block;
  margin: 14px 0 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}
fieldset {
  padding: 0;
  margin: 0;
  border: 0;
  min-width: 0;
}
form {
  min-width: 0;
}
small,
.hint {
  display: block;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-3);
}
.hint {
  margin-top: 6px;
}
/* The exploration composer's own "Shift+Enter for new line": it has no
   Reply | Think switch to anchor a row to, so it stays the 2026-09-30
   overlay on the field's own first line rather than a row of its own
   (which used to grow the composer by a row's height on focus) — self-
   contained rather than built on the shared `.hint`, since it does not sit
   inside a flex row that supplies its position. `top` matches the field's
   own top padding (`.composer-box textarea`'s `padding: 6px 0`) and
   `line-height` matches the field's own first-line box height (its 16px
   font at 1.45 line-height) exactly, so the two line boxes start at the
   same offset and are the same height — their vertical centres coincide
   without needing flex centring. `pointer-events: none` keeps it from
   blocking a click into the text under it. `src/client/hint-crowd.ts` adds
   `.crowded` once the first line's own text would run under it, which
   fades it out rather than letting the two overlap. The topic composer
   (and any composer with the switch) instead uses `.row-hint` below, on
   the switch's own row, since 2026-09-30 (Jean: "move shift+enter to the
   top-right of this composer, aligned with the toggle/switch. do not
   change it for the composer that doesn't have the switch",
   docs/work/2026-09-30-hint-top-right.md;
   docs/work/2026-09-30-composer-hint-inline.md). */
.keys-hint {
  position: absolute;
  top: 6px;
  right: 0;
  margin: 0;
  font-size: 12px;
  line-height: 23.2px;
  color: var(--ink-3);
  pointer-events: none;
  transition: opacity var(--motion-fast) var(--motion-ease);
}
.keys-hint.crowded {
  opacity: 0;
}
/* The field wrapper the exploration composer's hint overlays. */
.hint-field {
  position: relative;
}
/* The same hint in a composer that has the Reply | Think switch: a plain
   flex child of `.composer-row`, pushed to the row's right edge by the
   `.spacer` before it, not absolutely positioned — the row already exists
   for the switch and is already tall enough for it, so mounting or
   unmounting this hint cannot move or grow the row, and the row's own
   `align-items: center` centres it with the switch with no extra rule
   needed. It no longer overlays the field's typed text at all, so it needs
   no crowd-avoidance. `white-space: nowrap` matters here in a way it did
   not for the absolutely-positioned `.keys-hint` above (which shrinks to
   fit regardless): as an ordinary flex child, `.row-hint` would otherwise
   wrap onto two lines once the row runs narrow (a resized desktop window;
   the hint hides only on `pointer: coarse`, not by width), which would
   grow the row — the exact regression this file exists to prevent, just
   triggered by width instead of by mounting
   (docs/work/2026-09-30-hint-top-right.md). */
.row-hint {
  margin: 0;
  font-size: 12px;
  white-space: nowrap;
}
.error {
  margin-top: 8px;
  font-size: 13px;
  color: var(--danger);
}
p[role="status"]:empty {
  display: none;
}
[hidden] {
  display: none !important;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------- buttons ---------- */
.btn,
.btn-primary,
.btn-ghost,
.btn-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 40px;
  padding: 8px 14px;
  border-radius: var(--radius);
  font-weight: 500;
  font-size: 14px;
  white-space: nowrap;
}
.btn {
  border: 1px solid var(--line-2);
  background: var(--surface);
}
.btn:hover:enabled {
  background: var(--surface-2);
}
.btn-primary {
  background: var(--ink);
  color: var(--paper);
}
.btn-primary:hover:enabled {
  opacity: 0.9;
}
.btn-ghost {
  color: var(--ink-2);
}
.btn-ghost:hover:enabled {
  background: var(--surface-2);
  color: var(--ink);
}
.btn-danger {
  border: 1px solid var(--line-2);
  color: var(--danger);
  background: var(--surface);
}
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  color: var(--ink-2);
  flex: none;
}
/* A dictation button while the microphone is open (src/client/dictation.tsx):
   ink and a tinted background (the cue that survives reduced motion and a
   phone's sticky hover), plus the pulse. Never the accent: blue is the
   agent's colour and nothing else's (user, 2026-09-28). */
.icon-btn.listening,
.icon-btn.listening:hover:enabled {
  color: var(--ink);
  background: var(--surface-2);
  animation: pulse 1.4s ease-in-out infinite;
}
.icon-btn:hover:enabled {
  background: var(--surface-2);
  color: var(--ink);
}
/* The mic with what a recording brings beside it: the elapsed time (then
   "Writing it down…") and the discard button. One inline group, so a
   composer's row places it as it placed the lone mic. */
.dictation {
  display: inline-flex;
  align-items: center;
  flex: none;
  min-width: 0;
}
/* Held to talk: the press is ours, never a scroll, a text selection or the
   browser's long-press menu (src/client/mic-hold.ts). */
.dictation .mic {
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.dictation .mic.off {
  opacity: 0.45;
}
/* Held, the mic's row becomes the recording, as in Claude's app (user,
   2026-09-27): the row's other controls step aside, and what a release
   does, a live waveform and the clock fill it; nothing floats over the
   field. */
:is(.composer-row, .pickers):has(> .dictation.holding) > :not(.dictation) {
  display: none;
}
.dictation.holding {
  flex: 1;
  gap: 10px;
  padding-right: 6px;
}
.dictation-hint {
  flex: none;
  font-size: 14px;
  color: var(--ink-2);
  white-space: nowrap;
}
.dictation-hint.off {
  color: var(--danger);
}
.dictation-wave {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
  height: 24px;
  overflow: hidden;
}
.dictation-wave i {
  flex: none;
  display: block;
  width: 3px;
  height: 3px;
  border-radius: 2px;
  background: var(--ink);
}
.dictation-wave.off i {
  background: var(--ink-3);
}
.dictation-status {
  font-size: 13px;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding: 0 2px;
}
/* The mic while the text is written down: unavailable, but not `disabled`,
   so focus stays on it. */
.icon-btn[aria-disabled="true"],
.icon-btn[aria-disabled="true"]:hover {
  background: none;
  color: var(--ink-2);
  cursor: default;
}
.glyph,
.icon-btn svg,
.btn svg,
.btn-ghost svg,
.scroll-to-end svg {
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.row-end {
  justify-content: flex-end;
}
.actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
}
.stack > * + * {
  margin-top: 10px;
}

/* ---------- sign in / loading ---------- */
.gate {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px max(20px, env(safe-area-inset-right)) 32px
    max(20px, env(safe-area-inset-left));
}
.gate-card {
  width: 100%;
  max-width: 380px;
}
.gate-card h1 {
  font-size: 26px;
  margin-top: 12px;
}
.gate-card .lead {
  margin: 8px 0 20px;
  color: var(--ink-2);
}
.gate-card form,
.gate-card .btn,
.gate-card .btn-primary {
  width: 100%;
}
.gate-card .btn-primary {
  margin-top: 12px;
  min-height: 44px;
}
.gate-card .btn {
  margin-top: 10px;
  min-height: 44px;
}
.wordmark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 9px;
  background: var(--ink);
  color: var(--paper);
  font-weight: 700;
  font-size: 15px;
}
/* The page's own opening card (src/http/index.html), before any script runs.
   Its reload line stays invisible for twelve seconds, longer than any normal
   open, then appears for the person whose bundle is not coming; with reduced
   motion the animation is off and the line simply shows, quietly. */
.boot-slow {
  margin-top: 14px;
  font-size: 14px;
  color: var(--ink-2);
  animation: boot-slow 200ms linear 12s both;
}
@keyframes boot-slow {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
/* The opening card's ellipsis types itself, one dot at a time, in a loop: one
   dot, two, three, then none, about 1.2s per cycle. `.boot-dots` is a plain,
   empty container around three plain `<span class="boot-dot">` children, each
   drawing one "." with its own `::before`, not one span whose `::before`
   draws the whole "..." as a single run of generated content revealed by
   animating `clip-path` on that one box: an earlier version did exactly that
   (and, before it, gave each dot its own fixed-width `inline-block` box,
   which floated every dot above the heading's real baseline — CSS2.1
   §10.8). The single-span `clip-path` design worked in Chromium (the browser
   the earlier bug report named) but WebKit does not animate `clip-path` on an
   inline, non-replaced element the way Chromium does, so on an iPhone all
   three dots appeared and disappeared together instead of one after another
   (Jean, 2026-09-30, docs/work/2026-09-30-boot-dots-sequence.md — not
   reproducible here, this container has no WebKit). `opacity` is paint-only
   on inline text in every engine, so each dot's own animation steps on
   independently without moving or resizing anything: no `display` override,
   no `<i>`, no per-engine risk, and the heading's width still never changes
   across any combination of dots shown (`tests/offline.test.ts`), since all
   three are always present and only their opacity toggles. The dots stay
   generated content, so — as before — they never enter the heading's real
   `textContent`. `steps(1, jump-end)` still makes each dot appear instantly
   rather than fading. Reduced motion overrides this to a static, fully-drawn
   "…" (below). */
.boot-dot {
  opacity: 0;
  animation: boot-dot-1 1.2s steps(1, jump-end) infinite;
}
.boot-dot::before {
  content: ".";
}
.boot-dot:nth-child(2) {
  animation-name: boot-dot-2;
}
.boot-dot:nth-child(3) {
  animation-name: boot-dot-3;
}
@keyframes boot-dot-1 {
  0%,
  74.99% {
    opacity: 1;
  }
  75%,
  100% {
    opacity: 0;
  }
}
@keyframes boot-dot-2 {
  0%,
  24.99% {
    opacity: 0;
  }
  25%,
  74.99% {
    opacity: 1;
  }
  75%,
  100% {
    opacity: 0;
  }
}
@keyframes boot-dot-3 {
  0%,
  49.99% {
    opacity: 0;
  }
  50%,
  74.99% {
    opacity: 1;
  }
  75%,
  100% {
    opacity: 0;
  }
}
.notice {
  margin-top: 14px;
  font-size: 14px;
  color: var(--ink-2);
}
.notice:empty {
  display: none;
}

/* ---------- app frame ---------- */
.app {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  height: var(--app-height, 100dvh);
  overflow: hidden;
  /* Cut, never scrolled: with the drawer open `.main` sits a drawer's width
     to the right, so this box is wider than the frame, and `hidden` still
     lets the browser scroll it (an iPhone does, to reveal a field taking
     focus as the drawer closes), leaving Ask Workroom shifted left with the
     drawer showing at its right edge. `clip` is no scroll container
     (docs/work/2026-11-02-ask-sheet-shift.md). */
  overflow: clip;
}
.sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  /* Follows the app frame above a phone's keyboard, as #root does. */
  top: var(--app-top, 0px);
  bottom: auto;
  height: var(--app-height, 100dvh);
  width: var(--drawer);
  z-index: 1;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--line);
  padding-top: var(--safe-top);
}
/* Keep the covered navigation painted behind the opaque foreground. React's
   inert state, rather than visibility, makes it unreachable while closed. */
/* The scrim travels with the foreground, dimming only that surface while the
   navigation beneath it stays readable and interactive. */
.scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(0, 0, 0, 0.28);
  border-radius: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease-out;
}
.scrim.open {
  opacity: 1;
  pointer-events: auto;
}
.sidebar-head {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 10px 10px 6px;
}
.sidebar-head .switcher {
  flex: 1;
  min-width: 0;
}
.switcher {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 6px 8px;
  border-radius: 8px;
  text-align: left;
  font-weight: 600;
}
.switcher:hover {
  background: var(--surface-2);
}
.switcher .mark {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  background: var(--ink);
  color: var(--paper);
  font-size: 12px;
  font-weight: 700;
  display: grid;
  place-items: center;
  flex: none;
}
.switcher .name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.switcher svg {
  width: 14px;
  height: 14px;
  stroke: var(--ink-3);
  fill: none;
  stroke-width: 2;
  flex: none;
}
.menu {
  position: absolute;
  z-index: 40;
  min-width: 220px;
  max-width: calc(100vw - 24px);
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14);
}
/* The workspace menu lists every Settings section above the workspaces, so
   it scrolls rather than running off a short screen. */
.switcher ~ .menu {
  max-height: calc(100dvh - var(--safe-top) - 72px);
  overflow-y: auto;
}
.menu button {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 8px 10px;
  text-align: left;
  border-radius: 8px;
}
.menu button:hover {
  background: var(--surface-2);
}
.menu button[aria-current="true"] {
  font-weight: 600;
}
.menu .menu-head {
  padding: 6px 10px 8px;
  font-size: 13px;
  color: var(--ink-2);
  /* Wraps rather than cutting the role off a long workspace name: the role
     is what the head is for. */
  overflow-wrap: anywhere;
}
.menu .menu-head strong {
  color: var(--ink);
}
/* A group label inside the workspace menu, as Settings' list labels it. */
.menu .menu-group {
  padding: 8px 10px 2px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}
/* Line glyphs in a menu, like the ones on buttons: without this they drew
   filled, as blobs. */
.menu svg,
.settings-chevron {
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.menu hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: 6px 0;
}
.menu form {
  padding: 4px 4px 6px;
}
.menu form input {
  min-height: 40px;
  /* 16 px, never less, on every field a phone shows: WebKit on iOS zooms the
     page into a focused field set smaller (docs/design.md, forms). */
  font-size: 16px;
}
.sidebar-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 10px 12px;
}
/* Section gap > group gap > row gap, so the hierarchy reads at a glance
   (Jean, 2026-10-01, from a screenshot where EXPLORATIONS/TOPICS and
   PINNED/RECENT read as the same level: docs/design.md#sidebar). A section
   carries the full 30 px above its heading; the first one is the exception,
   since it follows the top block's own rows rather than another section
   (next rule). */
.section {
  margin-top: 30px;
}
/* The top block (Ask <name>, On my to-do) is its own .nav-list, a sibling of
   the first .section rather than nested in it: this is the one place 30 px
   would read as too much air under two rows that are themselves the first
   thing on screen. */
.nav-list + .section {
  margin-top: 24px;
}
.section-head {
  /* Anchors a section's settings menu, which spans the heading so it fits
     the sidebar at any width. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 24px;
  padding: 0 4px 0 10px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
}
/* A small gap between the heading and its first row, rather than the rows'
   own 1 px list gap running straight into it. */
.section-head + .nav-list {
  margin-top: 4px;
}
/* A section head that folds its list (Explorations): the whole head is the
   target, as tall as a row, and reads as the label it replaces. */
.section-toggle {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 40px;
  margin-left: -4px;
  padding: 0 6px 0 4px;
  font-size: inherit;
  font-weight: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
}
/* Without this, Explorations' head (a button, which keeps its 40 px touch
   target below) would sit taller than Topics' plain-text head on desktop,
   undoing the point of the shorter .section-head above: the two should read
   as the same kind of heading. Mouse-gated like the row override above, so a
   touch tablet laid out at 840px+ keeps the full tap target. */
@media (min-width: 840px) and (hover: hover) and (pointer: fine) {
  .section-toggle {
    min-height: 24px;
  }
}
.section-toggle:hover {
  color: var(--ink-2);
}
.section-toggle svg {
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A section's ⋯ (Explorations: what the list shows), at the heading's right
   edge in line with the rows' ⋯, and shown the way theirs is: with a mouse
   when the heading is hovered, keyboard-focused or its menu open; always on
   touch (the user, 2026-09-28: "copy the pattern for topics, the three dots
   that appear on hover"). Its menu drops under the heading and does not take
   the heading's small capitals. */
/* The head keeps 4px on its right; the rows' ⋯ reach the list's edge. */
.section-menu {
  /* flex, not the default block: an inline-flex child alone in a block box
     still takes a line box sized by text metrics, leaving a couple of
     pixels of half-leading under it — which held Explorations' heading a
     little taller than Topics' even with both children at the same height. */
  display: flex;
  margin-right: -4px;
}
.section-menu > .icon-btn {
  width: 40px;
  height: 40px;
  min-height: 0;
  padding: 0;
  color: var(--ink-3);
}
.section-menu > .icon-btn:hover {
  color: var(--ink-2);
}
@media (hover: hover) and (pointer: fine) {
  .section-menu > .icon-btn {
    opacity: 0;
  }
  .section-head:hover .section-menu > .icon-btn,
  .section-menu > .icon-btn:focus-visible,
  .section-menu > .icon-btn[aria-expanded="true"] {
    opacity: 1;
  }
}
/* Shrinks alongside .section-toggle above, on the same query: this button
   sits in the same flex row, and its 40px box would otherwise keep
   Explorations' heading tall even once the toggle itself narrows. */
@media (min-width: 840px) and (hover: hover) and (pointer: fine) {
  .section-menu > .icon-btn {
    width: 24px;
    height: 24px;
  }
}
.section-menu .menu {
  left: 4px;
  right: 4px;
  top: 100%;
  min-width: 0;
  font-size: 15px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
}
.nav-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.nav-list button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 36px;
  padding: 6px 10px;
  border-radius: 8px;
  text-align: left;
  color: var(--ink);
  font-size: 14.5px;
}
/* A mouse-driven desktop can afford a denser row than a thumb needs, which
   is most of the hierarchy win (Jean, 2026-10-01, docs/design.md#sidebar):
   touch/mobile keeps the 36 px documented exception above (design.md,
   mobile-first) rather than shrinking further. Gated on the input, not only
   the width — a touch tablet laid out at 840px+ still gets a thumb's row,
   the same split the rest of the file already uses for touch/mouse (the
   `(hover: hover) and (pointer: fine)` blocks below). */
@media (min-width: 840px) and (hover: hover) and (pointer: fine) {
  .nav-list button {
    min-height: 32px;
    /* The label's own line-height (14.5px at the body's 1.5) is taller than
       32px minus the base 6px/6px padding, so the row would still render at
       ~34px on content alone; trimmed to actually reach 32px. */
    padding-top: 5px;
    padding-bottom: 5px;
  }
}
.nav-list button:hover {
  background: var(--surface-2);
}
/* Where you are is a fill; what is new is bold; never the same cue for both.
   Until 2026-09-20 the open row was bold on a --surface-2 fill too faint for a
   phone, so it read as one more unread topic (user, from a screenshot). */
.nav-list button[aria-current="page"] {
  background: var(--surface-3);
}
.nav-list button .glyph {
  width: 16px;
  height: 16px;
  color: var(--ink-3);
}
.nav-list button[aria-current="page"] .glyph {
  color: var(--ink);
}
.nav-list button .label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* An exploration row for a thread in a topic: the title, then a quiet line
   naming the topic, where it opens. The line never takes the row's bold. */
.nav-list button .row-text {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
}
.nav-list button .row-text .label {
  flex: none;
}
.nav-list button .row-context {
  overflow: hidden;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.3;
  color: var(--ink-3);
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Ask <name> and On my to-do: an ink-filled CTA and a plain row read as
   unbalanced at the sidebar's top (Jean, 2026-09-30, from a screenshot of
   docs/work/2026-09-30-discuss-cta.md's result); flattened the same day to
   ordinary rows of the same shape, size, padding and left edge as the rest
   of the list — no fill, no border. Regular weight like a read topic row:
   bold read as an unread channel at first glance (Jean, 2026-10-06,
   docs/work/2026-10-06-ask-not-bold.md), so Ask <name> keeps the row's
   normal weight, and the open state is the shared `--surface-3` fill. Hover
   and every other row state are the shared `.nav-list button` rules above,
   unchanged. The mobile pill is the Ask pill (`.dock-pill`). */
/* The entry point names the Workroom agent, whose name a workspace's admins
   may make 24 characters long: the row wraps rather than cut the name off
   (docs/work/2026-09-28-agent-name.md). */
#discuss .label {
  white-space: normal;
  overflow-wrap: anywhere;
}
/* Each row's icon takes its own row's text colour rather than the fainter
   --ink-3 every other sidebar icon uses (`.nav-list button .glyph` above):
   today's icons read lighter than their label, which stood out once the row
   above went from a filled pill (--paper icon on --ink) to plain text
   (docs/work/2026-09-30-sidebar-top-rows.md). currentColor keeps it correct
   through the row's own colour changes (aria-current, hover) without a
   separate rule per state. */
#discuss .glyph,
#whats-next .glyph {
  color: currentColor;
}
/* A topic row: the topic's button, then its actions glyph, muted until it is
   used; on touch it shows only on the open row, on hover with a mouse (the
   rules below). */
/* The topic list's groups (Pinned, Mentions, Unread, Recent): a small muted
   label above each, in the section head's type, and the rows as before. */
.nav-group + .nav-group {
  margin-top: 12px;
}
/* A second, quieter label level, sentence case rather than small caps, so a
   sub-group (Pinned, Mentions, Unread, Recent) never reads as a section of
   its own next to EXPLORATIONS/TOPICS (docs/design.md#sidebar). The label
   text itself is already sentence case (topic-sections.ts); only the case
   transform and tracking change here. */
.nav-group-head {
  min-height: 22px;
  padding: 4px 10px 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ink-3);
}
.nav-list .topic-row {
  display: flex;
  align-items: center;
  min-width: 0;
  border-radius: 8px;
}
.nav-list .topic-row > button {
  flex: 1;
  min-width: 0;
}
/* One surface for the whole row: hovering or holding the open topic lights
   the row under the title and the actions button together, and neither child
   lights a pill of its own. */
.nav-list .topic-row:hover {
  background: var(--surface-2);
}
/* Its own rule: a browser without :has() drops only this line and keeps the
   hover above. */
.nav-list .topic-row:has(> button[aria-current="page"]) {
  background: var(--surface-3);
}
.nav-list .topic-row > button:hover,
.nav-list .topic-row > button[aria-current="page"],
.nav-list .topic-row .topic-actions > .icon-btn:hover:enabled {
  background: transparent;
}
/* With a mouse the actions button appears when the row is hovered, when the
   keyboard reaches the button, or while its menu is open; a thumb has no
   hover, so on touch it shows on the open row alone (below). Opacity, not
   display: the button keeps its place and stays reachable by keyboard.
   Keyboard focus only (:focus-visible): a mouse click on the title focuses
   the title, and that must not leave the open topic's button showing when
   every other row's is hidden. */
@media (hover: hover) and (pointer: fine) {
  .nav-list .topic-row .topic-actions > .icon-btn {
    opacity: 0;
  }
  .nav-list .topic-row:hover .topic-actions > .icon-btn,
  .nav-list .topic-row .topic-actions > .icon-btn:focus-visible,
  .nav-list .topic-row .topic-actions > .icon-btn[aria-expanded="true"] {
    opacity: 1;
  }
}
/* On touch (Jean, 2026-09-30: only the selected channel's ⋯, and a long
   press on any row to open it) the button shows only on the row that is
   open, the one already filled by the rule below it: elsewhere it stays in
   the accessibility tree, visually hidden, the post's own react-btn/more-btn
   technique on touch (above) — reachable by a screen reader and by the row's
   own long press (workspace.tsx SidebarRow), not by a thumb looking for
   something to tap. Collapsing it (position: absolute, out of the flex row)
   is also what gives the title the row's full width, since `.topic-actions`
   then has nothing left in flow to size itself by. A browser without
   `:has()` drops only this rule and keeps every row's button shown, as
   before ([record](../docs/work/2026-09-30-sidebar-long-press.md)). */
@media (pointer: coarse) {
  /* The height stays the row's own 36 px, collapsed only in width: the menu
     below is positioned `top: 100%` of this box (`.topic-actions .menu`
     above), and a height collapsed to the clipped button's own 1 px would
     anchor it mid-row instead of under it. */
  .nav-list
    .topic-row:not(:has(> button[aria-current="page"]))
    .topic-actions {
    height: 36px;
  }
  .nav-list
    .topic-row:not(:has(> button[aria-current="page"]))
    .topic-actions
    > .icon-btn {
    /* `top`/`right` pin it, rather than leaving them to the browser's own
       "static position" guess for an absolutely positioned box whose
       collapsed parent has nothing left in flow to place it by: guessed
       wrong, that guess landed the 1 px box at the sidebar's own right
       edge, where a phone's drawer scrim (`.main`, translated over that
       same edge while open) intercepted its taps
       (docs/work/2026-09-30-sidebar-long-press.md). */
    position: absolute;
    top: 0;
    right: 0;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  /* The row itself is what a held finger now acts on (the long press above),
     so it refuses the text selection and callout iOS would otherwise offer
     for a held run of text, the same as a post under a long press
     (`.post.pressing` above). */
  .nav-list .topic-row {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
}
/* The row is as tall as its tallest child: the actions button keeps its
   40 px width for the thumb but matches the row's own height — 36px on
   touch, or this fixed height would hold a topic/exploration row at 36px
   even where .nav-list button above narrows to 32px on desktop. */
.nav-list .topic-row .topic-actions > .icon-btn {
  height: 36px;
}
@media (min-width: 840px) and (hover: hover) and (pointer: fine) {
  .nav-list .topic-row .topic-actions > .icon-btn {
    height: 32px;
  }
}
.topic-actions {
  position: relative;
  flex: none;
}
.topic-actions > .icon-btn {
  width: 40px;
  height: 40px;
  min-height: 0;
  padding: 0;
}
.topic-actions .menu {
  right: 0;
  top: 100%;
  min-width: 160px;
}
.nav-list .topic-row .topic-actions > .icon-btn {
  color: var(--ink-3);
}
/* The title as a field, in a row or the top bar. */
.title-editor {
  width: 100%;
  min-height: 36px;
  padding: 4px 8px;
  font: inherit;
  font-weight: 600;
}
/* In an Explorations row the field takes the row's width beside its ⋯, at
   16 px on touch so iOS does not zoom into it, and the row's own 14.5 px
   where a mouse drives (docs/design.md, mobile-first). */
.nav-list .topic-row > .title-editor {
  flex: 1;
  min-width: 0;
  font-size: 16px;
}
@media (hover: hover) and (pointer: fine) {
  .nav-list .topic-row > .title-editor {
    font-size: 14.5px;
  }
}
.topbar .title-button {
  max-width: 100%;
  padding: 2px 4px;
  border-radius: 6px;
  text-align: left;
  font: inherit;
  font-weight: 600;
  color: inherit;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topbar .title-button:hover {
  background: var(--surface-2);
}
/* Archived topics, folded away under the open ones. Sentence case and the
   list's own quiet ink-3, not a section's small caps: "Archived" and "Show
   N more" are the list's own tail, not a third heading level
   (docs/design.md#sidebar). */
.archived {
  margin-top: 8px;
}
/* "Other topics · N": the fold under the topic groups, in the list's quiet
   ink-3 like Archived (docs/work/2026-11-04-sidebar-unread-pinned-last-7-days.md). */
.nav-list .other-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 36px;
  margin-top: 4px;
  padding: 6px 10px;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-3);
  text-align: left;
}
.other-toggle svg {
  transition: transform 0.15s;
}
.other-toggle[aria-expanded="true"] svg {
  transform: rotate(90deg);
}
@media (prefers-reduced-motion: reduce) {
  .other-toggle svg {
    transition: none;
  }
}
.archived > summary {
  display: flex;
  align-items: center;
  min-height: 30px;
  padding: 6px 10px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-3);
}
.nav-list button .count {
  min-width: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 11px;
  font-weight: 700;
  text-align: center;
  line-height: 20px;
}
/* A topic with posts this person has not seen. */
.nav-list button.unread .label {
  font-weight: 600;
}
/* A read row (no unread post, not muted): --ink-sidebar-read rather than
   full --ink, so bold is not the only thing telling it apart from an unread
   row — on a dark background regular and bold white read at nearly the same
   weight until the colour also steps down (user screenshot, 2026-09-28:
   "hard to see Unread / read difference in Dark Mode"). --ink-2 was tried
   first (6.4:1 light, 7.2:1 dark) but read as "too gray in both dark and
   light modes" once lived with (user, 2026-09-29) — most sidebar rows sit in
   this state, so the whole list read as washed out. --ink-sidebar-read sits
   a step closer to --ink instead: 9.6:1 light and 9.7:1 dark against the
   sidebar's --surface — still clearly short of bold full --ink (16.9:1
   light, 14.4:1 dark), so unread stays unmistakably the brighter row
   ([record](../docs/work/2026-09-29-sidebar-row-grey.md);
   [record](../docs/work/2026-09-28-dark-unread-contrast.md)). Explorations
   and archived rows share this markup (docs/design.md) and recede the same
   way; :not(.muted) only matters for topics, since it is the one class this
   selector could otherwise also match. */
.nav-list .topic-row > button:not(.unread):not(.muted) .label {
  color: var(--ink-sidebar-read);
}
/* A muted topic: thinner, and never bold however much is waiting in it (the
   class replaces .unread rather than joining it). The cue is a colour token,
   not opacity — a row has to stay readable on a phone in daylight, and the
   1.11:1 fill this sidebar shipped in 2026-09 is what that mistake looks
   like. Since a read row above sits at --ink-sidebar-read, muted stays on
   its own quieter token, --ink-muted, so it still reads quieter than a read
   row: 5.2:1 dark / 4.0:1 light against --surface — under the 4.5:1 text
   minimum on purpose, since this is the deliberately quietest state and
   --ink-3 at 2.5:1 was already ruled out as too faint for a name. The
   mention pill keeps its full contrast: being mentioned is exactly what a
   mute lets through ([record](../docs/work/2026-09-21-muted-topics.md);
   [record](../docs/work/2026-09-28-dark-unread-contrast.md)). */
.nav-list button.muted .label {
  font-weight: 400;
  color: var(--ink-muted);
}
/* Show more under the newest explorations: quieter than a row, sitting
   right under the list like the Archived toggle it reads alongside
   (docs/design.md#sidebar). */
.nav-list button.nav-more {
  min-height: 30px;
}
.nav-list button.nav-more .label {
  font-size: 13px;
  color: var(--ink-3);
}
.nav-empty {
  padding: 6px 10px;
  font-size: 13px;
  color: var(--ink-3);
}
.inline-form {
  padding: 6px 4px 4px;
}
.inline-form input,
.inline-form select {
  min-height: 40px;
  font-size: 16px;
  margin-bottom: 6px;
}
.inline-form .actions {
  margin-top: 4px;
}
.inline-form .btn,
.inline-form .btn-primary {
  min-height: 36px;
  padding: 6px 12px;
  font-size: 13.5px;
}
.sidebar-foot {
  border-top: 1px solid var(--line);
  padding: 8px 10px calc(8px + var(--safe-bottom));
}
.account {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 6px 8px;
  border-radius: 8px;
  text-align: left;
}
.account:hover {
  background: var(--surface-2);
}
.avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 700;
  display: grid;
  place-items: center;
  flex: none;
  text-transform: uppercase;
  /* A picture fills the circle; the initials stay as they were. */
  overflow: hidden;
}
.avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The profile's own preview, twice the size, above the file input. */
.avatar-large {
  width: 64px;
  height: 64px;
  font-size: 24px;
}
/* A think thread's own ask (transcript H1, docs/design.md), matching the
   agent's `.think .turn-assistant .mark` beside it; never used outside one. */
.avatar-small {
  width: 20px;
  height: 20px;
  font-size: 9px;
}
.picture-row {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 8px 0 4px;
}
.picture-row input[type="file"] {
  min-width: 0;
  flex: 1;
  font-size: 14px;
}
.picture-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}
.account .who {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.account svg {
  width: 18px;
  height: 18px;
  stroke: var(--ink-3);
  fill: none;
  stroke-width: 1.75;
  flex: none;
}
/* Mobile only (docs/work/2026-10-01-mobile-ask-pill.md): Ask <name> leaves
   the top rows (see #discuss's `desktop() &&` guard in workspace.tsx) and
   the footer becomes a floating pill beside an avatar-only profile button,
   over the list rather than a bordered static row — Claude's own mobile
   sidebar shape, from a screenshot (Jean, 2026-10-01). Desktop keeps
   today's #discuss row and .sidebar-foot exactly, both above, outside this
   block. */
@media (max-width: 839.98px) {
  .sidebar-body {
    /* Room for the floating footer to clear the last row: its own height
       below (44px row, the avatar's own diameter and the pill's matching
       height + 8px top padding + 8px bottom padding) and the safe-area
       inset it already reserves. */
    padding-bottom: calc(60px + var(--safe-bottom));
  }
  /* The avatar sits at the drawer's left edge and the pill at its right
     edge, any free space falling as the gap between them (Jean, 2026-10-01,
     relayed through the hub: "if there is a gap, it's in the middle
     between the profile button and the pill") — neither control grows to
     fill the row. */
  .sidebar-foot {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    border-top: none;
    /* Opaque, like the sidebar it sits on: without a background of its own,
       list rows under `.sidebar-body` (now filling the flex space this
       absolute footer vacated) paint through the footer's own transparent
       padding strips as the list scrolls past, since an absolutely
       positioned box only wins hit-testing, not paint order. */
    background: var(--surface);
    padding: 8px 10px calc(8px + var(--safe-bottom));
  }
  /* The soft fade a floating control casts over what scrolls under it,
     rather than a hard edge: a gradient the height of the footer's own
     clearance above. */
  .sidebar-foot::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 24px;
    background: linear-gradient(to bottom, transparent, var(--surface));
    pointer-events: none;
  }
  /* Avatar only: the name and the settings glyph stay in the DOM (so
     #identity keeps reading the saved username, as every sign-up test
     checks) and in the accessible name, but are not drawn — only the
     circle is, as in the reference screenshot. */
  .sidebar-foot .account {
    width: 44px;
    height: 44px;
    flex: none;
    justify-content: center;
    padding: 0;
    gap: 0;
  }
  /* The circle itself fills the 44 px tap target (Jean's Claude
     screenshot, ~44-48 px), not just the button around it: the base
     .avatar rule is 28 px everywhere else, sized for sitting beside a
     name, which this row no longer shows. */
  .sidebar-foot .account .avatar {
    width: 44px;
    height: 44px;
    font-size: 17px;
  }
  .sidebar-foot .account .who,
  .sidebar-foot .account svg {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  /* The pill: the page's own Ask pill (`.dock-pill`, src/client/bottom-sheet.tsx)
     with the home field's mic (docs/work/2026-10-19-sidebar-ask-mic.md),
     sized to its own content and right-aligned, not stretched to fill the
     footer (Jean, relayed: "aligned to the right side and not be full
     width"), a full 44 px tap high like the avatar beside it. Its
     sidebar-only rules: no entrance fade (it is part of the drawer), and
     it may shrink to the room beside the avatar, so a long custom agent
     name ellipsizes rather than overflow the drawer. */
  .sidebar-foot .dock-pill {
    flex: 0 1 auto;
    min-width: 0;
    margin-left: auto;
    animation: none;
  }
  .sidebar-foot .dock-pill-jump {
    min-width: 0;
    padding-top: 6px;
    padding-bottom: 6px;
    line-height: 1.2;
  }
  /* The agent's name is never cut off (docs/work/2026-09-28-agent-name.md):
     a name too long for the room beside the avatar wraps between words, and
     the pill grows a line. Never inside a word: breaking anywhere gave
     "Ask / Workroo / m" (docs/work/2026-10-22-ask-button-recording.md). While
     the mic is busy the label is folded away, so it never wraps then. */
  .sidebar-foot .dock-pill-label {
    white-space: normal;
    overflow-wrap: break-word;
    text-overflow: clip;
  }
  /* Held, the recording takes the footer's whole row, as it takes a
     composer's row: the avatar steps aside, and the wave fills what the
     hint and the elapsed time leave. */
  .sidebar-foot:has(.dictation.holding) .account {
    display: none;
  }
  /* Busy beside the avatar (recording, writing it down, kept, a note), the
     room is short: the Ask half steps aside as it does for a held recording,
     and the mic's first button takes the pill's left end, so the avatar stays
     and the words fit (docs/work/2026-10-22-ask-button-recording.md). */
  .sidebar-foot .dock-pill:has(.dictation:not([data-state="idle"])) .dock-pill-jump {
    display: none;
  }
  .sidebar-foot .dock-pill:has(.dictation:not([data-state="idle"])) .dock-pill-mic {
    border-left: 0;
  }
  .sidebar-foot .dock-pill .dock-pill-mic .dictation:not([data-state="idle"]) > .icon-btn:first-child {
    border-radius: 999px 0 0 999px;
  }
  /* A full 44 px tap each, and the words keep a small margin. */
  .sidebar-foot .dock-pill .dock-pill-mic .dictation:not([data-state="idle"]) > .icon-btn.icon-btn {
    width: 44px;
  }
  .sidebar-foot .dock-pill .dock-pill-mic .dictation:not([data-state="idle"]) > :is(.dictation-status, .visually-hidden).dictation-status,
  .sidebar-foot .dock-pill .dock-pill-mic .dictation:not([data-state="idle"]) > .visually-hidden.visually-hidden {
    padding: 0 4px;
  }
  .sidebar-foot .dock-pill:has(.dictation.holding) {
    flex: 1 1 auto;
  }
  .sidebar-foot .dock-pill:has(.dictation.holding) .dock-pill-mic,
  .sidebar-foot .dock-pill-mic .dictation.holding {
    flex: 1 1 auto;
    min-width: 0;
  }
  .sidebar-foot .dock-pill-mic .dictation-wave {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
  }
}

/* ---------- main column ---------- */
.main {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  height: var(--app-height, 100dvh);
  background: var(--paper);
  transform: translateX(0);
  transition: transform 0.18s ease-out;
}
/* Whatever overflows the foreground's height is cut there, never painted
   over the drawer parked beneath it (vertically only; the edge shadow is the
   box's own). Phone only: on desktop `.main` is a static grid cell. */
@media (max-width: 839.98px) {
  .main {
    overflow-y: clip;
  }
}
.app:not(.collapsed) .main {
  transform: translateX(var(--drawer));
  box-shadow: -16px 0 40px rgba(0, 0, 0, 0.16);
}
/* The sidebar's own raised surface, not the conversation's --paper beneath
   it (user rule, 2026-10-01, on every screen): read as one block with the
   sidebar and the pinned to-do card, which already share --surface. The
   hairline below it stays on every screen, including On my to-do and
   Waiting on others, except the one below (an exploration's own pinned
   list). */
.topbar {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
  min-height: var(--bar);
  padding: var(--safe-top) 8px 0;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
/* An exploration whose latest turn pins a list (a think thread included)
   sits the bar directly on that card, which is --surface too: one
   continuous block, no seam, the same rule the card itself already
   follows against the sidebar (docs/design.md). `:has()` reads forward to
   .content's first child, so this needs no class on the bar itself.
   On my to-do and Waiting on others dropped this hairline too when the
   rule first shipped (#1244), then Jean asked for it back on exactly
   those two screens (GitHub, 2026-10-01: "there should be a subtle line
   separator between top bar and todo list, same for waiting for others"),
   so the selector below only reaches `.content.exploration`, the class
   that is unique to the exploration view
   (docs/work/2026-10-01-list-screens-no-flash.md); `.list-screen`, the
   shared shell behind both named screens, keeps its ordinary hairline by
   simply not matching. */
/* A pull request's preview, so nobody mistakes it for production: both top
   bars in its colour (src/client.tsx sets the attribute and the browser's
   own bar from /api/config; docs/work/2026-10-07-pr-previews.md). */
:root[data-preview] .topbar,
:root[data-preview] .sidebar-head {
  background: var(--preview-bar);
}
/* The drawer pads the notch itself; that band is part of its top bar. */
:root[data-preview] .sidebar {
  background:
    linear-gradient(var(--preview-bar), var(--preview-bar)) top / 100%
      var(--safe-top) no-repeat,
    var(--surface);
}
:root[data-preview] .menu-btn .count {
  box-shadow: 0 0 0 2px var(--preview-bar);
}
/* The menu button says what waits in the list it hides: heavier bars and
   full ink for unread, the mention pill at its corner, the same two cues a
   row uses ([record](../docs/work/2026-09-23-menu-signal-keyboard-explorations.md)). */
.menu-btn {
  position: relative;
}
.menu-btn.unread {
  color: var(--ink);
}
.menu-btn.unread svg {
  stroke-width: 2.75;
}
.menu-btn .count {
  position: absolute;
  top: 2px;
  right: 0;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  /* Knocks the pill out from the bar behind it, now --surface too
     (independent review, 2026-10-01: this is the one other --paper
     reference .menu-btn's own bar background left behind). */
  box-shadow: 0 0 0 2px var(--surface);
  pointer-events: none;
}
/* Mobile only (Jean, 2026-10-01, narrowing the same day's first request):
   the menu button on the left and the topic's ⋯ menu (or a close button) on
   the right each reserve the same width, whether or not they hold anything,
   so .title's own flex: 1 below always has the same free space on both
   sides and reads as centred on the bar rather than in whatever space is
   left over (docs/work/2026-10-01-topbar-trim.md). 40px matches .icon-btn
   and .topic-actions > .icon-btn, the only things either side ever holds.
   The 840px override below undoes the fixed width, back to each side
   sizing to its own content (0 when empty) exactly as before this change,
   since the title stays left-aligned on desktop. */
.topbar-side {
  flex: 0 0 40px;
  min-width: 40px;
  display: flex;
  align-items: center;
}
.topbar-side-start {
  justify-content: flex-start;
}
.topbar-side-end {
  justify-content: flex-end;
}
/* The back button (docs/work/2026-10-08-app-back-button.md): a chevron and
   the title of the screen it returns to, beside the menu, truncated. While
   it shows, the start side sizes to its content and the title follows it,
   left-aligned on a phone too: centred between a wide start and the 40px
   end it would be squeezed to a few letters. */
.back-btn {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  max-width: 128px;
  height: 40px;
  padding: 0 8px 0 2px;
  color: var(--ink-2);
  font-size: 14px;
  font-weight: 500;
  overflow-wrap: normal;
}
.back-btn .glyph {
  flex: none;
}
.back-btn .label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.back-btn:hover {
  background: var(--surface-2);
  color: var(--ink);
}
.topbar.has-back .topbar-side-start {
  flex: 0 1 auto;
  max-width: 168px;
}
.topbar.has-back .title {
  text-align: left;
  padding-left: 2px;
}
.topbar .title {
  flex: 1;
  min-width: 0;
  padding: 0 6px;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topbar .subtitle {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
}
.topbar .subtitle button {
  color: var(--ink);
  text-decoration: underline;
  font-size: inherit;
}
.content {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  position: relative;
}
/* A picture dragged over a topic or an exploration screen (src/client/topic.tsx,
   src/client/exploration.tsx): a subtle inset outline and one sentence, gone
   the moment the drag leaves or drops. Both are `pointer-events: none`, so
   they never take the drag events they are drawn in response to — those keep
   reaching the stream and composer beneath, which is what the depth counter
   in `useFileDropTarget` (src/client/attachments.tsx) counts. Both sit below
   every popup that can be open over the composer while a drag passes over it
   — the mention list (z-index 5) and the model/thinking sheet (3) — so a
   drag started while one is open dims the screen behind it rather than the
   popup itself. */
.content.drop-target::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--ink-3);
  background: var(--surface-2);
  opacity: 0.5;
}
.drop-target-hint {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  margin: 0;
  padding: 8px 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font-size: 14px;
  font-weight: 500;
  pointer-events: none;
}
.column {
  width: 100%;
  max-width: var(--column);
  margin: 0 auto;
  padding: 0 max(16px, env(safe-area-inset-right)) 0
    max(16px, env(safe-area-inset-left));
}
.status-line {
  font-size: 13px;
  color: var(--ink-3);
  padding: 8px 0 0;
}
.alert {
  margin: 10px 0;
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--ink);
  font-size: 14px;
}

/* ---------- empty state ---------- */
.empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 32px 20px;
  color: var(--ink-2);
}
/* The one-field screens — home and new topic — in the middle (src/client/workspace.tsx). */
.new-topic {
  width: min(100%, 520px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
}
/* AsyncForm (src/client/forms.tsx) wraps its children in a bare <fieldset>,
   whose own block-level box swallowed this 10px gap entirely: the field, the
   pickers row and the hint all touched, which is what put the round Send
   button flush against the textarea's bottom border on a phone (user,
   2026-09-24, from a screenshot; docs/work/2026-09-24-home-field-phone.md).
   `display: contents` removes the fieldset's own box so its children lay out
   as this flex column's direct items, as if it were not there; the fieldset
   still disables the fields inside while a submit is pending, since that is
   HTML's doing and not this box's. */
.new-topic > fieldset {
  display: contents;
}
/* The home field, the message editor (docs/work/2026-10-08-editor-bullets.md):
   a bordered box at 17px with a focus ring, its words left-aligned in the
   centred screen. */
.home-editor {
  text-align: left;
}
.home-editor .cm-editor {
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font-size: 17px;
}
.home-editor .cm-editor.cm-focused {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px var(--surface-2);
}
.home-editor .cm-scroller {
  min-height: 94px;
  max-height: 40dvh;
  overflow: auto;
  font-family: inherit;
  line-height: 1.45;
}
.home-editor .cm-content {
  padding: 10px 12px;
  caret-color: var(--ink);
}
.home-editor .cm-line {
  padding: 0;
}
.new-topic .hint {
  margin: 0;
}
.empty h2 {
  color: var(--ink);
  font-size: 20px;
}
.empty p {
  max-width: 380px;
  margin: 8px 0 18px;
}
.stream {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  .stream {
    scroll-behavior: auto;
  }
  * {
    transition: none !important;
    animation: none !important;
  }
  /* Not motion, a delay: the opening card's reload line must still wait its
     twelve seconds rather than show on every open. */
  .boot-slow {
    animation: boot-slow 0s linear 12s both !important;
  }
  /* The dots' own animations are already cancelled by the `*` rule above;
     unlike the earlier `clip-path` design, this override is load-bearing: a
     cancelled animation reverts to its cascaded, non-animated value, which
     for `.boot-dot` is `opacity: 0` (the base rule above) — without this,
     every dot would simply vanish instead of showing the static, fully-drawn
     "…" this block promises. */
  .boot-dot {
    opacity: 1 !important;
  }
}
.stream .column {
  padding-top: 16px;
  padding-bottom: 16px;
}
.turn {
  margin: 0 0 22px;
  min-width: 0;
}
.turn-user {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}
.bubble {
  max-width: min(85%, 560px);
  padding: 10px 14px;
  border-radius: 18px;
  border-bottom-right-radius: 6px;
  background: var(--user-bubble);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* A person's own message is Markdown (src/client/written-text.tsx): a
   heading first sits at the top and a fold last at the bottom, as in a post
   (`.post .text`). */
.bubble > :first-child,
.ask > .turn-avatar + * {
  margin-top: 0;
}
.bubble > :last-child,
.ask > :last-child {
  margin-bottom: 0;
}
/* A held voice note's first message before its words arrive
   (src/client/voice-pending.tsx): the person's own bubble, holding the busy
   line in the shared shimmer, the offline line, or what went wrong. */
.voice-pending-bubble.problem {
  color: var(--ink-2);
}
.turn-user .meta {
  margin-top: 4px;
  padding-right: 4px;
}
/* A post or message on its way (send-outbox.ts, send-status.tsx;
   docs/work/2026-10-05-instant-send.md): drawn as its saved copy, with
   "Sending…" in the shared shimmer where its time will be, or "Couldn't
   send." in the run notes' error colour with Retry and Discard. Grey and the
   error red only: blue is think mode's. */
.send-failed {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 4px;
  font-size: 13px;
  color: var(--danger);
}
.send-failed button {
  min-height: 40px;
  padding: 0 6px;
  color: var(--ink);
  font-weight: 500;
}
.post .send-line {
  margin: 2px 0 0;
}
.meta {
  font-size: 12px;
  color: var(--ink-3);
}
.turn-assistant {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 10px;
}
/* A drafting run's turn (transcript.tsx): nothing to show once its note is
   empty, so it takes no room in the thread. While it is queued or needs the
   person, its note is there and so is the turn. */
.turn-assistant.draft-run:has(> .turn-body:empty) {
  display: none;
}
.turn-assistant .mark {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: var(--ink);
  color: var(--paper);
  font-size: 11px;
  font-weight: 700;
  display: grid;
  place-items: center;
  margin-top: 2px;
}
/* A turn that opens on one step line (waiting, live or folded step): the
   avatar's centre sits on that line's centre, not 7px above it. The line is
   a 36px row under the steps' 4px top margin, so its centre is 22px down;
   the 26px avatar needs a 9px top margin. docs/work/2026-10-02-waiting-line-fix.md
   A run claimed with no step yet opens on its "Working" row, which comes
   after the turn's hidden live region (an empty span, nothing on screen),
   so that order counts as the first line too.
   docs/work/2026-10-03-flaky-queued-line.md */
.turn-assistant:has(
    > .turn-body
      > :is(.steps:first-child, .visually-hidden:first-child + .steps)
      > .step:first-child
      > :is(.step-row, summary):first-child
  ) > .mark {
  margin-top: 9px;
}
.turn-assistant .mark.busy {
  animation: pulse 1.4s ease-in-out infinite;
}
/* Stop tapped, the worker not there yet (docs/work/2026-10-22-instant-run-stop.md):
   the turn stills on the tap. The mark stops pulsing and rests dim, and the
   live line's words stop shimmering, while the note says Stopping…; text
   already on its way still arrives. */
.turn-assistant[data-stopping] .mark.busy {
  animation: none;
  opacity: 0.45;
}
.turn-assistant[data-stopping] .step-live .glyph.spin {
  animation: none;
  opacity: 0.45;
}
.turn-assistant[data-stopping] .step-live .text-shimmer {
  animation: none;
  background-image: none;
  color: var(--shimmer-base, var(--ink-3));
  -webkit-text-fill-color: var(--shimmer-base, var(--ink-3));
}
/*
 * H1 (docs/design.md, docs/work/2026-09-30-transcript-h1.md): a topic's
 * inset think thread only, never the exploration screen — Jean, 2026-09-30,
 * after reviewing both in the hub: "we want the H1 layout only for the
 * think threads inside conversations, but we want to keep explorations as
 * they are now." `.think` is the thread's own wrapper
 * (`section.think` in src/client/thread.tsx) and Transcript is never
 * rendered outside one or the other, so scoping under it is exact; `.ask`,
 * `.turn-avatar`, `.avatar-small` and `.step-card` below are new classes
 * the classic markup above never emits, so they need no scoping of their
 * own.
 */
.think .turn-user {
  display: block;
}
.ask {
  font-weight: 400;
  color: var(--think);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/*
 * The person's own avatar (an ask) and a step-note's mark sit inside a line
 * of otherwise-inline content, so `inline-block` + `vertical-align: middle`
 * centres them on that line correctly (a float only aligns to the TOP of
 * the line box, which read as pushing the row's text down — Jean,
 * 2026-09-30, from the first screenshots). A step's own row (`summary`,
 * `.step-row`) and a leading card's flex wrapper (`.step-card`, below)
 * instead centre the mark through their own `align-items`, since a flex
 * item ignores both `float` and `vertical-align` — this rule still applies
 * there, its `display`/`vertical-align` are just moot.
 */
.turn-avatar {
  /* inline-grid, not inline-block: it keeps the avatar's own centring
     (`place-items`), so initials sit in the middle of the 20px circle
     instead of at its top-left (docs/work/2026-10-06-think-about-post.md). */
  display: inline-grid;
  line-height: 1;
  vertical-align: middle;
  flex: none;
  margin: 0 8px 0 0;
}
/* Inside a step's own flex row (`summary`, `.step-row`) or a leading
   card's flex wrapper (`.step-card`, below) the row's own `gap` already
   spaces the mark from what follows, so its own margin would double it. */
.step summary > .turn-avatar,
.step-row > .turn-avatar,
.step-card > .turn-avatar {
  margin: 0;
}
/* Since the ask is Markdown (src/client/written-text.tsx,
   docs/work/2026-10-14-markdown-rendering-gaps.md) its words start in a
   block, which an inline-block mark cannot share a line with, so the mark
   floats beside the first line as an answer's does (below), and the ask
   holds the float. 0.5px centres it on that line's text, as
   tests/browser-think-transcript.test.ts measures (1px was 0.5px low). */
.ask {
  display: flow-root;
}
.ask > .turn-avatar {
  float: left;
  margin-top: 0.5px;
}
/* A list, quote, callout, code or table first: its own box beside the mark,
   so a list's numbers and a callout's bar never run under it. A paragraph or
   heading first wraps under the mark, as before. */
.ask > .turn-avatar + :not(p, h1, h2, h3) {
  display: flow-root;
}
.ask > .turn-avatar + details {
  overflow: hidden;
}
/*
 * An answer's own leading mark (no step rows before it) sits directly
 * before Markdown output, which is block-level even for a single paragraph
 * of text (`.prose`'s `<p>`): an inline-block sibling cannot merge onto a
 * following block's first line — a block always starts its own — so the
 * mark rendered alone on a row above the text (Jean, 2026-09-30, second
 * round of screenshots: "the W avatar sits alone on its own line above the
 * answer"). A float is the one mechanism that lets a following block's
 * first line wrap around it instead (the classic drop-cap trick), so this
 * one case floats rather than using the inline-block default above;
 * `flow-root` on `.answer` keeps the float's height inside it instead of
 * bleeding into the next sibling. A float has no `vertical-align`, so the
 * small top margin is what centres the 20px mark on the 22.5px (15px font,
 * 1.5 line-height) line box the text sits in.
 */
.answer {
  display: flow-root;
}
.answer > .turn-avatar {
  float: left;
}
/* Specificity note: `.think .turn-assistant .mark` below also sets
   `margin-top` (to size the 20px mark, resetting the classic layout's own
   `margin-top: 2px`); its three classes outrank this rule's two, so the
   float's own vertical offset needs the same three-class weight plus one
   to win regardless of source order. */
.think .turn-assistant .answer > .turn-avatar {
  margin-top: 1px;
}
.think .file-chips {
  justify-content: flex-start;
  max-width: 100%;
}
.think .turn-assistant {
  display: block;
}
.think .turn-assistant .mark {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  font-size: 9px;
  margin-top: 0;
}
/* A card that is an answer's leading part (see `StepRow`'s `wrap` in
   transcript.tsx): held beside the mark on a flex row, top-aligned since
   the card can run taller than one line. Only ever rendered inside a think
   thread — a non-leading, or an exploration's, card returns unwrapped. */
.step-card {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  min-width: 0;
}
/* A dictation's words come in one after another, as if streamed
   (docs/work/2026-10-05-voice-cleanup.md); each word's start is set inline
   by the editor (`wordReveal`, editor-view.ts). Reduced motion turns the
   animation off, and the words simply show. */
.cm-word-in {
  animation: word-in 160ms var(--motion-ease) backwards;
}
@keyframes word-in {
  from {
    opacity: 0;
  }
}
@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}
.turn-body {
  min-width: 0;
}
.steps {
  margin: 4px 0 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.step {
  min-width: 0;
}
.step summary,
.step .step-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 4px 6px;
  margin-left: -6px;
  border-radius: 8px;
  list-style: none;
  cursor: pointer;
  font-size: 13.5px;
  color: var(--ink-2);
}
.step summary::-webkit-details-marker {
  display: none;
}
/* A call the assistant made that came back refused. It did nothing, so it has
   no check mark (until 2026-09-19 it drew one, and a person believed a post
   had been drafted when none had), but it is not an error either: the agent
   was told why and carries on. So the row keeps the step colour, `--ink-2`,
   and says "Not available" (the user, 2026-10-14). Red is for a run that
   failed. */
.step summary:hover {
  background: var(--surface-2);
  color: var(--ink);
}
.step .glyph {
  width: 16px;
  height: 16px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Any spinner glyph spins, in whatever component mounts it — a step row, an
   icon button, a memory line, or the rewrite pill's own "Rewriting…"
   (docs/work/2026-10-01-rewrite-shimmer.md): one shared rule, replacing the
   `.icon-btn`/`.step`-scoped pair that quietly missed the rewrite pill,
   which carries neither class. `.memory-line .glyph.spin` below still wins
   there with a faster spin, by its own higher selector specificity. */
.glyph.spin {
  animation: spin 1s linear infinite;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
.step .name {
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A tool's label is a few words ("Read topic"); cut, it names nothing, so the
   detail beside it takes the ellipsis instead. Most of the row still bounds
   an unknown tool's long technical name, or a preset's full sentence
   ("Checked what's waiting on you", read_memory's waiting_on_me — longer
   than the seventeen characters every label held to before it, docs/work/2026-09-30-readable-tool-steps.md). */
.step-tool .name {
  flex-shrink: 0;
  max-width: 70%;
}
.step .detail {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-3);
}
/* A run of step rows as one line (docs/work/2026-10-02-steps-in-place.md):
   while the run works, the current step replaced in place with the timer at
   its end; once done, "6 steps · 14s". One line, always the same height, so
   nothing under it moves when a step changes: a long label ends in an
   ellipsis instead of wrapping. A button, since a tap unfolds the rows. */
.step-fold > .step-row {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  white-space: nowrap;
}
/* Hover only where a pointer hovers: on a phone a tap would leave the line
   lit after it unfolds. */
@media (hover: hover) {
  .step-fold > .step-row:hover {
    background: var(--surface-2);
    color: var(--ink);
  }
}
.step-fold .name {
  min-width: 0;
}
/* The live step reads at a glance (Jean, 2026-10-02): the words sit at a
   step row's own `--ink-2`, not the shared shimmer's dimmer `--ink-3`
   (about 2.6:1 on `--paper` in light), and the band lifts them to `--ink`. */
.step-live {
  --shimmer-base: var(--ink-2);
}
/* A run that failed: a warning on the folded line, in `--danger`. A refused
   call along the way adds nothing here. */
.step-fold .glyph.failed {
  color: var(--danger);
}
.step-fold[data-failed] > .step-row {
  color: var(--danger);
}
.step-fold .elapsed {
  flex: none;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.step-fold .elapsed::before {
  content: "· ";
}
.step-fold .glyph.chevron {
  color: var(--ink-3);
  transition: transform var(--motion-fast) var(--motion-ease);
}
.step-fold .glyph.chevron.open {
  transform: rotate(90deg);
}
/* The running turn's note at the end of its live line: "Read database ·
   9s ›  Answer now" (docs/work/2026-10-20-ask-sheet-run-line.md), the note
   at the line's end so it does not move as the status changes. The status
   gives way with an ellipsis before the note moves; the note keeps a
   44 px target that overhangs the 36 px line rather than making it taller.
   The confirm, longer, takes a line of its own under the status. */
.step-live:has(> .run-note) {
  display: flex;
  align-items: center;
  min-width: 0;
}
.step-live:has(> .run-note) > .step-row {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
}
.step-live:has(> .run-note) .name {
  min-width: 0;
}
.step-live > .run-note {
  flex: none;
  margin: 0 0 0 8px;
}
.step-live > .run-note.answer-now button {
  margin: -4px 0;
}
.step-live:has(#answer-now-confirm) {
  flex-wrap: wrap;
}
.step-live > .run-note:has(#answer-now-confirm) {
  flex: 1 1 100%;
  margin-left: 0;
}
.steps-unfolded {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.step .body {
  margin: 2px 0 8px 24px;
  padding-left: 12px;
  border-left: 2px solid var(--line);
  font-size: 13.5px;
  color: var(--ink-2);
}
.step .body pre {
  margin: 4px 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--ink);
}
.step .body .thought {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* Prose: any Markdown a person reads (an answer, a topic post). */
.prose {
  overflow-wrap: anywhere;
}
/* Between two rendered Markdown blocks, not after the answer's own leading
   mark: that mark is a floated DOM sibling of the first block (H1, above),
   which the plain `* + *` form still counts, pushing the first paragraph
   down past the mark instead of letting it sit on the same row. */
.prose > *:not(.turn-avatar) + * {
  margin-top: 0.7em;
}
.prose p {
  white-space: pre-wrap;
}
.prose h1,
.prose h2,
.prose h3 {
  margin-top: 1.1em;
  font-size: 15px;
}
/* Headings and the text under them: a heading that follows a block opens 1.2em
   above (about 18-20px, near twice the 0.7em block gap), and the block right
   under it sits 0.3em below (about 5px), so it reads as that heading's text.
   Written to outrank the block-gap rule above. A body's first heading has no
   top gap: a post's `.post .text > :first-child`, a page's rule below. */
.prose > *:not(.turn-avatar) + :is(h1, h2, h3) {
  margin-top: 1.2em;
}
.prose > :is(h1, h2, h3) + :not(.turn-avatar) {
  margin-top: 0.3em;
}
.prose h1 {
  font-size: 18px;
}
.prose h2 {
  font-size: 16px;
}
.prose ul,
.prose ol {
  margin: 0.5em 0;
  padding-left: 1.4em;
}
.prose li + li {
  margin-top: 0.25em;
}
/* A nested list (a `ul`/`ol` inside an `<li>`): the descendant selector above
   already gives it the same 1.4em step every level indents by, so only the
   vertical gap needs its own rule — the block margin above reads as a hole
   between an item's text and its own sub-list on a phone, so a nested list
   opens flush and keeps line height instead. */
.prose li > ul,
.prose li > ol {
  margin: 0.25em 0 0;
}
.prose code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--surface-2);
}
/* A fenced code block is a widget: a 36 px header with the language and a copy
   button top right (the dense-control exception, like step rows), then the code. */
.code-block {
  margin: 0.6em 0;
  border-radius: var(--radius);
  background: var(--surface-2);
  min-width: 0;
}
.code-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 0 4px 0 12px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
  color: var(--ink-3);
}
.code-lang {
  font-family: var(--font-mono);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.code-copy {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: auto;
  min-height: 36px;
  padding: 0 8px;
  flex: none;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ink-2);
}
.code-copy:hover:enabled {
  background: var(--surface);
  color: var(--ink);
}
.code-copy .glyph {
  width: 15px;
  height: 15px;
}
.code-block pre {
  margin: 0;
  padding: 10px 12px;
  border-radius: 0 0 var(--radius) var(--radius);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.45;
  white-space: pre;
}
.code-block pre code {
  padding: 0;
  background: none;
  font-size: inherit;
}
/* A GFM table in any Markdown. The wrapper scrolls sideways, as a code block
   does, so a wide table never widens the page on a phone. The table is as
   wide as its content and each cell wraps, between words, only past 28ch
   (on a phone, fit to the screen with a floor per cell, below):
   left to fit the column, a six-column table squeezed its first column to
   66 px at 390 px, a word per line, where it now keeps 261 px and scrolls
   (measured in Chromium). Cells drop the prose's `overflow-wrap: anywhere`,
   which would let a column shrink to one character. Hairlines between rows,
   a heavier one under the header, no fill: tokens only, so dark mode needs
   nothing of its own. Not under `.prose`, as `.code-block` is not: a brief,
   a file page, a to-do's details and memory's evidence render Markdown
   outside it (docs/work/2026-10-01-markdown-tables.md). */
.markdown-table {
  max-width: 100%;
  overflow-x: auto;
  margin: 0.6em 0;
}
.markdown-table table {
  width: max-content;
  border-collapse: collapse;
  font-size: 14px;
  line-height: 1.45;
}
.markdown-table th,
.markdown-table td {
  max-width: 28ch;
  padding: 6px 12px 6px 0;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
  overflow-wrap: normal;
}
.markdown-table th:last-child,
.markdown-table td:last-child {
  padding-right: 0;
}
.markdown-table th {
  font-weight: 600;
  border-bottom-color: var(--line-2);
}
.markdown-table tbody tr:last-child td {
  border-bottom: 0;
}
.markdown-table .align-center {
  text-align: center;
}
.markdown-table .align-right {
  text-align: right;
}
/* The first column stays put while the rest scrolls under it, so each row
   keeps its name on a phone (docs/work/2026-10-21-long-posts-fold.md). It
   needs the surface's own fill to cover what passes beneath: `--table-bg`,
   the page's paper unless the card around the table is tinted. Its edge
   shows only while the table is scrolled (below). */
.markdown-table {
  --table-bg: var(--paper);
}
.post.post-recap .text .markdown-table {
  --table-bg: var(--surface-3);
}
.post.post-proposal .text .markdown-table,
.prose .markdown-callout .markdown-table {
  --table-bg: var(--surface-2);
}
.post.lifted .markdown-table {
  --table-bg: var(--surface);
}
.bubble .markdown-table {
  --table-bg: var(--user-bubble);
}
.markdown-table th:first-child,
.markdown-table td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--table-bg);
}
/* While scrolled, a hairline and a soft shadow on the held column's right,
   so the columns passing under it read as passing, not cut. `data-scrolled`
   is set by the table's own scroll handler (`TableBlock`): static, so the
   reduced-motion rule, which turns every animation off, keeps it too. */
.markdown-table[data-scrolled] th:first-child,
.markdown-table[data-scrolled] td:first-child {
  box-shadow:
    inset -1px 0 var(--line-2),
    6px 0 8px -6px rgb(0 0 0 / 22%);
}
/* On a phone a table fits the screen when it can, and each cell is squeezed
   no further than two lines of its own text (`--cell-min`, set per cell by
   `phoneCellMin` in src/client/markdown-table.ts); past that it scrolls, and
   columns sit 8 px apart. Fit first: the table is as wide as the screen
   allows, the browser sharing the width by content between each cell's floor
   and the 28ch maximum. A per-cell cap, the first version, wrapped every long
   cell even in a two-column table that fit on one line (user, 2026-10-01:
   "did you just overfit for 2 rows?"); one cap for every cell before it left
   a two-line row's other cells on one line. On the user's six-column
   comparison at 390 px: 850 px wide (from 1,087 at 28ch), every row at most
   2 lines (measured in Chromium, 2026-10-01; iOS honours cell widths, as
   their screenshot wrapped at 28ch). Two lines is a promise for ordinary
   text: `ch` is the width of "0", so text mostly in capitals, or a word past
   28ch, can take more. Same breakpoint as the list view below. */
@media (max-width: 559px) {
  .markdown-table table {
    width: auto;
  }
  /* `box-sizing: border-box` puts the gap inside the cell's width, so the
     floor adds it back: without that, the gap ate the slack and a
     middle cell of wide letters took a third line (review of #1305). */
  .markdown-table th,
  .markdown-table td {
    min-width: calc(var(--cell-min, 0px) + 8px);
    padding-right: 8px;
  }
  .markdown-table th:last-child,
  .markdown-table td:last-child {
    min-width: var(--cell-min, 0px);
  }
  /* A sticky first column at most two fifths of the screen, so the columns
     scrolling beside it keep room to be read; its own text wraps more. A
     one-column table has nothing beside it and keeps its width. */
  .markdown-table th:first-child:not(:last-child),
  .markdown-table td:first-child:not(:last-child) {
    min-width: min(calc(var(--cell-min, 0px) + 8px), 40vw);
    max-width: 40vw;
  }
}
/* ---------- the list view ----------
   The page format's one tabular primitive, written as a ```list fence: one
   item per line, its title left and its properties right on the same line,
   the way a Notion list view reads. No headers, no gridlines, a hairline
   between items, and every cell cropped to one line — the properties crop
   first, since the title is what the item is
   (docs/work/2026-09-23-markdown-list-view.md). Tokens only, so dark mode
   needs nothing of its own. */
.list-view {
  display: flex;
  flex-direction: column;
  margin: 0.6em 0 0;
}
.list-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 7px 0;
  min-width: 0;
}
.list-row + .list-row {
  border-top: 1px solid var(--line);
}
/* Both cell kinds crop rather than wrap: one item is one line. The full text
   is in each cell's `title`, which the renderer always sets. */
.list-title,
.list-prop {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.list-title {
  flex: 1 1 45%;
}
/* The properties, as one group so they can be placed and capped together. */
.list-props {
  flex: 0 1 auto;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  min-width: 0;
  max-width: 55%;
  color: var(--ink-3);
  font-size: 0.85em;
  text-align: right;
}
/* Each cell is capped on its own as well as in the group, because flexbox
   shrinks siblings in proportion to their width: without this a long status
   drags `Francescu` and `Mon` down with it and both crop. `20ch` is twenty
   zero-widths, so roughly twenty-five lowercase characters: comfortably past
   any value the format asks for (an owner, a date, a status) and short of a
   sentence, so the long cell is the one that gives. */
.list-prop {
  flex: 0 1 auto;
  max-width: 20ch;
}
/* Mobile first, and this is where the primitive is decided. A title and three
   properties need about 350 px of text and a 390 px row has 358, so on a phone
   one line only works while nothing is ordinary: flexbox shrinks siblings in
   proportion to their width, so a long status drags its neighbours down with
   it and `Francescu | Mon` came out as `F… | M`. Measured, not guessed — the
   screenshot is in the record. So below 560 px the properties take the line
   under the title, still on the right, still one line, still cropped when they
   are too long: every value stays readable and the title is never squeezed by
   them (docs/work/2026-09-23-markdown-list-view.md, decision 3). From 560 px
   the row is the single line the primitive describes. */
@media (max-width: 559px) {
  .list-row {
    flex-wrap: wrap;
    row-gap: 2px;
  }
  .list-title {
    flex: 1 0 100%;
  }
  .list-props {
    flex: 1 1 100%;
    max-width: 100%;
  }
}
.prose blockquote {
  margin: 0.6em 0;
  padding-left: 12px;
  border-left: 2px solid var(--line-2);
  color: var(--ink-2);
}
.prose .markdown-callout {
  box-sizing: border-box;
  min-width: 0;
  max-width: 100%;
  margin: 0.7em 0;
  padding: 0.7em 0.85em;
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--ink);
  overflow-wrap: anywhere;
}
.prose .markdown-callout.callout-warning,
.prose .markdown-callout.callout-important {
  border-left-color: var(--danger);
}
.prose .markdown-callout-title,
.prose .markdown-callout > summary {
  font-weight: 700;
  line-height: 1.4;
}
.prose .markdown-callout > summary {
  cursor: pointer;
  overflow-wrap: anywhere;
}
.prose .markdown-callout > summary:focus-visible {
  outline: 2px solid var(--ink-2);
  outline-offset: 3px;
  border-radius: 2px;
}
.prose .markdown-callout-body {
  min-width: 0;
  margin-top: 0.5em;
}
.prose .markdown-callout-body > :first-child {
  margin-top: 0;
}
.prose .markdown-callout-body > :last-child {
  margin-bottom: 0;
}
/* A footnote marker: a small numbered link to its definition, a source row. */
.prose sup.footnote-ref {
  font-size: 0.72em;
  line-height: 0;
  vertical-align: super;
  margin-left: 1px;
}
.prose sup.footnote-ref a {
  text-decoration: none;
  color: var(--ink);
  font-weight: 600;
}
.prose ol.markdown-footnotes {
  margin: 0.6em 0 0;
  padding-left: 1.6em;
  font-size: 0.86em;
  color: var(--ink-2);
}
.prose ol.markdown-footnotes li {
  overflow-wrap: anywhere;
}
/* The sources block: one line closed (label, icons, count), one row per source open. */
.prose .markdown-sources {
  padding: 0;
}
.prose .markdown-sources > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  padding: 4px 10px;
  font-weight: 600;
  font-size: 0.92em;
  list-style: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.prose .markdown-sources > summary::-webkit-details-marker {
  display: none;
}
.prose .markdown-sources > summary::after {
  content: "";
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--ink-3);
  border-bottom: 1.5px solid var(--ink-3);
  transform: rotate(-45deg);
  flex: none;
}
.prose .markdown-sources[open] > summary::after {
  transform: rotate(45deg);
}
.prose .sources-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.prose .sources-icons {
  display: inline-flex;
  align-items: center;
  padding-left: 6px;
  flex: none;
}
.prose .sources-count {
  flex: none;
}
.prose .source-icon {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  flex: none;
  box-sizing: border-box;
  /* A favicon still loading, or one that never answers, is a quiet dot. */
  background: var(--line);
  object-fit: contain;
}
.prose .source-icon.glyph {
  padding: 3px;
  color: var(--ink-3);
  border: 1px solid var(--line);
}
.prose .sources-icons .source-icon {
  margin-left: -6px;
  border: 2px solid var(--surface-2);
  width: 22px;
  height: 22px;
}
.prose .sources-more,
.prose .sources-count {
  font-size: 12px;
  color: var(--ink-3);
  font-weight: 600;
}
.prose .sources-more {
  margin-left: 4px;
}
.prose .markdown-sources .source-rows {
  margin: 0;
  padding: 0 10px 8px;
  list-style: none;
  border-top: 1px solid var(--line);
}
.prose .source-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  min-width: 0;
}
.prose .source-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.92em;
}
.prose .markdown-sources .source-text a {
  color: var(--ink);
  text-decoration: none;
}
.prose .markdown-sources .source-text a:hover {
  text-decoration: underline;
}
.prose hr {
  border: 0;
  border-top: 1px solid var(--line);
}
.caret {
  display: inline-block;
  width: 7px;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.15em;
  background: var(--ink);
  border-radius: 2px;
  animation: pulse 1s ease-in-out infinite;
}
.run-note {
  margin-top: 6px;
  font-size: 13px;
  color: var(--ink-3);
}
.run-note.failed {
  color: var(--danger);
}
.run-note .run-detail {
  display: block;
  color: var(--ink-3);
}
.run-note button {
  margin-left: 8px;
  color: var(--ink);
  font-weight: 500;
}
/* Answer now: deliberately quiet — the note's own grey, no weight — with a
   44 px hit area that the padding gives without making the words any
   larger (docs/work/2026-09-29-run-caps-continue.md). */
.run-note.answer-now {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 8px;
}
.run-note.answer-now button {
  margin-left: 0;
  min-height: 44px;
  padding: 0 4px;
  color: var(--ink-3);
  font-weight: 400;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.run-note.answer-now #answer-now-confirm {
  color: var(--ink);
  font-weight: 500;
}

/* ---------- composer ---------- */
.composer {
  position: relative;
  flex: none;
  padding: 8px 0 max(10px, var(--safe-bottom));
  background: var(--paper);
}
/* The way back down: a round button floating above the composer while the
   newest message is more than a screen away (src/client/scroll-to-end.tsx).
   Under the mention list and the picker menu, which open upward from the
   composer box and win the overlap. No transition: nothing animates but
   the scroll itself. */
.scroll-to-end {
  position: absolute;
  bottom: calc(100% + 12px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--line-2);
  background: var(--surface);
  color: var(--ink-2);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.scroll-to-end:hover {
  background: var(--surface-2);
  color: var(--ink);
}
/* Who is typing or thinking in this topic (src/client/presence.tsx,
   docs/work/2026-10-13-typing-thinking-presence.md): one quiet line just
   above the composer, laid over the end of the feed on a paper fade so it
   never takes room and the feed never moves when it comes and goes.
   Typing in ink, thinking in Think's blue. */
.presence-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  z-index: 1;
  pointer-events: none;
}
/* The fade sits on the column only, so it never paints over the sidebar's edge. */
.presence-line .column {
  background: linear-gradient(to top, var(--paper) 55%, transparent);
}
.presence-line p {
  margin: 0;
  padding: 10px 0 2px;
  font-size: 13px;
  line-height: 18px;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The shared busy-text sweep (docs/work/2026-10-02-text-shimmer.md), from
   each part's own colour to a brighter band: grey for typing, Think blue
   for thinking. Reduced motion shows the plain colour (the shared rule). */
.presence-line .typing {
  color: var(--ink-2);
  --shimmer-base: var(--ink-2);
}
.presence-line .thinking {
  color: var(--think);
  --shimmer-base: var(--think);
  --shimmer-peak: color-mix(in srgb, var(--think) 45%, var(--paper));
}
@media (prefers-color-scheme: dark) {
  .presence-line .thinking {
    --shimmer-peak: color-mix(in srgb, var(--think) 50%, white);
  }
}
.presence-line .text-shimmer {
  overflow: hidden;
  text-overflow: ellipsis;
}
.presence-line .sep {
  color: var(--ink-3);
}
/* Docked onto a Think thread, the box is inset on desktop and the gap above it
   is painted with the thread's blue (`.composer-box.flush`, below): the line
   follows the box's left edge, and where the blue shows through it carries no
   paper fade, so the blue runs unbroken behind the text. */
@media (min-width: 601px) {
  .composer:has(.composer-box.attached) .presence-line p {
    padding-left: 38px;
  }
}
.composer:has(.composer-box.flush) .presence-line .column {
  background: none;
}
.composer:has(.composer-box.flush) .presence-line p {
  padding-left: 14px;
  padding-right: 14px;
}
@media (min-width: 601px) {
  .composer:has(.composer-box.flush) .presence-line p {
    padding-left: 52px;
  }
}
.composer:has(> .presence-line:not(:empty)) > .scroll-to-end {
  bottom: calc(100% + 34px);
}
.composer-box {
  border: 1px solid var(--line-2);
  border-radius: 20px;
  background: var(--surface);
  padding: 8px 8px 6px 14px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
  /* The mode switch and the dock/undock read as one gesture: the border,
     tint, ring and desktop inset all move on the same clock. Only paint and
     the box's own left margin move — never a property that would resize it,
     since docking must not change its height
     (docs/work/2026-09-25-composer-mode-motion.md). */
  transition:
    border-color var(--motion-fast) var(--motion-ease),
    background-color var(--motion-fast) var(--motion-ease),
    box-shadow var(--motion-fast) var(--motion-ease),
    border-radius var(--motion-fast) var(--motion-ease),
    margin-left var(--motion-fast) var(--motion-ease);
}
/* The @-mention picker: a list above the field it completes into. */
.composer-box,
.new-topic {
  position: relative;
}
.mention-list {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: calc(100% + 6px);
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  z-index: 5;
  max-height: 40dvh;
  overflow-y: auto;
}
.mention-list li {
  display: flex;
  gap: 8px;
  align-items: baseline;
  min-height: 40px;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
}
.mention-list li.on {
  background: var(--surface-2);
}
.mention-list li span {
  color: var(--ink-3);
  font-size: 13.5px;
}
/* The picker's section headings, shown only when both People and Topics have
   something to offer (src/client/mentions.tsx). */
.mention-list li.mention-section {
  min-height: 0;
  padding: 6px 10px 2px;
  color: var(--ink-3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  cursor: default;
}
.mention-list li .topic-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.mention-list li .topic-row .glyph {
  width: 15px;
  height: 15px;
  color: var(--ink-3);
  flex: none;
}
/* The `:shortcode` picker (src/client/emoji-shortcut-picker.tsx): the same
   `.mention-list` box and row, with the emoji itself at a size that reads
   next to its shortcode rather than the 13.5px text beside it. */
.emoji-list li .emoji-char {
  font-size: 18px;
  line-height: 1;
  flex: none;
}
.composer-box:focus-within {
  border-color: var(--ink-3);
}
/* The composer's field (the editor's scroller, and the disabled textarea of
   the stand-in composer a voice exploration shows while it starts,
   voice-pending.tsx): one shared rule for every metric that can move a
   glyph — box model, font and spacing alike. */
.composer-box textarea,
.composer-editor .cm-scroller {
  min-height: 24px;
  /* A share of the app frame, not of the layout viewport: with a phone's
     keyboard up the frame is shorter than `dvh` says (src/client/viewport.ts),
     and a field capped at 32dvh plus the reply quote, the switch and the
     toolbar outgrew it, spilling the composer under the foreground where the
     drawer showed through (docs/work/2026-10-01-reply-paste-layout.md). The
     300px is the top bar plus everything in the composer but the field, so
     a short frame keeps the toolbar and Send in reach. */
  max-height: max(
    48px,
    min(
      calc(var(--app-height, 100dvh) * 0.32),
      calc(var(--app-height, 100dvh) - 300px)
    )
  );
  padding: 6px 0;
  line-height: 1.45;
  font-size: 16px;
  font-family: inherit;
  font-weight: 400;
  letter-spacing: normal;
  word-spacing: normal;
  tab-size: 4;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  -webkit-text-size-adjust: 100%;
}
/* On a phone the field grows into the room that is really there
   (docs/work/2026-10-02-composer-room-mobile.md): the frame less the top bar,
   the composer's own chrome as measured (src/client/composer-room.ts: the
   switch row, a reply quote, link cards, the toolbar) and a 64px strip of the
   conversation that must stay visible. The fixed 300px above left a
   two-line field in a 450px frame (keyboard and autofill bar up) even with no
   quote. Never above 40% of the frame, so a paste with no keyboard does not
   take the screen. Snapped down to whole lines (`1lh`, plus the 12px of
   padding) so the field's edges never slice a line at rest. Keyed on touch as
   well as width, so a landscape phone or a touch tablet, laid out past 840px,
   still gets it; only a mouse at desktop width keeps the rule above
   (docs/work/2026-10-03-composer-room-leak-and-touch.md). */
@media (max-width: 839.98px), not ((hover: hover) and (pointer: fine)) {
  /* The strip of conversation kept above the composer: 64px, shrinking to 32px
     as the frame falls from 362px to 330px (an iPhone with the autofill bar
     up, measured from a report: about 330px, which showed two lines), where the
     room is worth more as text. A reply's quote keeps one line meanwhile. */
  :root {
    --composer-strip: clamp(32px, calc(32px + var(--app-height, 100dvh) - 330px), 64px);
  }
  :root[data-keyboard] .replying-to .reply-excerpt {
    -webkit-line-clamp: 1;
    line-clamp: 1;
  }
  .composer-box textarea,
  .composer-editor .cm-scroller {
    max-height: max(
      48px,
      min(
        calc(var(--app-height, 100dvh) * 0.4),
        calc(
          var(--app-height, 100dvh) - var(--bar) - var(--composer-chrome, 120px) -
            var(--composer-strip, 64px)
        )
      )
    );
  }
  @supports (max-height: round(down, 10px, 1lh)) {
    .composer-box textarea,
    .composer-editor .cm-scroller {
      max-height: max(
        calc(1lh + 12px),
        calc(
          round(
            down,
            min(
              calc(var(--app-height, 100dvh) * 0.4),
              calc(
                var(--app-height, 100dvh) - var(--bar) -
                  var(--composer-chrome, 120px) - var(--composer-strip, 64px)
              )
            ) - 12px,
            1lh
          ) + 12px
        )
      );
    }
  }
}
/* Faux-bold that never changes a glyph's advance width: a heavier
   `font-weight` (the first version of this rule used 600) measurably widens
   or narrows a word against the same text at the field's own regular
   weight. A stroke only adds paint, never layout, so bold reads distinct
   while every character stays where the plain text would have put it. */
.draft-heading,
.draft-mention {
  font-weight: inherit;
  -webkit-text-stroke: 0.45px currentcolor;
}
.draft-mark {
  color: var(--ink-3);
}
/* The rest of a post's Markdown, drawn while it is typed
   (docs/work/2026-10-02-composer-live-markdown.md). Only paint, never
   layout: a code span keeps the field's own font, since a monospace face
   would move every character after it off the field's caret, so it reads
   as code by its tint alone; a quote by its colour, not a bar that would
   take room; a link by its underline. The tint is a share of the ink, so it
   shows on any field's background, a proposal's grey block included. */
.draft-code,
.draft-fence {
  border-radius: 3px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.draft-fence.draft-mark {
  background: none;
}
.draft-link {
  text-decoration: underline;
}
.draft-quote {
  color: var(--ink-2);
}
.draft-mark.draft-list {
  color: var(--ink-2);
}
/* The cards a draft would carry, under the field in Reply mode, one line
   each so the keyboard leaves room on a phone: a picture or a glyph, the
   site or the post's author and topic on a quiet line, the title or first
   words under it, and a 40 px × that sends the post without that card
   (docs/work/2026-09-25-removable-cards.md). Nothing is reserved until a
   card is known. */
.composer-cards {
  display: grid;
  gap: 4px;
  margin: 4px 0 2px;
  padding: 0;
  list-style: none;
  min-width: 0;
}
.composer-card {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 0 0 0 8px;
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 13px;
  line-height: 1.35;
}
.composer-card > .glyph {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--ink-3);
}
.composer-card-picture {
  width: 32px;
  height: 32px;
  flex: none;
  object-fit: cover;
  border-radius: 6px;
  background: var(--surface-3);
}
.composer-card-words {
  display: grid;
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 0;
}
.composer-card-head,
.composer-card-line {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.composer-card-head {
  font-size: 12px;
  color: var(--ink-3);
}
.composer-card-line {
  color: var(--ink-2);
}
.composer-card-remove {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--ink-3);
  background: none;
}
.composer-card-remove .glyph {
  width: 16px;
  height: 16px;
}
.composer-card-remove:hover,
.composer-card-remove:focus-visible {
  color: var(--ink);
}
.composer-box textarea {
  position: relative;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  resize: none;
  field-sizing: content;
  /* Already fully restyled above (no border, no background); this also
     drops WebKit's own inner text inset on a textarea, which CSS padding
     alone does not always override. */
  -webkit-appearance: none;
  appearance: none;
}
.composer-box textarea::placeholder {
  color: var(--ink-3);
}
.composer-box textarea:focus {
  box-shadow: none;
}
/* The composer as a real editor (src/client/message-editor.tsx,
   docs/work/2026-10-07-composer-editor.md): the editor draws its own text, so
   the caret, the selection and scrolling are the browser's, and Markdown can
   take real room — a quote's bar, monospace code, a larger heading — where
   the mirror above could only paint. The metrics and the height cap are the
   textarea's (the rules above name `.cm-scroller`, which is what scrolls). */
.composer-editor .cm-editor {
  font-size: 16px;
  color: var(--ink);
  background: none;
}
.composer-editor .cm-editor.cm-focused {
  outline: none;
}
.composer-editor .cm-scroller {
  min-height: 0;
  padding: 0;
  font-family: inherit;
  line-height: 1.45;
  overflow: auto;
  overscroll-behavior: contain;
}
.composer-editor .cm-content {
  min-height: 24px;
  padding: 6px 0;
  caret-color: var(--ink);
}
.composer-editor .cm-line {
  padding: 0;
}
/* A rewrite in flight in the editor: its own letters carry the shared
   `.text-shimmer` sweep (the rule above names `.shimmering .cm-content`, the
   element whose text it is), in every colour the marks and blocks would
   otherwise paint. */
.composer-editor.shimmering .cm-content * {
  -webkit-text-fill-color: transparent;
}
/* Without the sweep (reduced motion, forced colours, below) the letters show
   steady again, in the colour the rule above gives the whole field. */
@media (prefers-reduced-motion: reduce), (forced-colors: active) {
  .composer-editor.shimmering .cm-content * {
    -webkit-text-fill-color: currentColor;
  }
}
/* An empty field's placeholder, drawn on its one line rather than as a node
   in it, so a long-press on an iPhone lands on editable text and offers
   Paste (src/client/editor-view.ts, docs/work/2026-10-07-composer-editor.md).
   A block after the line's own (empty) text, pulled up by one line: the
   caret stands at the line's start at the text's height, and words that wrap
   still grow the field, as the placeholder they replace did. */
.message-editor .cm-line[data-placeholder]::after {
  content: attr(data-placeholder);
  /* No text for screen readers: they have `aria-placeholder` already. */
  content: attr(data-placeholder) / "";
  display: block;
  margin-top: -1.45em;
  margin-top: -1lh;
  color: var(--ink-3);
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}
.message-editor .draft-bold {
  font-weight: 600;
}
.message-editor .draft-italic {
  font-style: italic;
}
.message-editor .draft-mention {
  font-weight: 600;
  -webkit-text-stroke: 0;
}
.message-editor .draft-code,
.message-editor .draft-line-fence {
  font-family: var(--font-mono);
  font-size: 0.9em;
}
.message-editor .draft-line-quote {
  padding-left: 10px;
  border-left: 2px solid var(--line-2);
  color: var(--ink-2);
}
.message-editor .draft-line-heading {
  font-size: 1.12em;
  font-weight: 600;
}
.message-editor .draft-heading {
  -webkit-text-stroke: 0;
}
/* `.cm-line` in the selector so a field's own `.cm-line { padding: 0 }`
   (`.draft-editor`, a next-step card's draft) cannot take the padding the
   indent hangs into: without it a numbered item's number was cut at the
   field's left edge (docs/work/2026-10-14-markdown-rendering-gaps.md). */
.message-editor .cm-line.draft-line-list {
  padding-left: 1.2em;
  text-indent: -1.2em;
}
/* A `- ` item, its indent and dash drawn as one bullet
   (docs/work/2026-10-08-editor-bullets.md): each level steps in by the
   bullet's width, and wrapped lines hang under the words, not the bullet. */
.message-editor .draft-line-list.draft-bullet-line {
  /* The step a sent post's nested list takes (`.prose ul`). */
  padding-left: calc(1.4em * (var(--bullet-level, 0) + 1));
  text-indent: -1.4em;
}
/* The mark is drawn, not a `•` character, whose size changes with the font:
   a disc, then a circle one level in, then a square, as a sent post's list. */
.message-editor .draft-bullet {
  display: inline-block;
  position: relative;
  width: 1.4em;
  height: 1em;
  text-indent: 0;
  color: var(--ink);
}
.message-editor .draft-bullet::before {
  content: "";
  position: absolute;
  left: 0.45em;
  bottom: 0.12em;
  width: 0.38em;
  height: 0.38em;
  box-sizing: border-box;
  border-radius: 50%;
  background: currentColor;
}
.message-editor .draft-bullet-circle::before {
  background: none;
  border: 1.5px solid currentColor;
}
.message-editor .draft-bullet-square::before {
  bottom: 0.14em;
  width: 0.34em;
  height: 0.34em;
  border-radius: 1px;
}
/* While a rewrite runs the words dim to the shimmer's base colour; the mark,
   painted rather than written, dims with them. */
.composer-editor.shimmering .draft-bullet {
  color: var(--shimmer-base, var(--ink-3));
}
/* A high-contrast theme repaints backgrounds with the page colour, which
   would hide the disc and the square: they keep the theme's text colour. */
@media (forced-colors: active) {
  .message-editor .draft-bullet::before {
    forced-color-adjust: none;
    background: CanvasText;
  }
  .message-editor .draft-bullet-circle::before {
    background: none;
    border-color: CanvasText;
  }
}
.message-editor .draft-line-fence {
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}
.message-editor .draft-line-fence .draft-fence {
  background: none;
}
.composer-row {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 2px;
}
.composer-row .spacer {
  flex: 1;
}
/* A post being edited (src/client/post-editor.tsx): the composer's own box
   in place of the post's words. A post's column is narrower than the
   composer's, so the tools close up to 34 px and Cancel and Save fit inside
   the box at 390 px (docs/work/2026-10-19-post-editing-memory.md). */
.post-editor {
  margin-top: 4px;
  border-color: var(--ink-3);
}
.post-editor .composer-row {
  min-width: 0;
}
.post-editor .composer-row .icon-btn {
  width: 34px;
}
.post-editor .edit-action {
  min-height: 32px;
  height: 32px;
  padding: 0 12px;
  border-radius: 999px;
  font-size: 14px;
  flex: none;
}
.post-editor .edit-action + .edit-action {
  margin-left: 6px;
}
/* The "Improve writing" button (docs/work/2026-09-30-improve-writing-icon.md):
   a round hit area the same size class as the send button beside it, no
   fill at rest so it reads as quiet as the mic and picture buttons, a
   light fill on hover/focus matching `.icon-btn`'s own rule. */
.improve-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 50%;
  color: var(--ink-2);
}
/* Nothing to improve: invisible but in place, so the mic and Send stay put. */
.improve-btn-empty {
  visibility: hidden;
  pointer-events: none;
}
.improve-btn:hover:enabled {
  background: var(--surface-2);
  color: var(--ink);
}
.improve-btn svg {
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  /* The toolbar standard (docs/design.md, Icons), as the mic, picture and
     emoji beside it: it was 2 and read heavier (Jean, 2026-10-21). */
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* "Rewriting…" and "Undo" both take the improve button's own place, one at
   a time — never more than one extra control beside Send. */
.rewrite-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 34px;
  padding: 0 10px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 13px;
  white-space: nowrap;
  flex: none;
}
button.rewrite-pill:hover:enabled {
  color: var(--ink);
}
.rewrite-pill svg {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The one busy-text shimmer (user, 2026-10-02; docs/work/2026-10-02-text-shimmer.md):
   a soft light band moves through the letters themselves, never across a
   rectangle behind them. Used by the "Rewriting…" label and the draft being
   rewritten, by On my to-do's "Updating" label while a refresh runs (its rows
   stay at rest: docs/work/2026-10-18-todo-refresh-speed.md). Neutrals only, no `--think`: shimmer is motion,
   not the think-mode colour design.md reserves blue for. The text sits at
   `--ink-3` and lifts to `--ink` at the band's centre, so it reads in light
   and dark alike. `background-clip: text` needs a transparent fill, hence
   the paired `-webkit-text-fill-color`, kept for Safari. */
.text-shimmer,
.composer-editor.shimmering .cm-content {
  /* `--shimmer-base` is the text's own colour between bands: `--ink-3` by
     default, raised where the words must read at a glance (the live step
     line, docs/work/2026-10-02-steps-in-place.md). */
  background-image: linear-gradient(
    100deg,
    var(--shimmer-base, var(--ink-3)) 0%,
    var(--shimmer-base, var(--ink-3)) 38%,
    var(--shimmer-peak, var(--ink)) 50%,
    var(--shimmer-base, var(--ink-3)) 62%,
    var(--shimmer-base, var(--ink-3)) 100%
  );
  /* A band every 360px, whatever the width of the words: a long row's text
     is not left dim for most of each pass while the band crosses empty space. */
  background-size: 360px 100%;
  background-repeat: repeat-x;
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: shimmer-sweep var(--shimmer-speed) linear infinite;
}
@keyframes shimmer-sweep {
  from {
    background-position: 0 0;
  }
  to {
    background-position: 360px 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* The global `* { animation: none !important }` rule already stops the
     sweep; without this the gradient would still freeze mid-band instead
     of showing the plain, steady dimmed text. */
  .text-shimmer,
  .composer-editor.shimmering .cm-content {
    background-image: none;
    color: var(--shimmer-base, var(--ink-3));
    -webkit-text-fill-color: var(--shimmer-base, var(--ink-3));
  }
}
@media (forced-colors: active) {
  /* A system high-contrast theme replaces every colour with its own
     palette and may not honour `-webkit-text-fill-color: transparent`,
     which would clip the text to nothing readable. */
  .text-shimmer,
  .composer-editor.shimmering .cm-content {
    background-image: none;
    color: CanvasText;
    -webkit-text-fill-color: currentColor;
  }
}
/* A rewrite in flight: the editor's own letters shimmer (`.shimmering`
   above) and the field stops taking input (`readOnly`); the row's other
   controls dim and stop taking taps, until it returns. */
/* The new draft fading in once a rewrite lands, in place of popping straight
   to the replaced text: applied for one short animation only, by the
   composer that just saw `rewrite.control` turn from "rewriting" to "undo"
   (`composer.tsx`, `exploration.tsx`), and removed again once it finishes. */
.composer-editor.landed .cm-content {
  animation: rewrite-fade-in 320ms var(--motion-ease);
}
@keyframes rewrite-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.composer-row.rewriting > :not(.rewrite-pill):not(.send) {
  opacity: 0.45;
  pointer-events: none;
}
/* The one control for the model and the thinking level, on every row that
   has one (src/client/model-picker.tsx); the home field's row also carries
   the mic and the picture button and still wraps. */
.pickers {
  display: flex;
  gap: 4px;
  min-width: 0;
  flex-wrap: wrap;
}
/* The home field's Send sits at the row's far end, where the thumb is, on
   whichever line it lands if the row ever wraps. */
#home-send {
  margin-left: auto;
}
/* The model and the thinking level, as one control on the composer's row:
   one line whatever a model is called, with the menu behind it. */
/* `.picker` below has the same specificity and comes later, so its padding
   shorthand would put back the 22 px gutter a `<select>`'s caret needed and
   this button does not have. Two classes, so the pill keeps its own width. */
.picker.picker-button {
  padding-right: 8px;
}
/* Above the composer, never under it: on a phone the composer sits on the
   keyboard, so the only room is upwards. */
/* Anchored to the composer box, not to the control: the control sits mid-row,
   and a menu measured from there runs off one edge or the other on a narrow
   phone. Spanning the box is the one width that always fits, and it reads as
   a sheet over the composer. */
/* `hidden` must win over the grid below, which would otherwise show it. */
.picker-menu[hidden] {
  display: none;
}
.picker-menu {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 8px;
  right: 8px;
  z-index: 3;
  display: grid;
  gap: 2px;
  padding: 10px 12px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  /* The same bound the mention list above this composer keeps, and for the
     same reason: the sheet grows upwards from the composer, nothing in the
     chain scrolls (`.app` is clipped), so an unbounded sheet on a landscape
     phone or under a raised keyboard puts the Model label off the top of the
     screen with no way to reach it. */
  max-height: 40dvh;
  overflow-y: auto;
}
/* The global label rule carries `margin: 14px 0 6px`, which is most of the
   sheet's height and none of its spacing: the grid's own gap places these. */
.picker-menu label {
  margin: 0;
  font-size: 12px;
  color: var(--ink-3);
}
.picker-menu label + select {
  margin-bottom: 6px;
}
.picker-menu select {
  width: 100%;
  /* A full-size control, not the row's dense-control exception: there is room
     for it here, and these are the two things the sheet exists to set. */
  min-height: 40px;
}
.picker {
  width: auto;
  min-height: 30px;
  max-width: 38vw;
  padding: 3px 22px 3px 8px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-2);
  font-size: 12.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.picker:hover:enabled {
  background: var(--surface-2);
  color: var(--ink);
}
.picker:focus {
  box-shadow: none;
  border-color: var(--line-2);
}
/* ---------- the next step, proposed with the draft in it ----------
   A card under the assistant turn that offered it: the reason in one line,
   the draft (or the rows of a split), one primary button naming the act and
   Not now beside it. Neutral like the exits above, and for the same reason —
   the blue surface belongs to think mode — with a hairline frame so it reads
   as an offer rather than as part of the answer. Full 40 px controls: this is
   the screen's primary action while it is open. */
.proposal {
  margin: 10px 0 4px;
  padding: 12px;
  border: 1px solid var(--line-2);
  border-radius: 12px;
  background: var(--surface);
}
/* Whose voice is whose. The card's own name over the assistant's reason, and
   a caption over the draft saying it is the person's post — reported after use
   on a phone: the two read as one message from the AI otherwise. 12 px like
   the composer's review line, and `--ink-2` like it too: these are labels to
   be read, not metadata, which is what `--ink-3` is for. */
.proposal-label,
.proposal-caption {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-2);
}
.proposal-reason {
  margin: 0 0 10px;
  font-size: 13.5px;
  color: var(--ink-2);
}
.proposal-caption {
  margin-top: 10px;
}
/* The draft itself, as the person would send it: body text, wrapping, with
   its newlines kept. What Post sends is these exact characters. Elsewhere
   these are fields of their own (`textarea.proposal-draft`, a coding
   prompt, a visual's); on a post card it is the message editor
   (docs/work/2026-10-09-draft-card-editor.md), its Markdown drawn as the
   composer draws it, in the same grey block, growing with its text. */
textarea.proposal-draft {
  display: block;
  width: 100%;
  box-sizing: border-box;
  resize: none;
  field-sizing: content;
  color: var(--ink);
  font-family: inherit;
  /* 16 px, never less, on every field a phone shows: WebKit on iOS zooms the
     page into a focused field set smaller (docs/design.md, forms). */
  font-size: 16px;
}
textarea.proposal-draft:focus-visible {
  outline: 2px solid var(--ink-2);
  outline-offset: 1px;
}
.draft-editor .cm-editor {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--ink);
  font-size: 16px;
}
.draft-editor .cm-editor.cm-focused {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}
.draft-editor .cm-scroller {
  font-family: inherit;
  line-height: 1.5;
}
.draft-editor .cm-content {
  padding: 10px 12px;
  caret-color: var(--ink);
}
.draft-editor .cm-line {
  padding: 0;
}
/* A card being written (docs/work/2026-10-07-card-after-answer.md, user
   decision 2026-10-08): the card's frame at its smallest where the finished
   card will sit, its label and one line of its newest words, cut at the
   start so the latest stay in view. No field, no buttons. */
.proposal-shell {
  padding: 8px 12px;
}
.proposal-shell .proposal-label {
  margin-bottom: 2px;
}
.proposal-shell-line {
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  /* Right to left for the cut alone, so the overflow and its "…" fall at
     the start of the line; the words inside stay left to right. */
  direction: rtl;
  text-align: left;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-2);
}
.proposal-shell-line > span {
  direction: ltr;
  unicode-bidi: isolate;
}
.proposal-error {
  margin: 8px 0 0;
}
.proposal-decided a {
  color: inherit;
}
.proposal-draft {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  padding: 10px 12px;
  /* The boundary the caption promises: the same framed, tinted block the
     composer's review state shows the words it replaced in. */
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  font-size: 15px;
  line-height: 1.5;
}
/* The pictures the offered post carries, under the draft as a post shows
   them under its words: the composer's staged thumbnails, each with its
   remove button (docs/work/2026-09-29-proposal-attachments.md). */
.proposal-pictures {
  padding-top: 8px;
}
/* The recap the same offer may carry. The draft's block, tinted the way a
   published recap is tinted in the topic (`--surface-3`, as `.post-recap`
   uses, a step deeper than the draft's own `--surface-2` so the two still
   read apart without colour), so what is on the card looks like what will
   be in the stream.
   Shown whole and never folded: the topic folds recaps because it holds many
   of them, and this is the one the person is being asked to read before it
   posts on the tap. */
.proposal-recap {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-3);
  font-size: 15px;
  line-height: 1.5;
}
/* Both are Markdown, drawn as the post will be (src/client/written-text.tsx):
   a heading first sits at the top of the block, a fold last at its bottom. */
.proposal-recap > :first-child,
.proposal-sent-text > :first-child {
  margin-top: 0;
}
.proposal-recap > :last-child,
.proposal-sent-text > :last-child {
  margin-bottom: 0;
}
/* The half of a `both` offer that has already gone, on a card the person came
   back to: a quiet line where the recap block was, so the primary below it
   reads as the one act still open. */
.proposal-recap-done {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 10px 0 0;
}
/* A settings change Workroom proposed (src/client/settings-action.tsx): the
   next-step card's frame, its exact target in the draft's grey block or,
   for an invite, the address as a field the person may correct; what Send
   does in quiet lines under it; and its outcome kept in place once
   decided. The label sits on the field, as the global label rule spaces it
   for a form, so it is pulled back to the caption's own spacing. */
.settings-action .proposal-caption {
  display: block;
}
.settings-action-field {
  margin-bottom: 4px;
}
.settings-action-person {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 8px;
  align-items: baseline;
}
.settings-action-handle {
  color: var(--ink-3);
  font-size: 13.5px;
}
.settings-action-consequence,
.settings-action-agent {
  margin: 8px 0 0;
}
.settings-action-outcome {
  margin: 10px 0 0;
}
.settings-action-failed {
  font-size: 13.5px;
  color: var(--danger);
}
.settings-action-link {
  margin-top: 10px;
}
/* A change of a page's icon: the one it has, an arrow, the one it would get. */
.proposal-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.proposal-row label {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--surface-2);
  cursor: pointer;
}
.proposal-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin: 2px 0 0;
  flex: none;
  accent-color: var(--ink);
}
.proposal-row-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* A title is the model's own text under an 80-character cap, and a name of
     80 characters without a space is one it can write. `min-width` lets the
     flex item shrink below its content; the wrap is what stops the row
     pushing the card sideways at 320 px. */
  min-width: 0;
  overflow-wrap: anywhere;
}
.proposal-row-where {
  font-size: 12px;
  color: var(--ink-3);
}
.proposal-opening {
  font-size: 13.5px;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.proposal-row input:disabled + .proposal-row-body {
  opacity: 0.55;
}
.proposal-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding-top: 10px;
}
.proposal-actions button {
  min-height: 40px;
  /* A flex item's minimum is its content; a long topic title in the primary's
     label would otherwise push Not now off a 320 px screen. */
  min-width: 0;
  padding: 0 14px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font-size: 13.5px;
  font-weight: 500;
}
.proposal-actions button.pri {
  border-color: transparent;
  background: var(--ink);
  color: var(--paper);
  /* A long topic title must not push Not now off a 320 px screen. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.proposal-actions button:disabled {
  opacity: 0.55;
}
.proposal-hint {
  padding-top: 6px;
  margin-bottom: 0;
}
/* The other Claude Code route on a Mac or Windows desktop, a line of text
   ending the caption rather than another button
   (docs/work/2026-10-01-claude-desktop-app-route.md). */
.proposal-hint .coding-prompt-alternative {
  color: var(--ink-2);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.proposal-hint .coding-prompt-alternative:hover {
  color: var(--ink);
}
/* An instruction change the assistant prepared for the person to confirm
   (docs/context.md#ask-then-apply). The proposal card's frame, captions and
   buttons; its one block of its own is the diff, framed like the draft and
   painted with the file history's add and remove rows. Inside a step group,
   so it drops the group's 2 px rhythm for the card's own margin. */
.instruction-change {
  margin: 6px 0;
}
.instruction-change-diff {
  padding: 6px 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  overflow: hidden;
}
.proposal-gone {
  padding-top: 8px;
  margin-bottom: 0;
}
/* Answered: the record of what happened, where the offer was. Never a second
   button. */
.proposal-decided {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 8px 0 4px;
  font-size: 13px;
  color: var(--ink-3);
}
.proposal-decided .glyph {
  width: 15px;
  height: 15px;
  flex: none;
}
/* A sent card (docs/work/2026-09-29-sent-state-next-step.md): the card's
   frame stays where the offer was, folded to its one line with Show; shown,
   the card as it was sent, read only. */
.proposal-sent {
  padding: 0 12px;
}
.proposal-sent > .proposal-decided {
  margin: 0;
}
.proposal-sent-body {
  padding: 4px 0 12px;
  border-top: 1px solid var(--line);
}
.proposal-sent-note {
  margin: 4px 0 0;
}
.proposal-sent-rows .proposal-row {
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--surface-2);
}
/* What a split's primary did, in the pill's shape but not a control: there
   is no one place to go to. */
.proposal-sent-primary {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  max-width: 100%;
  padding: 0 14px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink-2);
  font-size: 13.5px;
  font-weight: 500;
  overflow-wrap: anywhere;
}
/* A tapped hint act, in place of its pill. */
.proposal-hint-done {
  margin: 0;
  min-height: 40px;
  overflow-wrap: anywhere;
}

/* The router's exits (docs/work/2026-09-24-router-exits-tiers.md). The hint
   card (docs/work/2026-09-24-hint-card.md): the next-step card's frame,
   caption and pills, with no draft; its acts stack as full-width 40 px pills,
   since each names a place and would otherwise wrap unevenly on a phone. */
.proposal-hint-acts {
  flex-direction: column;
  align-items: stretch;
}
.proposal-hint-acts button {
  width: 100%;
  padding: 8px 14px;
  /* A topic title is the workspace's text: it wraps inside the pill rather
     than pushing the card sideways at 320 px. */
  overflow-wrap: anywhere;
  line-height: 1.35;
}
/* The acts in body ink, Not now in the card's quieter ink under them. */
.proposal-hint-acts button[data-action="open-hint"] {
  color: var(--ink);
}
.proposal-hint-drafting {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
}
.proposal-hint-drafting .glyph {
  width: 15px;
  height: 15px;
  flex: none;
}
/* A visual card and its hint (docs/work/2026-10-03-agent-visuals.md): the
   next-step card's frame, captions and pills around what is made on the
   press — a picture, a mockup page, an annotated or rebuilt screenshot.
   Neutrals only: the blue surface belongs to think mode. */
.visual-kind {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.visual-kind .glyph {
  width: 15px;
  height: 15px;
  flex: none;
}
.visual-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: -8px;
}
.visual-head .visual-kind {
  margin: 0;
}
.proposal .proposal-link.visual-expand {
  gap: 6px;
  color: var(--ink-2);
  text-decoration: none;
}
.proposal .proposal-link.visual-expand:hover {
  color: var(--ink);
}
.visual-expand .glyph {
  width: 16px;
  height: 16px;
  flex: none;
}
.visual-title {
  margin: 0 0 6px;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.visual-inputs {
  padding-top: 8px;
}
.visual-model {
  display: block;
  width: 100%;
  min-height: 40px;
  padding: 0 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  /* The label alone; the price has its own line under the field. */
  text-overflow: ellipsis;
  /* 16 px, the phone rule for fields (docs/design.md). */
  font: inherit;
  font-size: 16px;
}
/* Square | Landscape | Portrait: one segmented control, the chosen shape
   raised on --surface inside a --surface-2 track, each a 40 px target. */
.visual-sizes {
  display: inline-flex;
  max-width: 100%;
  padding: 2px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface-2);
}
.visual-sizes button {
  min-height: 40px;
  min-width: 0;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--ink-2);
  font-size: 13.5px;
  font-weight: 500;
}
.visual-sizes button[aria-checked="true"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 0 0 1px var(--line-2);
}
.visual-sizes button:disabled {
  opacity: 0.55;
}
/* Who receives the prompt and the pictures: a sentence to be read before
   the first press, so secondary ink rather than metadata grey. */
.visual-consent {
  margin: 10px 0 0;
  color: var(--ink-2);
}
.visual-unset {
  margin: 10px 0 0;
  color: var(--ink-2);
}
/* The chosen model's price, under the select, so a phone never cuts it off. */
.visual-price {
  margin: 4px 0 0;
}
/* The run did not build the page: said quietly, the press button below. */
.visual-run-failed {
  margin: 10px 0 0;
  color: var(--ink-2);
}
.visual-result .attachments {
  margin-top: 10px;
}
/* A pressed card, folded in place (docs/work/2026-09-29-sent-state-next-step.md):
   its title, then the status line that never says Done, with Show what was
   sent; shown, the card as it was sent, read only, under a rule. */
.visual-sent-line {
  align-items: flex-start;
  margin: 0;
  line-height: 20px;
}
/* The text is on the toggle's 40 px line; the check sits on its middle,
   and stays on the first line when the toggle wraps under it on a phone. */
.visual-sent-line > span {
  padding: 10px 0;
  min-width: 0;
  overflow-wrap: anywhere;
}
.visual-sent-line > span:has(> .proposal-link:only-child) {
  padding: 0;
}
.visual-sent-line .glyph {
  margin-top: 12.5px;
}
.visual-sent-line .proposal-link {
  vertical-align: middle;
  margin: -10px 0;
}
.visual-sent-body {
  margin-top: 6px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
}
.visual-sent-options {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  column-gap: 12px;
  margin: 10px 0 0;
}
.visual-sent-options dt {
  margin: 0;
}
.visual-sent-options dd {
  margin: 0;
  font-size: 14px;
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* The wait, in the shape of what is coming: a picture-shaped placeholder,
   a page-shaped skeleton, or the screenshot dimmed under the words. The
   sweep is the placeholders' own (`.skeleton-line`); the words shimmer as
   busy text does; reduced motion stops both. */
.visual-loader {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 10px;
}
.visual-loader-picture {
  width: 100%;
  max-width: 420px;
  border-radius: var(--radius);
}
.visual-loader-picture.tall {
  max-width: 220px;
}
.visual-loader-text {
  margin: 0;
  font-size: 13.5px;
  font-weight: 500;
}
.visual-loader-page {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: 420px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.visual-loader-page .skeleton-block {
  display: block;
}
.visual-loader-bar {
  width: 40%;
  height: 12px;
}
.visual-loader-heading {
  width: 70%;
  height: 18px;
}
.visual-loader-row {
  width: 100%;
  height: 10px;
}
.visual-loader-row.short {
  width: 60%;
}
.visual-loader-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.visual-loader-tiles .skeleton-block {
  height: 56px;
}
.visual-loader-shot {
  position: relative;
  max-width: 320px;
  min-height: 96px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
}
.visual-loader-shot img {
  display: block;
  width: 100%;
  opacity: 0.3;
}
.visual-loader-over {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 12px;
  text-align: center;
  font-size: 13.5px;
  font-weight: 500;
  /* The words read over the dimmed screenshot at a glance. */
  --shimmer-base: var(--ink-2);
}
/* Expand: the card takes the whole screen, scrolls inside itself, and keeps
   its content in the reading column on a desktop; Escape or Collapse gives
   the screen back. */
.visual-card.expanded {
  position: fixed;
  inset: 0;
  z-index: 100;
  margin: 0;
  padding: calc(var(--safe-top) + 20px) 16px calc(var(--safe-bottom) + 24px);
  border: 0;
  border-radius: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.visual-card.expanded > * {
  max-width: var(--column);
  margin-left: auto;
  margin-right: auto;
}
.visual-card.expanded .visual-head {
  margin-top: -8px;
}
/* A second opinion (docs/work/2026-10-07-second-opinion-review.md): the
   card is the next-step card's frame, captions and pills around the brief
   another model reads and the model that reads it; the review comes back as
   that model's own message, its family's glyph where the agent's mark would
   be, its answer set off by the reading rule a coding prompt uses; the
   parent's answer to it carries one small label. Neutrals only. */
.second-opinion-help {
  margin: 6px 0 0;
}
.second-opinion-same {
  margin: 6px 0 0;
}
.second-opinion-card > .proposal-caption + .second-opinion-model {
  margin-top: 0;
}
.turn-review .mark-review {
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--line-2);
  box-sizing: border-box;
}
.turn-review .mark-review .glyph,
.review-head .glyph,
.review-response .glyph,
.review-of .glyph {
  width: 15px;
  height: 15px;
  flex: none;
}
.review-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 8px;
  margin: 0 0 2px;
  min-height: 30px;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-2);
}
.review-name {
  min-width: 0;
  overflow-wrap: anywhere;
}
.review-open {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  margin: -5px 0;
  color: var(--ink);
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.review-answer {
  padding-left: 12px;
  border-left: 2px solid var(--line-2);
}
.review-response {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-2);
}
.review-of {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 16px;
  font-size: 13px;
  color: var(--ink-2);
}
.review-of > span {
  min-width: 0;
  overflow-wrap: anywhere;
}
.review-of a {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}
/* The pages and files a proposed post carries, under its pictures: the
   sent message's chips, starting at the card's edge rather than the
   bubble's, each with its remove button. */
.file-chips.proposal-carried {
  justify-content: flex-start;
  max-width: none;
  margin-top: 6px;
  padding-top: 0;
}
.file-chip .chip-open {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 40px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.file-chip .chip-open > svg {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  stroke: var(--ink-2);
  fill: none;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.file-chip .remove:disabled {
  opacity: 0.55;
}
/* A carried chip's name keeps clear of its remove button at 390 px: the
   open button clips its own text and leaves room before the ×. */
.proposal-carried .file-chip {
  gap: 10px;
}
.proposal-carried .file-chip .chip-open {
  overflow: hidden;
  padding-right: 6px;
}
/* The pages a topic post carries, under its words and above its pictures. */
.post-pages {
  max-width: 560px;
}
/* A quiet text button inside a card (Edit prompt, Change, Keep private,
   Stop suggesting coding tasks, Undo): the Done here link's look, never a
   pill, with the 40 px target the principles ask for. */
.proposal .proposal-link,
.proposal-decided .proposal-link {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 0 2px;
  border: 0;
  border-radius: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.proposal-hint-acts .proposal-link {
  align-self: center;
  width: auto;
}
/* A card's primary that is a link, not a button (a coding card opens its
   tool by navigation, in the tap): the primary pill's look. */
.proposal-actions a.pri {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 40px;
  min-width: 0;
  max-width: 100%;
  padding: 0 14px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 13.5px;
  font-weight: 500;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A small round choice beside a primary: a 40 px circle holding one glyph,
   named by its aria-label and its title. */
.proposal-actions .proposal-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 40px;
  min-height: 40px;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
}
.proposal-actions .proposal-icon .glyph {
  width: 18px;
  height: 18px;
}

/* The coding card (docs/work/2026-09-28-coding-agent-handoff.md): the
   next-step card's frame around the shared prompt block. The task's name
   under the caption, the prompt in the draft's block folded to four lines,
   then the repository and the note's topic as quiet lines, the note in the
   draft's block, and the big link with its two small choices. */
.coding-card-title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.coding-prompt-caption {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 2px;
}
.coding-prompt-tools {
  display: flex;
  gap: 12px;
  flex-shrink: 0;
  white-space: nowrap;
}
/* The prompt to read: the card's full width, no inner box, the secondary
   size, a thin rule marking it as quoted text (founder, 2026-09-29). */
.coding-prompt-reading {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 2px 0 0;
  padding: 0 0 0 10px;
  border-left: 2px solid var(--line-2);
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ink);
}
.coding-prompt-text[data-folded] {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
}
textarea.coding-prompt-text {
  max-height: 50dvh;
  overflow-y: auto;
}
.coding-prompt-count {
  margin: 4px 0 0;
}
.coding-prompt-count[data-over] {
  color: var(--danger);
}
.coding-prompt-meta {
  margin: 6px 0 0;
  font-size: 13.5px;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.coding-prompt-meta strong {
  color: var(--ink);
  font-weight: 600;
}
/* The coding card's layout (docs/work/2026-10-14-simplify-coding-card.md):
   one Prompt link that unfolds the prompt, the destination above the big
   button as a pill that opens a menu of topics, and the other choices behind
   one ... menu. */
.coding-prompt-toggle {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-height: 40px;
  padding: 0 2px;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
}
.coding-prompt-toggle .glyph {
  width: 16px;
  height: 16px;
  transition: transform 0.15s;
  transform: rotate(-90deg);
}
.coding-prompt-toggle[aria-expanded="true"] .glyph {
  transform: none;
}
.coding-card-dest {
  margin: 6px 0 8px;
  font-size: 14.5px;
  color: var(--ink);
}
/* The draft post card's destination, under the agent's reason. */
.proposal .proposal-dest {
  margin: 0 0 4px;
}
.coding-card-dest .coding-menu {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.coding-card-dest-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 40px;
  max-width: 100%;
  padding: 0 12px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  overflow-wrap: anywhere;
  text-align: left;
  cursor: pointer;
}
.coding-card-dest-pill .glyph {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--ink-2);
}
/* The started row: its check sits on the first line, and its 40 px links do
   not make that line tall (as .visual-sent-line's). */
.coding-card .proposal-decided {
  align-items: flex-start;
}
.coding-card .proposal-decided .glyph {
  margin-top: 2px;
}
.coding-card .proposal-decided .proposal-link {
  vertical-align: middle;
  margin: -10px 0;
}
.coding-menu {
  position: relative;
  flex: none;
}
.coding-menu-list {
  top: calc(100% + 4px);
  left: 0;
  width: max-content;
  min-width: 220px;
  max-width: min(320px, calc(100vw - 48px));
}
/* With a search field the width holds still while typing filters the rows. */
.coding-menu-list[data-search] {
  width: min(320px, calc(100vw - 48px));
}
.coding-menu-list[data-align="right"] {
  left: auto;
  right: 0;
}
/* Opened above its button where the room below is too short (`fit`). */
.coding-menu-list[data-placement="up"] {
  top: auto;
  bottom: calc(100% + 4px);
}
.proposal-actions .coding-menu-list button,
.proposal-dest .coding-menu-list button,
.coding-menu-list a {
  display: flex;
  width: 100%;
  box-sizing: border-box;
  border: 0;
  background: none;
  font: inherit;
  font-size: 14px;
  font-weight: 400;
  text-align: left;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 8px 10px;
  border-radius: 8px;
  color: inherit;
  text-decoration: none;
}
.coding-menu-list a:hover,
.coding-menu-list a:focus-visible,
.proposal-actions .coding-menu-list button:hover,
.proposal-actions .coding-menu-list button:focus-visible,
.proposal-dest .coding-menu-list button:hover,
.proposal-dest .coding-menu-list button:focus-visible {
  background: var(--surface-2);
}
/* The destination menu: a search field, at most six rows of topics before the
   list scrolls, and Don't post under a rule. A check stays on a wrapped
   name's first line. */
.coding-menu-list .coding-menu-search {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 2px 6px;
  padding: 0 10px;
  min-height: 40px;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  background: var(--surface-2);
}
.coding-menu-search .glyph {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--ink-3);
}
.coding-menu-list .coding-menu-search:focus-within {
  border-color: var(--ink);
}
.coding-menu-search input,
.coding-menu-search input:focus {
  display: block;
  flex: 1;
  width: auto;
  min-width: 0;
  min-height: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: none;
  color: var(--ink);
  font: inherit;
  /* 16 px: a smaller field makes iOS zoom the page when it takes focus. */
  font-size: 16px;
  outline: none;
  padding: 0;
}
.coding-menu-results {
  max-height: 240px;
  overflow-y: auto;
}
.coding-menu-results button,
.coding-menu-last {
  align-items: flex-start;
}
.coding-menu-list .coding-menu-last {
  margin-top: 4px;
  border-top: 1px solid var(--line);
  border-radius: 0 0 8px 8px;
}
.coding-menu-name {
  flex: 1;
  min-width: 0;
  overflow-wrap: anywhere;
}
.coding-menu-empty {
  margin: 0;
  padding: 10px;
  font-size: 14px;
  color: var(--ink-2);
}
.coding-menu-check {
  display: inline-flex;
  align-items: center;
  width: 16px;
  height: 22px;
  flex: none;
}
/* Discard under a draft card, and its confirmation in place
   (docs/work/2026-10-08-coding-card-draft.md). */
.coding-card-discard {
  margin: 4px 0 0;
}
.coding-card-discard > .hint {
  margin: 8px 0 0;
}
/* A discarded or replaced card's prompt, shown read-only under its line. */
.coding-set-aside-prompt {
  margin: 0 0 8px 21px;
}
.coding-prompt-actions .pri {
  flex: 1 1 auto;
}
/* Did that work? under an opened card: its own rule above it
   (docs/work/2026-09-29-coding-feedback-answered.md). Unanswered, one small
   line: the question in the secondary size and three quiet text choices,
   wrapping under it on a narrow phone rather than overflowing; the 40 px
   targets come from .proposal-link's height. */
.coding-feedback {
  margin-top: 10px;
  padding-top: 2px;
  border-top: 1px solid var(--line);
}
.coding-feedback-ask {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 8px;
  font-size: 13px;
}
.coding-feedback-question {
  color: var(--ink-2);
}
.coding-feedback .proposal-link {
  font-size: inherit;
}
.coding-feedback .proposal-link:disabled {
  opacity: 0.55;
  cursor: default;
}
/* Answered: a much smaller line, what was chosen and Change, no glyph. */
.coding-feedback-answer {
  margin: 0;
  font-size: 12px;
  color: var(--ink-3);
}
/* What went wrong? under Didn't work, a small select on its own line. */
.coding-feedback-more {
  display: block;
  margin: 0 0 4px;
  font-size: 12px;
  color: var(--ink-3);
}
.coding-feedback-more select {
  width: auto;
  max-width: 100%;
  margin-top: 4px;
}
.done-here button.link {
  min-height: 40px;
  padding: 0 2px;
  border: 0;
  background: none;
  color: var(--ink-3);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.done-here button.link:disabled {
  opacity: 0.55;
  cursor: default;
}
/* Post all: one primary under a card of several posts. */
.proposal-all {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 8px 0 4px;
}
.proposal-all button.pri {
  min-height: 40px;
  max-width: 100%;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 13.5px;
  font-weight: 500;
}
.proposal-all button.pri:disabled {
  opacity: 0.55;
}
.proposal-all .hint {
  margin: 0;
  font-size: 13px;
}
/* Done here: the quietest control on the screen, under the last answer. */
.done-here {
  margin: 4px 0;
  font-size: 13px;
}
.done-here button.link {
  color: var(--ink-3);
}
.done-here-line {
  margin: 4px 0;
  font-size: 13px;
}
/* The rest row (docs/work/2026-10-12-exploration-run-details-fold.md): Done
   at its left, Details at its right, which opens and closes the run details
   (the usage line and Saved activity) under it; one 40 px row whether Done
   shows or not. */
.rest-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 12px;
  min-height: 40px;
}
/* Drawn up into the last turn's margin only right under a turn: a waiting or
   following line above keeps its whole 40 px row to tap. */
.stream .column:not(:has(#waiting-line, #following-line)) .rest-row {
  margin-top: -10px;
}
.rest-row .done-here,
.rest-row .done-here-line {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
}
.run-details-toggle {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-height: 40px;
  margin-left: auto;
  padding: 0 2px;
  border: 0;
  background: none;
  color: var(--ink-3);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.run-details-toggle svg {
  width: 12px;
  height: 12px;
  transition: transform var(--motion-fast) var(--motion-ease);
}
.run-details-toggle[aria-expanded="true"] svg {
  transform: rotate(180deg);
}
.run-details-toggle:hover {
  color: var(--ink-2);
}
/* While the run details are closed the rest row is the history's last line,
   8 px above the composer, so the answer sits close to it; open, the column
   keeps its usual 16 px. */
.stream .column:has(#run-details[hidden]) {
  padding-bottom: 8px;
}
/* Where the history has no rest row (a page's Ask, On my to-do), a running
   turn's live line is its last line, 8 px above the composer as the rest
   row is on the exploration's own screen: the turn's own space under it,
   there to separate it from a next turn, and the column's would otherwise
   leave an empty band under the line
   (docs/work/2026-10-20-ask-sheet-run-line.md). Nothing is reserved for
   the answer: the history follows its end as it grows (follow-end.ts). */
.stream .column:not(:has(> .tail)):has(> .turn-assistant:is([data-status="running"], [data-status="queued"]):last-of-type) {
  padding-bottom: 8px;
}
.stream .column:not(:has(> .tail)) > .turn-assistant:is([data-status="running"], [data-status="queued"]):last-of-type {
  margin-bottom: 0;
}
.run-details > .usage-line {
  padding-top: 8px;
  border-top: 1px solid var(--line);
}
/* The waiting line (src/client/waiting-line.tsx): under the last turn, what
   the exploration waits on. The alarm clock pulses as the assistant's mark
   does while it runs (`.turn-assistant .mark.busy`); reduced motion stills it
   (the global rule above, and here, explicitly). One or two things are rows
   under it (`.waiting-list`), each label giving way with an ellipsis. */
.waiting {
  margin: 4px 0;
}
.waiting-line {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  height: 40px;
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: 13px;
  color: var(--ink-2);
}
/* An alarm clock in ink, pulsing as the dot it replaced did: something here
   will wake the conversation, where the Following line's eye stays still
   (user's pick, 2026-10-03, docs/work/2026-10-03-subscription-kind-icons.md). */
.waiting-mark {
  flex: none;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  color: var(--ink);
  animation: pulse 1.4s ease-in-out infinite;
}
.waiting-lead {
  flex: none;
}
.waiting-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  color: var(--ink);
}
/* How long a thing has left
   (docs/work/2026-10-03-waiting-line-time-left.md): metadata ink and size,
   never clipped; the label gives way first. */
.waiting-time {
  flex: none;
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-3);
  white-space: nowrap;
}
.waiting-toggle .waiting-time::before,
.waiting-list .waiting-time::before {
  content: "· ";
}
.waiting-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  height: 40px;
  color: var(--ink-2);
}
.waiting-toggle .waiting-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Inside the line's own clip, so the ring shows whole. */
.waiting-toggle:focus-visible {
  outline-offset: -2px;
}
.waiting-toggle[aria-expanded="true"] .glyph {
  transform: rotate(180deg);
}
.waiting-list {
  list-style: none;
  margin: 0 0 4px;
  padding: 0 0 0 14px;
}
.waiting-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 40px;
  font-size: 13px;
}
.waiting-list .waiting-label {
  flex: 0 1 auto;
  min-width: 0;
  font-weight: 400;
}
/* What kind of thing each row is (src/client/subscription-row.tsx): a 16 px
   glyph in the row's quiet ink, the same one the app draws for that thing
   elsewhere. */
.waiting-list .waiting-kind {
  flex: none;
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  color: var(--ink-3);
}
/* A label that opens its thing: a full-height tap target, with a quiet
   underline so it reads as a link on a phone too, where nothing hovers. */
.waiting-list a.waiting-open {
  display: block;
  line-height: 40px;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--line-2);
  text-underline-offset: 3px;
}
.waiting-list a.waiting-open:hover,
.waiting-list a.waiting-open:focus-visible {
  text-decoration-color: currentColor;
}
.waiting-list button.link {
  flex: none;
  min-height: 40px;
  padding: 0 2px;
  color: var(--ink-3);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.waiting-error {
  margin-top: 0;
}
@media (prefers-reduced-motion: reduce) {
  .waiting-mark {
    animation: none;
  }
}
/* The "Following" line (src/client/following-line.tsx): what the
   exploration follows passively, under the waiting line. Quiet where that
   one pulses: fainter ink, a still eye, collapsed to one 40 px line until
   tapped; the list it opens is the waiting list's. */
.following {
  margin: 0 0 4px;
}
.following-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  height: 40px;
  font-size: 13px;
  color: var(--ink-3);
}
.following-toggle:hover {
  color: var(--ink-2);
}
.following-toggle .following-glyph,
.following-toggle .glyph {
  flex: none;
  width: 14px;
  height: 14px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
}
.following-toggle[aria-expanded="true"] .glyph {
  transform: rotate(180deg);
}
.following-hint {
  margin: 0;
  padding: 0 0 0 14px;
  font-size: 13px;
  color: var(--ink-3);
}

/* The three-way splitting control in Settings -> Workspace. */
.segmented {
  display: flex;
  gap: 0;
  margin: 8px 0;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  overflow: hidden;
  max-width: 420px;
}
.segmented button {
  flex: 1 1 0;
  min-height: 40px;
  border: 0;
  border-left: 1px solid var(--line-2);
  background: var(--surface);
  color: var(--ink-2);
  font-size: 13.5px;
  font-weight: 500;
}
.segmented button:first-child {
  border-left: 0;
}
.segmented button.on {
  background: var(--ink);
  color: var(--paper);
}
/* One row at every width (docs/work/2026-09-23-usage-line-one-row.md): the
   figures give way before the ring's words do, first the token counts, which
   only a line of 560 px or more shows, then with an ellipsis. */
.usage-line {
  container: usage-line / inline-size;
  padding: 4px 0 10px;
  white-space: nowrap;
}
.usage-figures {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.usage-tokens {
  display: none;
}
@container usage-line (min-width: 560px) {
  .usage-tokens {
    display: inline;
  }
}
.usage ul {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
}
.usage li {
  padding: 8px 0;
  border-top: 1px solid var(--line);
  font-size: 13.5px;
  color: var(--ink-2);
}
.usage li strong {
  display: block;
  color: var(--ink);
  font-weight: 500;
}
.usage h3 {
  margin-top: 16px;
}
.usage details {
  margin-top: 16px;
}
.usage summary {
  cursor: pointer;
  font-weight: 600;
}
.usage details > button {
  margin-top: 8px;
}
.proposal-stats h3 {
  margin-top: 16px;
}
/* A small table that cannot always fit 320 px: it scrolls sideways within
   its own frame rather than wrapping or widening the settings panel, the
   same rule the code block above gives wide content. */
.proposal-stats-scroll {
  overflow-x: auto;
}
.proposal-stats table {
  border-collapse: collapse;
  font-size: 13px;
  white-space: nowrap;
}
.proposal-stats th,
.proposal-stats td {
  padding: 6px 10px 6px 0;
  text-align: right;
}
.proposal-stats thead th {
  color: var(--ink-3);
  font-weight: 500;
  border-bottom: 1px solid var(--line);
}
.proposal-stats tbody th {
  text-align: left;
  font-weight: 500;
  color: var(--ink-2);
}
.proposal-stats tbody tr + tr th,
.proposal-stats tbody tr + tr td {
  border-top: 1px solid var(--line);
}
.send {
  width: 40px;
  height: 40px;
  /* A circle, not whatever the row has left: without this the flex row
     shrinks the button to its glyph and 50% draws an ellipse. */
  flex: none;
  border-radius: 50%;
  background: var(--ink);
  color: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--motion-fast) var(--motion-ease);
}
/* The topic composer's primary, Send or Think: the Return glyph (Jean,
   2026-09-29) — the key that does the same thing, so the tooltip/aria-label
   still carries the word. 32 px is the documented dense-control exception
   (docs/design.md), shared with the Reply | Think switch beside it. Reply's
   fill is the base `.send`'s own `--ink`/`--paper` (Jean, 2026-09-30: "the
   send cta with the enter icon should be black in reply mode"), not set
   here — the same ink/paper pair the exploration composer's Send button
   and the Reply|Think toggle's own thumb already use, so it reads as the
   strongest control in both colour schemes without a rule of its own; Think
   still overrides it below. */
.send.pill {
  width: 32px;
  min-width: 32px;
  height: 32px;
  min-height: 32px;
  flex: none;
  padding: 0;
  border-radius: 999px;
}
/* Stop heard: the same circle at full strength with the spinner the voice
   stop also turns into, so it reads as working, not as switched off. */
.send.stopping:disabled {
  opacity: 1;
}
.send svg {
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.composer .error,
.composer p[role="status"] {
  padding: 4px 14px 0;
  font-size: 13px;
  color: var(--ink-3);
}
.composer .error {
  color: var(--danger);
}
.check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 36px;
  /* In a control row this is the item that gives way: its label wraps so the
     buttons keep their words. */
  min-width: 0;
  margin: 0;
  font-size: 14px;
  font-weight: 400;
  color: var(--ink-2);
}
.check input {
  width: 18px;
  min-height: 18px;
  height: 18px;
  flex-shrink: 0;
  accent-color: var(--ink);
}

/* ---------- pictures: staged in the composer, shown under a message ---------- */
/* The thumbnails above the box, one per staged picture, in every composer
   that takes them (the topic box, a thread's box, the exploration screen).
   The row exists only while something is staged, so the box keeps one height
   in both modes until a picture is actually there. */
.staged-images {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 0 2px;
}
.staged-images figure {
  position: relative;
  width: 76px;
  height: 76px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
}
.staged-images img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Dimmed while the picture is still on its way up; the send buttons wait too. */
.staged-images figure.uploading {
  opacity: 0.5;
}
/* A picture shared from the iPhone holds its place, with a spinner, until
   the app has handed it over (shared-pictures.ts). */
.staged-images figure.waiting {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-3);
}
.staged-images figure.waiting svg {
  width: 22px;
  height: 22px;
}
/* An upload that failed stays, dimmed, with Try again at its centre: a
   picture the person picked never disappears on its own. */
.staged-images figure.failed img {
  opacity: 0.4;
}
.staged-video.failed .staged-video-name {
  color: var(--ink-2);
}
.staged-video .retry {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
}
.staged-images .retry {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
}
.staged-images .retry svg,
.staged-video .retry svg {
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* 32 px, the same documented exception for dense secondary controls as the
   picker row: a larger circle would cover the thumbnail it sits on. */
.staged-images .remove {
  position: absolute;
  top: 2px;
  right: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
}
.staged-images .remove svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The pictures a post or a turn carries: containers of one shape, two per row
   on a phone, each a button that opens the full picture. The picture is
   fitted inside its container rather than cropped to fill it (user,
   2026-09-22: a screenshot cut to a strip says nothing), so a wide screenshot
   and a tall photo both show whole; the container keeps its 4:3 shape
   whatever the picture's own is (a GIF's own shape is square instead, below),
   and a line and a bed under the picture say where its edges are. A picture
   on its own takes the grid's full width: fitted into half of it, a
   screenshot would read smaller than the crop it replaced. */
.attachments {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  /* A turn on the exploration screen stacks its parts to the right
     (`.turn-user` is a flex column that does not stretch them; a think
     thread's own `.turn-user` is a plain block instead, transcript H1), so
     without a width of its own the grid is as wide as the picture inside it
     is: a small picture was a stamp there while the same picture filled the
     container in a topic. The width is what the block already had
     everywhere else. */
  width: 100%;
  max-width: 420px;
  margin-top: 8px;
}
.attachments > button {
  display: block;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--line-2);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
  line-height: 0;
}
/* A picture on its own takes the row rather than half of it, but three
   quarters of its width, not all: the height of a fitted box is its width
   over its ratio, so with the shape kept at 4:3 (user, 2026-09-22) the only
   way to spend less of the screen on one picture is to make it narrower.
   Three quarters is a quarter less height than the full row on either
   platform, and still half again the width of a picture in a pair.
   The dialog the open picture renders into is a sibling of a different type,
   so a lone picture is still on its own here. */
.attachments > button:only-of-type {
  grid-column: 1 / -1;
  width: 75%;
  justify-self: start;
}
/* A GIF's box is square, not 4:3, at the same height a picture's box has in
   the same spot (user, 2026-09-25: "GIFs should have a default container
   ratio that is square instead of four by three and the same height as
   images"; docs/work/2026-09-25-gif-square.md). Both dials stay width-driven,
   like every other box here, so a row's height never depends on which of its
   boxes is a GIF: alone, three quarters of a column's width (not the row's)
   makes a square exactly as tall as a lone picture's 0.75×0.75 box, and next
   to another picture, three quarters of its own column, centred in it, is
   exactly as tall as that picture's 0.75×column-width. */
.attachments > button.gif {
  aspect-ratio: 1;
}
.attachments > button.gif:only-of-type {
  width: 56.25%;
}
.attachments > button.gif:not(:only-of-type) {
  width: 75%;
  justify-self: center;
}
.attachments img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* A post's video in the feed: the poster frame at the video's own shape and
   a lone picture's height, with a play button over it. Tapping play plays
   the video in the tile itself, in the feed, with Slack-like controls; no
   byte is fetched before that tap (docs/work/2026-09-26-video-inline-play.md,
   following docs/work/2026-09-25-video-viewer.md). The height is a lone
   picture's box: 4:3 at three quarters of the row, so 56.25% of the row's
   width, taken from the row through a container unit. The width follows from
   the shape (the inline aspect-ratio, clamped to 9:16..16:9 in
   src/client/video-view.ts), so a poster that loads late widens the tile
   without moving the stream; the poster (or the playing video) is cropped to
   fill it, never letterboxed. */
.post-videos {
  container-type: inline-size;
  width: 100%;
  max-width: 420px;
  margin-top: 8px;
}
.video-tile {
  position: relative;
  display: block;
  height: 56.25cqw;
  width: auto;
  max-width: 100%;
  min-width: 0;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
  line-height: 0;
}
/* A browser without container units (Safari before 16) gets a picture's own
   box instead: 4:3 at three quarters of the row. */
@supports not (height: 1cqw) {
  .video-tile {
    width: 75%;
    height: auto;
    aspect-ratio: 4 / 3 !important;
  }
}
/* The poster button (before play) and the playing video fill the tile the
   same way. */
.video-open {
  display: block;
  width: 100%;
  height: 100%;
}
.video-tile img,
.video-tile video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The play button: a dark disc with a light triangle, readable on any poster
   and on the bed when there is none. Not a token: it sits on a picture, not
   on the page's surfaces. */
.video-play {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin: -24px 0 0 -24px;
  border-radius: 999px;
  background: rgb(0 0 0 / 55%);
  color: #fff;
}
.video-play svg {
  width: 24px;
  height: 24px;
  margin-left: 3px;
}
.video-open:hover .video-play,
.video-open:focus-visible .video-play {
  background: rgb(0 0 0 / 72%);
}
/* The viewer: the picture viewer's dialog, with a black stage for the video
   on every screen and the controls laid over its foot. */
.video-stage {
  position: relative;
  display: flex;
  flex: 1;
  min-height: 0;
  background: #000;
}
.video-stage video {
  /* A finger's vertical drag is the swipe that closes the viewer, not a pan
     of the page behind it (which would cancel the pointer). */
  touch-action: pan-x pinch-zoom;
  flex: 1;
  min-width: 0;
  min-height: 0;
  width: 100%;
  object-fit: contain;
}
.video-controls {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  padding: 24px 12px 8px;
  background: linear-gradient(to top, rgb(0 0 0 / 70%), rgb(0 0 0 / 0%));
  color: #fff;
  opacity: 1;
  transition: opacity 0.2s;
}
.video-stage.idle {
  cursor: none;
}
.video-stage.idle .video-controls {
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .video-controls {
    transition: none;
  }
}
/* The scrub bar: a 24 px hit area around a 4 px track, the buffered part in
   a lighter grey and the played part in white, with a thumb at the head. */
.video-scrub {
  position: relative;
  height: 24px;
  cursor: pointer;
  touch-action: none;
}
.video-scrub::before,
.video-scrub-buffered,
.video-scrub-played {
  position: absolute;
  top: 10px;
  left: 0;
  height: 4px;
  border-radius: 2px;
}
.video-scrub::before {
  content: "";
  right: 0;
  background: rgb(255 255 255 / 25%);
}
.video-scrub-buffered {
  background: rgb(255 255 255 / 45%);
}
.video-scrub-played {
  background: #fff;
}
.video-scrub-thumb {
  position: absolute;
  top: 6px;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 999px;
  background: #fff;
}
.video-buttons {
  display: flex;
  align-items: center;
  gap: 4px;
}
.video-buttons .icon-btn,
.video-speed {
  color: #fff;
}
.video-buttons .icon-btn:hover:enabled,
.video-speed:hover {
  background: rgb(255 255 255 / 14%);
}
.video-buttons svg {
  width: 20px;
  height: 20px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.video-time {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.video-spacer {
  flex: 1;
}
.video-speed {
  min-width: 44px;
  height: 36px;
  border-radius: 8px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.video-volume {
  width: 72px;
  accent-color: #fff;
}
/* A phone has hardware volume: the slider would only crowd the row. */
@media (pointer: coarse) {
  .video-volume {
    display: none;
  }
}
/* The staged video above the box: its name, how far its upload has got, and
   one button that cancels it or takes it off (32 px, the staged pictures'
   dense-control exception). */
/* Files (PDF, text) on a message: staged chips above the box while they
   upload and are read, and the same chips under a sent message, each opening
   its file (docs/work/2026-09-28-exploration-files.md). */
.staged-files,
.file-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 4px 0 2px;
}
.file-chips {
  justify-content: flex-end;
  margin-top: 6px;
  max-width: min(85%, 560px);
}
.file-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  min-height: 44px;
  padding: 6px 6px 6px 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--ink);
  font-size: 13px;
  text-decoration: none;
}
a.file-chip {
  padding-right: 12px;
}
a.file-chip:hover {
  border-color: var(--line-2);
}
.file-chip > svg {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  stroke: var(--ink-2);
  fill: none;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.file-chip-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.file-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}
.file-chip-meta {
  color: var(--ink-3);
  font-size: 12px;
}
.file-chip-meta progress {
  width: 72px;
  height: 6px;
  accent-color: var(--ink);
}
.file-chip.failed {
  border-color: var(--danger);
  background: var(--danger-bg);
}
.file-chip.failed .file-chip-meta {
  color: var(--danger);
  white-space: normal;
}
.file-chip .retry {
  flex: 0 0 auto;
  min-height: 40px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  background: var(--surface);
  color: var(--ink);
  font-size: 13px;
}
.file-chip .remove {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
}
.file-chip .remove svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.staged-video {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 2px;
  padding: 6px 6px 6px 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  font-size: 13px;
}
.staged-video-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.staged-video progress {
  flex: 0 0 72px;
  width: 72px;
  height: 6px;
  accent-color: var(--ink);
}
.staged-video .remove {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
}
.staged-video .remove svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The full picture, over the screen, as a page's Open shows a page. On a
   phone it sits on solid black (not a token: no token in docs/design.md
   covers this, and one rule does not earn a new one) so its edges read
   against the background rather than against --paper, in light and dark
   mode alike; desktop below uses --surface-3, a light grey stage, for the
   same reason — a white image's edges must read against it too. */
dialog.image-view {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: #000;
  color: #fff;
}
dialog.image-view::backdrop {
  background: #000;
}
dialog.video-view {
  overscroll-behavior: contain;
}
.image-view-frame {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  padding-bottom: var(--safe-bottom);
}
.image-view-head {
  display: flex;
  justify-content: flex-end;
  flex: none;
  min-height: var(--bar);
  padding: var(--safe-top) 8px 0;
}
/* --ink-2 is too faint on black; the close button gets a fixed light
   colour here instead of the token. */
.image-view-head .icon-btn {
  color: #fff;
}
.image-view-head .icon-btn:hover:enabled {
  background: rgb(255 255 255 / 14%);
  color: #fff;
}
/* The stage takes every touch itself (the picture viewer zooms, see
   image-zoom-stage.tsx); the picture is scaled about its centre. */
.image-view-stage {
  display: flex;
  flex: 1;
  min-height: 0;
  padding: 0 8px 8px;
  overflow: hidden;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.image-view-stage img {
  flex: 1;
  min-width: 0;
  min-height: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform-origin: center;
}


/* ---------- think mode ---------- */
.mode-seg {
  position: relative;
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: var(--surface-2);
}
/* The "on" pill: one thumb sliding behind the two labels instead of the
   background jumping from one button to the other. Its geometry (2px inset,
   half the segment's width less the shared 2px gap) is the same math the
   `.think` transform below shifts it by, so the thumb's right edge always
   meets the segment's own 2px padding. */
.mode-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: calc(50% - 3px);
  height: calc(100% - 4px);
  border-radius: 999px;
  background: var(--ink);
  transform: translateX(0);
  transition:
    transform var(--motion-fast) var(--motion-ease),
    background-color var(--motion-fast) var(--motion-ease);
  pointer-events: none;
}
.mode-seg.think .mode-thumb {
  transform: translateX(calc(100% + 2px));
  background: var(--think);
}
.mode-seg button {
  position: relative;
  z-index: 1;
  flex: 1 1 0;
  min-height: 32px;
  padding: 0 14px;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-2);
  font-size: 13.5px;
  font-weight: 500;
  /* flex-basis: 0 shrinks a button's automatic minimum size to its
     min-content, which wraps "Reply" onto two lines; nowrap keeps the word
     whole and the thumb's 50% math (above) still lands under it. */
  white-space: nowrap;
  transition: color var(--motion-fast) var(--motion-ease);
}
.mode-seg button.on {
  color: var(--paper);
}
.composer-box.think {
  border-color: var(--think);
  background: var(--think-bg);
  box-shadow: 0 0 0 4px var(--think-ring);
}
.composer-box.think .send {
  background: var(--think);
}
/* What a rewrite replaced, read-only under the review line: the body font, so
   it reads as the person's own words, wrapped so nothing widens the column. */
#original {
  max-height: 22dvh;
  margin: 0 0 8px;
  padding: 8px 10px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--ink-2);
  font-family: inherit;
  font-size: 13.5px;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* Mounted fresh each time the mode or the docked thread changes (a plain
   span, or the attached-label button, replacing one another), so a mount-in
   animation is what crosses this label between its states rather than it
   snapping into place. */
@keyframes priv-label-in {
  from {
    opacity: 0;
    transform: translateY(2px);
  }
}
.priv-label {
  font-size: 12px;
  color: var(--think);
  animation: priv-label-in var(--motion-fast) var(--motion-ease) both;
}
.review-line {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 0 6px;
  font-size: 12px;
  color: var(--ink-2);
}
.review-line button {
  color: var(--ink);
  font-weight: 500;
}
/* The inline thread only (the composer box carries the same class and keeps
   its own frame): a rounded tint with no border. */
section.think {
  margin: 8px 0 8px 38px;
  padding: 10px 14px 12px;
  border-radius: 10px;
  background: var(--think-bg);
  /* Its bottom corners square off when the composer docks onto it
     (`.attached` below): the same clock as the box it joins to. */
  transition: border-radius var(--motion-fast) var(--motion-ease);
}
@media (max-width: 600px) {
  section.think {
    margin-left: 0;
  }
}
.think-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--ink-2);
}
/* On a phone the row is tight: the meta line wraps, and the pill and the two
   buttons keep their words whole (they broke mid-word at 375 px). */
.think-head .only,
.think-head button {
  flex: none;
  white-space: nowrap;
}
.think-head .only {
  display: inline-block;
  padding: 0 8px;
  border-radius: 999px;
  background: var(--think);
  color: var(--paper);
  font-size: 11px;
  font-weight: 600;
  line-height: 18px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.think-head .spacer {
  flex: 1;
}
.think-head button {
  color: var(--think);
  font-weight: 500;
}
/* The thread the composer continues: the composer's Think box is its reply
   field, docked onto it as its footer (docs/work/2026-09-24-think-composer.md).
   Which thread, if any, follows the reader's place in the feed; the label
   names it and a tap brings it back into view. */
/* The label gives way to the Reply | Think switch, never the other way. */
.composer-row .mode-seg {
  flex-shrink: 0;
}
.attached-label {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 32px;
  margin-left: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}
/* On a phone the blue box already says it is private; the room goes to the
   thread's name. */
@media (max-width: 600px) {
  .attached-label .priv-prefix {
    display: none;
  }
}
.attached-label strong {
  font-weight: 600;
}
/* Docked: the thread and the box are one card, the box its footer. Nothing
   here changes a height: whether the box is docked follows the reader's
   scroll, and a feed that resized as they scrolled would move under them (and
   the keep-at-the-end logic would read it as the conversation growing). So
   the gap between the thread and the box, its margin and the paddings of the
   feed and the composer, is painted over with the thread's tint instead of
   being removed, and only while the thread's end is what fills that gap
   (`flush`, src/client/think-anchor.ts): mid-thread, or with a post peeking
   in, the tint would cover what the reader is looking at. */
section.think.attached {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.composer-box.attached,
.composer-box.attached:focus-within {
  border-color: transparent;
  border-top-color: var(--think-line);
  border-radius: 0 0 10px 10px;
  /* Not `none`: transitioning box-shadow to `none` cannot interpolate, so the
     ring would jump instead of shrinking. A same-shaped shadow at zero spread
     fades out along the same curve as everything else here. */
  box-shadow: 0 0 0 0 var(--think-ring);
}
/* Always present so its opacity can cross-fade rather than the tint popping
   in; harmless off-flow the rest of the time (docs/work/2026-09-25-composer-
   mode-motion.md). */
.composer-box::before {
  content: "";
  position: absolute;
  left: -1px;
  right: -1px;
  bottom: 100%;
  /* The thread's bottom margin, the feed's bottom padding and the
     composer's top padding, plus the box's own top border. */
  height: calc(8px + 16px + 8px + 1px);
  background: var(--think-bg);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-fast) var(--motion-ease);
}
.composer-box.flush::before {
  opacity: 1;
}
@media (min-width: 601px) {
  /* Narrows the box's own content width, so it can — for a draft sitting
     exactly at the 38px-wide margin between these two widths — rewrap onto
     an extra line and change the box's height. Pre-existing (an instant jump
     before this file animated it, reproduced and left as a known limitation
     in docs/work/2026-09-25-composer-mode-motion.md): fixing it means either
     giving up this inset or decoupling the textarea's wrap width from the
     box's own, and neither is this change's to decide. */
  .composer-box.attached {
    margin-left: 38px;
  }
}
.share-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
}
.share-actions button {
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 500;
}
/* `.share-actions` is the exploration screen's own "Promote to recap" card
   (transcript.tsx), which is not think mode, so its think-blue border and
   fill are scoped to a think thread ancestor rather than given to the bare
   class. */
section.think .share-actions button {
  border-color: var(--think-line);
}
/* Under a think thread's answer: "Draft a reply" at the left, Done at the right
   (docs/work/2026-10-02-think-actions-calm.md). */
.think-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  margin-top: 6px;
}
/* The chevron that folds a thread (and unfolds its stub): an icon where the
   words Collapse and Expand stood, with a 40 px target. */
.think-chev {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
}
.think-chev::after {
  content: "";
  position: absolute;
  inset: -6px;
}
.think-chev svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* "Draft a reply": a neutral pill at the foot's left, Done at its right (blue
   is think mode, and the block's tint already is). It shows on the latest
   answer while the thread is open, and steps aside while a draft from this
   thread is in the composer. */
.draft-reply {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 12px 0 10px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 13px;
  font-weight: 500;
}
.draft-reply::after {
  /* A 40 px target around the 32 px pill. */
  content: "";
  position: absolute;
  inset: -4px;
}
.draft-reply svg {
  flex: none;
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.draft-reply:disabled {
  opacity: 0.7;
}
/* Drafting: the shimmer is the signal, so the pill keeps its full strength. */
.draft-reply[aria-busy="true"]:disabled {
  opacity: 1;
}
/* Done, in the foot: the quietest control, at the right edge. */
.done-here-row {
  display: contents;
}
.done-here-row button.link {
  position: relative;
  margin-left: auto;
  min-height: 32px;
  padding: 0 4px;
  text-decoration: none;
  font-size: 13px;
}
.done-here-row button.link::after {
  content: "";
  position: absolute;
  inset: -4px;
}
.done-here-row button.link:hover:not(:disabled) {
  color: var(--ink);
}
.done-here-row .error {
  flex-basis: 100%;
  order: 2;
  margin: 0;
}
/* A next step this thread proposed, waiting on the exploration screen where
   its card is. A line, not a card: the inset thread has no destination picker
   and nowhere to navigate to, so it says an offer is there and hands over.
   Its button is the only way through, so it is a real target — 40 px, as the
   design rules require — rather than underlined words inside a sentence. */
.proposal-waiting {
  margin: 10px 0 0;
}
.proposal-waiting button {
  min-height: 40px;
  padding: 0 12px;
  border: 1px solid var(--think-line);
  border-radius: 999px;
  color: var(--think);
  font-weight: 500;
  /* Its own line on a phone, so the sentence around it never squeezes it. */
  display: inline-flex;
  align-items: center;
  margin: 6px 0;
}
/* The same offer on a collapsed stub, which reads nothing of the thread: the
   list already knows one is pending, and Expand beside it is the way in. */
.think-stub-waiting {
  color: var(--think);
  font-weight: 500;
}
/* The thread's answer is waiting: bold in the stub's own Think blue, the
   sidebar's cue for an answer, so the thread holding it is found at a glance. */
.think-stub-answer {
  color: var(--think);
  font-weight: 600;
}
/* The recap "Share thinking" (a think thread) or "Promote to recap" (an
   exploration page) drafted, under the exits: hairlines on the plain
   surface, because the blue fill belongs to think mode's composer and the
   thread's border, not to everything inside it — and a page's card is not
   think mode at all, so it keeps the plain hairline always. */
.share-block {
  margin-top: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
section.think .share-block {
  border-color: var(--think-line);
}
.share-head {
  font-size: 12px;
  color: var(--ink-2);
}
.share-text {
  margin-top: 6px;
  font-size: 14px;
}
.share-text > :first-child {
  margin-top: 0;
}
/* The same 32 px dense controls as the exits above them (docs/design.md).
   Ink-filled outside think mode, like any other primary pill; a think
   thread's own recap keeps the think fill (below). */
.share-actions button.pri {
  border-color: transparent;
  background: var(--ink);
  color: var(--paper);
}
section.think .share-actions button.pri {
  background: var(--think);
}
.think-stub {
  /* Padded and rounded so the arrival tint paints as a think surface does,
     rather than as a tight rectangle behind one line of text. */
  padding: 3px 8px;
  border-radius: 10px;
  font-size: 12.5px;
  color: var(--think);
}
.think-stub button {
  color: var(--think);
  font-weight: 500;
}

/* ---------- disclosures ---------- */
.disclosure {
  border-top: 1px solid var(--line);
}
.disclosure > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-2);
}
.disclosure > summary::-webkit-details-marker {
  display: none;
}
.disclosure > summary::before {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s;
  margin: 0 4px;
  flex: none;
}
.disclosure[open] > summary::before {
  transform: rotate(45deg);
}
.disclosure > summary .count {
  margin-left: auto;
  font-weight: 400;
  color: var(--ink-3);
}
.disclosure-body {
  padding: 0 0 14px;
}
.panel-tail {
  flex: none;
  border-top: 1px solid var(--line);
  background: var(--paper);
}
.panel-tail .disclosure {
  border-top: 0;
}
.panel-tail .disclosure + .disclosure {
  border-top: 1px solid var(--line);
}
.activity-table {
  max-height: 40dvh;
  overflow: auto;
  font-size: 13px;
}
.activity-table details {
  border-top: 1px solid var(--line);
}
.activity-table summary {
  min-height: 36px;
  display: flex;
  align-items: center;
  cursor: pointer;
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: 12px;
}
.activity-table pre {
  margin: 0 0 8px;
  padding: 8px;
  border-radius: 8px;
  background: var(--surface-2);
  font-size: 12px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---------- topic ---------- */
/* ---------- the wait before a topic's posts ----------
   The posts' own shape, tinted and shimmering: a person reads the layout of
   what is coming instead of a line of text. The sweep is one background
   animation on each block, so nothing reflows; the reduced-motion rule near
   the top of this sheet stops it, and the tint alone still says "not yet". */
.skeleton-post .avatar {
  /* The longhand, not the shorthand: `background` here would reset the
     gradient below to none and leave an animation with nothing to move. */
  background-color: var(--surface-2);
  /* A post's avatar is a circle; the block radius below must not square it. */
  border-radius: 50%;
}
.skeleton-block {
  display: block;
  border-radius: 6px;
  background: var(--surface-2);
}
.skeleton-line {
  /* Two stops of the page's own greys, swept across twice the width. The
     middle one is its own token because the sheet's `--line` is seven values
     from `--surface-2` in dark mode, which is not a sweep anyone can see. */
  background-image: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--skeleton-sweep) 40%,
    var(--surface-2) 80%
  );
  /* The same band period and keyframes as the busy text. */
  background-size: 360px 100%;
  background-repeat: repeat-x;
  animation: shimmer-sweep var(--shimmer-speed) linear infinite;
}
.skeleton-name {
  width: 92px;
  height: 12px;
  margin: 3px 0;
}
.skeleton-text {
  height: 11px;
  margin: 7px 0;
}
/* ---------- the wait before a workspace's sidebar ----------
   Same shimmer as above: placeholders in the sidebar's own shape while a
   workspace switch's explorations and topics are on their way, instead of
   the empty list a switch used to flash before the real ones arrived. These
   come after .skeleton-block/.skeleton-line above, so their own sizing (a
   16px avatar-shaped bar over a 6px-radius block, say) is not overridden by
   it: two same-specificity single-class rules resolve to whichever is later
   in the file. */
.sidebar-skeleton-head {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  min-height: 40px;
  padding: 6px 8px;
}
.skeleton-workspace-name {
  height: 16px;
  width: 55%;
}
#sidebar-skeleton .skeleton-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 6px 10px;
}
.skeleton-glyph {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  flex: none;
}
.skeleton-row-label {
  height: 16px;
}
.post {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  gap: 10px;
  padding: 8px 0;
}
.post .avatar {
  margin-top: 2px;
}
/* A post that continues the group above it (Slack's grouping,
   src/client/post-groups.ts) has no avatar, name or time: its text keeps the
   group's column, it sits about 4 px under the post above (the normal gap is
   16 px), and its clock waits in the avatar's gutter, shown with a mouse on
   hover or keyboard focus, in the head's 11 px --ink-3. The clock is right
   aligned and may run into the column's margin, as Slack's does. */
.post.continued {
  position: relative;
  margin-top: -6px;
  padding-top: 2px;
}
.post-gutter {
  position: relative;
  align-self: start;
}
.post-gutter time {
  position: absolute;
  top: 2px;
  right: 0;
  white-space: nowrap;
  font-size: 11px;
  line-height: inherit;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  opacity: 0;
}
@media (hover: hover) and (pointer: fine) {
  .post.continued:hover .post-gutter time,
  .post.continued:focus-within .post-gutter time {
    opacity: 1;
  }
}
/* A continued head with nothing but the hover toolbar (React and **…**)
   takes no line: the toolbar floats at the post's top right, inside the
   post's own box so it never covers the post above, and the text keeps
   clear of it. */
.post .head.bare {
  position: absolute;
  top: 0;
  right: 0;
}
.post .head.bare .post-tools {
  margin: 0;
}
@media (hover: hover) and (pointer: fine) {
  .post .head.bare ~ .text {
    /* Three 32px buttons and the toolbar's border: React, Save, …. */
    padding-right: 104px;
  }
}
/* With no name or time before it, a continued post's first note needs no
   leading dot. */
.post.continued .head > .ai:first-child::before {
  content: none;
}
.post .head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}
.post .head strong {
  font-weight: 600;
}
.post .text {
  margin-top: 2px;
}
.post .text > :first-child {
  margin-top: 0;
}
.post .text > :last-child {
  margin-bottom: 0;
}
.post.post-proposal .text,
.post.post-recap .text {
  margin-top: 6px;
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.post.post-recap .text {
  background: var(--surface-3);
  border-color: transparent;
}
/* A long message or a published recap, folded: one card (Jean, 2026-10-21:
   "the same visual entity"), holding its title (two lines at most), its
   opening lines fading out, its `##` sections as a row of chips and Read
   more, which opens the whole post in the reader
   (docs/work/2026-10-21-long-posts-fold.md,
   docs/work/2026-10-21-recaps-long-post-card.md). A recap keeps its RECAP
   badge, its author line and its Source under the card; the card itself is
   the same, so the recap's own tint is set back here. Tokens only. */
.post .long-post,
.post.post-recap .text.long-post {
  margin-top: 6px;
  padding: 12px;
  border-radius: var(--radius);
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.post .long-post .long-post-title {
  margin: 0;
  font-size: 15px;
  line-height: 1.45;
  letter-spacing: normal;
  font-weight: 600;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
/* A title holding a live link is never clamped: the clamp would paint the
   anchor nowhere while Tab still reached it (measured 2026-09-21). */
.post .long-post .long-post-title.unclamped {
  -webkit-line-clamp: none;
  line-clamp: none;
}
/* The opening lines under the title, fading out into the card at the third
   line, as a folded long prompt does in Claude (Jean, 2026-10-21). Hidden
   from assistive technology: the reader has the whole text, and Read names
   it. */
.post .long-post .long-post-preview {
  margin-top: 4px;
  max-height: calc(3 * 1.5em);
  overflow: hidden;
  line-height: 1.5;
  color: var(--ink-2);
  -webkit-mask-image: linear-gradient(to bottom, #000 35%, transparent);
  mask-image: linear-gradient(to bottom, #000 35%, transparent);
}
.post .long-post .long-post-preview > * {
  margin: 0 0 0.4em;
}
.post .long-post .long-post-outline {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
}
.post .long-post .long-post-outline li {
  margin: 0;
  min-width: 0;
  max-width: 100%;
}
/* On a phone the outline keeps to one row, a +N chip for the rest
   (`useOneRow` in long-post-view.tsx); `measuring` lays every chip out on
   that row at its own width for the count, the first frame only. The one
   chip left when even it barely fits shrinks to an ellipsis. */
@media (max-width: 559px) {
  .post .long-post .long-post-outline {
    flex-wrap: nowrap;
    overflow: hidden;
  }
  .post .long-post .long-post-outline li {
    flex: 0 1 auto;
  }
  .post .long-post .long-post-outline li.more,
  .post .long-post .long-post-outline.measuring li {
    flex: none;
  }
}
.long-post-outline button,
.reader-outline button {
  max-width: 100%;
  min-height: 30px;
  padding: 4px 10px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font: inherit;
  font-size: 13px;
  line-height: 1.3;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}
.long-post-outline button:hover,
.reader-outline button:hover {
  color: var(--ink);
  background: var(--surface-3);
}
/* Read more is the card's way in, the same quiet text as a recap's Read
   more (Jean, 2026-10-21: "Read more would be better than read + icon"). */
.post .long-post .long-post-read {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  margin: 6px 0 -6px -6px;
  padding: 0 6px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--ink-2);
  font: inherit;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
}
.post .long-post .long-post-read:hover {
  color: var(--ink);
  background: var(--surface-3);
}
/* A published recap's cover, at the top of its card above the title, its
   frame known before the picture loads (from the size the topic's read
   carries; docs/work/2026-11-03-recap-cover-picture.md). The kept-page cover
   rule (docs/work/2026-10-22-changelog-covers.md): a picture at least as wide
   as tall fills a 16:10 frame anchored top right; a taller one (a phone's
   screenshot) is never cut to its top strip but drawn whole, at most 360 px
   tall, on the bed in a --line-2 border, as a post's pictures are. A wide
   frame stops at 240 px tall (below). */
.recap-cover {
  display: block;
  width: 100%;
  padding: 0;
  margin: 0 0 10px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-3);
  line-height: 0;
  cursor: zoom-in;
  aspect-ratio: 16 / 10;
  /* At a desktop card's width 16:10 is ~380 px, so the card no longer fits
     the screen: the frame stops at 240 px and crops more, still top right.
     A phone's card (~180 px) is under it. */
  max-height: 240px;
}
.recap-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: right top;
}
.recap-cover[data-shape="tall"] {
  height: 360px;
  max-height: none;
  aspect-ratio: auto;
  border-color: var(--line-2);
}
.recap-cover[data-shape="tall"] img {
  object-fit: contain;
  object-position: center;
}
/* In the reader the cover is drawn whole at its own shape (set inline), as
   wide as the page, never taller than most of the screen. */
.recap-cover[data-place="reader"] {
  height: auto;
  max-height: 70vh;
  margin: 0 0 20px;
}
.recap-cover[data-place="reader"] img {
  object-fit: contain;
  object-position: center;
}
/* The reader's sections: one row under the head that scrolls sideways, so
   it stays in reach wherever the page is. */
.reader-outline {
  flex: none;
  display: flex;
  gap: 6px;
  padding: 8px 14px;
  overflow-x: auto;
  scrollbar-width: none;
  border-bottom: 1px solid var(--line);
}
.reader-outline button {
  flex: none;
  max-width: 70vw;
}
.post-reader h2 {
  scroll-margin-top: 16px;
}
/* The reader's author line is a post's meta line, not a file
   name: the app's sans at the meta size, not the file page's mono. */
.post-reader-root .file-name {
  font: 400 12px/1.3 var(--font-sans);
  color: var(--ink-3);
}
/* The one address a published recap offers: the source the server attached to
   the post, shown as its host. Small and quiet, under the words, because it is
   a way to check the entry rather than part of it. On an automation's entry it
   is the only anchor on the card: the body's own links render as text. */
.post-source {
  margin: 6px 0 0;
  font-size: 11px;
  color: var(--ink-3);
  overflow-wrap: anywhere;
  min-width: 0;
}
/* Where the reader stopped last time: a hairline with a centred label. The
   rules flex, so the row can never be wider than the column. */
.new-posts {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 2px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.new-posts::before,
.new-posts::after {
  content: "";
  flex: 1 1 0;
  min-width: 0;
  height: 1px;
  background: currentcolor;
  opacity: 0.35;
}
.badge {
  display: inline-block;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 11px;
  font-weight: 600;
  line-height: 18px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
/* Reactions: the React glyph at the right of the author line (a 32 px hit
   area whose overflow sits in the head's line, so the head grows by nothing),
   then, only when there is something to show, a row under the text: chips,
   the viewer's own in the think tint, and the palette: six quick emojis and
   a plus for any other. Every control
   here is the 32 px dense-control exception (docs/design.md), never smaller.
   With a mouse the glyph and the **…** beside it form the hover toolbar,
   shown on hover, on focus within the post and while one of them has its
   menu open (user, 2026-09-24: the post's actions behind **…**); on touch it
   is not shown and a long press on the post opens the message sheet (below). */
.post .head .post-tools {
  position: relative;
  display: flex;
  align-items: center;
  margin: -6px 0 -6px auto;
}
.post .head .react-btn,
.post .head .save-btn,
.post .head .more-btn {
  width: 32px;
  height: 32px;
  color: var(--ink-3);
}
.post .head .react-btn svg,
.post .head .save-btn svg,
.post .head .more-btn svg {
  width: 16px;
  height: 16px;
}
/* In For me: the button in ink (docs/work/2026-11-04-for-me-done-states.md). */
.post .head .save-btn.on {
  color: var(--ink);
}
/* In For me: the small muted line above a post in the reader's For me. */
.post .saved-line {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 0 0 2px;
  color: var(--ink-3);
  font-size: 12.5px;
  font-weight: 500;
}
.post .saved-line svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
@media (hover: hover) and (pointer: fine) {
  .post .head .post-tools {
    border: 1px solid transparent;
    border-radius: 9px;
    opacity: 0;
    transition: opacity 120ms;
  }
  .post:hover .head .post-tools,
  .post:focus-within .head .post-tools,
  .post .head .post-tools.open,
  .post .head .post-tools:has([aria-expanded="true"]) {
    opacity: 1;
    border-color: var(--line-2);
    background: var(--surface);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  }
}
/* On touch the toolbar is not shown at all: a long press on the post opens
   the sheet (src/client/topic.tsx; user, 2026-09-19). Its buttons stay in
   the document, visually hidden, so a screen reader still finds React and
   Message actions on every post; they take no room in the head. */
@media not ((hover: hover) and (pointer: fine)) {
  .post .head .react-btn,
  .post .head .save-btn,
  .post .head .more-btn {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .post .head {
    position: relative;
  }
}
/* While a long press is under way, and while its menu is open with the
   finger possibly still down, the post refuses the selection and the callout
   a held finger would otherwise begin: WebKit's own hold may fire a moment
   after our 500 ms timer, when `pressing` alone has already gone. On touch
   layouts the post refuses them always (below): the menu's Copy is how a post
   is copied there, and a rule set only when the finger lands leaves WebKit a
   race it was seen to win (user, 2026-09-22: the slide scrolled the list).
   The Copy fallback lets the text go for a moment before selecting it
   (topic.tsx), since Safari refuses a scripted selection under none. */
.post.pressing,
.post.lifted {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
@media not ((hover: hover) and (pointer: fine)) {
  .post {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  /* A link or a picture keeps the platform's own long-press menu. */
  .post a,
  .post img {
    -webkit-touch-callout: default;
  }
}
/* The gesture log (src/client/gesture-log.ts), over everything, for a phone
   with `?gesture=log` on its address. */
.gesture-log {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  max-height: 45vh;
  overflow: auto;
  margin: 0;
  padding: calc(var(--safe-top) + 6px) 8px 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.3;
  white-space: pre-wrap;
  background: rgba(0, 0, 0, 0.82);
  color: #fff;
  pointer-events: none;
}
/* The long-press menu (src/client/topic.tsx; user, 2026-09-22): the post
   lifts above a scrim that dims the rest of the main surface, the way the
   phone's chat apps present a message's reactions and actions. The scrim is
   the post's sibling, fixed: `.main` carries a transform, so it fills the
   main surface (the viewport on a phone), and it takes no scroll. Stacking:
   the scrim at 40, the lifted post at 41, the message sheet at 42 (also
   `.main`'s sibling, not the lifted post's child — see `.message-sheet`
   below), all in `.main`'s context, above the top bar and the composer. */
.post-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(0, 0, 0, 0.35);
  touch-action: none;
  animation: post-scrim-in 120ms ease-out;
}
@keyframes post-scrim-in {
  from {
    opacity: 0;
  }
}
.post {
  transition: box-shadow 120ms ease-out;
}
/* Room at the stream's end for the sheet while a post is lifted, so the
   newest post can scroll up above it (--sheet-room, set by topic.tsx). */
.stream:has(.post.lifted) {
  padding-bottom: var(--sheet-room, 0px);
}
.post.lifted {
  position: relative;
  z-index: 41;
  margin: 0 -8px;
  padding: 8px;
  border-radius: 12px;
  background: var(--surface);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
  /* No transform: the message sheet inside it is fixed to the main
     surface, which a transformed ancestor would take over. The list stays
     put while the menu is open, as a native one does; pointer events still
     fire, so the second long press closes it. */
  touch-action: none;
}
/* Lifting a continued post keeps its text where it sat: the lifted padding
   (8 px) grows upward over the 2 px it had. */
.post.continued.lifted {
  margin-top: -12px;
}
/* The message sheet (src/client/message-menu.tsx): WhatsApp's long-press
   shape, fixed to the foot of the main surface above the scrim and the
   lifted post, both of whose stacking context it shares (the scrim's
   sibling in topic.tsx, not the lifted post's child — a WebKit fixed-clip
   quirk otherwise cuts it off at the stream's scroll box,
   docs/work/2026-09-24-message-sheet-clip.md): the quick row, then the
   actions as 48 px rows, destructive ones in the danger colour, then
   Cancel. */
.message-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 42;
  max-width: 560px;
  margin: 0 auto;
  padding: 6px 12px calc(10px + var(--safe-bottom));
  background: var(--surface);
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
  animation: message-sheet-in 160ms ease-out;
  outline: none;
}
@keyframes message-sheet-in {
  from {
    transform: translateY(100%);
  }
}
.message-sheet-grip {
  width: 36px;
  height: 4px;
  margin: 0 auto 8px;
  border-radius: 2px;
  background: var(--line-2);
}
.message-sheet .palette {
  justify-content: space-between;
  padding: 2px 0 8px;
}
.message-sheet-actions {
  padding: 4px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.message-sheet-actions button,
.message-sheet-cancel {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 12px;
  min-height: 48px;
  padding: 10px;
  border-radius: 10px;
  text-align: left;
  font-size: 16px;
}
.message-sheet-cancel {
  justify-content: center;
  margin-top: 6px;
  font-weight: 600;
}
.message-sheet-actions button svg,
.message-menu button svg {
  width: 18px;
  height: 18px;
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The **…** menu (src/client/message-menu.tsx) is fixed to the viewport and
   placed by script beside its toolbar: an absolute one made the feed's
   scroll box taller and moved it (docs/work/2026-10-19-post-menu-no-scroll.md).
   Its own class: the coding card's menus share `.message-menu`'s look but
   stay absolute. */
.menu.fixed-menu {
  position: fixed;
  width: max-content;
  overflow-y: auto;
}
.message-menu button svg {
  width: 16px;
  height: 16px;
  color: var(--ink-3);
}
.message-sheet-actions button:active,
.message-sheet-cancel:active {
  background: var(--surface-2);
}
.message-sheet-actions button.danger,
.message-menu button.danger,
.message-menu button.danger svg {
  color: var(--danger);
}
@media (prefers-reduced-motion: reduce) {
  .message-sheet {
    animation: none;
  }
}
/* A deleted message: one quiet line where its words were
   (docs/work/2026-09-24-delete-message.md). */
.post-deleted:focus {
  outline: none;
}
.post .deleted {
  margin: 0;
  font-size: 13px;
  font-style: italic;
  color: var(--ink-3);
}
/* Copied, or selected for a manual copy, said under the post for a moment;
   empty (and without height or margin) the rest of the time. */
.post .copied {
  margin: 6px 0 0;
  /* A link to copy by hand is one long word: it wraps inside a phone's width. */
  overflow-wrap: anywhere;
}
.post .copied:empty {
  margin: 0;
}
.reactions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
.reaction,
.reaction-add {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font-size: 14px;
  line-height: 30px;
}
/* Who reacted: a mouse hovering the chip, the keyboard focusing it, or a
   touch long press (reaction-chip.tsx). Its own text already carries the
   emoji and names, and the chip's aria-label carries the same words for a
   screen reader, so the popover itself is decorative.
   `width: max-content` is load-bearing, not decoration: with `width: auto`
   (the default) an absolutely positioned box with only `left` set (no
   `right`) is shrink-to-fit against the *available space in its containing
   block* — here `.reaction`, the chip itself, often under 60px wide — so
   the popover collapsed to a sliver and a long name wrapped one or two
   letters per line (Jean's iPhone screenshot, 2026-09-28). `max-content`
   sizes from the text's own preferred width instead, still capped by
   `max-width` below. `overflow-wrap: normal` overrides the inherited
   `anywhere` every `button` gets (this span's ancestor is one), which is
   what let a name break mid-word in the first place; `white-space: nowrap`
   plus the ellipsis pair keep it to one line, truncating only a name that
   still does not fit at the viewport cap. Reaction-chip.tsx additionally
   shifts this box in from `--who-shift` when it would clip a viewport edge,
   since centering alone does not keep it on screen for a chip near one. */
.reaction-who {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(calc(-50% + var(--who-shift, 0px)));
  z-index: 40;
  padding: 6px 10px;
  width: max-content;
  max-width: min(220px, calc(100vw - 32px));
  overflow: hidden;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  color: var(--ink);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.3;
  text-align: center;
  white-space: nowrap;
  overflow-wrap: normal;
  text-overflow: ellipsis;
  pointer-events: none;
}
.reaction:disabled {
  opacity: 1;
  cursor: default;
}
.reaction .count {
  font-size: 12px;
  font-weight: 600;
}
.reaction.mine {
  border-color: var(--ink);
  background: var(--surface-2);
  color: var(--ink);
}
/* The add-reaction chip at the end of the row (src/client/topic.tsx; Jean,
   2026-10-01): a reaction chip's own size and shape, icon only. With a mouse
   it stays out of the way until the post is hovered or has focus within it,
   the same reveal `.post-tools` uses, but it never hides on touch, where
   there is no hover to reveal it on — `opacity: 0` keeps it in the layout
   and the tab order (unlike the toolbar's touch rule, which clips it away:
   there a long press is the way in; here the chip itself is it). The
   `aria-expanded` fallback covers Safari, which does not focus a button on
   click, so `:focus-within` alone would drop it the moment the pointer
   leaves the post while the palette it opened is still open. */
.reaction-add {
  justify-content: center;
  color: var(--ink-3);
}
.reaction-add svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
}
.reaction-add:hover {
  background: var(--surface-2);
  color: var(--ink);
}
@media (hover: hover) and (pointer: fine) {
  .reaction-add {
    opacity: 0;
    transition: opacity 120ms;
  }
  .post:hover .reaction-add,
  .post:focus-within .reaction-add,
  .reaction-add[aria-expanded="true"] {
    opacity: 1;
  }
}
.palette {
  display: flex;
  gap: 2px;
}
.palette button {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  font-family: var(--font-emoji);
  font-size: 18px;
  line-height: 32px;
}
.palette button {
  transition: transform 80ms ease-out;
}
/* With a mouse, the emoji under it; on touch, hover is what the finger last
   touched and would light the emoji under a resting finger, so the slide
   sets `active` itself (src/client/topic.tsx). */
@media (hover: hover) {
  .palette button:hover:enabled {
    background: var(--surface-2);
  }
}
.palette button[aria-pressed="true"] {
  background: var(--surface-2);
}
/* The emoji under the sliding finger, grown a little, the way WhatsApp's is. */
.palette button.active {
  background: var(--surface-2);
  transform: scale(1.25);
}
/* On touch the quick emojis are the 40 px targets docs/design.md asks for. */
@media not ((hover: hover) and (pointer: fine)) {
  .palette button {
    width: 40px;
    height: 40px;
    line-height: 40px;
    font-size: 22px;
  }
}
/* The last control: a plus that opens the full picker below. */
.palette button.any {
  color: var(--ink-3);
}
.palette button.any svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
}
/* The emoji picker (src/client/emoji-picker.tsx): a sheet from the bottom on
   a phone, a card on desktop (below, with the other dialogs). The element
   inside takes the app's tokens through its own custom properties; its emoji
   are 40 px hit areas (24 px glyph + 8 px padding), the touch minimum. */
dialog.emoji-sheet {
  width: 100%;
  max-width: none;
  height: min(62dvh, 520px);
  max-height: none;
  margin: auto 0 0;
  padding: 0;
  border: 0;
  border-radius: 16px 16px 0 0;
  background: var(--surface);
  color: var(--ink);
  /* Panning only: a pinch or a double tap on the sheet (or its backdrop,
     whose touches land on the dialog) is not a zoom of the page behind it.
     WebKit on iOS (every browser there) reads this only in part; src/client/emoji-picker.tsx
     cancels its gesture events too (user, 2026-09-19). */
  touch-action: pan-x pan-y;
}
dialog.emoji-sheet::backdrop {
  background: rgba(0, 0, 0, 0.35);
}
.emoji-sheet-frame {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
.emoji-sheet-frame .settings-head {
  padding-top: 0;
}
.emoji-sheet-note {
  padding: 16px;
  color: var(--ink-3);
  font-size: 14px;
}
.emoji-sheet-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* The favorites row clears the phone's home indicator. */
  padding-bottom: var(--safe-bottom);
}
.emoji-sheet-host {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.emoji-sheet-body emoji-picker {
  flex: 1;
  min-height: 0;
  width: 100%;
  height: auto;
  --background: var(--surface);
  --border-color: var(--line-2);
  --border-size: 0;
  --indicator-color: var(--ink);
  --input-border-color: var(--line-2);
  --input-border-radius: 8px;
  --input-padding: 6px 10px;
  /* 16 px, never less: WebKit on iOS zooms the page into a focused field
     set smaller than that, which is the zoom the user saw on tapping the
     search box (2026-09-19); at 16 px it leaves the page alone. */
  --input-font-size: 16px;
  --input-font-color: var(--ink);
  --input-placeholder-color: var(--ink-3);
  --outline-color: var(--ink);
  --category-font-color: var(--ink-3);
  --category-font-size: 12px;
  --category-emoji-size: 1.125rem;
  --emoji-size: 1.5rem;
  --emoji-padding: 0.5rem;
  --emoji-font-family: var(--font-emoji);
  --num-columns: 8;
  --button-hover-background: var(--surface-2);
  --button-active-background: var(--line-2);
}
/* AI's hand in a post, said in the smallest words the head carries: the
   size of a badge's text, the colour of the time beside it, no pill. It
   follows the time so the author's line still opens with who and when. */
.post .head .ai {
  font-size: 11px;
}
.post .head .ai::before {
  /* The dot is for the eye; a screen reader gets the words alone. */
  content: "·" / "";
  margin-right: 6px;
}
/* The door that opens the post's Claude session: the same small words,
   underlined only on hover so the line stays quiet
   (docs/work/2026-11-04-claude-door-labels.md). */
.post .head a.door-link {
  color: var(--ink-3);
  text-decoration: none;
}
/* The dot stays out of the underline. */
.post .head a.door-link::before {
  display: inline-block;
}
.post .head a.door-link:hover,
.post .head a.door-link:focus-visible {
  text-decoration: underline;
}
.list-plain {
  list-style: none;
  margin: 0;
  padding: 0;
}
.card {
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.card + .card {
  margin-top: 8px;
}
.card .text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-top: 4px;
}
.card .actions {
  margin-top: 8px;
}

/* ---------- artifacts: pages built in an exploration ----------
   The frame is the only place model-written markup runs, and it runs in an
   opaque origin (sandbox without allow-same-origin, plus the route's own
   policy). The card is quiet around it: the page is what is being read. */
.artifact-card {
  margin: 8px 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.artifact-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 6px 10px;
  font-size: 14px;
}
.artifact-head .artifact-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.artifact-head .spacer {
  flex: 1 1 auto;
}
.artifact-head .btn {
  flex: none;
  padding: 6px 12px;
}
.artifact-frame {
  display: block;
  width: 100%;
  height: 240px;
  border: 0;
  border-top: 1px solid var(--line);
  /* A page that sets no background of its own is read on white, as it would
     be in a browser tab, rather than through the app's dark surface. */
  background: #fff;
}
.artifact-card .actions {
  margin: 0;
  padding: 8px 10px;
  border-top: 1px solid var(--line);
}
/* The recap a promoted page drafted takes the whole action row, as the
   thread's share block takes the whole thread. */
.artifact-card .actions > .share-block {
  flex: 1 1 100%;
  margin-top: 0;
}
dialog.artifact-dialog {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--paper);
  color: var(--ink);
}
dialog.artifact-dialog::backdrop {
  background: rgb(0 0 0 / 35%);
}
.artifact-dialog-frame {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  padding-bottom: var(--safe-bottom);
}
.artifact-dialog-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  min-height: var(--bar);
  padding: var(--safe-top) 8px 0;
  border-bottom: 1px solid var(--line);
}
.artifact-dialog-head h2 {
  flex: 1;
  min-width: 0;
  padding: 0 6px;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.artifact-dialog-head .btn {
  flex: none;
  padding: 6px 12px;
  text-decoration: none;
  color: inherit;
}
.artifact-frame-full {
  flex: 1;
  height: auto;
  min-height: 0;
  border-top: 0;
}
/* ---------- Markdown files Workroom writes ----------
   (src/client/markdown-file.tsx, docs/work/2026-10-13-markdown-files.md.)
   The artifact card's frame and head, a rendered start of the file under a
   fade instead of a page preview, and one row of actions that fits a 390 px
   phone even inside a post. No overflow on the card itself, so More's menu
   is never clipped. Tokens only, so dark mode needs nothing of its own. */
.md-file-card {
  margin: 8px 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.md-file-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 6px 10px;
  font-size: 14px;
}
.md-file-head .glyph {
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--ink-2);
}
.md-file-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
.md-file-head .badge {
  flex: none;
}
.md-file-head .spacer,
.md-file-actions .spacer {
  flex: 1 1 auto;
}
.md-file-head .btn {
  flex: none;
  padding: 6px 12px;
}
/* The start of the file, cut at a height and faded into the card. */
.md-file-preview {
  position: relative;
  max-height: 148px;
  overflow: hidden;
  padding: 10px 14px 12px;
  border-top: 1px solid var(--line);
  font-size: 14px;
  color: var(--ink-2);
}
.md-file-preview .prose > :first-child {
  margin-top: 0;
}
.md-file-preview .prose h1,
.md-file-preview .prose h2,
.md-file-preview .prose h3 {
  font-size: 15px;
  color: var(--ink);
}
.md-file-preview:has(.prose)::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 40px;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none;
}
.md-file-preview .status-line {
  padding: 0;
}
.md-file-failed {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
}
.md-file-failed .status-line {
  padding: 0;
}
.md-file-foot {
  padding: 8px 10px;
  border-top: 1px solid var(--line);
}
/* In the opened file, the same row under the head, on the 720 px column. */
.md-file-bar {
  flex: none;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
/* Lined up with the page's text, as \`.column\` places it. */
.md-file-bar > * {
  max-width: var(--column);
  margin-inline: auto;
  padding-inline: max(16px, env(safe-area-inset-left))
    max(16px, env(safe-area-inset-right));
}
.md-file-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}
.md-file-actions .btn {
  flex: none;
  padding: 0 12px;
}
.md-file-more {
  position: relative;
  flex: none;
}
.md-file-note {
  margin-top: 6px;
  font-size: 13px;
  color: var(--ink-2);
}
.md-file-manual {
  display: block;
  width: 100%;
  margin-top: 6px;
  font: 16px/1.4 var(--font-mono);
}
/* ---------- the attached file, read as a page ----------
   A full-screen dialog like the artifact page's, holding the topic's markdown
   file rendered in the 720 px column; Page | Markdown in its head swaps the
   rendering for the file itself. The page's own typography is under
   `.file-page` further down. */
dialog.file-dialog {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--paper);
  color: var(--ink);
}
dialog.file-dialog::backdrop {
  background: rgb(0 0 0 / 35%);
}
.file-frame {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
.file-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  min-height: var(--bar);
  padding: var(--safe-top) 8px 0 14px;
  border-bottom: 1px solid var(--line);
}
.file-title {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.file-title h2,
.file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-title h2 {
  font-size: 15px;
}
.file-name {
  font: 500 12px/1.3 var(--font-mono);
  color: var(--ink-3);
}
/* The view switch: the same rail as a segmented control, at the dense size. */
.file-views {
  flex: none;
  margin: 0;
  max-width: none;
}
.file-views button {
  flex: none;
  min-height: 32px;
  padding: 0 12px;
  white-space: nowrap;
}
.file-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--safe-bottom);
}
.file-body > .column {
  padding-top: 24px;
  padding-bottom: 48px;
}
/* A topic's Attachments: the feed's file rows stretched to the column, newest
   post first, each with who posted it and when on the right, which goes to
   that post (docs/work/2026-10-01-topic-attachments.md). */
.attachment-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.attachment-row {
  display: flex;
  align-items: stretch;
  gap: 4px;
  min-width: 0;
}
.attachment-row .file-chip {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.attachment-row .file-chip-name {
  max-width: none;
}
.attachment-row .file-chip-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* One width for every row, so the file rows line up whatever the name. */
.attachment-post {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 2px;
  width: 112px;
  min-height: 44px;
  padding: 0 2px 0 8px;
  border-radius: 10px;
  color: var(--ink-3);
  font-size: 12px;
  text-decoration: none;
}
.attachment-post:hover {
  background: var(--surface-2);
  color: var(--ink-2);
}
.attachment-post-text {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  flex: 1;
  min-width: 0;
}
.attachment-author,
.attachment-when {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.attachment-author {
  color: var(--ink-2);
}
.attachment-post .glyph {
  flex: none;
  width: 16px;
  height: 16px;
}
/* The page: the file's markdown at the scale of a document rather than a
   post. Titles, body, lists, numbered lists and bold are the format's v0
   (user, 2026-09-22); everything else the renderer knows is left readable but
   not designed for. Tokens only, so dark mode needs nothing of its own. */
.file-page {
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.file-page > * + * {
  margin-top: 1em;
}
.file-page > :first-child {
  margin-top: 0;
}
.file-page p {
  white-space: pre-wrap;
}
.file-page h1 {
  margin-top: 0;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
  font-size: 32px;
  line-height: 1.2;
  letter-spacing: -0.02em;
}
.file-page h2 {
  margin-top: 2em;
  font-size: 22px;
  line-height: 1.25;
  letter-spacing: -0.015em;
}
.file-page h1 + h2,
.file-page h2 + h3 {
  margin-top: 1em;
}
.file-page h3 {
  margin-top: 1.5em;
  font-size: 17px;
  line-height: 1.3;
}
.file-page strong {
  font-weight: 600;
}
.file-page ul,
.file-page ol {
  margin: 0.6em 0 0;
  padding-left: 1.5em;
}
.file-page ul {
  list-style: disc;
}
.file-page ol {
  list-style: decimal;
}
.file-page li + li {
  margin-top: 0.4em;
}
.file-page li::marker {
  color: var(--ink-3);
}
.file-page a {
  color: var(--ink);
  text-decoration: underline;
}
.file-page a:hover {
  color: var(--ink-2);
}
.file-page code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--surface-2);
}
.file-page .code-block {
  max-width: 100%;
  margin: 1em 0 0;
}
/* On the page the list view has a document's rhythm: taller rows, the caption
   size for the properties, and a rule above the first item as well so the
   block reads as one object under its heading. */
.file-page .list-view {
  margin: 1em 0 0;
  border-top: 1px solid var(--line);
}
.file-page .list-row {
  padding: 10px 0;
}
.file-page .list-props {
  font-size: 13.5px;
}
.file-page blockquote {
  margin: 1em 0 0;
  padding-left: 14px;
  border-left: 2px solid var(--line-2);
  color: var(--ink-2);
}
/* The callout: the page's one-glance recap, the quote directly under the `# `
   title, which the renderer marks (`calloutAt` in src/client/markdown.tsx).
   A plain soft box on the raised surface — rounded corners, the page's own
   body text, no accent border and nothing else marking it (user decision,
   2026-09-23). Scoped to `.file-page`, so a quote in a post is untouched.
   It sits under the title's hairline and above the first section. */
.file-page blockquote.callout {
  margin: 20px 0 0;
  padding: 14px 16px;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  color: var(--ink);
  line-height: 1.6;
}
/* Its own paragraphs carry the box's rhythm, not the page's. */
.file-page blockquote.callout p {
  margin: 0;
}
.file-page blockquote.callout p + p {
  margin-top: 0.6em;
}
/* A section heading after the callout does not need its usual two ems: the
   box already separates it from the title. */
.file-page blockquote.callout + h2 {
  margin-top: 1.5em;
}
.file-page hr {
  margin: 2em 0;
  border: 0;
  border-top: 1px solid var(--line);
}
/* One line over a text view saying which end of the pipeline it is. */
.file-caption {
  margin: 0 0 10px;
  font-size: 13.5px;
  color: var(--ink-2);
}
/* The file itself, for comparing with the page: the text as written. */
.file-source {
  margin: 0;
  padding: 16px;
  font: 13.5px/1.55 var(--font-mono);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink);
}
/* The recap text under a published page: the memory behind the presentation,
   one disclosure away. Same chevron as a panel disclosure, one size down. */
.recap-text {
  margin-top: 4px;
}
.recap-text > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  cursor: pointer;
  list-style: none;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--ink-2);
}
.recap-text > summary::-webkit-details-marker {
  display: none;
}
.recap-text > summary::before {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentcolor;
  border-bottom: 1.5px solid currentcolor;
  transform: rotate(-45deg);
  transition: transform 0.15s;
  flex: none;
}
.recap-text[open] > summary::before {
  transform: rotate(45deg);
}

/* A mention in a post: the handle at the weight a name has everywhere else,
   and nothing under the post repeating who was addressed. */
.post .mention,
.for-me-original .mention {
  font-weight: 600;
}
/* The reader's own mention: the same ink on a soft neutral tint, rounded, so it
   stands out at a glance in a line that names several people (WhatsApp does
   the same). Padding and radius only: the vertical padding is zero and the
   box decorates, so the line height never moves. Neutral, never `--think`
   (docs/work/2026-10-02-self-mention-highlight.md). */
.post .mention-self,
.for-me-original .mention-self {
  background: var(--surface-3);
  border-radius: 6px;
  padding: 0 4px;
  margin: 0 -1px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
/* Inside bold text the mention is as bold as its surroundings, not lighter. */
.post strong .mention,
.for-me-original strong .mention {
  font-weight: inherit;
}
/* A topic a post links: the same weight a mention has, with the topic glyph
   in front of the title so the two kinds of link read as related without
   colouring one of them differently (docs/work/2026-09-23-topic-links.md).
   A post a post references is drawn the same way behind the message glyph
   (docs/work/2026-09-23-permalinks.md), and a coding task behind the tool
   glyph (docs/work/2026-09-29-coding-task-link-enrichment.md). */
.post .topic-link,
.for-me-original .topic-link,
.post .post-link,
.for-me-original .post-link,
.post .exploration-link,
.for-me-original .exploration-link {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  color: inherit;
  text-decoration: none;
}
.post a.topic-link:hover,
.for-me-original a.topic-link:hover,
.post a.topic-link:focus-visible,
.for-me-original a.topic-link:focus-visible,
.post a.post-link:hover,
.for-me-original a.post-link:hover,
.post a.post-link:focus-visible,
.for-me-original a.post-link:focus-visible,
.post a.exploration-link:hover,
.for-me-original a.exploration-link:hover,
.post a.exploration-link:focus-visible,
.for-me-original a.exploration-link:focus-visible {
  text-decoration: underline;
}
.post .topic-link .glyph,
.for-me-original .topic-link .glyph,
.post .post-link .glyph,
.for-me-original .post-link .glyph,
.post .exploration-link .glyph,
.for-me-original .exploration-link .glyph {
  width: 14px;
  height: 14px;
  /* Optically on the text's baseline rather than the box's. */
  transform: translateY(2px);
  flex: none;
}

/* A topic and a post named in a sentence are chips of their kind, as a row
   and a page are (`.tw-chip`): the same raised neutral box, the kind's own
   mark in front (the topic's bubble; the author's picture or initials for a
   post), the name at 500 (docs/work/2026-10-14-llm-ids.md, 6b). */
.ref-chip,
.post .ref-chip,
.for-me-original .ref-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: 0 6px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface-2);
  color: var(--ink);
  font-weight: 500;
  font-size: 0.93em;
  line-height: 1.45;
  text-decoration: none;
  vertical-align: baseline;
  white-space: nowrap;
}
a.ref-chip:hover,
a.ref-chip:focus-visible,
.post a.ref-chip:hover,
.post a.ref-chip:focus-visible {
  background: var(--surface-3);
  color: var(--ink);
  text-decoration: none;
}
.ref-chip .glyph,
.post .ref-chip .glyph,
.for-me-original .ref-chip .glyph {
  width: 12px;
  height: 12px;
  transform: none;
  color: var(--ink-3);
  flex: none;
}
.ref-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The author of a post chip: a 14 px circle, their picture or their initials
   (drawn from `data-initials`, so they are never part of the chip's text). */
.ref-chip-avatar {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-3);
  color: var(--ink-2);
  font-size: 7px;
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
}
.ref-chip-avatar:empty::before {
  content: attr(data-initials);
}
.ref-chip-avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* A post chip that names nothing the reader can see, and a deleted post's:
   the same box, quieter. Unavailable is dashed and empty inside, so it reads
   as a hole rather than a thing; deleted keeps its place and its topic
   (docs/work/2026-10-21-post-links-smart.md). */
.ref-chip.ref-chip-gone,
.post .ref-chip.ref-chip-gone,
.for-me-original .ref-chip.ref-chip-gone {
  border-style: dashed;
  background: transparent;
  color: var(--ink-3);
}
.ref-chip.ref-chip-deleted,
.post .ref-chip.ref-chip-deleted,
.for-me-original .ref-chip.ref-chip-deleted {
  color: var(--ink-3);
}
.menu .menu-note {
  padding: 8px 10px 8px 22px;
  font-size: 13px;
  color: var(--ink-3);
}

/* A preview of each post a post references, under its words, like a link
   preview in a team chat: who, where and when on one quiet line, then the
   first lines of the post, four at most, as plain text. The whole card is
   the tap target and opens the post in place
   (docs/work/2026-09-24-share-and-reference.md). It never grows past the
   text column, so a long title or an unbroken word cannot widen the page. */
/* A reply's quote of the post it answers, above its words
   (docs/work/2026-09-25-replies-or-threads.md): a plain rounded
   `--surface-2` block, no edge bar and no accent (blue is for AI alone,
   user 2026-09-25); the author in the ink's 600 weight, two lines of the
   post at most. The whole quote opens the post in place, lit. The
   composer's "Replying to" strip draws the same block. */
.reply-quote {
  display: block;
  min-width: 0;
  max-width: 560px;
  margin: 4px 0 6px;
  padding: 7px 12px;
  border-radius: 10px;
  background: var(--surface-2);
  color: inherit;
  text-decoration: none;
  font-size: 13px;
  line-height: 1.4;
}
/* A quote is a link to its post: no iOS callout (link preview) on a long
   press. Text selection in message bodies is untouched. */
a.reply-quote {
  -webkit-touch-callout: none;
}
a.reply-quote:hover,
a.reply-quote:focus-visible {
  background: var(--line);
}
.reply-quote.removed {
  color: var(--ink-3);
  font-style: italic;
}
.reply-author {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-weight: 600;
  color: var(--ink);
}
.reply-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
/* A quote of a coding task's line: the feed's task row, small, on one line
   (docs/work/2026-10-14-quoted-task-row.md). */
.reply-task {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 13px;
  line-height: 20px;
}
.reply-task > .task-badge,
.reply-task > .task-status-glyph {
  flex: none;
}
.reply-task-words {
  /* A long handle gives way, but never takes more than half the row. */
  flex: 0 1 auto;
  max-width: 50%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-3);
}
.reply-task-title {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  color: var(--ink);
}
.reply-task-title.missing {
  font-weight: 400;
  font-style: italic;
  color: var(--ink-3);
}
.reply-excerpt.deleted {
  color: var(--ink-3);
  font-style: italic;
}
/* In the composer: under the Reply | Think row, the quote with its cancel
   at the right. */
.replying-to {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  margin: 10px 0 8px;
}
.replying-to .reply-quote {
  flex: 1 1 auto;
  margin: 0;
  max-width: none;
}
.replying-to .icon-btn {
  flex: none;
}
/* "Think about this…": the same quote, on Think's tint, the post's author in
   the think blue (docs/work/2026-10-06-think-about-post.md). Above the box in
   the composer, and above the question in the thread it starts. */
.thinking-about .reply-quote,
.reply-quote.think-quote {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--think-line);
}
.thinking-about .reply-author,
.think-quote .reply-author {
  color: var(--think);
}
.reply-quote.think-quote {
  margin: 10px 0 6px;
}
a.reply-quote.think-quote:hover,
a.reply-quote.think-quote:focus-visible {
  background: var(--think-bg);
}
.post .reference-cards {
  display: grid;
  gap: 6px;
  margin: 8px 0 0;
  min-width: 0;
  max-width: 560px;
}
:is(.post, .answer-card) .reference-card {
  display: block;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface-2);
  color: inherit;
  text-decoration: none;
  font-size: 13.5px;
  line-height: 1.4;
}
:is(.post, .answer-card) a.reference-card:hover,
:is(.post, .answer-card) a.reference-card:focus-visible {
  border-color: var(--line-2);
  border-left-color: var(--ink-3);
}
/* The author's × over a card's corner, as a web card has it
   (docs/work/2026-09-25-removable-cards.md). */
:is(.post, .answer-card) .reference-card-item {
  position: relative;
  min-width: 0;
}
.post .reference-card-item:has(.link-hide) .reference-card {
  padding-right: 40px;
}
.post .reference-card-item .link-hide {
  position: absolute;
  top: 0;
  right: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--ink-3);
  background: none;
}
.post .reference-card-item .link-hide .glyph {
  width: 22px;
  height: 22px;
  padding: 4px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line);
}
:is(.post, .answer-card) .reference-card.removed {
  color: var(--ink-3);
  font-style: italic;
}
:is(.post, .answer-card) .reference-head {
  display: flex;
  align-items: baseline;
  gap: 4px;
  min-width: 0;
  white-space: nowrap;
}
:is(.post, .answer-card) .reference-head .glyph {
  width: 14px;
  height: 14px;
  flex: none;
  transform: translateY(2px);
  color: var(--ink-3);
}
/* Both halves of the head may shrink, so a 32-character handle or a long
   title ends in an ellipsis instead of widening the page on a phone. */
:is(.post, .answer-card) .reference-author {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 600;
}
:is(.post, .answer-card) .reference-meta {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  color: var(--ink-3);
}
/* A coding task's card, E9 (design v3 of
   docs/work/2026-09-29-coding-task-link-enrichment.md): one 12 px-radius
   --surface box with a 1 px --line border and no left edge, three columns
   centred on one row — the co-author badge with the time under it, the
   title on up to two lines, the status's glyph and word at the right. 56 px
   with a one-line title, 64 px with two. The status control is the item's
   own, laid over the card's right column (below). */
:is(.post, .answer-card) .reference-card.task-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 10px;
  min-height: 56px;
  padding: 10px 12px;
  border-left-width: 1px;
  border-radius: 12px;
  background: var(--surface);
  --badge-ring: var(--surface);
}
:is(.post, .answer-card) a.reference-card.task-card:hover,
:is(.post, .answer-card) a.reference-card.task-card:focus-visible {
  border-left-color: var(--line-2);
}
:is(.post, .answer-card) .reference-card.task-card.removed {
  display: block;
  min-height: 0;
}
:is(.post, .answer-card) .task-card-who {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}
:is(.post, .answer-card) .task-card-time {
  font-size: 11px;
  line-height: 11px;
  color: var(--ink-3);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
:is(.post, .answer-card) .task-card-title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  font-size: 14px;
  line-height: 21px;
  font-weight: 600;
  overflow-wrap: anywhere;
}
:is(.post, .answer-card) .task-card-status {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--ink-2);
}
/* The co-author badge (design v3, decision 2): the person behind, the tool
   in front, overlapping by a third, each disc ringed 1.5 px in the surface
   behind it (--badge-ring: the page, or the card's --surface). */
.task-badge {
  --badge: 22px;
  display: inline-flex;
  flex: none;
  vertical-align: middle;
}
.task-badge[data-size="20"] {
  --badge: 20px;
}
.task-badge[data-size="18"] {
  --badge: 18px;
}
.task-badge > .member-badge,
.task-badge > .task-tool {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  width: var(--badge);
  height: var(--badge);
  border-radius: 50%;
  box-shadow: 0 0 0 1.5px var(--badge-ring, var(--paper));
}
.member-badge {
  z-index: 0;
  background: var(--surface-3);
  color: var(--ink-sidebar-read);
  font-size: 9px;
  font-weight: 600;
  line-height: 1;
  font-style: normal;
  letter-spacing: 0;
}
/* A picture fills the disc; the ring is the disc's own shadow, outside it. */
.task-badge > .member-badge {
  overflow: hidden;
}
.task-badge > .member-badge img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.task-badge > .task-tool {
  z-index: 1;
  margin-left: calc(var(--badge) / -3);
  background: var(--ink-3);
  color: #fff;
}
.task-badge > .task-tool:first-child {
  margin-left: 0;
}
.task-badge > .task-tool[data-tool="claude_code"],
.task-badge > .task-tool[data-tool="claude"] {
  background: var(--vendor-claude);
}
.task-badge > .task-tool[data-tool="codex"] {
  background: var(--vendor-codex);
  color: var(--surface);
}
.task-badge > .task-tool .glyph {
  width: calc(var(--badge) * 0.62);
  height: calc(var(--badge) * 0.62);
  stroke-width: 2.25;
  transform: none;
}
/* The avatar column's variant (a post a tool published with nobody
   reviewing it, docs/work/2026-10-02-coauthor-avatar-unattended.md): the
   person at the column's 28 px, ringless, with a post avatar's own initials
   (.avatar), and the tool's disc at 18 px in front, across the lower right
   corner, ringed in the page so the two read apart. */
.task-badge[data-size="28"] {
  --badge: 28px;
  position: relative;
  align-self: start;
  margin-top: 2px;
}
.task-badge[data-size="28"] > .member-badge {
  box-shadow: none;
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
}
.task-badge[data-size="28"] > .task-tool {
  position: absolute;
  right: -5px;
  bottom: -5px;
  width: 18px;
  height: 18px;
  margin-left: 0;
}
.task-badge[data-size="28"] > .task-tool .glyph {
  width: 11px;
  height: 11px;
}
/* A lifted post sits on --surface: the ring follows it. */
.post.lifted {
  --badge-ring: var(--surface);
}
/* A coding task's line in its home topic (design v3, decision 4): the
   task's badge, then in 13 px --ink-3 the handle at 600 in --ink-2, the
   verb, the task's chip and its glyph, and the time at the right of the
   first row; no bubble, no card. The chip stays on the first row and takes
   the room left there, a title too long ending in an ellipsis; it moves
   whole to a second row under the sentence only when fewer than about
   120 px remain after the words (PR G). 32 px on one row, about 50 on
   two. */
.event-line {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  column-gap: 10px;
  align-items: start;
  margin: 0;
  padding: 2px 0;
  color: var(--ink-3);
  font-size: 13px;
  line-height: 20px;
}
.event-line.bare {
  grid-template-columns: minmax(0, 1fr) auto;
}
/* The 20 px badge with its tool overlapping by a third is 33 px wide: it is
   centred on the 28 px avatar column, under the posts' pictures, and runs
   into the gap by 3 px on each side. */
.task-line > .task-badge {
  justify-self: center;
  margin: 0 calc((28px - var(--badge) * 5 / 3) / 2);
}
/* The words and the chip are two flex items: the chip stays on the first
   row and takes the room left there while at least 120 px remain after the
   words, else it wraps whole to the second row (PR G). */
.event-line-text {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 4px;
  min-width: 0;
  overflow-wrap: anywhere;
}
.event-line-words {
  flex: 0 1 auto;
}
.event-line-author {
  font-weight: 600;
  color: var(--ink-2);
}
.task-line-missing {
  flex: 1 1 120px;
  min-width: 0;
  font-style: italic;
}
.event-line-chip {
  display: flex;
  flex: 1 1 120px;
  align-items: center;
  gap: 4px;
  min-width: 0;
}
.task-line-chip .task-link {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.event-line-time {
  white-space: nowrap;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
/* A task's chip, in a sentence and on the line: the title as link text in
   ink at weight 500 (design v3, decision 3, moved off --accent 2026-09-30);
   in a sentence it has the 18 px badge in front and the status glyph after,
   and wraps like any link. */
.post .task-link,
.for-me-original .task-link,
.task-line .task-link {
  color: var(--ink);
  text-decoration: none;
  font-weight: 500;
}
.post .task-link > .task-badge,
.for-me-original .task-link > .task-badge {
  margin-right: 4px;
  vertical-align: -4px;
}
.post .task-link > .task-status-glyph,
.for-me-original .task-link > .task-status-glyph {
  display: inline-flex;
  margin-left: 3px;
  vertical-align: -2px;
}
.post a.task-link:hover .task-link-title,
.for-me-original a.task-link:hover .task-link-title,
.post a.task-link:focus-visible .task-link-title,
.for-me-original a.task-link:focus-visible .task-link-title,
.task-line a.task-link:hover,
.task-line a.task-link:focus-visible {
  text-decoration: underline;
}
/* In the stream the line keeps the post's place and menu, without its
   picture or head: the handle is inside the sentence, and the line spans
   both of the post's columns, its badge taking the picture's place and its
   sentence starting where post text does. */
.post.post-line {
  padding: 4px 0;
}
.post.post-line > .avatar {
  display: none;
}
.post.post-line > div {
  grid-column: 1 / -1;
  min-width: 0;
}
/* The head keeps only its tools, so the line keeps its desktop menu. */
.post.post-line .head > strong,
.post.post-line .head > .meta,
.post.post-line .head > .badge {
  display: none;
}
.post.post-line .head {
  position: absolute;
  right: 0;
  min-height: 0;
}
.post.post-line {
  position: relative;
}
/* With a mouse, the head's toolbar floats at the line's right on hover:
   the line keeps its room, as a continued post's text does, so the time
   is never under it. */
@media (hover: hover) and (pointer: fine) {
  .post.post-line .event-line {
    /* React, Save for later and …: three 32px buttons and the border. */
    padding-right: 104px;
  }
}
/* A to-do's words matter whole: the chip keeps its natural width, moves
   whole to the second row when it does not fit after the words, and only
   there wraps inside itself, never cut short. */
/* Dark: the light still falls from above, so the chip's top is the lighter. */
/* An event line's memory (event-memory.tsx,
   docs/work/2026-10-03-event-line-brain.md): a 12 px brain right of the
   time instead of memory's line under the post. Faint and still while
   queued (`--ink-3` at 45%: there is no `--ink-4`), a slow pulse while
   running, steady `--ink-3` once memory wrote something, `--danger` when its
   job failed. The pulse is this glyph's alone; a coding task's own status
   glyph keeps its rule of no pulse. */
.event-line-end {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.event-brain {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 12px;
  height: 12px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-3);
}
.event-brain svg {
  width: 12px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.event-brain.queued {
  opacity: 0.45;
}
.event-brain.running {
  animation: event-brain-pulse 1.6s ease-in-out infinite alternate;
}
@keyframes event-brain-pulse {
  from {
    opacity: 0.35;
  }
  to {
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .event-brain.running {
    animation: none;
  }
}
.event-brain.failed {
  color: var(--danger);
}

/* The glyph is a button: its hit area grows to 32 px past the 12 px drawing
   without moving the line. It overlaps the time to its left (which takes no
   tap), never the task's chip, so a thumb on it opens the popover. */
button.event-brain {
  position: relative;
  cursor: pointer;
}
button.event-brain::before {
  content: "";
  position: absolute;
  inset: -10px;
}
button.event-brain:hover {
  color: var(--ink-2);
}
button.event-brain.failed:hover {
  color: var(--danger);
}
/* What the brain opens: a small popover anchored under it, at the line's
   right edge, holding the rows memory's line would have shown (each still
   opening its fold), in the menus' raised shape. Never wider than the
   screen less its gutters. */
.event-memory-pop {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 30;
  box-sizing: border-box;
  width: max-content;
  min-width: 220px;
  max-width: min(360px, calc(100vw - 32px));
  padding: 4px 8px;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14);
  text-align: left;
  white-space: normal;
}
.event-memory-pop .memory-line {
  margin-left: 0;
  max-width: 100%;
}
/* Its row in the long-press sheet: memory's line shape above the actions,
   a hairline between. */
.event-memory {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.message-sheet .event-memory {
  padding: 4px 0;
  border-top: 1px solid var(--line);
}
.message-sheet .event-memory .memory-line {
  min-height: 40px;
  margin-left: 0;
  font-size: 14px;
}
.event-memory .memory-line {
  width: 100%;
}
/* The chevron follows the summary, as on memory's own group row. */
/* Above the sheet's own 18 px, 1.75-stroke glyph rules. */
.event-memory .memory-line svg.chevron {
  width: 14px;
  height: 14px;
}
/* A coding task's status: one glyph, three states and no pulse — a quiet
   dot while opened, a green check when done, a red dot when stale; dropped
   is the quiet dot muted beside its word (decision 10 of
   docs/work/2026-09-29-coding-task-link-enrichment.md). The control sits
   over the card's corner, outside its link, as the author's × does. */
.task-status-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex: none;
}
/* Opened (Start tapped, nothing seen yet): a hollow ring, so waiting and
   running differ by shape as well as colour
   (docs/work/2026-10-01-task-activity-proof.md, user, 2026-10-01). */
.task-status-glyph::before {
  content: "";
  box-sizing: border-box;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid var(--ink-2);
}
.task-status-glyph[data-state="dropped"]::before,
.task-status-glyph[data-state="stale"]::before,
.task-status-glyph[data-state="working"]::before {
  border: 0;
}
.task-status-glyph[data-state="dropped"]::before {
  width: 7px;
  height: 7px;
  background: var(--line-2);
}
.task-status-glyph[data-state="stale"]::before {
  width: 7px;
  height: 7px;
  background: var(--danger);
}
/* A coding agent was seen working on it: filled, in GitHub's in-progress
   amber, inside a faint amber halo that reads as "live" without moving. */
.task-status-glyph[data-state="working"]::before {
  width: 7px;
  height: 7px;
  background: var(--task-working);
  box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--task-working) 28%, transparent);
}
.task-status-glyph[data-state="done"]::before,
.task-status-glyph[data-state="merged"]::before {
  content: none;
}
/* Merged, not live yet (docs/work/2026-10-01-task-merged-live.md). */
.task-status-glyph[data-state="merged"] .glyph {
  width: 13px;
  height: 13px;
  color: var(--task-merged);
}
.task-status-glyph[data-state="done"] .glyph {
  width: 14px;
  height: 14px;
  color: var(--done);
}
.task-status {
  position: relative;
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
.task-status-button {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 32px;
  padding: 0 8px;
  border-radius: 999px;
  background: none;
  color: var(--ink-2);
  font-size: 12px;
}
.task-status-button:hover,
.task-status-button:focus-visible {
  background: var(--surface-3);
}
/* The word is coloured only for done and stale (design v3, decision 5);
   Working keeps the plain word beside its blue dot. */
.task-status-word[data-state="done"] {
  color: var(--done);
}
.task-status-word[data-state="merged"] {
  color: var(--task-merged);
}
.task-status-word[data-state="stale"] {
  color: var(--danger);
}
.task-status-menu {
  display: inline-flex;
  gap: 4px;
}
.task-status-menu button {
  min-height: 32px;
  padding: 0 10px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 12px;
}
.task-status-error {
  flex-basis: 100%;
  color: var(--danger);
  font-size: 12px;
}
/* The control's face sits over the card's right column, centred on its
   row; the marks and any error open below the card, so they never cover
   its title on a phone. */
:is(.post, .answer-card) .task-card-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}
:is(.post, .answer-card) .task-card-item > .reference-card {
  grid-area: 1 / 1;
  padding-right: 88px;
}
:is(.post, .answer-card) .task-card-item .task-status {
  display: contents;
}
:is(.post, .answer-card) .task-card-item .task-status-button {
  grid-area: 1 / 1;
  justify-self: end;
  align-self: center;
  z-index: 1;
  margin-right: 4px;
}
:is(.post, .answer-card) .task-card-item .task-status-menu {
  grid-row: 2;
  justify-self: end;
  margin-top: 4px;
}
:is(.post, .answer-card) .task-card-item .task-status-error {
  grid-row: 3;
  margin-top: 4px;
}
:is(.post, .answer-card) .exploration-card.private .reference-meta {
  font-style: italic;
}
:is(.post, .answer-card) .reference-excerpt {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
  margin-top: 2px;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/* An item an answer shows in place, full width, where its `!<#kind:id>`
   line stands (docs/work/2026-10-01-answer-markers-cards.md): the card a
   post draws under its words. Each kind has a fixed height in an answer —
   a post's excerpt is held to two lines — and the box drawn while the card
   is on its way has the same, so the answer streaming below it does not
   move when the card arrives. A stub (removed, hidden) is shorter. */
.answer-card {
  margin: 10px 0;
  max-width: 560px;
}
.answer-card .reference-excerpt {
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
.answer-card .reference-card:not(.removed, .private, .task-card, .exploration-card) {
  min-height: 77px;
}
.answer-card a.reference-card {
  text-decoration: none;
}
.answer-card .reference-card.task-card {
  min-height: 64px;
}
.answer-card .reference-card.exploration-card:not(.removed, .private) {
  min-height: 58px;
}
.answer-card-pending {
  height: 77px;
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface-2);
}
.answer-card-pending[data-kind="task"] {
  height: 64px;
  border-left-width: 1px;
  border-radius: 12px;
  background: var(--surface);
}
.answer-card-pending[data-kind="exploration"] {
  height: 58px;
}
/* A to-do item in an answer (a test, docs/work/2026-10-02-todo-answer-card.md):
   the person's own row in a box like a task's card, without the list's
   outdent, which lines a whole list up with the avatar mark. */
.answer-card-todo .reference-card.removed {
  min-height: 0;
}
.answer-card-pending[data-kind="todo"] {
  height: 64px;
  border-left-width: 1px;
  border-radius: 12px;
  background: var(--surface);
}

/* A web link's preview, under the post's words and pictures, like an unfurl
   in Slack (docs/work/2026-09-25-link-previews.md): the site on a quiet line,
   the title in the link colour, two lines of description, then the page's
   picture capped in height. A link that is itself a picture is drawn like an
   attached one. The whole card is one link to a new tab; the author's × sits
   over its corner, a 40 px target drawn smaller. Nothing is reserved before a
   card is known, and the picture's declared size keeps it from shifting. */
.post .link-previews {
  display: grid;
  gap: 6px;
  margin: 8px 0 0;
  min-width: 0;
  max-width: 560px;
}
.post .link-preview {
  position: relative;
  min-width: 0;
}
.post .link-card {
  display: block;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
.post .link-card.page {
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 13.5px;
  line-height: 1.4;
}
.post .link-preview:has(.link-hide) .link-card.page {
  padding-right: 40px;
}
.post a.link-card.page:hover,
.post a.link-card.page:focus-visible {
  border-color: var(--line-2);
  border-left-color: var(--ink-3);
}
.post a.link-card.page:hover .link-title,
.post a.link-card.page:focus-visible .link-title {
  color: var(--ink-2);
}
.post .link-words {
  display: block;
  min-width: 0;
}
.post .link-site {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 12px;
  color: var(--ink-3);
}
.post .link-title,
.post .link-description {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.post .link-title {
  font-weight: 600;
  color: var(--ink);
  text-decoration: underline;
}
.post .link-description {
  margin-top: 2px;
  color: var(--ink-2);
}
.post .link-picture {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 240px;
  margin-top: 8px;
  border-radius: 8px;
  background: var(--surface-3);
}
.post .link-card.image {
  display: block;
  max-width: 100%;
  border: 1px solid var(--line-2);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
  line-height: 0;
  vertical-align: top;
}
.post .link-card.image .link-picture {
  max-width: 100%;
  max-height: 320px;
  margin: 0;
  border-radius: 0;
}
.post .link-hide {
  position: absolute;
  top: 0;
  right: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--ink-3);
  background: none;
}
.post .link-hide .glyph {
  width: 22px;
  height: 22px;
  padding: 4px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--line);
}
.post .link-hide:hover .glyph,
.post .link-hide:focus-visible .glyph {
  color: var(--ink);
}
/* A picture's card shrinks to the picture, so the × sits on its corner. */
.post .link-preview:has(.link-card.image) {
  justify-self: start;
  max-width: 75%;
}

/* ---------- about this topic: the brief, in its dialog ---------- */
.topic-brief .brief-text {
  font-size: 14.5px;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.topic-brief .brief-text > :first-child {
  margin-top: 0;
}
.topic-brief .brief-text h1,
.topic-brief .brief-text h2,
.topic-brief .brief-text h3 {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  margin: 12px 0 4px;
}
.topic-brief .brief-text p,
.topic-brief .brief-text ul,
.topic-brief .brief-text ol {
  margin: 4px 0;
}
.topic-brief .brief-text .code-block {
  max-width: 100%;
}
.topic-brief .actions {
  margin-top: 8px;
}
.topic-brief .actions .btn,
.topic-brief .actions .btn-primary {
  margin-top: 0;
}
.topic-brief textarea {
  font-size: 16px;
  min-height: 140px;
  margin-bottom: 6px;
}
.topic-brief .inline-form label {
  font-size: 13.5px;
  color: var(--ink-2);
  margin-bottom: 6px;
}

/* ---------- memory files: the editor, its history, one version's diff ----------
   A file is edited in the same field the brief and the knowledge always had;
   what is new under it is the version line, the History disclosure and the
   diff dialog a version opens. docs/work/2026-09-22-memory-files.md. */
.file-editor textarea {
  font-size: 16px;
  min-height: 140px;
}
/* The state line and the one control a failed read offers, on one row with
   the save button. The brief's own copy is a paragraph of its own. */
.brief-state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.brief-state .hint,
.brief-state .error {
  margin-top: 0;
}
.file-rows li,
.file-versions li {
  display: flex;
  align-items: center;
  gap: 8px;
  border-top: 1px solid var(--line);
}
.file-rows li:first-child,
.file-versions li:first-child {
  border-top: 0;
}
/* The whole row is the target, so a name and its title are one tap on a
   phone rather than two. 44 px, the touch minimum. */
.file-row,
.file-version {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  min-height: 44px;
  padding: 6px 0;
  text-align: left;
  font-size: 14px;
}
.file-row-title,
.file-version-time {
  color: var(--ink-3);
  font-size: 12px;
}
.file-version-line {
  color: var(--ink-2);
}
.file-history,
.file-archived {
  margin-top: 4px;
}
/* An archived row: the name reads as a row but is not a target; the control
   beside it is. */
.file-archived .file-row {
  flex: 1;
}
/* One version against the one it was based on: full screen on a phone, a
   card from 840 px. Lines are mono at 12.5 px and wrap, so a long line stays
   in the dialog instead of scrolling it sideways. */
dialog.file-diff {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--paper);
  color: var(--ink);
}
/*
 * What's next as a to-do list (src/client/todo-list.tsx,
 * docs/work/2026-09-29-whats-next-todo.md): a checkbox, the line, then a
 * thumbs-down and a chevron at the right, each 40 px. The thumbs-down keeps
 * its place while hidden, so showing it never moves the line.
 *
 * The card's left margin is pulled out to equal its right one
 * (docs/work/2026-09-30-whats-next-hide-prompt.md): `.turn-assistant`'s
 * grid gives every answer a 26px avatar column plus a 10px gap (36px)
 * before its content starts, so a block that stays inside that column reads
 * far more indented on the left than the standard 16px gutter it keeps on
 * the right. The negative margin below cancels exactly that 36px, so the
 * list's own left edge lands flush with the avatar's, matching the 16px
 * gutter the text already keeps on the right (width:auto absorbs the
 * margin change, so the right edge does not move). `.todo-tick`'s own
 * margin, which used to pull the checkbox back toward that far-right
 * starting point, is dropped for the same reason once the whole card sits
 * at the gutter instead.
 *
 * This is the "On my to-do" exploration's own layout, not a think thread's:
 * transcript H1 (docs/work/2026-09-30-transcript-h1.md) is scoped to
 * `.think` and leaves this grid/indent alone, so the hack below is still
 * live. Inside a think thread — `.think .todo-list`, below — there is no
 * such column to begin with.
 */
/*
 * When the list is the very first thing in the answer — the common case, a
 * run whose whole reply is the list — the outdent above puts its first row
 * at the same x and y as the avatar mark beside it (both start at the
 * gutter, both near the top of the turn), so the row draws over the mark
 * instead of beside it. 28px (the mark's own 26px plus its 2px top margin)
 * clears the mark's own bottom edge; `.steps`' own 4px top margin (a flex
 * item's margin, so it does not collapse into this one) adds a further 4px
 * of daylight, which is fine — the goal is no overlap, not a tight fit —
 * without touching a list that already follows a step or a line of text
 * above it.
 */
/* A think thread's own card, holding it via `.step-card`'s flex row when it
   is the answer's leading part (as every other card does) rather than this
   grid-specific hack, which does not apply there. */
/* No separators (founder, 2026-09-29): the rows are told apart by space. */
/* The list skeleton's own row (pinned-list-screen.tsx): not a real
   `.todo-row` (deliberately, so a `.todo-row` query never matches a
   placeholder), but the same minimum-width reset its `.todo-line` child
   needs to lay out. */
/* A new item a refresh is still reading (docs/work/2026-10-18-todo-refresh-speed.md):
   an empty dashed box where the tick will be, the bar beside it. */
.todo-line {
  display: flex;
  align-items: flex-start;
  min-width: 0;
}
/* An item a change in place just added or reworded: a soft wash that fades
   (docs/work/2026-09-30-todo-add-item.md). */
@keyframes todo-fresh {
  0%,
  35% {
    background: var(--surface-2);
  }
  100% {
    background: transparent;
  }
}
/*
 * The chevron sits right after the item's last word (founder, 2026-09-29),
 * not at the list's edge. Its 40 px tap height is kept; the negative block
 * margins keep it from making the 22 px line taller.
 */
/* Only a pointer that hovers: on touch a tapped control would keep it. */
@media (hover: hover) {
  /* With the pointer on a line, only that line's shows: a tap or a keyboard
     focus left on another line must not keep a second one up. */
}
/* A line's "…" menu on On my to-do, which replaced its thumbs-down
   (docs/work/2026-10-09-todo-item-chat.md): the button at the line's end,
   the post menu's popover under it. With a mouse it shows on the line's
   hover or focus, and stays while its menu is open; on touch it is always
   there, quiet, and opens the sheet. */
@media (hover: hover) and (pointer: fine) {
  /* With the pointer on a line, only that line's shows. */
}
/* On a phone (Jean, 2026-10-09): no "…" until a tap on the line's words
   shows it, one line at a time; a long press opens the sheet directly. The
   hidden button stays in the accessibility tree, so it is reachable without
   a long press. The long press is the line's, not the browser's: no text
   selection, callout or tap highlight. */
/* A to-do line's sheet on a phone: no grey scrim over everything; the
   other lines dim and the line in question stays at full contrast. */
/* No head and no palette (a to-do line's sheet): no rule under the grip. */
.message-sheet > h2.visually-hidden + .message-sheet-actions {
  border-top: 0;
}
/* A short list shifted to bring its line to the middle behind the sheet
   (src/client/todo-line-focus.ts): the space the shift opens at the top (or
   the bottom) is the list's own background, never the column's. */
/* A sheet named after what it acts on (a to-do line's): its head, quiet. */
.message-sheet-title {
  margin: 0;
  padding: 2px 10px 10px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 13px;
  font-weight: 400;
  color: var(--ink-3);
}
/* The quoted line above a to-do chat message, as a Think question's post. */
.reply-quote.turn-quote {
  margin: 10px 0 4px auto;
  max-width: min(560px, 85%);
}
/* On my to-do accurate to the Ledger (docs/work/2026-10-02-todo-ledger-live.md).
   An item the Ledger closed, unmarked by the person: struck through like a
   tick, with a plain check in place of their checkbox and who closed it. */
/* An atomic inline box, so the row's strike-through never reaches it. */
/* Changed since the person last opened it: a small ink dot by the chevron,
   gone once opened. Ink, never the think blue (design, Blue is for think). */
/* On my to-do built from entries (docs/work/2026-10-07-todo-from-entries.md):
   two quiet hints after a line's words, never a state. "Done?" is text
   only; "Still on?" carries two small answers. They wrap under the name
   when the row is narrow, as at 320 px. */
/* 24 px tall to the eye, a 44 px wide touch target by its padding. */
/* Show under a line whose post changed since (docs/work/2026-10-19-post-editing-memory.md):
   a link drawn as the answers beside it. */
/* The tap area grows to 44 px tall without moving the line. */
/* A line's one pill under its name (docs/work/2026-10-14-todo-context-changed.md):
   "Still needed?", "Rename to …?" or "Done?", each with a one-line reason
   linking where it came from, then the question and Yes and No on their own
   line, which wraps on a phone. "Still on?" stays inline after the name. */
/* A line just removed: struck through in its place, the reasons under it. */
/* A 44 px tap target around a 24 px word. */
.tl-news {
  color: var(--ink);
}
/* Sticky groups (docs/work/2026-10-14-todo-sticky-groups.md): a quiet
   header per group (chevron, name, open count), Unsorted last and lighter.
   A header is the whole group's drop target while a line is dragged. */
/* While dragging: the line lifted, its group's place shown, the target lit. */
/* "Moved to …" with its Undo, under the line in its new place. */
/* Suggested, under the list (docs/work/2026-10-08-todo-suggested.md): a
   hairline, a quiet label, then rows with Yes and No after their words. No
   checkbox: an empty tick column keeps the words in line with the list's. */
/* Answered No: the struck title, then Undo on the same line. */
/* The person's ledger items that arrived after the list: apart, quiet. */
/* The Private section of On my to-do (docs/work/2026-10-03-todo-private-section.md):
   the person's open private items from their other conversations, folded
   to one quiet line under the list. Ink only. */
.todo-private {
  margin: 4px 0 0;
}
dialog.file-diff::backdrop {
  background: rgb(0 0 0 / 35%);
}
.file-diff-frame {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
.file-diff-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
  min-height: var(--bar);
  padding: var(--safe-top) 8px 6px 16px;
  border-bottom: 1px solid var(--line);
}
.file-diff-title {
  flex: 1;
  min-width: 0;
}
.file-diff-title h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-diff-title .hint {
  margin: 0;
  font-size: 12px;
}
.file-diff-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 12px 16px;
}
.file-diff-actions {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 10px 16px calc(10px + var(--safe-bottom));
  border-top: 1px solid var(--line);
}
.file-diff-actions .hint {
  margin: 0;
  flex: 1;
  min-width: 160px;
}
.diff {
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.5;
}
.diff-row {
  display: flex;
  gap: 8px;
  padding: 0 6px;
}
.diff-mark {
  flex: none;
  width: 1ch;
  color: var(--ink-3);
}
.diff-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  min-width: 0;
}
.diff-add {
  background: var(--surface-2);
}
.diff-del {
  background: var(--danger-bg);
}

/* ---------- settings dialog ---------- */
dialog.settings {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--paper);
  color: var(--ink);
}
dialog.settings::backdrop {
  background: rgba(0, 0, 0, 0.35);
}
.settings-frame {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
}
.settings-head {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
  min-height: var(--bar);
  padding: var(--safe-top) 8px 0;
  border-bottom: 1px solid var(--line);
}
.settings-head h2 {
  flex: 1;
  padding: 0 6px;
  font-size: 15px;
}
/* Settings' title sits in the middle of its bar, whether or not a back
   arrow is on its left: three columns, the outer two a button wide. */
#settings .settings-head {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) 40px;
}
#settings .settings-back {
  grid-column: 1;
}
#settings .settings-head h2 {
  grid-column: 2;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#settings .settings-head > :last-child {
  grid-column: 3;
}
.settings-title-all {
  display: none;
}
.settings-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.settings-tabs {
  display: flex;
  gap: 4px;
  flex: none;
  overflow-x: auto;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  scrollbar-width: none;
}
.settings-tabs::-webkit-scrollbar {
  display: none;
}
.settings-tabs button {
  flex: none;
  min-height: 40px;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-2);
}
.settings-tabs button[aria-selected="true"] {
  background: var(--ink);
  color: var(--paper);
}
.settings-pane {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 16px 20px calc(24px + var(--safe-bottom));
}
/* Settings' sections on a phone: a list first, then one section in its
   place with a back arrow in the head, the usual shape of a phone's settings
   screen — seven sections do not fit a row of pills
   (docs/work/2026-09-23-settings-split.md). Topic settings keeps its pills. */
.settings-tabs.settings-list {
  flex: 1;
  flex-direction: column;
  gap: 0;
  overflow-x: visible;
  overflow-y: auto;
  padding: 8px 12px calc(16px + var(--safe-bottom));
  border-bottom: 0;
}
.settings-list .settings-group {
  margin: 12px 0 2px;
  padding: 0 10px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.settings-list .settings-group:first-child {
  margin-top: 4px;
}
.settings-list .settings-group-foot {
  margin: 8px 0 0;
  border-top: 1px solid var(--line);
}
.settings-tabs.settings-list button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 16px;
  color: var(--ink);
  text-align: left;
}
.settings-tabs.settings-list button[aria-selected="true"] {
  background: none;
  color: var(--ink);
}
.settings-chevron {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--ink-3);
}
.settings-body[data-view="list"] .settings-pane,
.settings-body[data-view="section"] .settings-list {
  display: none;
}
.settings-pane h4.settings-whose {
  margin: 14px 0 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.settings-pane > h3,
.settings-pane .topic-brief > h3 {
  margin: 0 0 4px;
  font-size: 17px;
}
.settings-pane > .lead {
  color: var(--ink-2);
  margin-bottom: 8px;
}
.settings-pane .btn,
.settings-pane .btn-primary,
.settings-pane .btn-danger {
  margin-top: 12px;
}
.settings-pane hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: 22px 0;
}
.settings-pane .card {
  margin-top: 8px;
}
.github-connection {
  min-width: 0;
}
.github-connection h3 {
  margin-top: 0;
}
.github-consent {
  margin: 16px 0;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  color: var(--ink-2);
  font-size: 13.5px;
}
.github-consent p + p {
  margin-top: 10px;
}
.github-links {
  overflow-wrap: anywhere;
}
.token-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 44px;
  padding: 4px 0;
  border-top: 1px solid var(--line);
}
.token-row .btn {
  margin: 0;
  min-height: 40px;
}
.code-box {
  margin-top: 8px;
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.code-box textarea,
.code-box input {
  font-family: var(--font-mono);
  font-size: 12.5px;
  border: 0;
  padding: 0;
  min-height: 0;
  background: none;
  resize: none;
}
.code-box textarea:focus,
.code-box input:focus {
  box-shadow: none;
}
/* An invitation link with its copy button beside it; the button drops under it on narrow phones. */
.link-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.link-row .code-box {
  flex: 1 1 200px;
  min-width: 0;
  margin-top: 0;
}
.link-row .code-box input {
  width: 100%;
}
.link-row .btn {
  margin: 0;
}
.member-label small {
  display: block;
  color: var(--ink-3);
  font-size: 12px;
}
/* Touch browsers zoom into inputs under 16px; keep every focusable field at 16px there. */
@media (pointer: coarse) {
  .code-box textarea,
  .code-box input,
  .inline-form input,
  .inline-form select,
  .menu form input {
    font-size: 16px;
  }
}
.footnote {
  margin-top: 28px;
  font-size: 12px;
  color: var(--ink-3);
}

/* ---------- desktop ---------- */
@media (min-width: 840px) {
  .app {
    grid-template-columns: var(--sidebar) minmax(0, 1fr);
  }
  .sidebar {
    position: static;
    width: auto;
    height: auto;
    /* A grid item's minimum height is its content: without this the column
       grows past the 100dvh row and its body never scrolls. Either line
       alone bounds it; both say so on purpose. */
    min-height: 0;
    overflow: hidden;
    transform: none;
    visibility: visible;
    transition: none;
    box-shadow: none;
    padding-top: 0;
  }
  .main,
  .app:not(.collapsed) .main {
    position: static;
    transform: none;
    transition: none;
    box-shadow: none;
  }
  /* The sidebar's own raised edge, read only where the sidebar meets
     --paper (the stream, the composer) and not where it meets the pinned
     to-do card, which shares the sidebar's own --surface and reads as one
     layer with it — a shadow between two same-coloured surfaces looked
     wrong (user rule, 2026-10-01, from a screenshot). Put directly on
     .stream and .composer rather than on their ancestor .content: an
     inset box-shadow is part of an element's own background, painted
     under its children, so it would be hidden by any earlier opaque
     sibling if placed on .content (the pinned to-do card, confirmed
     broken into pieces that way before this landed) — but .stream and
     .composer have no opaque child hugging their own left edge, so their
     own shadow shows. Since .stream always follows .todo-pin in the flex
     column (whether expanded, collapsed, or absent), the shadow starts
     exactly at the card's bottom edge with no height math: below a card,
     or at the top of .content when there is none. Collapsed (no sidebar)
     keeps no shadow, since there is nothing beside .content to read as
     raised. */
  .app:not(.collapsed) .stream,
  .app:not(.collapsed) .composer {
    box-shadow: inset 6px 0 10px -6px var(--surface-shadow);
  }
  .app.collapsed {
    grid-template-columns: minmax(0, 1fr);
  }
  .app.collapsed .sidebar {
    display: none;
  }
  .scrim {
    display: none;
  }
  .topbar {
    min-height: 56px;
    padding: 0 16px;
  }
  /* Centring is mobile only (Jean, 2026-10-01: "center channel name on
     mobile only... on desktop the title stays left-aligned, as today").
     Each side goes back to sizing to its own content instead of the fixed
     40px reservation above, so an empty side takes no space here exactly
     as before this change, and the title stays flush left. */
  .topbar-side {
    flex: none;
    min-width: 0;
  }
  .topbar .title {
    font-size: 16px;
    text-align: left;
  }
  .back-btn {
    max-width: 240px;
    margin-right: 4px;
  }
  .topbar.has-back .topbar-side-start {
    max-width: none;
  }
  .column {
    padding-left: 24px;
    padding-right: 24px;
  }
  .stream .column {
    padding-top: 24px;
  }
  .todo-fold {
    padding-left: 0;
    padding-right: 0;
  }
  .todo-fold-inner {
    /* Capped and centred the same way .column is, so the list lines up
       with the conversation below it on a wide screen instead of hugging
       the left edge of the full-width card (user rule, 2026-10-01). The
       card's own background and separator stay full width: only this
       inner content is capped. */
    max-width: var(--column);
    margin-left: auto;
    margin-right: auto;
    padding-left: 24px;
    padding-right: 24px;
  }
  dialog.artifact-dialog,
  dialog.file-dialog,
  dialog.image-view {
    width: 90vw;
    height: 90vh;
    margin: auto;
    border: 1px solid var(--line-2);
    border-radius: var(--radius-lg);
    box-shadow: 0 24px 60px rgb(0 0 0 / 22%);
    overflow: hidden;
  }
  /* The mobile black background is a phone-only change (the user asked for
     mobile); desktop uses --surface-3 instead of the dialog's usual --paper
     so a white image's edges stay visible against the stage. */
  dialog.image-view {
    background: var(--surface-3);
    color: var(--ink);
  }
  dialog.image-view::backdrop {
    background: rgb(0 0 0 / 35%);
  }
  .image-view-head .icon-btn {
    color: var(--ink-2);
  }
  .image-view-head .icon-btn:hover:enabled {
    background: var(--surface-2);
    color: var(--ink);
  }
  dialog.settings {
    width: min(92vw, 720px);
    height: auto;
    max-height: min(86dvh, 720px);
    margin: auto;
    border-radius: 16px;
    border: 1px solid var(--line-2);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
  }
  dialog.file-diff {
    width: min(92vw, 720px);
    height: auto;
    max-height: min(86dvh, 720px);
    margin: auto;
    border-radius: 16px;
    border: 1px solid var(--line-2);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
  }
  dialog.emoji-sheet {
    width: 400px;
    height: min(72dvh, 480px);
    margin: auto;
    border-radius: 16px;
    border: 1px solid var(--line-2);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
  }
  .settings-frame {
    height: min(86dvh, 720px);
  }
  .settings-body {
    flex-direction: row;
  }
  .settings-tabs {
    flex-direction: column;
    flex: none;
    width: 190px;
    padding: 12px;
    border-bottom: 0;
    border-right: 1px solid var(--line);
    overflow: visible;
  }
  .settings-tabs button {
    text-align: left;
    border-radius: 8px;
    padding: 8px 10px;
  }
  .settings-tabs button[aria-selected="true"] {
    background: var(--surface-2);
    color: var(--ink);
  }
  .settings-pane {
    padding: 22px 28px 28px;
  }
  /* Desktop shows the list as the rail and a section beside it, always. */
  .settings-tabs.settings-list {
    flex: none;
    padding: 12px;
    border-right: 1px solid var(--line);
  }
  .settings-tabs.settings-list button {
    min-height: 36px;
    font-size: 14px;
    color: var(--ink-2);
  }
  .settings-tabs.settings-list button[aria-selected="true"] {
    background: var(--surface-2);
    color: var(--ink);
  }
  .settings-chevron,
  .settings-back,
  .settings-title-section {
    display: none;
  }
  .settings-title-all {
    display: inline;
  }
  .settings-body[data-view="list"] .settings-pane,
  .settings-body[data-view="section"] .settings-list {
    display: flex;
  }
  .settings-body[data-view="list"] .settings-pane {
    display: block;
  }
}

/* ---------- toast ---------- */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(18px + var(--safe-bottom));
  transform: translateX(-50%);
  z-index: 60;
  max-width: calc(100vw - 32px);
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 14px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  pointer-events: none;
}
.toast:empty {
  display: none;
}

/* ---------- app update ---------- */
.app-update {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: calc(16px + var(--safe-bottom));
  z-index: 70;
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: 440px;
  min-height: 64px;
  margin: 0 auto;
  padding: 10px 10px 10px 16px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.2);
  animation: app-update-in 180ms ease-out;
}
.app-update p {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 500;
}
.app-update .btn-primary {
  flex: none;
  min-height: 44px;
}
.app-update-ready .toast {
  bottom: calc(92px + var(--safe-bottom));
}
.app-update-ready .main {
  padding-bottom: calc(80px + var(--safe-bottom));
}
@keyframes app-update-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}
@media (min-width: 840px) {
  .app-update {
    right: 24px;
    left: auto;
    margin: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .app-update {
    animation: none;
  }
}

/* ---------- workspace administration (Settings → People) ---------- */
/* Member and catalog rows: label and role first, then actions that wrap under them on phones. */
.member-row,
.rows li {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  min-height: 44px;
  padding: 6px 0;
  border-top: 1px solid var(--line);
  min-width: 0;
}
.rows {
  list-style: none;
  padding: 0;
  margin: 0;
}
.member-row > .row {
  flex: 1;
  min-width: 0;
}
.member-label {
  overflow-wrap: anywhere;
  min-width: 0;
}
/* A person's name is bold at the same weight everywhere: the members list, a
   mentions card, the onboarding "Signed in as", like a post's author. */
.member-label strong,
.gate-card strong#identity {
  font-weight: 600;
}
/* A model shown as provider glyph + name (+ exact ID for admins). */
.model-name {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 6px;
  min-width: 0;
}
.model-icon {
  display: inline-flex;
  flex: none;
  color: var(--ink-2);
}
.model-icon svg {
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.model-title {
  font-weight: 500;
}
.model-id {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ink-3);
  overflow-wrap: anywhere;
  min-width: 0;
}
#model-form select {
  margin-bottom: 8px;
}
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0 4px;
  flex-basis: 100%;
}
@media (min-width: 840px) {
  .row-actions {
    flex-basis: auto;
    margin-left: auto;
  }
}
.row-actions .btn-ghost {
  min-height: 36px;
  padding: 4px 10px;
  margin: 0;
  font-size: 13.5px;
}
#catalog-editor,
#my-models {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

/* ---------- AI instructions (settings, topic, exploration panel) ---------- */
/* The priority order: up and down rather than drag, so it works on a phone.
   40 px targets, a deliberate step up from the 36 px dense-control exception
   the catalog rows use, because reordering is a deliberate act. */
#instruction-order {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}
#instruction-order .row-actions .btn-ghost {
  min-height: 40px;
  min-width: 56px;
  justify-content: center;
}
#instruction-order-state {
  margin-top: 8px;
}
/* One line the assistant leaves when it changed a standing instruction with
   its own tool: never anyone's text, only the level and the version. */
.step-note {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0;
  min-width: 0;
}
/* What memory did with a post (src/client/memory-line.tsx): quiet lines under
   the text in the think-stub shape, `--ink-3` at 12.5 px; the running check
   carries the steps' spinner. Each line is a button that opens the job's
   trace, drawn as text: no border, no fill, the same 32 px hit height as the
   reaction chips beside it. A declined note reads the same, not in red: the
   editor said no for a reason, which is not an error. */
.memory-line {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 8px;
  margin-left: -8px;
  border: 0;
  border-radius: 10px;
  background: none;
  font-size: 12.5px;
  color: var(--ink-3);
  text-align: left;
}
button.memory-line:hover {
  background: var(--surface-2);
  color: var(--ink-2);
}
.memory-line .glyph {
  width: 14px;
  height: 14px;
  flex: none;
}
.memory-line .glyph.spin {
  animation: spin 900ms linear infinite;
}
/* A note group's own row ("Added 3 facts and 1 commitment ›",
   memory-line.tsx's `NoteGroupRow`): the brain, the summary span, then the
   chevron as its own flex item (`.memory-line .glyph` already sizes it and
   keeps it from shrinking), which turns to point down while the group is
   open — the same rotate-on-`open` the to-do list's own chevron uses
   (`.todo-chevron .glyph`). */
.memory-line .glyph.chevron {
  transition: transform var(--motion-fast) var(--motion-ease);
}
.memory-line .glyph.chevron.open {
  transform: rotate(90deg);
}
/* A tap's optional note: a one-line field under the item after the tap is
   chosen, with Skip and Save, the full width on a phone
   (docs/work/2026-09-26-memory-v2-tap-notes.md). */
.tap-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.tap-note label {
  flex: 1 1 100%;
  margin: 0;
  font-size: 12.5px;
  color: var(--ink-2);
}
.tap-note input {
  box-sizing: border-box;
  flex: 1 1 200px;
  min-width: 0;
  min-height: 40px;
  font-size: 16px;
}
.tap-note-actions {
  display: flex;
  gap: 8px;
}
.tap-note-actions button {
  min-height: 40px;
}
.tap-notes {
  margin: 6px 0 0;
}
.tap-notes li {
  overflow-wrap: anywhere;
}
/* A file's update as a line in the stream (src/client/memory-line.tsx,
   `UpdateLine`; Slice E of the memory files record): the same quiet
   `.memory-line` row, standing on its own between posts rather than under
   one, indented to the posts' text column so it reads as part of the
   conversation. A held version is the same line in `--ink-2`, since it is
   waiting for the reader. The source control beside it, for a note from
   another topic, is a plain text button in ink. */
.memory-update {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 4px;
  padding: 0 0 0 38px;
}
.memory-update.held .memory-line {
  color: var(--ink-2);
}
.memory-line.link {
  color: var(--ink);
  font-weight: 500;
}
/* An open block in the conversation keeps its header at the top of the
   stream while its body scrolls under it, so it can be collapsed from where
   the reader is rather than from its top: an inset thread, the
   recap text under a page, a run of memory updates, a step, Saved activity
   ([record](../docs/work/2026-09-24-sticky-collapsible-headers.md)). Only
   inside `.stream`, the one element of either view that scrolls, and only
   the outermost block: a step inside an inset thread stays in the flow, so
   headers never stack. Each header wears its block's own background, so what
   passes under it is covered and nothing changes while it is not stuck, and a
   hairline under it. z-index 1: nothing positioned inside a block body sets
   one, and the drop-target tint (also 1, later in the tree) still covers it.
   Collapsing a block whose header is stuck is `revealCollapsed`
   (src/client/reveal-collapsed.ts). */
.stream .think-head,
.stream .recap-text[open] > summary,
.stream .saved-activity[open] > summary {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}
/* The step's rule stands alone: its `:not()` takes a complex selector
   (Selectors 4), and an engine without it drops the whole list it sits in. */
.stream .step[open]:not(.think *) > summary {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}
/* The tinted blocks: the header spans the block's padding too, so the tint
   and the hairline reach both edges of the card when it is stuck. */
.stream .think-head {
  margin: 0 -14px;
  padding: 0 14px 6px;
  background: var(--think-bg);
  border-bottom-color: var(--think-line);
}

.stream .step[open]:not(.think *) > summary {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

.stream .step[open]:not(.think *) > summary:hover {
  background: var(--surface-2);
}
/* The note on a message memory kept something from ("updated to-do.md"):
   the same 11 px `--ink-3` behind a middle dot as the AI notes beside it,
   as a button that opens the version, drawn as text. */
.post .head button.update-note {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  font-size: 11px;
  color: var(--ink-3);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.post .head button.update-note:hover {
  color: var(--ink-2);
}
/* The post a link asked for (`?post=`): lit for a moment on arrival, the
   tint fading out, so the eye lands on it and the tint then leaves the
   conversation as it was. Reduced motion drops the animation with the rest. */
.post.focused {
  animation: post-focus 2400ms ease-out;
  border-radius: 12px;
}
@keyframes post-focus {
  0%,
  40% {
    box-shadow: inset 0 0 0 100vmax var(--surface-2);
  }
  100% {
    box-shadow: none;
  }
}
/* Settings → Memory (memory v2 slice two): a row of tabs that wraps rather
   than scrolling sideways, from 320 px up; the open one filled as Settings'
   own pills are. Waiting on you and Contests carry a count. */
.memory-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 14px;
}
.memory-tabs button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-2);
  background: var(--surface-2);
}
.memory-tabs button[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}
/* A contest card's two sides (docs/work/2026-09-26-memory-v2-contest-sides.md):
   each labelled in words, the standing claim first, then memory's reading. */
/* A claim's source post deleted since: the words stay, quietly marked. */
.source-gone {
  color: var(--ink-3);
}

.memory-list .quote {
  overflow-wrap: anywhere;
}
/* The count pill on the Memory entry (the workspace menu and Settings'
   list) and on the Memory tabs: the mention pill's shape. */
.memory-tabs .count,
.menu button .count,
.settings-list button .count {
  display: inline-block;
  min-width: 20px;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 11px;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}
.memory-tabs button[aria-pressed="true"] .count {
  background: var(--paper);
  color: var(--ink);
}
.memory-tabs .count {
  margin-left: 0;
}
/* The context wheel (src/client/context-wheel.tsx): a 20 px ring at the start
   of the usage line and a sheet with the breakdown. The six category colours
   are the dataviz skill's reference categorical slots 1–6, in fixed order,
   validated for colour-vision separation in both modes (2026-09-23); the light
   steps are under 3:1 on the surface, so the sheet's labelled table carries
   every value. */
:root {
  --ctx-1: #2a78d6;
  --ctx-2: #eb6834;
  --ctx-3: #1baf7a;
  --ctx-4: #eda100;
  --ctx-5: #e87ba4;
  --ctx-6: #008300;
}
@media (prefers-color-scheme: dark) {
  :root {
    --ctx-1: #3987e5;
    --ctx-2: #d95926;
    --ctx-3: #199e70;
    --ctx-4: #c98500;
    --ctx-5: #d55181;
    --ctx-6: #008300;
  }
}
.context-ring {
  flex: none;
}
.context-ring .track {
  stroke: var(--line-2);
}
.context-ring .ctx-1 {
  stroke: var(--ctx-1);
}
.context-ring .ctx-2 {
  stroke: var(--ctx-2);
}
.context-ring .ctx-3 {
  stroke: var(--ctx-3);
}
.context-ring .ctx-4 {
  stroke: var(--ctx-4);
}
.context-ring .ctx-5 {
  stroke: var(--ctx-5);
}
.context-ring .ctx-6 {
  stroke: var(--ctx-6);
}
.context-ring .rest {
  stroke: var(--ink-3);
}
button.context-wheel {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  min-height: 40px;
  padding: 0 4px 0 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink-2);
  cursor: pointer;
  vertical-align: middle;
}
button.context-wheel .context-words {
  flex: none;
}
button.context-wheel .usage-figures {
  color: var(--ink-3);
}
button.context-wheel:hover .context-words {
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}
dialog.context-sheet {
  width: min(100vw - 32px, 480px);
  max-height: min(85dvh, 680px);
  padding: 12px 16px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
}
dialog.context-sheet::backdrop {
  background: rgba(0, 0, 0, 0.35);
}
.context-sheet-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.context-sheet-head strong {
  flex: 1;
}
.context-sheet-usage {
  display: grid;
  gap: 2px;
  margin: 0 0 12px;
  font-size: 13.5px;
  color: var(--ink-2);
}
.context-sheet-summary {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 4px 0 12px;
}
.context-sheet-summary p {
  margin: 0;
  display: grid;
  gap: 4px;
}
.context-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}
.context-table th,
.context-table td {
  padding: 6px 0;
  border-top: 1px solid var(--line);
  text-align: right;
  font-weight: 400;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.context-table th[scope="row"],
.context-table thead th:first-child {
  text-align: left;
  color: var(--ink);
}
.context-table thead th:first-child {
  padding-left: 0;
}
.context-table thead th {
  border-top: 0;
  white-space: nowrap;
  padding-left: 12px;
  font-size: 12px;
  color: var(--ink-3);
}
.context-table .swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 8px;
  border-radius: 2px;
  background: var(--line-2);
  vertical-align: -1px;
}
.context-table .swatch.ctx-1 {
  background: var(--ctx-1);
}
.context-table .swatch.ctx-2 {
  background: var(--ctx-2);
}
.context-table .swatch.ctx-3 {
  background: var(--ctx-3);
}
.context-table .swatch.ctx-4 {
  background: var(--ctx-4);
}
.context-table .swatch.ctx-5 {
  background: var(--ctx-5);
}
.context-table .swatch.ctx-6 {
  background: var(--ctx-6);
}
.context-table .swatch.rest {
  background: var(--ink-3);
}
.context-provenance {
  margin: 12px 0 0;
  padding-left: 18px;
  font-size: 13px;
  color: var(--ink-2);
}
/* A Google Doc the AI made: the artifact card's head, no frame, and one line
   saying who can open it (docs/work/2026-09-25-google-docs.md). */
.google-doc-card .google-doc-shared {
  margin: 0;
  padding: 0 10px 8px;
}

/* ---------- brain dump (src/client/brain-dump.tsx) ----------
   A private voice note to the AI, from any screen, held to talk: a finger
   from the right edge pulls the main surface left and this black strip
   opens behind it, exactly as wide as the surface moved; past the
   threshold it records while the finger stays down, and the strip itself
   shows it: the live waveform, the clock, what a release does. The pill
   at the top only follows the release. Black and white in both colour modes (user, 2026-09-26:
   "sleeker"); the red dot is the one colour, and only while recording
   (docs/work/2026-09-27-brain-dump-hold.md). */
.dump-strip {
  position: fixed;
  top: var(--app-top, 0px);
  right: 0;
  height: var(--app-height, 100dvh);
  width: 0;
  z-index: 4;
  overflow: hidden;
  /* Three bands: empty, the content centred, and the held hint centred in
     the space below, so it sits evenly between the recording and the
     strip's foot. */
  display: grid;
  grid-template-rows: 1fr auto 1fr;
  justify-items: center;
  background: #0c0c0d;
  color: #fafafa;
  pointer-events: none;
  transition: width 0.18s ease-out;
}
.dump-strip-core {
  grid-row: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.dump-strip-mark {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #fafafa;
  color: #0c0c0d;
  transform: scale(calc(0.55 + var(--dump-progress, 0) * 0.45));
  opacity: calc(0.35 + var(--dump-progress, 0) * 0.65);
  transition: transform 0.12s ease-out;
}
.dump-strip-mark svg {
  width: 24px;
  height: 24px;
}
/* The ring around the mark fills as the finger travels. */
.dump-strip-mark::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  background: conic-gradient(
    #fafafa calc(var(--dump-progress, 0) * 360deg),
    transparent 0
  );
  -webkit-mask: radial-gradient(circle, transparent 25px, #000 26px);
  mask: radial-gradient(circle, transparent 25px, #000 26px);
}
.dump-strip.ready .dump-strip-mark {
  transform: scale(1.12);
}
.dump-strip.cancel .dump-strip-mark {
  transform: scale(0.8);
  opacity: 0.45;
}
.dump-strip-label {
  max-width: 52px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  opacity: calc((var(--dump-progress, 0) - 0.5) * 2);
}

/* Held, a hint under the strip's words: pull on left for the full view,
   hands-free. It brightens, its chevron leads, and a bar under it fills as
   the finger pulls on (--dump-expand, 0 at the hold and 1 at the switch),
   so the switch is a visible threshold
   (docs/work/2026-09-28-edge-gesture-quality.md). */
.dump-strip-up {
  grid-row: 3;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 52px;
  line-height: 1.2;
  text-align: center;
  font-size: 10.5px;
  font-weight: 600;
  opacity: calc(0.55 + var(--dump-expand, 0) * 0.45);
}
.dump-strip-up svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform: translateX(calc(var(--dump-expand, 0) * -6px));
}
.dump-strip-fill {
  display: block;
  width: 36px;
  height: 3px;
  margin-top: 7px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.22);
  overflow: hidden;
}
.dump-strip-fill::after {
  content: "";
  display: block;
  height: 100%;
  background: #fafafa;
  transform: scaleX(var(--dump-expand, 0));
  transform-origin: right;
}
/* Growing into the full view: the strip's own words fade as it widens,
   and the view fades in over the same black. */
.dump-strip.growing .dump-strip-core,
.dump-strip.growing .dump-strip-up {
  opacity: 0;
  transition: opacity 140ms ease-out;
}
/* The full view, for a long dump: pulled on left past the hold, it records
   hands-free and the words land in a field to edit before sending
   (docs/work/2026-09-28-brain-dump-expand.md). Black and white, as round 6
   chose. */
.dump {
  position: fixed;
  /* The visible area, so the iOS keyboard never covers the field or Send
     (src/client/viewport.ts sets these while it is open). */
  top: var(--app-top, 0px);
  left: 0;
  right: 0;
  bottom: calc(100% - var(--app-top, 0px) - var(--app-height, 100%));
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: calc(20px + var(--safe-top)) 20px calc(20px + var(--safe-bottom));
  background: #0c0c0d;
  color: #fafafa;
  /* Waits for the strip to have grown most of the way (320 ms, in
     brain-dump.tsx), then fades in over it; its rows rise a little. */
  animation: dump-in 200ms ease-out 140ms both;
}
.dump > * {
  animation: dump-rise 260ms cubic-bezier(0.2, 0.8, 0.2, 1) 200ms both;
}
@keyframes dump-rise {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}
.dump-label {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 14px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 16px;
  font-size: 14px;
  font-weight: 600;
}
.dump-label svg {
  width: 16px;
  height: 16px;
}
.dump-sub {
  margin: 0;
  font-size: 13px;
  color: #9d9ea4;
  text-align: center;
}
.dump-field {
  flex: 1;
  width: 100%;
  max-width: 560px;
  min-height: 120px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.06);
  color: #fafafa;
  font: inherit;
  font-size: 17px;
  line-height: 1.45;
  resize: none;
}
.dump-field::placeholder {
  color: #6f7076;
}
.dump-field:focus-visible {
  outline: 2px solid #fafafa;
  outline-offset: 2px;
}
.dump-live {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-height: 0;
}
.dump-wave {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 56px;
}
.dump-wave i {
  display: block;
  width: 4px;
  height: 6px;
  border-radius: 2px;
  background: #fafafa;
}
.dump-timer {
  font-size: 22px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.dump-hint {
  min-height: 20px;
  max-width: 320px;
  margin: 0;
  font-size: 14px;
  color: #9d9ea4;
  text-align: center;
}
.dump-hint.problem {
  color: #ff8a80;
}
.dump-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  max-width: 560px;
}
.dump-stop {
  flex: none;
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #fafafa;
  color: #0c0c0d;
  cursor: pointer;
}
.dump-stop svg {
  width: 30px;
  height: 30px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.dump-stop.busy svg {
  animation: spin 1s linear infinite;
}
.dump-cancel,
.dump-send {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 96px;
  height: 44px;
  padding: 0 16px;
  justify-content: center;
  border-radius: 22px;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.dump-cancel {
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: none;
  color: #fafafa;
}
.dump-cancel svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.dump-send {
  border: 0;
  background: #fafafa;
  color: #0c0c0d;
}
.dump-send[aria-disabled="true"] {
  opacity: 0.35;
  cursor: default;
}
.dump-stop:focus-visible,
.dump-cancel:focus-visible,
.dump-send:focus-visible {
  outline: 2px solid #fafafa;
  outline-offset: 3px;
}
@media (prefers-color-scheme: dark) {
  .dump {
    background: #000;
  }
}
/* Just above the composer (Jean, 2026-10-05), where the eyes already are:
   brain-dump.tsx measures it into --dump-pill-bottom and centres the pill
   on it with --dump-pill-x; without one, above the safe area. */
.dump-pill {
  position: fixed;
  top: auto;
  bottom: var(--dump-pill-bottom, calc(var(--safe-bottom) + 16px));
  left: var(--dump-pill-x, 50%);
  z-index: 90;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  width: max-content;
  max-width: calc(100vw - 32px);
  padding: 8px 16px;
  border-radius: 22px;
  background: #0c0c0d;
  color: #fafafa;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);
  font-size: 14px;
  font-weight: 600;
  animation: dump-pill-in 120ms ease-out;
}
@media (prefers-color-scheme: dark) {
  .dump-strip,
  .dump-pill {
    background: #000;
  }
  .dump-pill {
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
  }
}
@keyframes dump-in {
  from {
    opacity: 0;
  }
}
/* The pill rises 6 px into place: its own keyframes, since the full view's
   dump-in ends the strip's growth (brain-dump.tsx, `entered`). */
@keyframes dump-pill-in {
  from {
    opacity: 0;
    transform: translate(-50%, 6px);
  }
}
.dump-pill-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 24px;
}
.dump-pill-row svg {
  flex: none;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  animation: spin 1s linear infinite;
}
/* Recording: the strip under the finger carries the live waveform, the
   clock and what a release does. */
.dump-strip-wave {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 42px;
}
.dump-strip-wave i {
  display: block;
  width: 3px;
  height: 6px;
  border-radius: 2px;
  background: #fafafa;
}
.dump-strip-time {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.dump-strip-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #ff3b30;
  animation: dump-blink 1.2s ease-in-out infinite;
}
.dump-strip.cancel {
  background: #3a3a3e;
}
.dump-strip.cancel .dump-strip-wave,
.dump-strip.cancel .dump-strip-dot {
  opacity: 0.35;
  animation: none;
}
@keyframes dump-blink {
  50% {
    opacity: 0.35;
  }
}
.dump-pill-line {
  margin: 0;
  text-align: center;
}
.dump-pill.failed {
  gap: 8px;
  padding: 12px 16px;
  font-weight: 400;
}
.dump-pill-actions {
  display: flex;
  gap: 8px;
}
.dump-pill-actions button {
  height: 36px;
  padding: 0 14px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 18px;
  background: none;
  color: #fafafa;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.dump-pill-actions button:first-child {
  background: #fafafa;
  color: #0c0c0d;
}
/* A sent voice note's Open, beside its line. */
.dump-pill-action {
  flex: none;
  height: 28px;
  margin-right: -8px;
  padding: 0 12px;
  border: 0;
  border-radius: 14px;
  background: #fafafa;
  color: #0c0c0d;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.dump-pill-action:focus-visible,
.dump-pill-actions button:focus-visible {
  outline: 2px solid #fafafa;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .dump-pill,
  .dump-strip-dot,
  .dump-pill-row svg,
  .dump-strip,
  .dump-strip-mark,
  .dump,
  .dump > * {
    animation: none;
    transition: none;
  }
}
/* A coding task (/_h/<id>) is a screen of the workspace view: the top bar,
   its close control and the scrolling pane (.repo-scroll, .column
   .repo-screen) are the repository screen's; these only lay out its parts
   (docs/work/2026-09-30-coding-task-page-shell.md). */
.coding-task-page-label { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; --badge-ring: var(--paper); }
.coding-task-page-facts { overflow-wrap: anywhere; }
.coding-task-page-status { margin: 8px 0 0; }
.coding-task-page-marker { margin: 12px 0 0; font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); overflow-wrap: anywhere; }
/* The brief needs no rule of its own: it is Markdown under `.prose`, set as
   an answer is, and a long code line scrolls inside its block
   (`.code-block pre`), never widening the page at 390 px
   (docs/work/2026-10-03-task-brief-markdown.md). */
/* A topic post's files sit under its words, on the post's side
   (docs/work/2026-09-29-reply-files.md). */
.file-chips.post-files {
  justify-content: flex-start;
  max-width: 100%;
}
.post-files .file-chip-name {
  max-width: min(60vw, 320px);
}

/* ---------- a connected repository (src/client/source.tsx) ----------
   In flight and Landed: one column of rows, each a title and a line of
   metadata, links to GitHub for the rest
   (docs/work/2026-09-29-github-source-simplified.md#the-screen). */
.repo-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.repo-screen {
  padding-top: 16px;
  padding-bottom: calc(24px + var(--safe-bottom));
}
.repo-head h1 {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
.repo-head-meta,
.repo-row-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--ink-3);
}
.repo-row-meta a {
  color: var(--ink-2);
}
.repo-row-meta code {
  font-family: var(--font-mono);
}
.repo-warning {
  margin: 10px 0 0;
  font-size: 13.5px;
  color: var(--danger);
}
.repo-actions a.btn {
  color: var(--ink);
  text-decoration: none;
}
.repo-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.repo-section {
  margin: 24px 0 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-2);
}
.repo-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}
.repo-row {
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
  min-width: 0;
}
.repo-row-title {
  margin: 0;
  font-size: 15px;
  overflow-wrap: anywhere;
}
.repo-row-title a {
  color: var(--ink);
}
.repo-untitled,
.repo-none {
  color: var(--ink-3);
}
.repo-none {
  margin: 8px 0;
  font-size: 13.5px;
}
.repo-checks-failure {
  color: var(--danger);
}
.repo-more {
  margin-top: 12px;
}

/* ---------- list screens: On my to-do and Waiting on others ---------- */
/* The shared shell (src/client/pinned-list-screen.tsx,
   docs/work/2026-10-01-todo-screen.md section 7): the to-do's pinned card
   alone on its screen, so open it takes the height it needs and scrolls
   inside; collapsed it keeps the card's own rule. Rows are the to-do
   card's (`.todo-row` and the rest). Ink and neutrals only: blue is
   Think's. */
/* A refresh running: the rows' words carry the shared `.text-shimmer`
   (docs/work/2026-10-02-text-shimmer.md), so the list reads as about to
   change while it keeps its shape and its taps. Rows a refresh added settle
   with a short fade (docs/work/2026-10-01-todo-refresh-visible.md). */
@keyframes todo-settle {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ---------- news on a to-do item (docs/work/2026-10-01-ledger-news-ui.md) ---------- */
/* The status line: the latest news, who said it, when, one quiet line
   under the words; a tap opens the post. An inline-block, so a done row's
   line-through does not reach it. */
/* A blocked suggestion: a small chip after the words. */
/* "Probably finished (2) ›", folded at the foot of the list. */
/* No checkbox here: the words line up with the list's words. */
/* The status line, then the two answers on its row, small and quiet
   (user, 2026-10-01, 18:05 UTC); too narrow, they wrap under it,
   right-aligned. */
/* Text-sized, in the status line's muted ink; the 44px tap target comes
   from padding, given back by negative margins so the line stays 18px. */
/* The Not done sheet: from the foot of the screen on a phone, a card on
   desktop. */

/* The news in full, in an item's expanded details (docs/work/2026-10-02-todo-news-in-dropdown.md):
   wrapped text, then its source and time as the link to the post. */

/* A to-do tick that closed an item this post holds (docs/work/2026-10-02-todo-ledger-tick.md, decision 7). */
.post-ticks {
  margin: 2px 0 0;
  font-size: 12px;
  color: var(--ink-3);
}

/* Read-only linked memory inspector. Everything remains inside this screen. */
/* To-do entries over the Ledger (docs/work/2026-10-06-todo-entries.md). */
/* The entries view's groups: pills that wrap, so every count shows at once
   on a phone (docs/work/2026-10-06-todo-entries.md). */

/* Add to memory (docs/work/2026-10-06-think-add-to-memory.md): the one
   question a think answer may end with when the person told Workroom
   something company memory doesn't have yet. The question names who doesn't
   know and what would go in; its chevron unfolds the exact fact, editable in
   place; Add to memory writes it with one tap, folded or not. After the tap
   the block folds into a quiet note saying what went in and who sees it.
   Blue only because it lives in a think thread: the button keeps the think
   fill there, like the thread's own exits. */

/* ---------- links pinned in a topic ---------- */
/* One row of chips above the topic's stream, in the conversation's column,
   44px tall in all: each chip a document's glyph and its title, cut at about
   18 characters; "+N" and "+" past the chips that fit. Outside the stream, it
   stays on screen while the stream follows its end. Restored after #2413
   deleted it with Pages (docs/work/2026-11-03-topic-pins-row-fix.md).
   Neutrals only. */
.topic-pages {
  flex: none;
  width: 100%;
  min-width: 0;
  max-width: var(--column);
  margin: 0 auto;
  padding: 4px max(16px, env(safe-area-inset-right)) 0
    max(16px, env(safe-area-inset-left));
}
/* On a wide screen, the column's own side padding, so the chips line up with
   the posts. */
@media (min-width: 840px) {
  .topic-pages {
    padding-left: 24px;
    padding-right: 24px;
  }
}
.topic-pages ul {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  /* Room for the focus ring, which the row would otherwise clip. */
  margin: 0 -4px;
  padding: 0 4px;
  list-style: none;
}
.topic-pages li {
  flex: none;
}
.topic-page,
.topic-pages-more {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-2);
  color: var(--ink);
  font-size: 14px;
  line-height: 1;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
.topic-page-title {
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topic-pages-more {
  color: var(--ink-2);
  font-size: 13px;
  font-weight: 600;
}
@media (hover: hover) {
  .topic-page:hover,
  .topic-pages-more:hover {
    background: var(--surface);
  }
}
.topic-page:active,
.topic-pages-more:active {
  background: var(--surface);
}

/* Links pinned to a topic (src/client/topic-links.tsx,
   docs/work/2026-11-02-topic-pins.md): the pinned pages' row, each chip the
   Documents grid's glyph in its tone; "+" ends the row. The sheets are bottom
   sheets on a phone and a centred card from 600 px. */
.topic-link .pin-glyph,
.pins-list .pin-glyph,
.pin-kind svg {
  display: inline-flex;
  flex: none;
}
.pin-glyph svg,
.pin-kind svg,
.topic-links-add svg,
.pins-list-add svg,
.pin-menu [role="menuitem"] svg,
.pin-menu [role="menuitemradio"] svg {
  stroke: currentColor;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.pin-glyph,
.pin-kind {
  color: var(--ink-2);
}
[data-tone="workroom"] { --pin-tone: var(--doc-workroom); }
[data-tone="file"] { --pin-tone: var(--doc-file); }
[data-tone="google"] { --pin-tone: var(--doc-google); }
[data-tone="notion"] { --pin-tone: var(--doc-notion); }
[data-tone="figma"] { --pin-tone: var(--doc-figma); }
[data-tone="claude"] { --pin-tone: var(--doc-claude); }
.pin-glyph[data-tone],
.pin-kind[data-tone] svg {
  color: var(--pin-tone, var(--ink-2));
}
/* The chips that fit, then "+N" and "+", which never leave the screen. */
.topic-links {
  display: flex;
  align-items: center;
  gap: 8px;
}
.topic-links .topic-links-chips {
  position: relative;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  -webkit-mask-image: none;
  mask-image: none;
}
/* Counted in "+N": measured for when the row widens, out of the flow and unseen. */
.topic-links-chips li[data-clipped="true"] {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
}
.topic-links-tail {
  display: flex;
  flex: none;
  align-items: center;
  gap: 8px;
}
.topic-links-add {
  width: 32px;
  justify-content: center;
  padding: 0;
}
dialog.pin-sheet {
  width: 100%;
  max-width: 100%;
  max-height: 85dvh;
  margin: auto 0 0;
  padding: 8px 16px max(20px, env(safe-area-inset-bottom));
  border: 0;
  border-radius: 20px 20px 0 0;
  background: var(--surface);
  color: var(--ink);
  overflow-y: auto;
}
dialog.pin-sheet::backdrop {
  background: rgba(0, 0, 0, 0.35);
}
.pin-sheet-grip {
  width: 36px;
  height: 4px;
  margin: 2px auto 12px;
  border-radius: 2px;
  background: var(--line-2);
}
@media (min-width: 600px) {
  dialog.pin-sheet {
    width: min(100vw - 32px, 440px);
    margin: auto;
    padding: 16px 20px 20px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  .pin-sheet-grip {
    display: none;
  }
}
.pin-sheet-title {
  margin: 0 0 4px;
  font-size: 18px;
  font-weight: 650;
}
.pin-sheet-note {
  margin: 0 0 14px;
  color: var(--ink-2);
  font-size: 14px;
}
.pin-form {
  display: grid;
  gap: 8px;
}
.pin-form .pin-sheet-title {
  margin-bottom: 0;
}
.pin-form .pin-sheet-note {
  margin-bottom: 6px;
}
.pin-kind {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 20px;
  margin: 0 0 4px 2px;
  font-size: 14px;
}
.pin-error {
  margin: 0;
  color: var(--danger);
  font-size: 14px;
}
.pin-submit {
  min-height: 44px;
  margin-top: 6px;
  border-radius: 12px;
  font-weight: 600;
}
.pin-menu [role="menu"] {
  display: grid;
}
.pin-menu [role="menuitem"],
.pin-menu [role="menuitemradio"],
.pins-list-add {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 48px;
  padding: 0 4px;
  font-size: 16px;
  text-align: left;
}
.pin-menu [role="menuitem"] + [role="menuitem"],
.pin-menu [role="menuitemradio"] + [role="menuitemradio"] {
  border-top: 1px solid var(--line);
}
.pin-menu [role="menuitem"] svg,
.pin-menu [role="menuitemradio"] svg,
.pins-list-add svg {
  color: var(--ink-2);
}
.pin-menu .danger,
.pin-menu .danger svg {
  color: var(--danger);
}
.pins-list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line);
}
.pins-list li {
  display: flex;
  align-items: center;
  gap: 4px;
  border-bottom: 1px solid var(--line);
}
.pins-list-open {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: center;
  gap: 14px;
  padding: 10px 2px;
  text-align: left;
}
.pins-list-words {
  display: grid;
  min-width: 0;
}
.pins-list-title,
.pins-list-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pins-list-title {
  font-size: 16px;
}
.pins-list-meta {
  color: var(--ink-2);
  font-size: 14px;
}
.pins-list-add {
  margin-top: 4px;
  color: var(--ink-2);
}
@media (hover: hover) {
  .pins-list-open:hover .pins-list-title,
  .pin-menu [role="menuitem"]:hover,
  .pin-menu [role="menuitemradio"]:hover,
  .pins-list-add:hover {
    text-decoration: underline;
  }
}

/* ---------- a database's table (src/client/database.tsx) ----------
   Deliberately simple and replaceable
   (docs/work/2026-10-07-memory-databases-prototype.md): a one-row header,
   compact cards with Workroom's proposals inside their row's card, a rows
   list, and a row page. Neutrals only; from 840px it sits in .column. */
.db-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--paper);
}
/* Rows in groups, in every layout: a header per group (a chevron, its
   label and count) that folds it. Cards and the list draw it above each
   group; the table in a header row (.db-grid below). */
/* A picture column's cell (src/client/picture.tsx): a square thumb or a
   circle at its given side, the picture cropped to fill it, never
   stretched; one that is gone leaves the quiet frame alone. */
.picture {
  display: inline-block;
  flex: none;
  overflow: hidden;
  vertical-align: middle;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.picture-avatar {
  border-radius: 50%;
}
.picture img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* A cover: across the top of a card, at most 160 px tall, cropped. */
.picture-cover {
  display: block;
  width: 100%;
  height: 160px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}
/* A checkbox as a switch (`toggle`). */
/* The Fields sheet, opened from Show as: in the menu's place on a wide
   screen, a sheet from the bottom on a phone. */
/* The table layout: a real table in a frame of its own. The name column
   stays put while the others scroll sideways inside the frame (never the
   page), a fade on the right edge while more is hidden there. On a phone
   the frame runs to the screen's right edge; from 840 px the screen leaves
   the 720 px column and the table takes the content's width. */
/* Out of the 720 px column at every width, so on a tablet the frame still
   reaches the screen's edge; from 840 px with room either side. */
/* One line per cell, cut with an ellipsis: a cell is read whole on its row page. */
/* The table's footer (record: docs/work/2026-10-13-database-no-brainers.md,
   PR E): the row count under the names, each column's total in quiet ink
   with its figure a step darker; a cell is a 40 px tap target that picks
   the next function. */
/* The board (docs/work/2026-10-13-database-no-brainers.md, PR D): a lane
   per option side by side, scrolling sideways in their own strip. On a
   phone a lane is most of the screen and snaps, the next one peeking; from
   840 px lanes are 280 px. */
/* What a proposal adds to and removes from a field holding several links. */
/* A select's options as coloured pills (the --opt-* tokens), the one
   coloured exception to the neutrals; several wrap on a card's line. */
.db-opt-green { background: var(--opt-green-bg); color: var(--opt-green); }
.db-opt-yellow { background: var(--opt-yellow-bg); color: var(--opt-yellow); }
.db-opt-orange { background: var(--opt-orange-bg); color: var(--opt-orange); }
.db-opt-red { background: var(--opt-red-bg); color: var(--opt-red); }
.db-opt-purple { background: var(--opt-purple-bg); color: var(--opt-purple); }
.db-opt-pink { background: var(--opt-pink-bg); color: var(--opt-pink); }
.db-opt-brown { background: var(--opt-brown-bg); color: var(--opt-brown); }
.db-opt-gray { background: var(--opt-gray-bg); color: var(--opt-gray); }
/* A fixed track, so bars line up whatever their number's width. */
td.cell-tint-green { background: var(--opt-green-bg); color: var(--opt-green); }
td.cell-tint-yellow { background: var(--opt-yellow-bg); color: var(--opt-yellow); }
td.cell-tint-orange { background: var(--opt-orange-bg); color: var(--opt-orange); }
td.cell-tint-red { background: var(--opt-red-bg); color: var(--opt-red); }
td.cell-tint-purple { background: var(--opt-purple-bg); color: var(--opt-purple); }
td.cell-tint-pink { background: var(--opt-pink-bg); color: var(--opt-pink); }
td.cell-tint-brown { background: var(--opt-brown-bg); color: var(--opt-brown); }
td.cell-tint-gray { background: var(--opt-gray-bg); color: var(--opt-gray); }
/* An option with a state reads by it: the state's colour and a small mark
   before its name (a tick, a half disc, a bang, a cross), drawn in CSS in
   the text's colour so it follows the theme (the policy allows no data:
   image). */
.db-state-done {
  background: var(--state-done-bg);
  color: var(--state-done);
}
.db-state-in_progress {
  background: var(--state-in-progress-bg);
  color: var(--state-in-progress);
}
.db-state-warning {
  background: var(--state-warning-bg);
  color: var(--state-warning);
}
.db-state-danger {
  background: var(--state-danger-bg);
  color: var(--state-danger);
}
/* The words set the card's height; the picture fills its box, cropped. */
/* A run of a table's lines drawn as one
   (docs/work/2026-10-14-group-table-change-lines.md): "· unconfirmed" in
   the line's own quiet ink, and the "6 changes" button as quiet text with
   a chevron, 40 px to the finger. Open, the individual lines sit under it,
   each as a single line is drawn. */
/* A topic split from another (docs/work/2026-10-08-topic-split-trail.md).
   In the origin, an event line: the branch glyph in the picture's column,
   "@ana started <topic> from here"; the topic is the app's topic link. */
.split-line > .split-line-badge {
  justify-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  color: var(--ink-3);
}
.split-line > .split-line-badge .glyph {
  width: 16px;
  height: 16px;
}
.split-line .split-line-chip {
  flex: 0 1 auto;
  display: inline;
}
.split-line .split-line-topic.removed {
  font-style: italic;
}
/* In the new topic, above its first post and not a post: a quiet band with
   the branch glyph, "Split from <origin>", the origin linking to its line. */
.split-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 12px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 13px;
  line-height: 20px;
}
.split-header > .glyph {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--ink-3);
}
.split-header > span {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The origin as a topic link reads everywhere: ink at the mention's weight,
   its glyph in front, underlined on hover. */
.split-header .topic-link,
.split-from-set .topic-link {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
}
.split-header a.topic-link:hover,
.split-header a.topic-link:focus-visible,
.split-from-set a.topic-link:hover,
.split-from-set a.topic-link:focus-visible {
  text-decoration: underline;
}
.split-header .topic-link .glyph,
.split-from-set .topic-link .glyph {
  width: 14px;
  height: 14px;
  transform: translateY(2px);
  flex: none;
}
/* A start card's "Split from" line, under the reason: as visible as the
   card's captions, a bordered chip with the branch glyph, then Change and
   Remove as the coding card's topic line has them. */
.proposal-split {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin: 8px 0 10px;
}
.proposal-split-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
  padding: 4px 10px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 13px;
  line-height: 20px;
}
.proposal-split-chip > span {
  min-width: 0;
  overflow-wrap: anywhere;
}
.proposal-split-chip strong {
  color: var(--ink);
  font-weight: 600;
}
.proposal-split-chip > .glyph {
  flex: none;
  width: 14px;
  height: 14px;
}
.proposal-split[data-split-from=""] .proposal-split-chip {
  border-style: dashed;
  color: var(--ink-3);
}
.proposal-split-pick {
  display: grid;
  gap: 4px;
  margin: 0 0 10px;
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 500;
}
.proposal-split-pick select {
  max-width: 100%;
  font-size: 16px;
}
.split-from-set {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-2);
}
.split-from-set > span {
  min-width: 0;
  overflow-wrap: anywhere;
}
.split-from-set > .glyph {
  width: 16px;
  height: 16px;
  color: var(--ink-3);
}
/* A to-do line Think reworded, with its Undo (docs/work/2026-10-08-think-update-todo.md):
   the steps' quiet note line, then Undo on its own row under the words
   (aligned with them), a 40 px text button. */

/* ---------- Settings → Glossary, the beta (docs/work/2026-10-13-glossary-beta.md) ---------- */
/* The nav's count of suggestions to review: the sidebar's ink count pill. */
.settings-tabs button .settings-label {
  flex: 1;
  min-width: 0;
}
.settings-count {
  flex: none;
  min-width: 20px;
  margin-left: 8px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 11px;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}
.settings-tabs.settings-list button .settings-chevron {
  margin-left: 8px;
}
.glossary h4.settings-whose {
  margin-top: 22px;
}
/* Underlined | On request: the page's two-way control, smaller than Settings' splitting one. */
/* Viewing the app as a member, read-only (docs/work/2026-10-13-view-as.md):
   a bar above every screen, and the app frame moved down beneath it. The
   bar takes the safe area, so the top bar below it does not take it again. */
html.viewing-as {
  --view-as-bar: calc(44px + env(safe-area-inset-top));
  --safe-top: 0px;
}
.view-as-bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--view-as-bar);
  padding: env(safe-area-inset-top) 12px 0 16px;
  background: var(--view-as-bg);
  border-bottom: 1px solid var(--view-as-line);
  color: var(--ink);
  font-size: 14px;
}
.view-as-icon {
  flex: none;
  color: var(--view-as);
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.view-as-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.view-as-read-only {
  color: var(--ink-2);
}
.view-as-exit {
  flex: none;
  align-self: center;
  height: 32px;
  min-height: 0;
  padding: 0 12px;
  line-height: 30px;
  border-radius: 8px;
  font-size: 14px;
  color: var(--ink);
}
html.viewing-as #root:has(.app) {
  top: calc(var(--app-top, 0px) + var(--view-as-bar));
  height: calc(var(--app-height, 100dvh) - var(--view-as-bar));
}
html.viewing-as .app,
html.viewing-as .main {
  height: calc(var(--app-height, 100dvh) - var(--view-as-bar));
}
html.viewing-as .sidebar {
  top: calc(var(--app-top, 0px) + var(--view-as-bar));
  height: calc(var(--app-height, 100dvh) - var(--view-as-bar));
}
.view-as-log li {
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
.view-as-log .hint {
  display: block;
  margin: 0;
}
.view-as-toast {
  z-index: 1001;
  white-space: nowrap;
  bottom: calc(96px + var(--safe-bottom));
}
.view-as-picker .hint {
  margin: 0 0 12px;
}
/* An agent's draft waiting above the composer, its author's alone
   (docs/work/2026-10-12-mcp-drafts.md): a dashed edge says not sent yet;
   Edit is the filled button, Send and Discard weigh the same. */
.agent-draft {
  margin: 8px 0;
  padding: 10px 12px 12px;
  border: 1px dashed var(--line-2);
  border-radius: 12px;
  background: var(--surface);
}
.agent-draft-label {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 6px;
  font-size: 12px;
  color: var(--ink-2);
}
.agent-draft-label svg {
  width: 14px;
  height: 14px;
  flex: none;
}
.agent-draft-label b {
  color: var(--ink);
  font-weight: 600;
}
.agent-draft-time {
  color: var(--ink-3);
}
.agent-draft-text {
  max-height: 30vh;
  overflow-y: auto;
  font-size: 14.5px;
  overflow-wrap: anywhere;
}
.agent-draft-text > :first-child {
  margin-top: 0;
}
.agent-draft-text > :last-child {
  margin-bottom: 0;
}
.agent-draft-actions {
  display: flex;
  gap: 8px;
  padding-top: 10px;
}
.agent-draft-actions button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 40px;
  padding: 0 14px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font-size: 13.5px;
  font-weight: 500;
}
.agent-draft-actions button.pri {
  border-color: transparent;
  background: var(--ink);
  color: var(--paper);
}
.agent-draft-actions button:disabled {
  opacity: 0.55;
}
.agent-draft-error {
  margin: 8px 0 0;
}
.pages-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.page-chip svg {
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
@media (min-width: 840px) {
  .pages-grid {
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 16px;
  }
}
/* Small and inline before the title, on its first line (Jean, 2026-10-20). */
.page-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 14px;
  color: var(--ink);
  -webkit-tap-highlight-color: transparent;
}
.page-chip:hover {
  background: var(--surface-3);
}
/* The box before the first question is a composer, as the conversation's
   is after it (docs/work/2026-10-22-page-ask-composer.md); Ask at the row's end. */
#page-ask-send {
  margin-left: auto;
}
/* The Ask pill (D4): one ink pill, Ask Workroom and, after a hairline, the
   Ask field's own mic (src/client/dictation.tsx), each a full tap high. The
   mic is drawn for a surface, so inside the pill the tokens it reads turn
   over: its ink is the pill's paper. Held, the recording takes the whole
   pill, as it takes a composer's row. */
.dock-pill {
  pointer-events: auto;
  display: flex;
  align-items: stretch;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  box-shadow: 0 6px 18px var(--surface-shadow);
  animation: page-sticky-in var(--motion-fast) var(--motion-ease);
}
.dock-pill-jump {
  display: flex;
  align-items: center;
  min-height: 44px;
  gap: 6px;
  padding: 0 16px 0 16px;
  border-radius: 999px 0 0 999px;
  color: inherit;
  font-size: 15px;
  font-weight: 500;
  -webkit-tap-highlight-color: transparent;
}
.dock-pill-jump svg {
  flex: none;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.dock-pill-label {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.dock-pill-dot {
  flex: none;
  width: 8px;
  height: 8px;
  margin-left: 2px;
  border-radius: 50%;
  background: currentColor;
}
.dock-pill-mic {
  --ink: var(--paper);
  --ink-2: color-mix(in srgb, var(--paper) 78%, transparent);
  --ink-3: color-mix(in srgb, var(--paper) 55%, transparent);
  --surface-2: color-mix(in srgb, var(--paper) 16%, transparent);
  display: flex;
  align-items: stretch;
  border-left: 1px solid color-mix(in srgb, var(--paper) 28%, transparent);
  /* "Release to discard" on the ink: the other scheme's --danger, 7.1:1 on
     light's ink pill (5.6:1 on dark's), where each scheme's own read 2.6:1
     and 2.0:1. */
  --danger: #f28b82;
}
@media (prefers-color-scheme: dark) {
  .dock-pill-mic {
    --danger: #b3261e;
  }
}
.dock-pill-mic:empty {
  display: none;
}
/* Pressed, held or listening, each half lights in its own shape: the
   pill's left end, or its right end after the hairline, never a circle or
   a square inside it (docs/work/2026-10-19-sidebar-ask-mic.md). The mic
   fills its half edge to edge, 50 px wide as its 44 px circle and the
   half's old padding were. */
.dock-pill-jump:active {
  background: color-mix(in srgb, var(--paper) 16%, transparent);
}
.dock-pill-mic .dictation {
  align-items: stretch;
}
.dock-pill-mic .dictation > :is(.dictation-hint, .dictation-wave, .dictation-status) {
  align-self: center;
}
.dock-pill-mic .icon-btn {
  width: 50px;
  height: auto;
  min-height: 44px;
  border-radius: 0;
  -webkit-tap-highlight-color: transparent;
}
.dock-pill-mic .icon-btn:active {
  background: var(--surface-2);
}
.dock-pill-mic .dictation > .icon-btn:last-of-type {
  border-radius: 0 999px 999px 0;
}
/* Held, the recording takes the whole pill and the mic sits at its left end. */
.dock-pill-mic .dictation.holding > .mic {
  border-radius: 999px 0 0 999px;
}
.dock-pill-mic .dictation.holding {
  padding: 0 12px 0 0;
}
.dock-pill-mic .dictation-wave {
  flex: 0 1 72px;
  width: auto;
  min-width: 16px;
}
.dock-pill:has(.dictation.holding) .dock-pill-jump {
  display: none;
}
.dock-pill:has(.dictation.holding) .dock-pill-mic {
  border-left: 0;
}
/* Every state of the mic inside the pill (docs/work/2026-10-22-ask-button-recording.md).
   A composer draws the recording's Stop and Discard as 40 px circles, Stop an
   ink disc; on the ink pill that disc is the pill's own colour and hid the
   stop square. Here each control is a half like the idle mic: 50 px wide, a
   full tap high, no circle; Stop is the red square, Discard the pill's quiet
   ink and the right end. While the mic is busy (recording, writing it down,
   a kept recording or a passing note) Ask folds to its icon, so its label
   never wraps or squeezes the controls; a screen reader still reads it. */
.dock-pill:has(.dictation:not([data-state="idle"])) .dock-pill-label,
.dock-pill:has(.dictation:not([data-state="idle"])) .dock-pill-dot {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.dock-pill:has(.dictation:not([data-state="idle"])) .dock-pill-jump {
  flex: none;
  padding: 0 14px 0 16px;
}
.dock-pill {
  min-width: 0;
  max-width: 100%;
}
/* Idle, Ask gives way (its label ellipsizes) and the mic keeps its half;
   busy, Ask is an icon and the mic's words give way instead. */
.dock-pill-mic {
  flex: none;
}
.dock-pill:has(.dictation:not([data-state="idle"])) .dock-pill-mic,
.dock-pill-mic .dictation {
  flex: 0 1 auto;
  min-width: 0;
}
.dock-pill-mic .dictation-hint {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dock-pill-mic .dictation .icon-btn.mic.listening,
.dock-pill-mic .dictation .icon-btn.mic.listening:hover:enabled,
.dock-pill-mic .dictation > .icon-btn:not(.mic),
.dock-pill-mic .dictation > .icon-btn:not(.mic):hover:enabled {
  flex: none;
  width: 50px;
  height: auto;
  min-height: 44px;
  border-radius: 0;
  background: none;
  transform: none;
}
.dock-pill-mic .dictation .icon-btn.mic.listening,
.dock-pill-mic .dictation .icon-btn.mic.listening:hover:enabled {
  color: var(--danger);
}
.dock-pill-mic .dictation.holding > .icon-btn.mic.listening,
.dock-pill-mic .dictation.holding > .icon-btn.mic.listening:hover:enabled {
  border-radius: 999px 0 0 999px;
  background: var(--surface-2);
  color: var(--ink);
}
.dock-pill-mic .dictation > .icon-btn:not(.mic),
.dock-pill-mic .dictation > .icon-btn:not(.mic):hover:enabled {
  border-radius: 0 999px 999px 0;
  color: var(--ink-2);
}
.dock-pill-mic .dictation .icon-btn.mic.listening:active,
.dock-pill-mic .dictation > .icon-btn:not(.mic):active {
  background: var(--surface-2);
  transform: none;
}
/* The time, "Writing it down…" or a note: one line, cut short rather than
   wrapped; the time keeps one width as it ticks. */
.dock-pill-mic .dictation-status {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 6px;
}
.dock-pill-mic .dictation:is([data-state="recording"], [data-state="holding"]) > .dictation-status {
  flex: none;
  min-width: 4ch;
  text-align: center;
}
.dock-pill-mic .dictation[data-state="note"] > .dictation-status {
  padding-right: 16px;
}
/* A kept recording says so in the pill, in the words its status line already
   has ("Recording kept"), between Try again and Discard; elsewhere that line
   is for a screen reader only. */
.dock-pill-mic .dictation[data-state="kept"] > .visually-hidden {
  position: static;
  align-self: center;
  width: auto;
  height: auto;
  min-width: 0;
  clip: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 6px;
  font-size: 13px;
  color: var(--ink-2);
}
@keyframes page-sticky-in {
  from {
    opacity: 0;
  }
}

/* A text written over several lines keeps them; other values draw as their renderers do. */
/* A page body's pictures, the ```image block
   (docs/work/2026-10-15-page-image-widget.md): rounded like a card with a
   hairline, so a white screenshot keeps its edge on the paper; one picture
   as wide as the column and at most 480 px tall, centred, its own ratio;
   two or three side by side from 840 px, stacked under it. */

/* Tiles (docs/work/2026-10-15-tiles-widget.md): a grid of cards, each an icon,
   a title and a line of text; as many as fit at 140 px, never more than three
   a row, so two on a phone and three in the column. The icon's square holds
   its place while its file loads, so nothing moves when it lands. */
.tiles-block {
  margin: 10px 0;
  white-space: normal;
}
.tiles-block > ul.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(140px, calc((100% - 16px) / 3)), 1fr));
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.tiles-block > * + * {
  margin-top: 8px;
}
.tiles > .tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  margin: 0;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
}
.tile-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 36px;
  height: 36px;
  margin-bottom: 8px;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--ink);
}
.tile-icon .glyph {
  width: 20px;
  height: 20px;
}
.tile-icon-missing {
  color: var(--ink-3);
}
.tile-icon-loading::after {
  content: "";
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: var(--surface-3);
}
.tile-title {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.tile-text {
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.tiles-block > p.tiles-text {
  white-space: pre-wrap;
}

/* Pairs (docs/work/2026-10-15-pairs-block.md): an inset grouped list. A
   section's heading is a small uppercase label over its card; each row is
   its label (grey, 104 px on a phone, 168 px from 600 px), its value with
   the detail under it, and a copy button at its end when asked. A source is
   a quiet arrow right after the value. Rows are divided by a hairline that
   starts at the row's padding, as iOS draws them. */
.pairs-block {
  --pair-label: 104px;
  margin: 10px 0;
  white-space: normal;
}
@media (min-width: 600px) {
  .pairs-block {
    --pair-label: 168px;
  }
}
.pairs-head {
  margin: 18px 0 6px 14px;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}
.pairs-block > .pairs-head:first-child {
  margin-top: 4px;
}
.pairs-block > ul.pairs-card {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}
.pairs-block > ul.pairs-card + ul.pairs-card,
.pairs-block > .pairs-text + ul.pairs-card,
.pairs-block > ul.pairs-card + .pairs-text {
  margin-top: 8px;
}
.pairs-card > .pair {
  position: relative;
  display: grid;
  grid-template-columns: var(--pair-label) minmax(0, 1fr) auto;
  column-gap: 10px;
  align-items: start;
  min-height: 44px;
  margin: 0;
  padding: 11px 14px;
  color: var(--ink);
}
.pairs-card > .pair:has(> .pair-copy) {
  padding-right: 4px;
}
.pairs-card > .pair + .pair::before {
  content: "";
  position: absolute;
  top: 0;
  left: 14px;
  right: 0;
  border-top: 1px solid var(--line);
}
.pair-label {
  padding-top: 1px;
  font-size: 14px;
  line-height: 1.4;
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.pair-value {
  min-width: 0;
  font-size: 15px;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.pair-detail {
  display: block;
  margin-top: 1px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink-3);
}
.pair-tail {
  white-space: nowrap;
}
.pair-source {
  display: inline-flex;
  /* A 26 px target around the 14 px arrow, drawn where the arrow alone was. */
  margin: -6px -6px -6px -2px;
  padding: 6px;
  border-radius: 4px;
  vertical-align: -2px;
  color: var(--ink-3);
}
.pair-source:hover,
.pair-source:focus-visible {
  color: var(--ink);
}
.pair-source .glyph {
  width: 14px;
  height: 14px;
}
.pair-copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 40px;
  margin: -9px 0;
  border-radius: 8px;
  color: var(--ink-3);
}
.pair-copy:hover,
.pair-copied {
  color: var(--ink);
}
.pair-copy .glyph {
  width: 16px;
  height: 16px;
}
/* With a mouse the copy button shows on the row's hover or focus (the copied
   check stays until it resets); touch has no hover, so it stays drawn. The
   button keeps its space, so the row never shifts. */
@media (hover: hover) and (pointer: fine) {
  .pair-copy:not(.pair-copied) {
    opacity: 0;
  }
  .pair:hover > .pair-copy,
  .pair:focus-within > .pair-copy,
  .pair-copy:focus-visible {
    opacity: 1;
  }
}
.pairs-block > p.pairs-text {
  margin-left: 14px;
  margin-right: 14px;
  white-space: pre-wrap;
}

/* The dock (src/client/bottom-sheet.tsx): a conversation beside a screen's
   content. Wide, a 360px side panel with its head and » to fold it away;
   narrower, a sheet over the dimmed screen with a grip and two stops, Half
   (most of the lower half, a phone's keyboard up or not) and Full (up to the
   top bar). It and its scrim sit in `.main`'s context, under the long-press
   menu's scrim (40) and sheet (42) a message in it can open. */
.bottom-sheet {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface);
}
.bottom-sheet[hidden] {
  display: none;
}
.bottom-sheet.side {
  flex: none;
  width: 360px;
  border-left: 1px solid var(--line);
}
.bottom-sheet:not(.side) {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 31;
  max-width: 720px;
  margin: 0 auto;
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
  transition: height var(--motion-fast) var(--motion-ease);
  animation: bottom-sheet-in var(--motion-fast) var(--motion-ease);
}
.bottom-sheet[data-snap="half"] {
  height: max(50%, min(360px, 100% - var(--bar) - var(--safe-top)));
}
.bottom-sheet[data-snap="half"][data-fit] {
  height: auto;
  max-height: calc(100% - var(--bar) - var(--safe-top));
}
/* Compact, a conversation's empty history takes no room: its box sits under
   the head, as the page's own box does (docs/work/2026-10-22-todo-ask-sheet.md). */
.bottom-sheet[data-fit] .exploration > .stream {
  flex: none;
  height: 0;
}
.bottom-sheet[data-snap="full"] {
  height: calc(100% - var(--bar) - var(--safe-top));
}
.bottom-sheet.dragging {
  transition: none;
}
/* The conversation in a sheet is a scroller inside a fixed box inside `.main`
   (a transformed, clipped ancestor) that slid in with a transform animation:
   a combination iOS has dropped touch scrolling on (Jean, 2026-10-15 and
   2026-10-22; ported from #2055, docs/work/2026-10-22-todo-ask-sheet.md). The
   scroller says what it takes (a vertical pan) and keeps its own momentum
   and its own edge, so a finger on it scrolls it and never the dimmed screen
   beneath. Only the grip and the head, `touch-action: none`, move the sheet. */
.bottom-sheet:not(.side) {
  overscroll-behavior: contain;
}
.bottom-sheet:not(.side) .stream {
  touch-action: pan-y pinch-zoom;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
}
@keyframes bottom-sheet-in {
  from {
    transform: translateY(100%);
  }
}
/* A conversation in the dock is drawn on the dock's surface: its composer
   strip too, and none of the desktop's inner edge shadow, which reads the
   sidebar beside the main surface, not a panel. */
.bottom-sheet .exploration > .stream,
.bottom-sheet .exploration > .composer {
  background: var(--surface);
  box-shadow: none;
}

/* ---------- a page that keeps its own data (docs/work/2026-10-15-tables-behind-pages.md, PR 4) ---------- */
.kept {
  margin: 4px 0 0;
}
/* History (docs/work/2026-10-16-keeper-history.md): one line per change, its time at the end; the list scrolls inside the sheet. */
/* The glyphs of a kept page, its card and a fold: line icons, drawn as every other. */
.kept svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A highlight: a soft shadow and a bigger title, with its cover (the owner's answer 2). */
.cl-highlight {
  box-shadow: 0 6px 20px var(--surface-shadow);
}
/* A row colour on an entry: its tint and a bar at its start, as a widget's row; never on its cover. */
@media (min-width: 840px) {
  .cl-highlight {
    padding: 20px 22px;
  }
}
/* The Edit's choices: a segmented control for three or fewer options, chips beyond; screenshots as a grid. */

/* A cards block's tile size (`size:`): compact cards side by side from 840 px, large ones with a bigger name. */
/* Share as a file is a link the browser saves (2e), drawn as the menu's other items. */

#page-delete svg{
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The composer row on a phone (the layout breakpoint above): the mic and Send
   share the right-hand slot, as in Claude's app. `data-slot` says which one
   (micSlot in src/client/dictation.tsx); Send stays in the DOM, hidden, while
   there is nothing to send. Once there is, the mic stays just left of it.
   Desktop keeps the mic at the row's left. */
@keyframes slot-in {
  from {
    opacity: 0;
  }
}
@media (max-width: 839.98px) {
  .composer-row[data-slot] > .dictation {
    order: 90;
  }
  /* The mic stays just left of Send, so several dictations can be chained
     into one draft (Jean, 2026-10-20). */
  /* Mic, then the rewrite wand, then Send: the wand sits against Send, the
     mic one step before it (Jean, 2026-10-21). */
  .composer-row[data-slot="send"] > .improve-btn,
  .composer-row[data-slot="send"] > .rewrite-pill {
    order: 91;
  }
  .composer-row[data-slot="send"] > .send {
    order: 92;
  }
  .composer-row[data-slot="mic"] > .send:not(#cancel) {
    display: none;
  }
  .composer-row[data-slot="mic"] > .dictation,
  .composer-row[data-slot="send"] > .send {
    animation: slot-in var(--motion-fast) var(--motion-ease);
  }
  /* Controls feel native: a long press or a drag selects nothing, drags
     nothing and opens no callout. The text field is not in this list. */
  .composer-row button,
  .composer-row .pickers,
  .composer-row label,
  .composer-row button svg,
  .composer-row button img {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    -webkit-tap-highlight-color: transparent;
    -webkit-user-drag: none;
  }
}

/* The recording's two buttons are round and tell their jobs apart (Jean,
   2026-10-19, as in Claude's app). Stop is not the AI's Stop: that one is the
   ink circle in Send's spot (Jean, 2026-10-20, after Francè could not tell
   stopping the AI from sending a voice note: "a stop button in a different
   spot"), so the recording's Stop is a red square on the quiet circle, in the
   mic's own place, and Send's spot keeps a dimmed Send. Both are 40px
   circles, the size of Send, so the tap target and the pressed state follow
   the circle. Behaviour is the buttons' own, untouched. */
.dictation .icon-btn.mic.listening,
.dictation .icon-btn.mic.listening:hover:enabled,
.dictation > .icon-btn:not(.mic),
.dictation > .icon-btn:not(.mic):hover:enabled {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  -webkit-tap-highlight-color: transparent;
}
/* The timer keeps 8px from the circle on each side. */
.dictation:has(> .icon-btn.mic.listening) > .dictation-status {
  padding: 0 8px;
}
.dictation .icon-btn.mic.listening,
.dictation .icon-btn.mic.listening:hover:enabled {
  background: var(--ink);
  color: var(--paper);
}
/* Only in a composer's row, where Send is beside it; the held mic and the
   other places a mic sits (the dock pill, pages) keep the ink circle. */
.composer-row[data-slot] > .dictation:not(.holding) > .icon-btn.mic.listening,
.composer-row[data-slot] > .dictation:not(.holding) > .icon-btn.mic.listening:hover:enabled {
  background: var(--surface-2);
  color: var(--danger);
}
.composer-row[data-slot] > .dictation:not(.holding) > .icon-btn.mic.listening svg {
  width: 16px;
  height: 16px;
}
.dictation .icon-btn.mic.listening:active,
.dictation > .icon-btn:not(.mic):active {
  transform: scale(0.94);
}
.dictation > .icon-btn:not(.mic),
.dictation > .icon-btn:not(.mic):hover:enabled {
  background: var(--surface-2);
  color: var(--ink-2);
}
.dictation > .icon-btn:not(.mic):active {
  background: var(--surface-3);
}

/* While a recording runs, is written down or is kept (Jean, 2026-10-19, as in
   Claude's mobile app), the composer's row holds the recording's controls:
   the quiet Cancel circle at the far left where the attach button was, the
   timer, and Stop (or Try again) in the mic's own place, never in Send's.
   On a phone that is just left of Send, which stays, dimmed and disabled; on
   desktop, where the mic sits at the row's left, Stop follows Cancel and
   Send keeps the right edge. Attach, emoji and the model step aside and come
   back when it ends. The dictation group dissolves into the row (`display:
   contents`) so its three parts can take those places. A held recording keeps
   its own wave layout above. Rows with `data-slot` only: Ask Workroom and the
   topic composer. */
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .dictation {
  display: contents;
}
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > :not(.dictation):not(.spacer):not(.send) {
  display: none;
}
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .send:not(#cancel) {
  display: inline-flex;
  order: 91;
  opacity: 0.28;
  pointer-events: none;
  animation: none;
}
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .dictation > .icon-btn:not(.mic) {
  order: 0;
  animation: none;
}
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .dictation > .icon-btn.mic {
  order: 1;
  animation: none;
}
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .dictation > .dictation-status {
  order: 2;
}
.composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .spacer {
  order: 3;
}
@media (max-width: 839.98px) {
  /* The phone: the timer follows Cancel and Stop keeps the mic's place. */
  .composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .dictation > .dictation-status {
    order: 1;
  }
  .composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .spacer {
    order: 2;
  }
  .composer-row[data-slot]:has(> .dictation:not(.holding) > .icon-btn:not(.mic)) > .dictation > .icon-btn.mic {
    order: 90;
  }
}

/* ---------- member groups (Settings → Groups) ---------- */
.group-pick label.row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
  cursor: pointer;
}
.group-pick input[type="checkbox"] {
  width: 20px;
  height: 20px;
  min-height: 0;
  flex: none;
  margin: 0;
  accent-color: var(--ink);
}
.group-editor .row-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
/* A block of a table's changes, opened under its line
   (docs/work/2026-10-22-tables-invisible.md, PR 3): each change a quiet
   line with its Confirm and Undo, the block's own buttons under them. On a
   phone the buttons wrap under the words. */
/* Documents (docs/work/2026-11-02-documents-grid.md), Jean's layout A: the
   Pages grid's card, a tile naming the kind, two columns on a phone and
   four from 840 px. The tones colour only the tile's glyph. */
:root {
  --doc-workroom: #5b5f68;
  --doc-file: #7a5c00;
  --doc-google: #1e7a3c;
  --doc-notion: #1b1d22;
  --doc-figma: #8250df;
  --doc-claude: #b4572e;
}
@media (prefers-color-scheme: dark) {
  :root {
    --doc-workroom: #a5a8b0;
    --doc-file: #e3b341;
    --doc-google: #81c995;
    --doc-notion: #ececec;
    --doc-figma: #c297ff;
    --doc-claude: #f0a37a;
  }
}
.documents-screen {
  max-width: 1180px;
  margin: 0 auto;
  padding: 12px 16px 24px;
}
.documents-filters {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 -16px;
  padding: 0 16px 10px;
  overflow-x: auto;
  scrollbar-width: none;
}
.documents-filters::-webkit-scrollbar {
  display: none;
}
.documents-chip,
.documents-topic {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  box-sizing: border-box;
  height: 32px;
  min-height: 0;
  padding: 0 12px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 13px;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
.documents-chip[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}
.documents-chip svg,
.documents-topic svg {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.documents-topic {
  position: relative;
  max-width: 220px;
  padding-right: 10px;
  border-color: var(--line-2);
  background: var(--surface);
  color: var(--ink);
}
.documents-topic select {
  max-width: 180px;
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-overflow: ellipsis;
  appearance: none;
  -webkit-appearance: none;
  outline: none;
}
.documents-topic:has(select:focus-visible) {
  outline: 2px solid var(--ink-2);
  outline-offset: 2px;
}
.documents-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Pinned, then All documents, only when something is pinned
   (docs/work/2026-11-03-documents-pins.md): the Pages list's group labels. */
.documents-group {
  margin: 4px 2px 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}
.documents-grid + .documents-group {
  margin-top: 20px;
}
.documents-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  border-radius: 14px;
  background: var(--surface);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.05),
    0 4px 14px rgba(0, 0, 0, 0.06);
  transition:
    transform var(--motion-fast) var(--motion-ease),
    box-shadow var(--motion-fast) var(--motion-ease);
}
@media (prefers-color-scheme: dark) {
  .documents-card {
    box-shadow: 0 0 0 1px var(--line);
  }
}
.documents-card:has(.documents-title:focus-visible) {
  outline: 2px solid var(--ink-2);
  outline-offset: 2px;
}
.documents-tile {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  height: 56px;
  margin: 8px 8px 0;
  padding: 0 12px;
  border-radius: 9px;
  background: var(--pages-inset);
}
.documents-tile svg {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.documents-tile[data-tone="workroom"] svg {
  color: var(--doc-workroom);
}
.documents-tile[data-tone="file"] svg {
  color: var(--doc-file);
}
.documents-tile[data-tone="google"] svg {
  color: var(--doc-google);
}
.documents-tile[data-tone="notion"] svg {
  color: var(--doc-notion);
}
.documents-tile[data-tone="figma"] svg {
  color: var(--doc-figma);
}
.documents-tile[data-tone="claude"] svg {
  color: var(--doc-claude);
}
.documents-tile[data-tone="link"] svg {
  color: var(--ink-2);
}
.documents-more-check {
  margin-left: auto;
}
.documents-suggestion,
.documents-provider-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 12px;
  margin: 0 0 12px;
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--surface-2);
  color: var(--ink);
  font-size: 14px;
}
.documents-suggestion-actions {
  display: flex;
  gap: 8px;
}
.documents-label {
  min-width: 0;
  overflow: hidden;
  color: var(--ink-2);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.documents-text {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  padding: 10px 12px 10px;
}
.documents-title {
  display: block;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.3;
  text-align: left;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.documents-title-text {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow-wrap: anywhere;
}
/* The whole card opens the document; "Post" sits above it. */
.documents-title::after {
  content: "";
  position: absolute;
  inset: 0;
}
.documents-title:focus-visible {
  outline: none;
}
.documents-topic-name,
.documents-by {
  min-width: 0;
  overflow: hidden;
  font-size: 12px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.documents-topic-name {
  color: var(--ink-2);
}
.documents-from {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  margin-top: auto;
  padding-top: 2px;
}
.documents-by {
  display: flex;
  flex: 1;
  color: var(--ink-3);
}
.documents-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.documents-when {
  flex: none;
  white-space: pre;
}

/* The card's "…" (Jean, 2026-11-02): above the card's stretched link, a 32 px
   target, its menu ("Go to original post") opening under it, right-aligned. */
.documents-from .coding-menu {
  position: relative;
  z-index: 1;
  margin: -6px -6px -6px 0;
}
.documents-more-button {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  color: var(--ink-2);
  -webkit-tap-highlight-color: transparent;
}
.documents-more-button[aria-expanded="true"] {
  background: var(--surface-2);
  color: var(--ink);
}
.documents-more-button svg,
.documents-from .coding-menu-list svg {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.documents-from .coding-menu-list {
  min-width: 0;
}
.documents-from .coding-menu-list button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 40px;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 14px;
  white-space: nowrap;
  text-align: left;
}
.documents-from .coding-menu-list button:focus-visible {
  background: var(--surface-2);
}
/* An open menu sits over the cards after it, hovered or not. */
.documents-card:has(.coding-menu-list) {
  z-index: 2;
  overflow: visible;
}
/* "Link copied", or the link to copy by hand when the browser refused: the
   Saved screen's toast shape, at the foot of the screen. */
.documents-toast {
  position: fixed;
  left: 50%;
  bottom: calc(20px + var(--safe-bottom));
  z-index: 50;
  max-width: calc(100vw - 32px);
  margin: 0;
  padding: 10px 16px;
  transform: translateX(-50%);
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--paper);
  font-size: 14px;
  overflow-wrap: anywhere;
}
.documents-toast:not([data-done]) {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-right: 8px;
}
/* "Link copied" only says so: a tap goes through to what is under it. */
.documents-toast[data-done] {
  pointer-events: none;
}
.documents-toast a {
  color: inherit;
}
.documents-toast button {
  flex: none;
  min-height: 32px;
  padding: 0 8px;
  border-radius: 6px;
  color: var(--paper);
  font-weight: 600;
}
.documents-more {
  display: block;
  margin: 16px auto 0;
}
.documents-empty {
  max-width: 300px;
  margin: 48px auto;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.45;
  text-align: center;
}
.documents-empty b {
  display: block;
  margin-bottom: 6px;
  color: var(--ink);
  font-size: 16px;
}
.documents-empty-icon {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  margin: 0 auto 12px;
  border-radius: 9px;
  background: var(--pages-inset);
}
.documents-empty-icon svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}
.documents-card-skeleton {
  min-height: 150px;
}
.documents-skeleton-title {
  width: 80%;
  height: 16px;
}
.documents-skeleton-meta {
  width: 50%;
  height: 12px;
}
@media (min-width: 840px) {
  .documents-screen {
    padding: 20px 24px 32px;
  }
  .documents-filters {
    flex-wrap: wrap;
    margin: 0;
    padding: 0 0 14px;
  }
  .documents-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 14px;
  }
}
@media (hover: hover) and (pointer: fine) {
  .documents-card:hover {
    transform: translateY(-2px);
    box-shadow:
      0 2px 4px rgba(0, 0, 0, 0.07),
      0 10px 24px rgba(0, 0, 0, 0.1);
  }
  .documents-from .coding-menu-list button:hover {
    background: var(--surface-2);
  }
  .documents-more-button:hover {
    background: var(--surface-2);
    color: var(--ink);
  }
}
@media (prefers-reduced-motion: reduce) {
  .documents-card {
    transition: none;
  }
  .documents-card:hover {
    transform: none;
  }
}

/* ---------- Ask: Recent (src/client/home-recent.tsx) ---------- */
/* At the very bottom of the Ask screen, small and out of the layout so the
   field stays centered, and exactly the field's width (`.new-topic`: 520px,
   inside the screen's 20px sides) so their edges line up
   (docs/work/2026-11-02-sidebar-no-explorations.md). */
.home-recent {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 12px;
  width: auto;
  max-width: 520px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  font-size: 12px;
  color: var(--ink-3);
}
.home-recent-head {
  padding: 0 0 2px;
  font-size: 11px;
  color: var(--ink-3);
}
.home-recent-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  width: 100%;
  padding: 3px 0;
  border: 0;
  background: none;
  color: var(--ink-2);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.home-recent-row:hover .home-recent-title {
  color: var(--ink);
}
.home-recent-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.home-recent-topic {
  flex: none;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink-3);
}
.home-recent-dot {
  flex: none;
  width: 6px;
  height: 6px;
  align-self: center;
  border-radius: 50%;
  background: var(--think);
}
.home-recent-time {
  flex: none;
  min-width: 3.5em;
  text-align: right;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* Hidden rather than squeezing the field: a short screen, or the field in use
   (on a touch screen that is the keyboard being up). */
@media (max-height: 700px) {
  .home-recent {
    display: none;
  }
}
@media (pointer: coarse) {
  .content:has(#home-form:focus-within) .home-recent {
    display: none;
  }
}

/* For me (docs/work/2026-11-02-for-me.md): the home screen's feed of what
   waits on you. Compact cards on the paper, the sentence opening with its
   author's pill, one row of Reply, topic and time; a Done check top right. */
.for-me-column {
  padding-top: 8px;
  padding-bottom: calc(80px + var(--safe-bottom));
}
.for-me-day-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 18px 2px 8px;
  color: var(--ink-3);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.for-me-day-head::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}
.for-me-cards {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.for-me-card {
  position: relative;
  padding: 10px 36px 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.for-me-words {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  line-height: 1.4;
  text-align: left;
  overflow-wrap: anywhere;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.for-me-person {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  vertical-align: bottom;
}
.for-me-person .avatar {
  flex: none;
}
/* New since the last visit: an ink dot before the person, the colour of the
   sidebar's unread count pill, never the think blue
   (docs/work/2026-11-03-for-me-new-dot.md). */
.for-me-new {
  flex: none;
  width: 8px;
  height: 8px;
  margin-right: 2px;
  border-radius: 50%;
  background: var(--ink);
}
@media (forced-colors: active) {
  .for-me-new {
    forced-color-adjust: none;
    background: CanvasText;
  }
}
.for-me-quote {
  color: var(--ink-2);
}
/* Done (docs/work/2026-11-04-for-me-done-states.md): a check in an outlined
   circle top right, the reply button's outline; on a done card a quiet muted
   check with no circle, still a button, that reopens it. */
.for-me-check {
  position: absolute;
  top: 6px;
  right: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.for-me-check.undo {
  border-color: transparent;
}
@media (hover: hover) {
  .for-me-check:hover {
    background: var(--surface-2);
    color: var(--ink);
  }
}
/* Seen: you reacted. In place, the words a step lighter, your emoji in the meta row. */
.for-me-card.seen .for-me-words {
  color: var(--ink-2);
}
.for-me-reacted {
  padding: 1px 8px;
  border: 1px solid var(--line);
  border-radius: 999px;
  line-height: 20px;
  color: var(--ink-2);
  white-space: nowrap;
}
/* Added by you: a small label above the sentence. */
.for-me-added {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 0 0 4px;
  color: var(--ink-3);
  font-size: 12px;
}
/* A done card is one line, in its chronological place: no border or
   background, muted; the check first (it reopens), the words ellipsized,
   the time at the right. */
.for-me-card.done {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 4px 2px 0;
  border: 0;
  background: none;
  font-size: 13.5px;
}
.for-me-card.done .for-me-check {
  position: relative;
  flex: none;
  width: 28px;
  height: 28px;
}
.for-me-card.done .for-me-words {
  color: var(--ink-3);
  flex: 1;
  width: auto;
  font-size: 13.5px;
  line-height: 1.4;
  min-width: 0;
  overflow: hidden;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.for-me-done-time {
  flex: none;
  color: var(--ink-3);
  font-size: 12.5px;
}
/* Done greys the card with a short fade. */
.for-me-card .for-me-words,
.for-me-card .for-me-quote {
  transition: color 0.2s ease;
}
@media (prefers-reduced-motion: reduce) {
  .for-me-card .for-me-words,
  .for-me-card .for-me-quote {
    transition: none;
  }
}
.for-me-meta {
  display: flex;
  flex-wrap: nowrap;
  min-width: 0;
  align-items: center;
  gap: 8px;
  margin-top: 7px;
  font-size: 12.5px;
  color: var(--ink-3);
}
/* Reply: an outlined circle with the reply arrow, first in the row
   (docs/work/2026-11-03-for-me-reply-icon.md). */
.for-me-reply {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .for-me-reply:hover {
    background: var(--surface-2);
  }
}
.for-me-topic {
  min-width: 0;
  flex: 0 1 auto;
  max-width: 100%;
  padding: 1px 8px;
  border-radius: 6px;
  line-height: 20px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.for-me-time,
.for-me-reacted {
  flex: none;
}
/* The card's one suggested action (docs/work/2026-11-05-for-me-suggested-action.md):
   an outlined pill after Reply, icon and label; quick replies as chips under
   the words, one row that scrolls sideways. */
.for-me-action {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 5px;
  box-sizing: border-box;
  height: 30px;
  padding: 0 11px 0 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.for-me-action svg {
  color: var(--ink-2);
}
.for-me-chips {
  display: flex;
  gap: 6px;
  margin: 8px -36px 0 0;
  padding-right: 36px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.for-me-chips::-webkit-scrollbar {
  display: none;
}
.for-me-chip {
  flex: none;
  height: 30px;
  padding: 0 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: 13.5px;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) {
  .for-me-action:hover,
  .for-me-chip:hover {
    background: var(--surface-2);
    border-color: var(--line-2);
  }
}
/* Cards put off with Later: one quiet line over the feed. */
.for-me-later-line {
  margin: 14px 2px 0;
  color: var(--ink-3);
  font-size: 13px;
}
.for-me-toast {
  position: fixed;
  left: 50%;
  bottom: calc(18px + var(--safe-bottom));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: calc(100vw - 32px);
  padding: 6px 8px 6px 18px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: 14px;
  white-space: nowrap;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}
.for-me-toast button,
.for-me-toast a {
  min-height: 32px;
  padding: 0 12px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--paper);
  font: inherit;
  font-weight: 600;
  line-height: 32px;
  text-decoration: none;
  cursor: pointer;
}
/* Start a task: the post quoted, the title (or the two the model gave), where its line goes. */
.for-me-start-quote {
  margin: 8px 0 14px;
  padding: 8px 12px;
  border-left: 3px solid var(--line-2);
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.for-me-start-quote strong {
  color: var(--ink);
}
.for-me-start-titles {
  display: grid;
  gap: 8px;
}
.for-me-start-title {
  display: flex;
  align-items: center;
  gap: 8px;
}
.for-me-start-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}
.for-me-start-field {
  flex: 1;
  min-width: 0;
  box-sizing: border-box;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
}
.for-me-start-field:focus {
  outline: 2px solid var(--ink);
  outline-offset: -1px;
}
.for-me-start-where {
  margin: 10px 0 16px;
  color: var(--ink-3);
  font-size: 13px;
}
.for-me-start-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}
.for-me-not-useful {
  padding: 6px 0;
  border: 0;
  background: none;
  color: var(--ink-3);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.for-me-topic.hue-purple { color: var(--opt-purple); background: var(--opt-purple-bg); }
.for-me-topic.hue-green { color: var(--opt-green); background: var(--opt-green-bg); }
.for-me-topic.hue-orange { color: var(--opt-orange); background: var(--opt-orange-bg); }
.for-me-topic.hue-brown { color: var(--opt-brown); background: var(--opt-brown-bg); }
.for-me-topic.hue-pink { color: var(--opt-pink); background: var(--opt-pink-bg); }
.for-me-topic.hue-yellow { color: var(--opt-yellow); background: var(--opt-yellow-bg); }
.for-me-topic.hue-gray { color: var(--opt-gray); background: var(--opt-gray-bg); }
/* The post a reply answers, in full, once Reply is open
   (docs/work/2026-11-03-for-me-reply-context.md): a muted quote block, the
   topic's blockquote rule, with its own scroll past a height that leaves the
   box on screen with a phone's keyboard up. */
.for-me-original {
  max-height: min(320px, 38dvh);
  margin-top: 8px;
  padding: 2px 4px 2px 12px;
  border-left: 2px solid var(--line-2);
  overflow-y: auto;
  overscroll-behavior: contain;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.for-me-original:focus-visible {
  outline: 2px solid var(--ink-3);
  outline-offset: 2px;
}
.for-me-original .reply-quote {
  margin-top: 2px;
}
.for-me-original .text > :first-child {
  margin-top: 0;
}
.for-me-original .text > :last-child {
  margin-bottom: 0;
}
.for-me-reply-box {
  margin-top: 8px;
}
.for-me-field {
  display: block;
  box-sizing: border-box;
  width: 100%;
  min-height: 56px;
  padding: 8px 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  line-height: 1.4;
  resize: vertical;
}
.for-me-field:focus {
  outline: none;
  border-color: var(--ink-3);
}
.for-me-reply-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}
.for-me-spacer {
  flex: 1;
}
.for-me-cancel {
  padding: 4px 8px;
  border: 0;
  background: none;
  color: var(--ink-2);
  font: inherit;
  font-size: 14px;
}
.for-me-send {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: var(--ink);
  color: var(--surface);
}
.for-me-send:disabled {
  opacity: 0.35;
}
.for-me-empty {
  max-width: 360px;
  margin: 0 auto;
  padding: 72px 8px 0;
  color: var(--ink-2);
  font-size: 14.5px;
  text-align: center;
}
.for-me-empty-mark {
  display: inline-grid;
  place-items: center;
  width: 52px;
  height: 52px;
  margin-bottom: 12px;
  border-radius: 50%;
  background: var(--opt-green-bg);
  color: var(--opt-green);
}
.for-me-empty strong {
  display: block;
  margin-bottom: 6px;
  color: var(--ink);
  font-size: 18px;
}
.for-me-empty p {
  margin: 0;
}
/* The line glyphs are strokes: drawn so wherever this screen shows one. */
.for-me-empty-mark svg,
.for-me-send svg,
.for-me-reply svg,
.for-me-check svg,
.for-me-added svg,
.for-me-action svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* A phone's finger: Done and Reply reach 44 px without growing the card. */
.for-me-check::after,
.for-me-reply::after {
  content: "";
  position: absolute;
  inset: -8px;
}
.for-me-reply {
  position: relative;
}

/* ---------- passkeys (docs/work/2026-11-03-passkeys.md) ---------- */
/* The offer to save a passkey: a card at the top of For me, the For me
   cards' frame, so it sits in the screen rather than over it. */
.passkey-offer {
  margin: 4px 0 16px;
  padding: 14px 14px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.passkey-offer h2 {
  font-size: 16px;
  margin: 0;
}
.passkey-offer .lead {
  margin: 4px 0 12px;
  color: var(--ink-2);
  font-size: 14px;
}
.passkey-offer .btn-primary,
.passkey-offer form {
  width: 100%;
}
.passkey-offer .btn-primary {
  min-height: 44px;
}
.passkey-offer form .btn-primary {
  margin-top: 12px;
}
.passkey-offer-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.passkey-offer-actions > * {
  flex: 1;
  min-height: 44px;
}
.passkey-offer-done {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--done);
}
/* The sign-in card's "or" between the passkey and the email. */
.gate-card .or-line {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 16px 0 4px;
  color: var(--ink-muted);
  font-size: 13px;
}
.gate-card .or-line::before,
.gate-card .or-line::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}
/* Settings › Profile: the person's passkeys. */
.passkey-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
}
.passkey-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.passkey-list .hint {
  display: block;
  margin: 0;
}
.settings-passkeys {
  margin: 24px 0;
}
/* ⌘↵ (Ctrl ↵ off a Mac) on a send button when ⌘Enter sends
   (src/client/send-icon.tsx): the modifier, then the Return glyph a little
   smaller, so both fit the 32 px pill. */
.send-chord {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  line-height: 1;
}
.send-chord-mod {
  font-size: 13px;
  font-weight: 600;
}
.send-chord-mod.ctrl {
  font-size: 9px;
  letter-spacing: -0.02em;
}
.send .send-chord svg,
.for-me-send .send-chord svg {
  width: 13px;
  height: 13px;
}
.passkey-offer-done svg {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Now (src/client/now.tsx, docs/work/2026-11-05-now-screen.md): the team's
   week at the top of the page (no card), then "People" and one card per
   member, two columns on a wide screen. Calm motion only:
   a breathing live dot, soft rings, a thin shimmer under a running state,
   and a shipped task moving to Shipped today and Lately; none under
   prefers-reduced-motion. Topic colours are For me's chip hues. */
.now-screen {
  max-width: 1040px;
  margin: 0 auto;
  padding: 12px 16px 32px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.now-people {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 12px;
}
@media (min-width: 840px) {
  .now-people {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
.now-card {
  min-width: 0;
  padding: 14px 16px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.now-card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.now-section-title {
  margin: 4px 0 0;
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
}
.now-people-title {
  margin-top: 12px;
}
.now-team {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.now-card-head h3 {
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.now-sub {
  font-size: 13px;
  color: var(--ink-3);
}
.now-label {
  margin: 14px 0 6px;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-3);
}
.now-empty {
  margin: 0;
  font-size: 13px;
  color: var(--ink-3);
}
.now-error {
  margin: 0;
  font-size: 13px;
  color: var(--danger);
}
.now-task {
  min-width: 0;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.now-working,
.now-lately,
.now-chips,
.now-legend {
  margin: 0;
  padding: 0;
  list-style: none;
}
.now-working li {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 32px;
  font-size: 14px;
  color: var(--ink);
}
.now-working .now-title {
  flex: 1;
}
/* A title and, when it is cut, its whole text floating over the page. */
.now-title {
  position: relative;
  display: inline-flex;
  min-width: 0;
  max-width: 100%;
}
.now-title .now-task {
  max-width: 100%;
}
/* The whole row opens the task, as before the popover. */
.now-working .now-title .now-task {
  flex: 1;
}
.now-full {
  position: absolute;
  top: calc(100% + 4px);
  left: -8px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  box-sizing: border-box;
  width: max-content;
  max-width: min(320px, calc(100vw - 32px));
  padding: 8px 10px;
  border: 1px solid var(--line-2);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  color: var(--ink);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
  /* Shifted back inside the viewport when it would overflow (now.tsx's useInside). */
  transform: translateX(var(--shift, 0px));
}
.now-full[data-kind="hover"] {
  pointer-events: none;
}
.now-full-open {
  min-height: 32px;
  padding: 0 12px;
  border: 0;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--ink);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
}
/* A task's state, as its line, card and page draw it (TaskStatusFace). */
.now-status {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--ink-3);
}
/* A shipped line leaving Working on. */
.now-working li.now-leaving {
  animation: now-leave 0.7s var(--motion-ease) forwards;
  pointer-events: none;
}
@keyframes now-leave {
  to {
    opacity: 0;
    transform: translateX(16px);
    min-height: 0;
    height: 0;
  }
}
.now-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.now-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  min-width: 0;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--state-done-bg);
  color: var(--state-done);
  font-size: 13px;
}
/* Shipped, then reopened: the chip goes plain, as its glyph does. */
.now-chip[data-state="opened"],
.now-chip[data-state="working"],
.now-chip[data-state="stale"] {
  background: var(--surface-2);
  color: var(--ink-2);
}
.now-chip[data-state="merged"] {
  background: color-mix(in srgb, var(--task-merged) 12%, transparent);
  color: var(--task-merged);
}
.now-chip.now-pop {
  animation: now-pop 0.5s var(--motion-ease) 0.35s backwards;
}
@keyframes now-pop {
  from {
    opacity: 0;
    transform: scale(0.6);
  }
}
/* Lately: up to three quiet chips under the team's bar. */
.now-lately {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* A newly shipped chip slides in first, its highlight fading over 3 s. */
.now-lately li.now-arrive {
  animation:
    now-arrive 0.6s var(--motion-ease) 0.35s backwards,
    now-highlight 3s ease-out 0.35s backwards;
}
@keyframes now-arrive {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
}
@keyframes now-highlight {
  from {
    box-shadow: 0 0 0 3px var(--state-done);
  }
  to {
    box-shadow: 0 0 0 0 transparent;
  }
}
/* "+N more" under Working on: quiet, opens the rest in place. */
.now-more {
  min-height: 28px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-3);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Where a week went: proportions only. */
.now-bar {
  display: flex;
  gap: 2px;
  height: 10px;
  overflow: hidden;
  border-radius: 999px;
  background: var(--surface-2);
}
.now-seg {
  flex: none;
  min-width: 4px;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: var(--seg, var(--line-2));
  cursor: default;
  -webkit-tap-highlight-color: transparent;
  transition:
    width 0.9s var(--motion-ease),
    opacity 0.15s ease;
}
/* Pointing at one topic dims the others, in the bar and its legend. */
.now-focus {
  position: relative;
}
.now-legend li {
  transition: opacity 0.15s ease;
}
.now-focus[data-active] .now-seg:not(.is-active),
.now-focus[data-active] .now-legend li:not(.is-active) {
  opacity: 0.4;
}
/* The topic's name over its segment, and nothing else. */
.now-tip {
  position: absolute;
  bottom: calc(100% + 6px);
  z-index: 40;
  max-width: min(240px, calc(100vw - 32px));
  padding: 4px 8px;
  overflow: hidden;
  border: 1px solid var(--line-2);
  border-radius: 6px;
  background: var(--surface);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
  color: var(--ink);
  font-size: 12px;
  white-space: nowrap;
  text-overflow: ellipsis;
  transform: translateX(calc(-50% + var(--shift, 0px)));
  pointer-events: none;
}
.now-tip[data-edge="start"] {
  transform: translateX(calc(-8px + var(--shift, 0px)));
}
.now-tip[data-edge="end"] {
  transform: translateX(calc(-100% + 8px + var(--shift, 0px)));
}
/* In dark mode the card is the surface too: a raised tone and a deeper shadow set them apart. */
@media (prefers-color-scheme: dark) {
  .now-full,
  .now-tip {
    background: var(--surface-2);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  }
}
.now-seg:last-child {
  flex: 1;
}
/* A bar's fills: the option palette's hues, but as fills chosen to stay
   apart side by side (hub's review, 2026-11-05). The palette's light tones
   are text colours, dark enough that yellow reads olive-brown beside the
   orange; these are brighter. Dark mode's tones are already fills. */
:root {
  --now-purple: #8b5cf6;
  --now-green: #2f9e5a;
  --now-orange: #e8772e;
  --now-pink: #d6458f;
  --now-yellow: #e2b007;
  --now-gray: #7c818b;
  --now-blue: #2f7fd6;
}
@media (prefers-color-scheme: dark) {
  :root {
    --now-purple: var(--opt-purple);
    --now-green: var(--opt-green);
    --now-orange: var(--opt-orange);
    --now-pink: var(--opt-pink);
    --now-yellow: var(--opt-yellow);
    --now-gray: var(--opt-gray);
    --now-blue: #8cb8f0;
  }
}
.hue-purple { --seg: var(--now-purple); }
.hue-green { --seg: var(--now-green); }
.hue-orange { --seg: var(--now-orange); }
.hue-brown { --seg: var(--opt-brown); }
.hue-pink { --seg: var(--now-pink); }
.hue-yellow { --seg: var(--now-yellow); }
.hue-gray { --seg: var(--now-gray); }
.hue-blue { --seg: var(--now-blue); }
/* Other is striped, never a flat grey, so it is never mistaken for a topic
   whose hue is grey; the same stripes in the legend and every bar. */
.now-seg.hue-other,
.now-dot.hue-other {
  background: repeating-linear-gradient(135deg, var(--ink-3) 0 1.5px, transparent 1.5px 4px);
}
.now-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 12px;
  margin-top: 6px;
}
.now-legend li {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  max-width: 100%;
  font-size: 12px;
  color: var(--ink-3);
}
.now-bar.now-bar-large {
  height: 14px;
}
.now-team .now-legend li {
  font-size: 13px;
  color: var(--ink-2);
}
.now-legend-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.now-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--seg, var(--line-2));
}
/* The live dot beside the title, breathing slowly. */
.now-live {
  flex: none;
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 8px;
  border-radius: 50%;
  background: var(--state-done);
  vertical-align: middle;
  animation: now-breathe 2.6s ease-in-out infinite;
}
@keyframes now-breathe {
  0%,
  100% {
    opacity: 1;
    box-shadow: 0 0 0 0 var(--state-done-bg);
  }
  50% {
    opacity: 0.55;
    box-shadow: 0 0 0 4px var(--state-done-bg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .now-live,
  .now-chip.now-pop,
  .now-lately li.now-arrive,
  .now-working li.now-leaving {
    animation: none;
  }
  .now-seg,
  .now-legend li {
    transition: none;
  }
}

/* The Coding tasks screen (docs/work/2026-11-05-coding-tasks-screen.md):
   three lines a row on a phone, columns Task · State · Pull request from 840 px. */
.nav-list button .quiet-count {
  margin-left: auto;
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-3);
}
.coding-tasks-screen {
  max-width: 1040px;
  margin: 0 auto;
  padding: 12px 16px 32px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.coding-tasks-filter {
  display: inline-flex;
  align-self: flex-start;
  padding: 2px;
  border-radius: 999px;
  background: var(--surface-2);
}
.coding-tasks-filter button {
  min-height: 36px;
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--ink-2);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.coding-tasks-filter button[aria-pressed="true"] {
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 1px 2px var(--surface-shadow);
}
.coding-tasks-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.coding-tasks-title {
  margin: 8px 0 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.coding-tasks-n {
  margin-left: 6px;
  font-weight: 500;
  color: var(--ink-3);
}
.coding-tasks-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}
/* Rows after the first; the head row is the list's first item. */
.coding-tasks-list > li ~ li ~ li .coding-tasks-row {
  border-top: 1px solid var(--line);
}
.coding-tasks-head {
  display: none;
}
.coding-tasks-row {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  column-gap: 10px;
  row-gap: 3px;
  padding: 12px 14px;
  color: inherit;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.coding-tasks-row:hover {
  background: var(--surface-2);
}
.coding-tasks-task {
  display: contents;
}
.coding-tasks-task > .task-badge {
  grid-column: 1;
  grid-row: 1 / span 3;
  align-self: start;
}
.coding-tasks-text,
.coding-tasks-narrow {
  grid-column: 2;
  min-width: 0;
}
.coding-tasks-text {
  display: flex;
  flex-direction: column;
}
.coding-tasks-name {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink);
}
.coding-tasks-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.coding-tasks-private {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-2);
}
.coding-tasks-private svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}
.coding-tasks-line {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--ink-2);
}
.coding-tasks-meta {
  color: var(--ink-3);
}
.coding-tasks-state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--ink);
  vertical-align: bottom;
}
.coding-tasks-wide,
.coding-tasks-cell {
  display: none;
}
.coding-tasks-more {
  align-self: flex-start;
  min-height: 40px;
  padding: 0 4px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--ink-2);
  cursor: pointer;
}
.coding-tasks-empty {
  padding: 28px 20px;
  border: 1px dashed var(--line-2);
  border-radius: var(--radius-lg);
  text-align: center;
}
.coding-tasks-empty h2 {
  margin: 0 0 6px;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.coding-tasks-empty p {
  margin: 0 auto;
  max-width: 420px;
  font-size: 14px;
  color: var(--ink-2);
}
@media (min-width: 840px) {
  .coding-tasks-head,
  .coding-tasks-row {
    display: grid;
    grid-template-columns: minmax(0, 2.6fr) minmax(0, 1fr) minmax(0, 1.6fr);
    column-gap: 16px;
    align-items: center;
  }
  .coding-tasks-head {
    padding: 8px 14px;
    border-bottom: 1px solid var(--line);
    font-size: 12px;
    font-weight: 500;
    color: var(--ink-3);
  }
  .coding-tasks-row {
    padding: 10px 14px;
  }
  .coding-tasks-task {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
  }
  .coding-tasks-task > .task-badge {
    align-self: center;
  }
  .coding-tasks-narrow {
    display: none;
  }
  .coding-tasks-wide,
  .coding-tasks-cell {
    display: block;
  }
}
