/* avy — compact, dense, two-theme. Hand-written, no build step.
 *
 * Theming: the light palette is the base definition on :root. Dark is redefined twice — once
 * behind prefers-color-scheme (guarded so an explicit light choice still wins) and once behind
 * [data-theme="dark"] (so the toggle wins in both directions). Nothing gets its only definition
 * inside a media query. */

:root {
  --bg-0: #ffffff;
  --bg-1: #f4f5f8;
  --bg-2: #e9ebf0;
  --bg-3: #dcdfe7;
  --border: #d6d9e2;
  --border-soft: #e4e6ed;
  --text-0: #1b1c21;
  --text-1: #4d505a;
  --text-2: #767987;
  --accent: #3f5ad4;
  --accent-2: #2c43ac;
  --on-accent: #ffffff;
  --ok: #12945a;
  --warn: #96690a;
  --bad: #c33049;
  --shadow: 0 10px 30px rgba(18, 22, 40, 0.18);
  --scrim: rgba(20, 24, 38, 0.42);
  --scrollbar: #c2c6d2;
  /* Code surfaces sit *under* the bubble rather than on it, so a fenced block reads as an inset
     panel in both palettes instead of a slightly different shade of the same card. */
  --code-bg: #f7f8fb;
  --code-border: #e0e3ec;
  --quote-bar: #c9cdda;

  --radius: 7px;
  --radius-lg: 10px;
  --rail-w: 236px;
  --chats-w: 272px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-0: #1a1b20;
    --bg-1: #212228;
    --bg-2: #2b2d35;
    --bg-3: #363943;
    --border: #383b45;
    --border-soft: #2c2e37;
    --text-0: #e8e9ee;
    --text-1: #b6b8c2;
    --text-2: #8a8c98;
    --accent: #6d8dff;
    --accent-2: #8aa3ff;
    --on-accent: #111420;
    --ok: #57c78a;
    --warn: #e0b34d;
    --bad: #e0616f;
    --shadow: 0 10px 34px rgba(0, 0, 0, 0.55);
    --scrim: rgba(0, 0, 0, 0.55);
    --scrollbar: #43464f;
    --code-bg: #131418;
    --code-border: #32353e;
    --quote-bar: #4a4d58;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg-0: #1a1b20;
  --bg-1: #212228;
  --bg-2: #2b2d35;
  --bg-3: #363943;
  --border: #383b45;
  --border-soft: #2c2e37;
  --text-0: #e8e9ee;
  --text-1: #b6b8c2;
  --text-2: #8a8c98;
  --accent: #6d8dff;
  --accent-2: #8aa3ff;
  --on-accent: #111420;
  --ok: #57c78a;
  --warn: #e0b34d;
  --bad: #e0616f;
  --shadow: 0 10px 34px rgba(0, 0, 0, 0.55);
  --scrim: rgba(0, 0, 0, 0.55);
  --scrollbar: #43464f;
  --code-bg: #131418;
  --code-border: #32353e;
  --quote-bar: #4a4d58;
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--bg-0);
  color: var(--text-0);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.45;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

a {
  color: inherit;
  text-decoration: none;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Every pane scrolls on its own now, so the bars are everywhere. Keep them out of the way. */
.rail-projects,
.chat-list,
.new-chat,
.transcript,
.landing,
.empty-state {
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar) transparent;
}

.rail-projects::-webkit-scrollbar,
.chat-list::-webkit-scrollbar,
.new-chat::-webkit-scrollbar,
.transcript::-webkit-scrollbar,
.landing::-webkit-scrollbar,
.empty-state::-webkit-scrollbar {
  width: 10px;
}

.rail-projects::-webkit-scrollbar-thumb,
.chat-list::-webkit-scrollbar-thumb,
.new-chat::-webkit-scrollbar-thumb,
.transcript::-webkit-scrollbar-thumb,
.landing::-webkit-scrollbar-thumb,
.empty-state::-webkit-scrollbar-thumb {
  background: var(--scrollbar);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}

.rail-projects::-webkit-scrollbar-track,
.chat-list::-webkit-scrollbar-track,
.new-chat::-webkit-scrollbar-track,
.transcript::-webkit-scrollbar-track,
.landing::-webkit-scrollbar-track,
.empty-state::-webkit-scrollbar-track {
  background: transparent;
}

/* ---------------------------------------------------------------- layout
 *
 * One viewport-tall grid that never itself scrolls; each pane owns its own scrollbar. The
 * explicit `minmax(0, 1fr)` row is what stops the panes from being sized by their content —
 * without it the implicit `auto` row grows past 100dvh and the whole document scrolls. */

.app-shell {
  display: grid;
  grid-template-columns: var(--rail-w) var(--chats-w) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  grid-template-areas: "rail chats main";
  height: 100dvh;
  overflow: hidden;
}

.app-shell.no-chats {
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  grid-template-areas: "rail main";
}

.app-shell.no-chats .chat-list-pane {
  display: none;
}

.rail {
  grid-area: rail;
  background: var(--bg-1);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.rail-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 10px 10px 14px;
  flex-shrink: 0;
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.02em;
  margin-right: auto;
}

/* The icon is already a circle with its own background, so it needs no ring or padding — just a
   fixed box so a slow image load can't reflow the header. */
.brand-mark {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border-radius: 50%;
}

.rail-filter {
  flex-shrink: 0;
  padding: 0 10px 8px;
}

.filter-input {
  width: 100%;
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 9px;
  font-size: 13px;
  color: var(--text-0);
}

.filter-input::placeholder {
  color: var(--text-2);
}

.filter-input:focus {
  outline: none;
  border-color: var(--accent);
}

.rail-projects {
  flex: 1 1 auto;
  min-height: 0;
  padding: 0 8px 12px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.rail-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 10px;
  border-radius: var(--radius);
  color: var(--text-1);
  min-width: 0;
  position: relative;
}

.rail-item:hover {
  background: var(--bg-2);
  color: var(--text-0);
}

.rail-item.active {
  background: var(--bg-3);
  color: var(--text-0);
  font-weight: 600;
}

.rail-item.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--accent);
}

.rail-item .project-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rail-hint,
.filter-empty {
  padding: 8px 10px;
}

.rail-meta {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-shrink: 0;
}

/* Stored-chat count. Deliberately quiet — it is a hint about where the work is, not a badge
   demanding attention. */
.count-pill {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  padding: 3px 6px;
  border-radius: 999px;
  background: var(--bg-3);
  color: var(--text-2);
}

.rail-item.active .count-pill,
.rail-item:hover .count-pill {
  color: var(--text-1);
}

.worker-dots {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}

.worker-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ok);
  display: inline-block;
}

/* ---------------------------------------------------------------- chat list pane */

.chat-list-pane {
  grid-area: chats;
  background: var(--bg-1);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.chat-list-header {
  flex-shrink: 0;
  padding: 14px 14px 8px;
}

.chat-list-header h2 {
  margin: 0;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-list {
  flex: 1 1 auto;
  min-height: 0;
  padding: 0 10px 10px;
}

.chat-list-group {
  margin-bottom: 12px;
}

.chat-list-group-header {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-2);
  padding: 6px 4px 4px;
  position: sticky;
  top: 0;
  background: var(--bg-1);
  z-index: 1;
}

/* The group header is sticky at the scrollport's top edge, so the gap under the divider has to
   come from the header's own padding rather than from padding on the scroll container — chats
   would otherwise slide through a transparent strip above it. */
.chat-list .chat-list-group:first-child .chat-list-group-header {
  padding-top: 10px;
}

/* Inside a worker's group, the chats are cut into "Today", "Yesterday" and so on. The band
   heading is deliberately quieter than the worker's — sentence case against its uppercase — so
   the two levels read as machine-then-time rather than as two lists. It is not sticky: the
   worker heading already holds the top edge, and a second one fighting it for the same pixels
   needs a magic number for the first one's height. */
.chat-band + .chat-band {
  margin-top: 6px;
}

.chat-band-header {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--text-2);
  padding: 8px 4px 4px;
}

/* The hairline runs from the end of the word to the edge, which is what makes a heading read as
   a divider through the list rather than as another row in it. */
.chat-band-header::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--border-soft);
}

/* The row exists to sit the delete button next to the link rather than inside it. The button
   is only revealed on hover or keyboard focus: a trash can beside every chat, always lit, reads
   as an invitation. focus-within covers the link having focus, which is exactly the state the
   Delete key acts on, so the affordance appears at the moment the shortcut becomes live. */
.chat-row {
  display: flex;
  align-items: center;
  gap: 2px;
}

.chat-row .chat-item {
  flex: 1;
  min-width: 0;
}

.chat-delete {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  opacity: 0;
}

.chat-delete svg {
  width: 15px;
  height: 15px;
}

.chat-row:hover .chat-delete,
.chat-row:focus-within .chat-delete {
  opacity: 1;
}

.chat-delete:hover,
.chat-delete:focus-visible {
  opacity: 1;
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, transparent);
}

/* Coarse pointers get no hover, so there is nothing to reveal it — leave it showing, dimmed. */
@media (hover: none) {
  .chat-delete {
    opacity: 0.55;
  }
}

.chat-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 10px;
  border-radius: var(--radius);
  color: var(--text-1);
  margin-bottom: 1px;
}

.chat-item:hover {
  background: var(--bg-2);
  color: var(--text-0);
}

.chat-item.active {
  background: var(--bg-3);
  color: var(--text-0);
  font-weight: 600;
}

.chat-item-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-state {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 1px 6px;
  border-radius: 999px;
  flex-shrink: 0;
  background: var(--bg-3);
  color: var(--text-2);
}

.chat-state.running {
  background: color-mix(in srgb, var(--warn) 18%, transparent);
  color: var(--warn);
}

.chat-state.cancelled {
  background: color-mix(in srgb, var(--text-2) 14%, transparent);
  color: var(--text-1);
}

.chat-state.failed {
  background: color-mix(in srgb, var(--bad) 18%, transparent);
  color: var(--bad);
}

/* The new-chat form sits between the pane header and the chat list, pinned above the scrolling
   list so it stays put however long that list gets. It scrolls inside itself, so a long stack of
   model / effort / permission controls can never push its own submit button out of view — nor
   squeeze the chat list below it down to nothing. */
.new-chat {
  flex-shrink: 0;
  max-height: 64%;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-bottom: 1px solid var(--border);
  background: var(--bg-1);
  padding: 4px 12px 10px;
}

.new-chat summary,
.new-project summary {
  cursor: pointer;
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 2px 0;
}

.new-chat summary::-webkit-details-marker,
.new-project summary::-webkit-details-marker {
  display: none;
}

.new-chat summary::before,
.new-project summary::before {
  content: "+";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 17px;
  height: 17px;
  border-radius: 5px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  font-weight: 700;
  line-height: 1;
}

.new-chat[open] summary::before,
.new-project[open] summary::before {
  content: "\2212";
}

.new-chat form,
.new-project form {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Stacked, full-width controls in the narrow pane — the row layout the composer uses runs the
   permission select clean off the edge at this width. */
.new-chat .model-effort {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.new-chat .model-effort .select {
  width: 100%;
}

.new-chat .btn-primary {
  width: 100%;
  margin-top: 2px;
}

/* ---------------------------------------------------------------- main pane */

.main {
  grid-area: main;
  background: var(--bg-0);
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

/* ---------------------------------------------------------------- top bar (narrow only) */

.topbar {
  grid-area: topbar;
  display: none;
  align-items: center;
  gap: 6px;
  padding: 7px 8px;
  background: var(--bg-1);
  border-bottom: 1px solid var(--border);
}

.topbar-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin: 0 4px 0 2px;
  flex: 0 1 auto;
  min-width: 0;
}

/* Pins the settings + status group right whether or not the title is showing. */
.topbar .settings-link {
  margin-left: auto;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text-1);
  padding: 6px;
  cursor: pointer;
  flex-shrink: 0;
}

/* Reached as an <a> too — the sessions link — which would otherwise be underlined. */
.icon-btn {
  text-decoration: none;
}

.icon-btn:hover {
  background: var(--bg-2);
  color: var(--text-0);
}

.icon-btn svg {
  width: 17px;
  height: 17px;
  display: block;
}

.scrim {
  display: none;
}

/* ---------------------------------------------------------------- status badge */

.status-badge {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-2);
  flex-shrink: 0;
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
}

.status-dot.status-ok {
  background: var(--ok);
}

.status-dot.status-warn {
  background: var(--warn);
}

.status-dot.status-bad {
  background: var(--bad);
}

/* A green dot beside a project name is a claim that some machine is up and carrying it. The
 * moment the path to that machine is in doubt, the dot has to stop making the claim — otherwise
 * the rail goes on looking exactly as it did while everything worked, which is the failure the
 * badge above was changed to stop telling. `data-health` is set on <body> at render and rewritten
 * live by app.js, so this follows a worker away without a navigation.
 *
 * "stale" means the coordinator link is down and the worker list is only the last thing it said;
 * "no-workers" means the link is fine and there is genuinely nothing behind it. Different truths,
 * and the badge's label is what tells them apart — but neither one is online. */
body[data-health="stale"] .worker-dot,
body[data-health="no-workers"] .worker-dot {
  background: var(--text-2);
  opacity: 0.55;
}

body[data-health="no-workers"] .worker-dot {
  background: var(--bad);
}

/* ---------------------------------------------------------------- landing / project grid */

.landing {
  flex: 1 1 auto;
  min-height: 0;
  padding: 24px;
}

.landing-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 18px;
}

.landing h1 {
  margin: 0;
  font-size: 20px;
}

.landing-head .hint {
  margin: 0;
}

/* The new-project form sits between the landing header and the project grid. Unlike the new-chat
   form it has the whole main pane to itself, so it is held to a readable column width rather than
   stretched across it. */
.new-project {
  max-width: 380px;
  margin-bottom: 18px;
}

.new-project .select,
.new-project .btn-primary {
  width: 100%;
}

.new-project .hint {
  margin: 0;
}

/* Empty until a create fails, so it must not open a gap in the stack before then. */
.new-project #new-project-error:empty {
  display: none;
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 12px;
}

.project-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  position: relative;
  min-width: 0;
}

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

.project-card h2 {
  margin: 0;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-2);
  margin-top: -2px;
}

.meta-sep {
  opacity: 0.5;
}

.multi-worker-tag {
  color: var(--accent);
  font-weight: 600;
}

.worker-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.worker-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  color: var(--text-1);
  background: var(--bg-2);
  border-radius: 999px;
  padding: 3px 8px;
}

.empty-state {
  flex: 1 1 auto;
  min-height: 0;
  padding: 32px 40px;
  display: grid;
  align-content: safe center;
  justify-items: start;
}

.empty-state h1 {
  margin-top: 0;
  font-size: 20px;
}

.empty-state p {
  color: var(--text-1);
  max-width: 52ch;
}

.empty-state .btn-primary {
  display: inline-block;
  margin-top: 8px;
}

/* ---------------------------------------------------------------- chat page */

.chat-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 20px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  min-width: 0;
}

.chat-header h1 {
  margin: 0;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Rename is a disclosure so the header shows the chat's name once, not twice. */
.rename {
  position: relative;
  margin-left: auto;
  flex-shrink: 0;
}

.rename > summary {
  list-style: none;
  cursor: pointer;
}

.rename > summary::-webkit-details-marker {
  display: none;
}

.rename[open] > summary {
  background: var(--bg-2);
  color: var(--text-0);
}

.rename-form {
  display: flex;
  gap: 6px;
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 5;
  padding: 8px;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.rename-form input {
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 5px 9px;
  width: 200px;
  min-width: 0;
}

.rename-form input:focus {
  outline: none;
  border-color: var(--accent);
}

.transcript {
  flex: 1 1 auto;
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-height: 0;
}

.msg {
  max-width: min(760px, 100%);
  border-radius: var(--radius-lg);
  padding: 10px 13px;
  background: var(--bg-1);
  border: 1px solid var(--border-soft);
  min-width: 0;
}

.msg-user {
  align-self: flex-end;
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-0));
  border-color: color-mix(in srgb, var(--accent) 32%, transparent);
}

.msg-assistant {
  align-self: flex-start;
}

.msg-system {
  align-self: center;
  background: transparent;
  border-style: dashed;
  border-color: var(--border);
  color: var(--text-2);
}

.msg-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--text-2);
  margin-bottom: 6px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.msg-role {
  font-weight: 700;
}

.msg-time {
  margin-left: auto;
}

.live-indicator {
  color: var(--warn);
}

.msg-text {
  word-break: break-word;
}

.msg-error {
  margin-top: 8px;
  color: var(--bad);
}

.msg-usage {
  margin-top: 8px;
  font-size: 11px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* Attached photos, above the message they came with. A row that wraps, so two pictures sit side
   by side on a desktop and stack on a phone. */
.msg-attachments {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}

.msg-attachments .attachment {
  display: block;
  line-height: 0;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border-soft);
  background: var(--bg-2);
}

.msg-attachments img {
  display: block;
  /* Bounded rather than natural size: a phone photo is several thousand pixels wide, and a
     transcript is a column. `contain` so nothing is cropped away — the point of the thumbnail is
     to say which picture it is. */
  max-width: 220px;
  max-height: 220px;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* The echo of a message that has only just been sent: the worker has not minted ids yet, so
   there is a name where the picture will be after the turn ends. */
.attachment-pending {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 5px 9px;
  font-size: 12px;
  border-radius: var(--radius);
  border: 1px dashed var(--border);
  color: var(--text-2);
}

.attachment-name {
  color: var(--text-1);
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.attachment-size {
  font-variant-numeric: tabular-nums;
}

.msg-tools {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 6px;
}

.msg-tools:empty {
  display: none;
  margin-bottom: 0;
}

.tool-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  padding: 4px 8px;
  font-size: 12px;
  font-family: var(--mono);
  min-width: 0;
}

.tool-name {
  font-weight: 700;
  color: var(--accent);
  flex-shrink: 0;
}

.tool-detail {
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tool-status {
  margin-left: auto;
  text-transform: uppercase;
  font-size: 10px;
  color: var(--text-2);
  flex-shrink: 0;
}

.tool-chip.ok .tool-status {
  color: var(--ok);
}

.tool-chip.err .tool-status {
  color: var(--bad);
}

.tool-chip.pending .tool-status {
  color: var(--warn);
}

.notice-line {
  font-size: 12px;
  color: var(--text-2);
  font-style: italic;
}

/* Reasoning during a live turn. It is not the answer and it is not kept once the turn lands, so
   it sits shut by default and stays visually quieter than the message body around it. */
.thinking {
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--text-2);
}

.thinking > summary {
  cursor: pointer;
  font-style: italic;
  color: var(--text-2);
  list-style: none;
}

.thinking > summary::before {
  content: "▸ ";
}

.thinking[open] > summary::before {
  content: "▾ ";
}

.thinking > summary::-webkit-details-marker {
  display: none;
}

.thinking-text {
  margin-top: 4px;
  padding: 6px 8px;
  max-height: 14em;
  overflow-y: auto;
  background: var(--bg-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius);
  font-family: var(--mono);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------- rendered markdown
 *
 * Message bodies are markdown (views/markdown.rs), so one bubble can hold anything from a single
 * sentence to a document with headings, tables and fenced code. Two rules keep that readable as
 * a chat message rather than as a web page:
 *
 *   - Vertical rhythm lives in the gaps *between* blocks, never in the outer edges: every child
 *     carries a bottom margin and the first/last are zeroed, so the bubble's own padding is the
 *     only thing touching the border. Nothing here depends on margin collapsing.
 *   - Anything that can outgrow the bubble — tables, fenced code — scrolls inside itself, so the
 *     transcript column never grows a horizontal scrollbar.
 */

.md > :first-child {
  margin-top: 0;
}

.md > :last-child {
  margin-bottom: 0;
}

.md p {
  margin: 0 0 10px;
}

.md h1,
.md h2,
.md h3,
.md h4,
.md h5,
.md h6 {
  margin: 18px 0 8px;
  line-height: 1.3;
  font-weight: 650;
}

/* An answer that opens with `##` and nests to `###` is the common case, so those two levels are
   the ones that have to be told apart at a glance: a rule under the section title, plain bold
   below it. `#` is rare enough to just be a slightly larger `##`. */
.md h1 {
  font-size: 19px;
  letter-spacing: -0.015em;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}

.md h2 {
  font-size: 16.5px;
  letter-spacing: -0.01em;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--border);
}

.md h3 {
  font-size: 14.5px;
  letter-spacing: -0.005em;
}

.md h4 {
  font-size: 13px;
  font-weight: 700;
}

.md h5,
.md h6 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-1);
}

.md ul,
.md ol {
  margin: 0 0 10px;
  padding-left: 22px;
}

.md li {
  margin-bottom: 3px;
}

.md li > p {
  margin-bottom: 6px;
}

/* A nested list belongs to its parent item, so it hugs it instead of opening the full
   between-blocks gap. */
.md li > ul,
.md li > ol,
.md li > p:last-child {
  margin-bottom: 0;
}

/* A drawn bullet rather than list-style: the browser's is tied to the font size, and at 14px it
   sits too heavy against text this dense. */
.md ul {
  list-style: none;
}

.md ul > li {
  position: relative;
}

.md ul > li::before {
  content: "";
  position: absolute;
  left: -13px;
  top: 0.62em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-2);
}

/* Task list items bring their own marker. */
.md ul > li:has(> input[type="checkbox"])::before {
  display: none;
}

.md input[type="checkbox"] {
  margin: 0 6px 0 -18px;
  accent-color: var(--accent);
}

.md a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
  text-underline-offset: 2px;
}

.md a:hover {
  text-decoration-color: currentColor;
}

.md strong {
  font-weight: 680;
  color: var(--text-0);
}

.md del {
  color: var(--text-2);
}

.md hr {
  height: 1px;
  border: 0;
  background: var(--border);
  margin: 16px 0;
}

.md blockquote {
  margin: 0 0 10px;
  padding: 1px 0 1px 12px;
  border-left: 3px solid var(--quote-bar);
  color: var(--text-1);
}

/* -------- code --------
 *
 * The bubble breaks long words to stay inside its column; code opts back out and scrolls
 * instead, which is the only way a flag like --dangerously-skip-permissions survives intact. */

/* `break-word` rather than `anywhere`: both stop a long path from overflowing, but only
   `anywhere` lets an identifier count as zero-width when the browser is deciding how wide a
   table column needs to be — which is how `RoundManagementWorldSubsystem` ends up split across
   two lines in a column that had room to hold it. */
.md code {
  font-family: var(--mono);
  font-size: 0.88em;
  word-break: normal;
  overflow-wrap: break-word;
}

/* No border: an identifier every second sentence turns a bordered pill into visual static. The
   fill alone is enough to say "this is a name, not prose". */
.md :not(pre) > code {
  background: var(--bg-2);
  border-radius: 4px;
  padding: 0.1em 0.32em;
}

.md pre {
  position: relative;
  margin: 0 0 12px;
  padding: 10px 12px;
  background: var(--code-bg);
  border: 1px solid var(--code-border);
  border-radius: var(--radius);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar) transparent;
}

.md pre code {
  display: block;
  font-size: 12.5px;
  line-height: 1.55;
  white-space: pre;
  word-break: normal;
  overflow-wrap: normal;
  background: none;
  border: 0;
  padding: 0;
}

/* The fence's language, printed in the corner. `data-lang` is only set when the fence named one,
   so a bare fence stays clean. It scrolls with the block's content rather than pinning to the
   viewport edge, which is the price of not wrapping every `pre` in an extra element. */
.md pre[data-lang]::before {
  content: attr(data-lang);
  position: absolute;
  top: 0;
  right: 0;
  padding: 2px 8px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-2);
  background: var(--code-bg);
  border-left: 1px solid var(--code-border);
  border-bottom: 1px solid var(--code-border);
  border-radius: 0 var(--radius) 0 var(--radius);
  pointer-events: none;
}

/* Sits over the language label — the label is decoration, the button is the thing you reach
   for. Hidden until the block is hovered or the button itself is focused, so a transcript full
   of code isn't a wall of buttons. */
.copy-code {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: 1;
  padding: 2px 8px;
  font-size: 11px;
  font-family: var(--font);
  color: var(--text-1);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms ease;
}

.code-wrap:hover .copy-code,
.copy-code:focus-visible,
.copy-code.copied {
  opacity: 1;
}

.copy-code.copied {
  color: var(--ok);
  border-color: color-mix(in srgb, var(--ok) 45%, transparent);
}

/* Wrapper added by app.js so the copy button and the language label are positioned against the
   block rather than against its scrolling contents. app.js moves `data-lang` up here at the same
   time, so only one of the two rules ever paints. */
.code-wrap {
  position: relative;
  margin-bottom: 12px;
}

.code-wrap > pre {
  margin-bottom: 0;
}

.md .code-wrap[data-lang]::before {
  content: attr(data-lang);
  position: absolute;
  top: 0;
  right: 0;
  /* The <pre> is positioned too and comes later in the box order, so without this the label is
     painted first and then covered by the block's own background. */
  z-index: 1;
  padding: 2px 8px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-2);
  background: var(--code-bg);
  border-left: 1px solid var(--code-border);
  border-bottom: 1px solid var(--code-border);
  border-radius: 0 var(--radius) 0 var(--radius);
  pointer-events: none;
}

/* -------- tables --------
 *
 * `display: block` is what makes the scroll container work at all: a table box ignores
 * `overflow`, so without it a wide table drags the whole transcript sideways. `max-content` then
 * gets the columns sized like a table again inside that box. */

.md table {
  display: block;
  width: max-content;
  max-width: 100%;
  margin: 0 0 12px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border-collapse: collapse;
  font-size: 13px;
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar) transparent;
}

.md th,
.md td {
  border: 1px solid var(--border);
  padding: 5px 10px;
  text-align: left;
  vertical-align: top;
}

.md th {
  background: var(--bg-2);
  font-weight: 650;
  white-space: nowrap;
}

.md tbody tr:nth-child(even) td {
  background: color-mix(in srgb, var(--bg-2) 45%, transparent);
}

.md img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius);
}

/* The un-rendered tail of a streaming message: whatever arrived since the last markdown
   re-render, shown as-is so the text never stalls waiting for one. */
.live-tail {
  white-space: pre-wrap;
}

/* The throbber at the foot of a streaming bubble: three dots trading opacity, so a turn's
   quiet stretches — before the first token, or mid tool call — still read as "working".
   Purely decorative; the "streaming…" meta line carries the same news for screen readers. */
.throbber {
  display: flex;
  gap: 5px;
  margin-top: 8px;
}

.throbber-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-2);
  animation: throb 1.2s ease-in-out infinite;
}

.throbber-dot:nth-child(2) { animation-delay: 0.15s; }
.throbber-dot:nth-child(3) { animation-delay: 0.3s; }

@keyframes throb {
  0%, 70%, 100% { opacity: 0.25; }
  35% { opacity: 1; }
}

/* Inside the accent bubble a user's own message is usually a sentence or two, and the heavier
   markdown furniture (rules under headings, panel fills) reads as noise. */
.msg-user .md h2 {
  border-bottom: 0;
  padding-bottom: 0;
}

.msg-user .md pre {
  background: color-mix(in srgb, var(--bg-0) 60%, transparent);
  border-color: color-mix(in srgb, var(--accent) 22%, transparent);
}

.msg-user .md :not(pre) > code {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

.msg-user .md th {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* ---------------------------------------------------------------- composer */

.composer {
  border-top: 1px solid var(--border);
  padding: 10px 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex-shrink: 0;
  background: var(--bg-0);
}

/* One box holds the textarea and everything that acts on it. The border belongs to the box
   rather than the textarea so the control row reads as part of the field being sent, not as a
   separate strip of page furniture under it. */
.composer-box {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 6px 8px 8px;
}

.composer-box:focus-within {
  border-color: var(--accent);
}

.composer-text {
  width: 100%;
  min-height: 52px;
  max-height: 40vh;
  resize: vertical;
  background: transparent;
  border: 0;
  border-radius: var(--radius);
  padding: 6px 4px 0;
}

.composer-text:focus {
  outline: none;
}

/* Icon and the filenames beside it. A flex item of the control row now, so the picker costs a
   button's width rather than a line. */
.composer-attach {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 1 auto;
  min-width: 0;
}

/* The file input is inside the label. Kept in the layout (rather than display:none) so it can
   still take focus and be operated from the keyboard, but sized to nothing so the icon is what
   you see and click. */
.attach-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.attach-btn {
  position: relative;
}

.attach-btn:focus-within {
  border-color: var(--accent);
  background: var(--bg-2);
}

.attach-chosen {
  font-size: 12px;
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  /* Sharing a row with the pickers now, so a couple of long filenames are capped rather than
     allowed to take the width the controls need. */
  max-width: 24ch;
}

.attach-chosen:empty {
  display: none;
}

/* A file the picker took but the limits will not: said in the composer, before the send, rather
   than as an error bubble after it. */
.attach-chosen.attach-bad {
  color: var(--bad);
}

.composer-controls {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.model-effort {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}

/* In the composer the control row lays the selects out itself. The wrapper stays in the DOM —
   `/partials/models` swaps it by `closest .model-effort` — but stops being a box, so the
   dangerous-mode warning wraps onto a full-width line of the row instead of making the wrapper
   two lines tall and stranding Send vertically centred beside it. */
.composer .model-effort {
  display: contents;
}

/* Status and buttons as one right-aligned group. `margin-left: auto` is what puts Send in the
   corner, and keeping the three in one flex item is what stops a crowded row from wrapping Cancel
   away from the Send it belongs beside. */
.composer-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  flex-shrink: 0;
}

.select {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 8px;
  min-width: 0;
  max-width: 100%;
}

/* The selects hold their labelled width and let the row wrap; it is the filename strip that
   gives, since it is the one thing here that ellipsises without losing its meaning. */
.composer-controls .select {
  font-size: 13px;
  padding: 5px 6px;
  flex-shrink: 0;
}

/* A permission mode the worker flagged as dangerous. Loud enough that you cannot pick it by
   accident, quiet enough that it is not shouting during the whole session. */
.select-danger {
  border-color: var(--bad);
  color: var(--bad);
}

.permission-warning {
  flex-basis: 100%;
  margin: 2px 0 0;
  padding: 5px 8px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--bad) 45%, transparent);
  border-radius: var(--radius);
}

.permission-warning strong {
  color: var(--bad);
}

/* The warning is emitted next to the select it explains, which in a wrapping row would put its
   full-width line above Send and cost a third row. `order` drops it to the bottom visually while
   leaving it before Send in the DOM — which is where a `role="alert"` about what this turn may do
   belongs for anyone hearing the page rather than seeing it. */
.composer-controls .permission-warning {
  order: 1;
}

.turn-status {
  font-size: 12px;
  color: var(--text-2);
  min-width: 0;
}

.composer-disabled .composer-text {
  opacity: 0.6;
}

/* Cancel is in the DOM for every composer but only matters while a turn is streaming; hiding it
   rather than rendering it conditionally is what lets the client reveal it the instant a turn
   starts, without waiting for the page to be re-rendered. */
.composer:not([data-running]) .composer-cancel {
  display: none;
}

/* ---------------------------------------------------------------- buttons & forms */

.btn-primary,
.btn-secondary,
.btn-danger {
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 7px 14px;
  cursor: pointer;
  font-weight: 600;
}

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

.btn-primary:hover {
  background: var(--accent-2);
}

.btn-primary:disabled {
  background: var(--bg-3);
  color: var(--text-2);
  cursor: not-allowed;
}

.btn-secondary {
  background: var(--bg-2);
  border-color: var(--border);
  color: var(--text-0);
}

.btn-secondary:hover {
  background: var(--bg-3);
}

.btn-danger {
  background: transparent;
  border-color: var(--bad);
  color: var(--bad);
}

.btn-danger:hover {
  background: color-mix(in srgb, var(--bad) 12%, transparent);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  color: var(--text-2);
}

.field input {
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 8px;
  color: var(--text-0);
  width: 100%;
}

.field input:focus {
  outline: none;
  border-color: var(--accent);
}

.hint {
  color: var(--text-2);
  font-size: 12px;
}

/* ---------------------------------------------------------------- banners */

.banner {
  border-radius: var(--radius);
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 13px;
}

.banner-error {
  background: color-mix(in srgb, var(--bad) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--bad) 35%, transparent);
  color: var(--bad);
}

.banner-warn {
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
  color: var(--warn);
}

.banner-detail {
  color: var(--text-1);
  font-weight: 400;
}

/* ---------------------------------------------------------------- narrow windows */

@media (max-width: 1080px) {
  :root {
    --rail-w: 200px;
    --chats-w: 240px;
  }
}

/* ---------------------------------------------------------------- login
 *
 * The one page that renders without the app shell, so it sets its own background rather than
 * inheriting one from `.app-shell`. */

body.bare {
  background: var(--bg-1);
  color: var(--text-0);
}

.login {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.login-card {
  width: min(340px, 100%);
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 24px;
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.login-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.02em;
  margin: 0;
}

.login-mark {
  width: 32px;
  height: 32px;
}

.login-hint {
  color: var(--text-2);
  font-size: 12px;
  margin: -8px 0 0;
}

.login-error {
  color: var(--bad);
  font-size: 12px;
  margin: 0;
}

/* The sign-out control is a form purely so it can POST; it must not become a flex item of its
   own between the settings link and the status badge. */
.logout-form {
  display: contents;
}

/* ---------------------------------------------------------------- settings & status
 *
 * /settings. Two jobs on one page: a preference that needs no explaining, and a dashboard read
 * only when something looks wrong. The cards are a plain auto-fill grid — a deployment has three
 * or four components, and any layout needing more thought than that is anticipating a scale avy
 * does not have. */

.settings-section {
  max-width: 900px;
  margin: 0 0 26px;
}

.section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}

.section-head h2 {
  margin: 0;
  font-size: 15px;
}

/* The action sits at the far end of its heading rather than under it: on a phone the heading row
   is the only line guaranteed to be on screen when the section is. */
.section-head .btn-secondary {
  margin-left: auto;
}

.btn-small {
  padding: 4px 10px;
  font-size: 12px;
}

/* An <a> wearing a button class needs the box a <button> gets for free. */
a.btn-small {
  display: inline-block;
}

.theme-choice {
  display: flex;
  gap: 8px;
  margin-top: 10px;
  flex-wrap: wrap;
}

.theme-option {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-1);
  padding: 7px 13px;
  cursor: pointer;
}

.theme-option:hover {
  background: var(--bg-2);
  color: var(--text-0);
}

.theme-option .ico svg {
  width: 16px;
  height: 16px;
  display: block;
}

/* Which one is chosen is read off <html> — the same attribute the pre-paint snippet in <head>
   sets — so the right button is lit on the very first frame, with no server round trip and
   nothing for the script to catch up on afterwards. */
:root[data-theme-mode="system"] .theme-option[data-theme-set="system"],
:root[data-theme-mode="light"] .theme-option[data-theme-set="light"],
:root[data-theme-mode="dark"] .theme-option[data-theme-set="dark"] {
  background: var(--bg-3);
  border-color: var(--accent);
  color: var(--text-0);
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: 12px;
  margin-top: 12px;
}

.status-card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 13px 15px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.status-card-head {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}

.status-card-head h3 {
  margin: 0;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.status-card-kind {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-2);
  flex-shrink: 0;
}

.status-card-state {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-2);
  flex-shrink: 0;
}

.status-facts {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  min-width: 0;
}

/* Label left, value right, and the value is the half allowed to wrap: a commit hash or an
   absolute path is the long part and a label never is. */
.status-fact {
  display: flex;
  gap: 10px;
  align-items: baseline;
  min-width: 0;
}

.status-fact dt {
  color: var(--text-2);
  flex: 0 0 auto;
}

.status-fact dd {
  margin: 0 0 0 auto;
  text-align: right;
  color: var(--text-1);
  min-width: 0;
  overflow-wrap: anywhere;
}

.status-fact code {
  font-family: var(--mono);
  font-size: 11px;
}

.status-muted,
.status-path {
  color: var(--text-2);
}

/* Loud on purpose and rare on purpose: it means two halves of one deployment are not the same
   build, which is the thing this page exists to make impossible to miss. */
.status-flag {
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: 11px;
  background: color-mix(in srgb, var(--warn) 18%, transparent);
  color: var(--warn);
}

/* `auto` above both, so the footer of every card lines up however many facts it carries — a grid
   row stretches its cards to the tallest one, and without this the shorter cards' last line
   floats in the middle of a box of empty background. */
.status-note {
  margin: auto 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--border-soft);
}

.status-actions {
  display: flex;
  margin-top: auto;
}

/* A ping is a real round trip to another machine and can take a visible moment; htmx marks
   whatever `hx-indicator` names for the duration, so the card or the section dims rather than
   the button looking like it did nothing. */
.status-card.htmx-request,
.settings-section.htmx-request {
  opacity: 0.55;
}

/* ---------------------------------------------------------------- sessions
 *
 * /settings/security. Read on a phone, in a hurry, about a device that is not in the room, so
 * every row states what it is and what ending it would cost before the button that ends it. */

.settings-lede {
  max-width: 62ch;
  margin: -8px 0 18px;
}

.sessions {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 720px;
}

.session {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-1);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* The row you are holding. An outline rather than a different background, so it reads as "this
   one" without looking like the one being warned about. */
.session-current {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
}

.session-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

/* The raw User-Agent, which is long and has no spaces to break on. */
.session-agent {
  font-size: 13px;
  color: var(--text-0);
  overflow-wrap: anywhere;
}

.session-tag {
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 999px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  flex-shrink: 0;
}

.session-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 12px;
  color: var(--text-2);
}

.session-revoke {
  display: flex;
}

/* The lost-device drill, set apart from the list: it is the one control here that acts on
   devices you are not looking at. */
.sessions-drill {
  max-width: 720px;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-start;
}

.sessions-drill .hint {
  max-width: 62ch;
  margin: 0;
}

/* Phones and narrow windows: one column for the conversation, with the project rail and the chat
   list as overlay drawers off the top bar. Each drawer still scrolls internally. */
@media (max-width: 800px) {
  .app-shell,
  .app-shell.no-chats {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    grid-template-areas:
      "topbar"
      "main";
  }

  .topbar {
    display: flex;
  }

  .rail,
  .chat-list-pane {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 30;
    /* Leaves a comfortable strip of scrim to tap for "close". */
    width: min(78vw, 320px);
    transform: translateX(-101%);
    transition: transform 160ms ease;
    box-shadow: var(--shadow);
  }

  body[data-drawer="projects"] .rail,
  body[data-drawer="chats"] .chat-list-pane {
    transform: none;
  }

  .scrim {
    position: fixed;
    inset: 0;
    z-index: 20;
    background: var(--scrim);
  }

  body[data-drawer] .scrim {
    display: block;
  }

  .topbar .status-label {
    display: none;
  }

  .rail-header {
    padding-top: 14px;
  }

  .new-chat {
    max-height: 70%;
  }

  /* A chat page has its own header with the title (and the rename control) in it, so the top
     bar drops its copy rather than printing the name twice down the screen. */
  body:has(.chat-header) .topbar-title {
    display: none;
  }

  .chat-header {
    padding: 10px 14px;
  }

  .rename-form {
    right: auto;
    left: 8px;
    width: calc(100vw - 32px);
  }

  .rename-form input {
    flex: 1 1 auto;
    width: auto;
  }

  .transcript {
    padding: 14px;
  }

  .composer {
    padding: 8px 10px calc(10px + env(safe-area-inset-bottom));
  }

  /* Selects share the width the attach button leaves, and Send takes the line under them: on a
     phone the last row of the composer is a thumb target, not a place to save 30px. */
  .composer-controls .select {
    flex: 1 1 90px;
  }

  .composer-actions {
    flex: 1 1 100%;
    margin-left: 0;
  }

  .composer-controls .btn-primary,
  .composer-controls .btn-danger {
    flex: 1 1 auto;
    padding: 9px 14px;
  }

  /* Thumbnails get most of the column on a phone, where the column is the screen. */
  .msg-attachments img {
    max-width: 60vw;
    max-height: 260px;
  }

  .msg {
    max-width: 100%;
  }

  .landing {
    padding: 16px;
  }

  /* A full-width target: ending a session is a deliberate act done with a thumb. */
  .session-revoke .btn-danger,
  .session-revoke .btn-secondary,
  .sessions-drill .btn-danger {
    width: 100%;
  }

  .sessions-drill {
    align-items: stretch;
  }

  .status-actions .btn-small {
    width: 100%;
  }

  .empty-state {
    padding: 32px 16px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rail,
  .chat-list-pane {
    transition: none;
  }

  /* Dots hold still but stay visible: the throbber is the only sign a turn is still
     running, so reducing motion must not reduce it to nothing. */
  .throbber-dot {
    animation: none;
    opacity: 0.6;
  }
}
