/* Flockdeck remote.
 *
 * It looks like the rest of Flockdeck because it is Flockdeck, seen from
 * somewhere else: the palette, the type and the status marks are the "Quiet
 * deck" direction's (design/tokens.css in the flockdeck repository, which is
 * where every value below is copied from). Like the application and the site it
 * commits to the dark ground agents are run on, so there is no light variant.
 *
 * The chrome is quiet so the signals can be loud: one cool neutral ramp,
 * hairline rules instead of shadows, small corners, no glow. The saturated
 * things are the states of agents and the one place the keyboard is. A status
 * is a shape before it is a colour -- a circle is alive, a triangle needs you,
 * a square has ended; filled means something is happening -- and is drawn in
 * CSS, so it looks the same on every phone. Every status still has its word.
 *
 * Mobile first: a phone held in one hand is the case this is for, and a
 * desktop browser gets the same layout with more room around it.
 */

/* The fonts are the site's, served from here rather than from a font service:
   the page's policy only admits this origin, and a phone opening a terminal
   should not have to ask anyone else for anything. Both are SIL OFL 1.1; the
   licences are beside the files. */
@font-face {
  font-family: "IBM Plex Sans";
  src: url("/app/fonts/ibm-plex-sans.woff2") format("woff2");
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/app/fonts/jetbrains-mono.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;

  --surface:   #0F1418;
  --raised:    #161C21;
  --sunken:    #0A0E11;
  --overlay:   #1B2329;
  --hover:     #1D252B;
  --active:    #252F36;

  --edge-soft: #1F272D;
  --edge:      #2C363D;
  --edge-hard: #6A7982;

  --text:      #E6EDF1;
  --muted:     #93A1A9;
  --faint:     #738087;

  --accent:        #4FD1DB;
  --accent-strong: #7BE0E8;
  --accent-subtle: #0E2B2E;
  --accent-edge:   #1F4E53;
  --on-accent:     #0F1418;

  --working:        #4FD1DB;
  --waiting:        #E2B341;
  --waiting-subtle: #2A2110;
  --waiting-edge:   #4E3D18;
  /* Named for the failure it has always been: a delivery, an upload, a tool
     that errored. A pane that has ended is --ended, not this. */
  --exited:         #F0776A;
  --exited-subtle:  #2C1512;
  --exited-edge:    #58251F;
  --idle:           #93A1A9;
  --ended:          #738087;
  /* A verdict -- passed, added -- never a live state. */
  --ok:             #64C97F;
  --on-waiting:     #1F1602;
  --on-failed:      #1F0907;
  --on-ok:          #06180C;

  --scrim:    rgb(5 8 10 / .62);
  --scrim-image: rgb(5 8 10 / .92);
  --shadow-1: 0 1px 2px rgb(0 0 0 / .4);
  --shadow-2: 0 8px 24px -8px rgb(0 0 0 / .6);
  --shadow-3: 0 20px 56px -16px rgb(0 0 0 / .75);

  --radius: 10px;
  --radius-sm: 6px;
  --radius-xs: 4px;

  --display: "IBM Plex Sans", "Segoe UI", -apple-system, BlinkMacSystemFont, Ubuntu, "Helvetica Neue", sans-serif;
  --body:    "IBM Plex Sans", "Segoe UI", -apple-system, BlinkMacSystemFont, Ubuntu, "Helvetica Neue", sans-serif;
  --mono:    "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, "SF Mono", monospace;

  --dur-1: 80ms;
  --dur-2: 140ms;
  --dur-3: 220ms;
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-in-out: cubic-bezier(.65, 0, .35, 1);
  --pulse: 1.6s;

  --tap: 44px;
  --gutter: max(16px, env(safe-area-inset-left));
  --gutter-r: max(16px, env(safe-area-inset-right));
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; background: var(--surface); }

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  color: var(--text);
  font: 1rem/1.6 var(--body);
  font-variant-ligatures: none;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

#app { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; min-height: 0; }

::selection { background: var(--accent-edge); color: var(--text); }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
@media (forced-colors: active) {
  :focus-visible { outline: 2px solid Highlight !important; }
}

h1:focus { outline: none; }

a { color: var(--accent); text-decoration-color: var(--accent-edge); text-underline-offset: 3px; }
a:hover { color: var(--accent-strong); }

code, kbd, pre { font-family: var(--mono); }
code {
  font-size: .88em;
  background: var(--sunken);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-xs);
  padding: 1px 6px;
  /* A span that wraps onto a second line still looks like one complete,
     rounded pill on each line it touches, rather than the border and
     background cutting off wherever the line happened to break. */
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}
kbd {
  font-size: 0.6875rem;
  background: var(--raised);
  border: 1px solid var(--edge);
  border-bottom-width: 2px;
  border-radius: var(--radius-xs);
  padding: 2px 6px;
  color: var(--muted);
  white-space: nowrap;
}
pre {
  margin: 8px 0;
  padding: 12px 16px;
  overflow-x: auto;
  background: var(--sunken);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
}
pre code { font-size: 0.8125rem; background: none; border: 0; padding: 0; color: var(--text); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

[hidden] { display: none !important; }

.loading { padding: 32px var(--gutter); color: var(--muted); font-family: var(--mono); font-size: 0.8125rem; }
.muted { color: var(--muted); }
.small { font-size: 0.875rem; }
.hint { color: var(--muted); font-size: 0.875rem; margin: 6px 0 12px; }
.error { color: var(--exited); }
.lede { font-size: 1.125rem; color: var(--muted); max-width: 60ch; margin: 0 0 24px; }

/* The small type: a mono label over a section, and a status line. Neither is
   a pill and neither is shouted in capitals. */
.eyebrow {
  font-family: var(--mono); font-size: 0.75rem; font-weight: 500;
  color: var(--accent); margin: 0 0 8px;
}
.kicker {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 0.8125rem;
  color: var(--muted);
  margin: 4px 0 16px;
}

/* ------------------------------------------------------------------ chrome */

.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: env(safe-area-inset-top) var(--gutter-r) 0 var(--gutter);
  background: var(--surface);
  border-bottom: 1px solid var(--edge-soft);
}

.brand, .back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  text-decoration: none;
  color: var(--text);
}
.brand:hover { color: var(--text); }
.mark { display: block; width: 20px; height: 20px; flex: none; }
.wordmark {
  font-family: var(--display); font-weight: 600; font-size: 1.0625rem;
  letter-spacing: -.01em;
}
.brand-sub {
  font-family: var(--mono); font-size: 0.6875rem; font-weight: 500;
  color: var(--muted);
  border: 1px solid var(--edge); border-radius: var(--radius-xs);
  padding: 1px 6px;
}
/* The pill is what pushed the Devices link past the edge of a 360-pixel
   phone, which widened the whole page to fit it. */
@media (max-width: 400px) { .brand-sub { display: none; } }
/* On a page with a way back, the mark alone is the way home. */
.brand-mark { min-width: var(--tap); justify-content: center; margin-left: -12px; }
.back {
  gap: 2px;
  font-family: var(--display); font-weight: 600; font-size: 1rem;
  color: var(--text);
  max-width: 60vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.back:hover { color: var(--accent-strong); }
.back > span[aria-hidden] { color: var(--accent); font-size: 1.25em; line-height: 1; }

.topnav { margin-left: auto; display: flex; gap: 2px; }
.navlink {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 12px;
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 0.875rem;
  text-decoration: none;
}
.navlink:hover { color: var(--text); background: var(--hover); }
.navlink[aria-current="page"] { color: var(--text); font-weight: 600; background: var(--active); }

/* The button keeps its size and the way back gives way: squeezed by a long
   desktop name, "Full interface" broke onto two lines and made the bar tall. */
.topbar > .button { margin-left: auto; flex: none; white-space: nowrap; }
/* The name on the way back, cut with an ellipsis when it is long. On the link
   itself, a flex box, an ellipsis is never drawn: the text was just clipped. */
.back-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.view {
  flex: 1;
  width: 100%;
  max-width: 780px;
  margin: 0 auto;
  padding: 20px var(--gutter-r) calc(24px + env(safe-area-inset-bottom)) var(--gutter);
}

h1 {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(30px, 8vw, 38px);
  line-height: 1.1;
  letter-spacing: -.022em;
  margin: 8px 0 12px;
  text-wrap: balance;
  /* A desktop is named after its host, which can be one long word; unbroken,
     it widened the whole page past the edge of a phone. */
  overflow-wrap: anywhere;
}
/* The accent, never the amber: amber is what a pane goes when it is blocked on
   you, and spending it on a heading would make it mean nothing in particular. */
h1 em { font-style: normal; color: var(--accent); }
h2 {
  font-family: var(--display); font-weight: 600;
  font-size: 1.25rem; line-height: 1.2; letter-spacing: -.02em;
  margin: 32px 0 8px;
}

.foot {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
  width: 100%; max-width: 780px; margin: 0 auto;
  padding: 16px var(--gutter-r) calc(20px + env(safe-area-inset-bottom)) var(--gutter);
  border-top: 1px solid var(--edge-soft);
  font-family: var(--mono); font-size: 0.75rem; color: var(--muted);
}
.foot-links { margin-left: auto; display: flex; flex-wrap: wrap; gap: 6px 16px; }
.foot a { color: var(--muted); text-decoration: none; }
.foot a:hover { color: var(--text); }

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

/* The site's buttons: a filled accent for the one thing a page is for, and an
   outline for everything else. */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--tap);
  padding: 0 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--edge-hard);
  background: transparent;
  color: var(--text);
  font-family: var(--display);
  font-weight: 500;
  font-size: 0.9375rem;
  text-decoration: none;
  cursor: pointer;
  touch-action: manipulation;
  transition: transform var(--dur-2) var(--ease-out), background-color var(--dur-1) linear, border-color var(--dur-1) linear, color var(--dur-1) linear;
}
.button:hover { background: var(--hover); color: var(--text); }
.button:active { background: var(--active); transform: translateY(1px); }
.button:disabled, .button[aria-disabled="true"] { opacity: .45; cursor: default; pointer-events: none; transform: none; }
.button[aria-disabled="true"]:hover { background: transparent; border-color: var(--edge-hard); }
.button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.button.primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: var(--on-accent); }
.button.primary:active { background: var(--accent-strong); }
.button.danger { color: var(--exited); border-color: var(--exited); }
.button.danger:hover { background: var(--exited); border-color: var(--exited); color: var(--on-failed); }
.button.small { min-height: var(--tap); padding: 0 12px; font-size: 0.875rem; }

.card {
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
  padding: 16px 16px;
  margin: 16px 0;
}
.card > :first-child { margin-top: 0; }
.card > :last-child { margin-bottom: 0; }

.form label { display: block; font-family: var(--display); font-weight: 500; margin: 16px 0 8px; }
.form > label:first-child { margin-top: 0; }
/* :not([type="checkbox"]) keeps this off the "In a new worktree" switch's
   own checkbox, which .switch input already styles as the small tickbox it
   is -- given the width and padding a text field needs here, it stopped
   looking like a checkbox at all. */
.form input:not([type="checkbox"]), .form select, .form textarea, .compose textarea {
  width: 100%;
  min-height: var(--tap);
  padding: 8px 12px;
  border: 1px solid var(--edge-hard);
  border-radius: var(--radius-sm);
  background: var(--sunken);
  color: var(--text);
  font-family: var(--body);
  font-size: 1rem; /* anything smaller and iOS zooms the page on focus */
}
.form input:not([type="checkbox"]):focus, .form select:focus, .form textarea:focus, .compose textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent);
}
.form input::placeholder, .form textarea::placeholder, .compose textarea::placeholder { color: var(--muted); }
/* .form label's own display: block otherwise wins over .switch's
   inline-flex (one more element in its selector beats .switch's one class),
   which put "In a new worktree"'s checkbox and its label touching with no
   gap between them -- gap only does anything on a flex container. */
.form .switch { display: inline-flex; margin-top: 16px; }
.form .row-actions { margin-top: 16px; }
.form .button { margin-top: 4px; }

.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin: 4px 0 12px; }
.notify { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }

/* The pairing steps, as the site draws a row of cards: each with its mono
   label in the accent over a heading. */
/* minmax(0, 1fr) and min-width: 0 are what keep a long command inside its
   card: left to their automatic minimum, a grid item grows to fit its widest
   line, and on a phone that ran the whole card off the side of the screen. */
.steps { list-style: none; padding: 0; margin: 8px 0 20px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; counter-reset: step; }
.steps li {
  min-width: 0;
  counter-increment: step;
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
  padding: 16px;
  color: var(--muted);
}
.steps li::before {
  content: "0" counter(step);
  display: block;
  font-family: var(--mono); font-size: 0.75rem; color: var(--accent);
  margin-bottom: 6px;
}
.steps li strong { color: var(--text); font-family: var(--display); font-weight: 600; }
.steps pre { margin: 8px 0 0; }

.tag {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 6px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--edge);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 500;
  vertical-align: 2px;
}

.toast {
  padding: 8px 16px;
  border-radius: var(--radius-sm);
  background: var(--overlay);
  border: 1px solid var(--edge);
  box-shadow: var(--shadow-2);
  font-size: 0.875rem;
  animation: sheet-in var(--dur-3) var(--ease-out);
}
.toast-error { border-color: var(--exited-edge); box-shadow: var(--shadow-2), inset 3px 0 0 var(--exited); }
@keyframes sheet-in { from { opacity: 0; transform: translateY(4px); } }

.codeline { display: flex; gap: 8px; align-items: center; }
.codeline pre { flex: 1; min-width: 0; margin: 0; }

.notice-card .status { font-size: 0.8125rem; }

/* Pairing by email (emailpair.js): the code the requesting browser shows,
   large enough to read across a desk and to type from, and the buttons of
   its pages, which wrap rather than widen a phone. */
.match-code-label { margin: 0 0 4px; color: var(--muted); font-size: 0.875rem; }
.match-code {
  margin: 0; font-family: var(--mono); font-size: 2rem; font-weight: 600;
  letter-spacing: .08em; overflow-wrap: anywhere; color: var(--text);
}
.pair-code-card { text-align: center; }
.pair-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0 0; }
.pair-actions .button { margin-top: 0; }
.pair-deny { margin-top: 0; }
.form .check { display: flex; align-items: flex-start; gap: 10px; min-height: var(--tap); margin: 8px 0; font-weight: 400; cursor: pointer; }
.check input { flex: none; width: 20px; height: 20px; margin: 2px 0 0; accent-color: var(--accent); }
.email-pairing-state { font-weight: 600; margin-bottom: 4px; }
.email-pairing .pair-actions { margin-top: 8px; }

/* The account's plan (plan.js): on Devices, and in place of a desktop once
   it has run out. A trial in its last week says so a little louder. */
.plan-line.plan-soon { font-weight: 600; }
.plan-line.plan-lapsed { color: var(--exited); }
.plan-warning { margin: 0 0 16px; }
.plan-notice .button { margin-top: 4px; }
.plan-subscribe { margin-top: 8px; }
.plan-subscribe .error { margin: 8px 0 0; }

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

/* A status is a shape, then a colour, then a word: a circle is alive, a
   triangle needs you, a square has ended; filled means something is
   happening, hollow means nothing is. Only working moves.

   The text glyph stays in the markup (ui.js's STATUS, for anything that reads
   the element), sized to nothing, and the mark is drawn on the same element in
   px, so it is the same on every phone and does not grow with the text beside
   it. That the glyphs were once text meant the most important marks in the
   client came from whichever font the phone substituted. */
.status {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 0.75rem; font-weight: 500;
  white-space: nowrap;
}
.status .glyph, .chat-status-dot, .chat-choices-wait-glyph, .elsewhere > [aria-hidden="true"]:first-child {
  display: inline-block; flex: none; width: 8px; height: 8px;
  font-size: 0; line-height: 0;
}
.status-working { color: var(--working); }
.status-waiting { color: var(--waiting); }
/* Connected is activity, not a verdict, so it is the accent. */
.status-online  { color: var(--accent); }
.status-idle, .status-starting, .status-exited, .status-failed, .status-offline, .status-unknown { color: var(--muted); }

/* circle, filled: working -- and a desktop that is live */
.status-working .glyph, .chat-status[data-status="working"] .chat-status-dot { border-radius: 50%; background: var(--working); }
.status-online .glyph { border-radius: 50%; background: var(--accent); }
/* circle, hollow: idle, and starting, which breathes; a state not known */
.status-idle .glyph, .status-starting .glyph, .status-unknown .glyph,
.chat-status[data-status="idle"] .chat-status-dot, .chat-status[data-status="starting"] .chat-status-dot {
  border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--idle);
}
/* triangle, filled: it needs you. A little larger, to weigh the same. */
.status-waiting .glyph, .chat-status[data-status="waiting"] .chat-status-dot, .chat-choices-wait-glyph,
.elsewhere > [aria-hidden="true"]:first-child {
  width: 10px; height: 10px; background: var(--waiting); clip-path: polygon(50% 6%, 100% 94%, 0 94%);
}
/* square, hollow: the process has ended -- an exited pane, a desktop offline.
   Red is for a real failure and is not spent here. */
.status-exited .glyph, .status-failed .glyph, .status-offline .glyph,
.chat-status[data-status="exited"] .chat-status-dot, .chat-status[data-status="failed"] .chat-status-dot {
  border-radius: 1.5px; box-shadow: inset 0 0 0 1.5px var(--ended);
}

/* ... and filled where the process ended in error: a square either way, so
   the two never read as a circle, but not the same square. */
.status-failed .glyph, .chat-status[data-status="failed"] .chat-status-dot { background: var(--ended); }

/* Working is the one state that moves, which is a channel colour cannot fake. */
.status-working .glyph, .status-starting .glyph,
.chat-status[data-status="working"] .chat-status-dot, .chat-status[data-status="starting"] .chat-status-dot {
  animation: pulse var(--pulse) var(--ease-in-out) infinite;
}
@keyframes pulse { 50% { opacity: .4; } }
@media (forced-colors: active) {
  .status .glyph, .chat-status-dot, .chat-choices-wait-glyph, .elsewhere > [aria-hidden="true"]:first-child { forced-color-adjust: none; }
}

/* -------------------------------------------------------------------- lists */

.list { list-style: none; margin: 0; padding: 0; }
.list > li + li { margin-top: 8px; }
.empty {
  padding: 16px; color: var(--muted);
  border: 1px dashed var(--edge); border-radius: var(--radius);
  font-family: var(--mono); font-size: 0.8125rem;
}

/* The filter over a long pane list (main.js's buildPaneList): a search box and
   a row of status chips that wraps rather than scrolls sideways. */
.list-filter { display: flex; flex-direction: column; gap: 8px; margin: 0 0 12px; }
.list-filter[hidden] { display: none; }
.list-filter-input {
  min-height: var(--tap);
  width: 100%;
  box-sizing: border-box;
  padding: 8px 12px;
  border: 1px solid var(--edge-hard);
  border-radius: var(--radius-sm);
  background: var(--sunken);
  color: var(--text);
  font-family: var(--body);
  font-size: 1rem; /* anything smaller and iOS zooms the page on focus */
}
.list-filter-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.list-filter-input::placeholder { color: var(--muted); }
.list-filter-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  min-height: var(--tap);
  padding: 0 14px;
  border: 1px solid var(--edge-hard);
  border-radius: 999px;
  background: transparent;
  color: var(--text);
  font-family: var(--display);
  font-size: 0.875rem;
  cursor: pointer;
  touch-action: manipulation;
}
.chip:hover { background: var(--hover); }
.chip[aria-pressed="true"] { background: var(--accent-subtle); border-color: var(--accent); color: var(--accent-strong); }
.list-filter-count { margin: 0; font-size: 0.8125rem; }
.list-filter-count[hidden] { display: none; }

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 68px;
  padding: 12px 16px;
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  transition: border-color var(--dur-1) linear;
}
/* The cascade row is taller than the ordinary rows around it, so it does not
   stretch to their fixed height and crowd its checklist. */
.row.cascade { align-items: stretch; min-height: 0; }
.cascade-body { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.cascade-list { display: flex; flex-direction: column; gap: 8px; }
.cascade-list li { display: flex; }
.cascade-list label { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
.cascade-list input[type="checkbox"] { margin-top: 4px; width: 18px; height: 18px; flex: none; accent-color: var(--accent); }
a.row:hover { border-color: var(--edge-hard); color: var(--text); }
.row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.row-head { display: flex; align-items: center; gap: 8px; }
.row-title {
  font-family: var(--display); font-weight: 600; font-size: 1rem;
  letter-spacing: -.01em; overflow-wrap: anywhere;
}
/* pre-wrap keeps the wide gaps the script puts between agent, branch and
   changes; collapsed to one space, "tab claude · opus" read as one phrase. */
.row-meta { color: var(--muted); font-family: var(--mono); font-size: 0.75rem; overflow-wrap: anywhere; white-space: pre-wrap; }
/* Who added a device or a desktop, or that nothing is known of it -- a plainer
   note than the mono metrics beside it, in the body face. */
.row-note { color: var(--muted); font-size: 0.8125rem; overflow-wrap: anywhere; }
.fingerprint-code {
  margin: 2px 0 0; padding: 4px 8px; width: fit-content;
  background: var(--sunken); border-radius: var(--radius-sm);
  font-family: var(--mono); font-size: 0.8125rem; overflow-wrap: anywhere;
}
/* Muted, not the faint grey, which is for glyphs and never for words. */
.row-when { margin-left: auto; color: var(--muted); font-family: var(--mono); font-size: 0.6875rem; }
.chev { color: var(--faint); font-size: 1.5rem; line-height: 1; }

/* A row's own buttons, beside its name, go under it on a phone too narrow for
   both rather than squeezing the name to a word a line. */
.row.static { flex-wrap: wrap; }
.row-actions { display: flex; flex-wrap: wrap; gap: 8px; }
/* A name edited in place, in the row it names. */
.rename { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.rename input {
  flex: 1 1 12em;
  min-width: 0;
  min-height: var(--tap);
  padding: 8px 12px;
  border: 1px solid var(--edge-hard);
  border-radius: var(--radius-sm);
  background: var(--sunken);
  color: var(--text);
  font-family: var(--body);
  font-size: 1rem; /* anything smaller and iOS zooms the page on focus */
}
.rename input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.rename .error { flex-basis: 100%; margin: 0; }
a.row:hover .chev { color: var(--accent); }

/* A pane waiting on you is found without reading every row, as it is on the
   site's deck: ringed and washed in the amber, and already at the top. */
.pane-row[data-status="waiting"] {
  background: var(--waiting-subtle);
  background: color-mix(in srgb, var(--waiting) 14%, var(--raised));
  border-color: var(--waiting);
  box-shadow: inset 0 2px 0 var(--waiting);
}
.pane-row[data-status="waiting"] .row-title { color: var(--waiting); }
.pane-row[data-status="waiting"]:hover { border-color: var(--waiting); }
/* An ended pane is not a failure, so it is not red: a quiet rule and a quiet
   name. */
.pane-row[data-status="exited"], .pane-row[data-status="failed"] { border-left: 3px solid var(--edge); }
.pane-row[data-status="exited"] .row-title, .pane-row[data-status="failed"] .row-title { color: var(--muted); }

/* The wide layout's own open row (main.js's renderWide/buildPaneList): never
   lost in a long list once its pane is the one filling the right column.
   After the waiting rule above, so the chosen fill wins where a row is both --
   knowing which one is open matters more there than the amber alone. The
   chosen row is the same everywhere in Flockdeck: the accent's tint, with the
   accent as a bar at its edge. */
.pane-row[aria-current="page"] { background: var(--accent-subtle); box-shadow: inset 2px 0 0 var(--accent); }

/* The "what did I miss?" above the list (main.js's renderDesktop,
   updateAwaySummary) -- a compact banner like .conn/.toast above it. It
   wraps rather than being cut with an ellipsis: each part is a link, and
   on a 360px phone the second ("N waiting on you") was the part the
   ellipsis took, a link on screen with no way to tap it. */
.away-summary {
  margin: 0 0 12px;
  padding: 8px 12px;
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
  background: var(--raised);
  color: var(--muted);
  font-size: 0.8125rem;
  white-space: normal;
}
.away-summary[hidden] { display: none; }
.away-summary-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--accent-edge);
  text-underline-offset: 2px;
}
.away-summary-link:hover { color: var(--accent-strong); }

/* ---------------------------------------------------------------- inbox
   A pane's row reads like a messaging app's conversation list: three lines at
   most, each with its own job -- who and when, what matters right now, and
   the small print -- rather than the four or five a card used to take. See
   ui.js's paneRow/inboxPreview, which build a row, and main.js's
   renderDesktop, which draws the list. */
.list.panes > li + li { margin-top: 8px; }
.list.panes .row { min-height: 0; padding: 8px 12px; gap: 8px; }
.pane-row .row-main { gap: 4px; }
.pane-row .row-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The bell beside a muted pane's name, on its row and in its own header
   (main.js's muteToggleButton draws its own, doubling as the toggle). Never
   shrunk by a long name beside it. */
.muted-badge { flex: none; font-size: 0.8125rem; opacity: .7; }
/* The status word -- "Waiting on you", "Working" -- stays in the markup, only
   hidden visually (ui.js's rowStatus adds .visually-hidden to it), so a
   screen reader still says what the glyph and the row's colour mean. */
.row-preview {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.row-preview-text {
  flex: 1;
  min-width: 0;
  color: var(--muted);
  font-size: 0.8125rem;
  overflow: hidden;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-clamp: 2;
}
.row-preview-text.error { color: var(--exited); }
/* The unread marker is colour and shape together, and named for a screen
   reader by the visually-hidden "Unread: " row-preview-text carries beside
   it -- never colour, or the dot alone. */
.unread-dot {
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: 4px;
  border-radius: 50%;
  background: var(--accent);
}
/* By the name, when there is no preview line for it to head. */
.row-head .unread-dot { margin-top: 0; }
.pane-row[data-unread] .row-title { font-weight: 800; }
/* The small print: agent · model · branch · uncommitted, ellipsised rather
   than wrapped -- a row this tight has no line to spare for it to wrap onto. */
.row-submeta {
  min-width: 0;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.6875rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A usage window near its limit (ui.js's spendPart) takes the same amber a
   waiting pane's own dot does -- never the only signal: the part's own
   visually-hidden "near its limit" text says so too. */
.spend-near { color: var(--waiting); }

/* A waiting row's own answer buttons (answer.js's buildAnswerRow): the row's
   own *sibling*, not its descendant -- a button nested inside the row's `<a>`
   is invalid HTML, handled inconsistently by browsers and assistive tech --
   so it is drawn as its own small card right under the row, in the same
   amber wash a waiting row itself carries, since it is only ever offered on
   one (rowAnswer requires status "waiting"). */
.row-answer {
  display: block;
  padding: 8px 12px 12px;
  background: var(--waiting-subtle);
  background: color-mix(in srgb, var(--waiting) 14%, var(--raised));
  border: 1px solid var(--waiting);
  border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
}
/* ...and joined to the row above it, so the two read as one card: the row's
   own bottom corners squared, its amber bottom edge the line between the
   question and its answers. Without :has the row keeps its rounded corners
   over the strip, which still reads as one waiting item. */
.list.panes li:has(> .row-answer) > .pane-row {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
/* The row's own inline answer buttons, at least a 44px touch target tall
   with room either side of the label -- .button.small, which they were drawn
   with before, is 36px, under that guideline. Left untouched everywhere else
   it is used, which is unrelated compact buttons app-wide. */
.row-answer .button {
  min-height: var(--tap);
  padding: 0 16px;
}

/* A lead's helpers (flockdeck spawn, or the fan-out), grouped under its row
   rather than listed as rows of their own -- ui.js's groupPanes/paneGroupItem,
   main.js's renderDesktop. The toggle is the group's own row, a sibling of the
   lead's <a> and never nested inside it, exactly like a waiting row's own
   answer strip (.row-answer) above. */
.group-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  min-height: var(--tap);
  margin-top: 6px;
  padding: 8px 12px;
  background: transparent;
  border: 1px dashed var(--edge-soft);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.75rem;
  text-align: left;
  cursor: pointer;
}
.group-toggle:hover { background: var(--hover); color: var(--text); }
.group-chev { font-size: 0.6875rem; }
/* Indented under the toggle, with a rule down the side so the eye reads them
   as belonging to the lead above rather than as rows of their own. */
.group-helpers {
  list-style: none;
  margin: 6px 0 0;
  padding-left: 12px;
  border-left: 2px solid var(--edge-soft);
}
.group-helpers > li + li { margin-top: 6px; }
.group-helpers[hidden] { display: none; }
/* A helper's own row, smaller than an ordinary pane's: the group it is under
   already says who runs it. */
.pane-row-helper .row { padding: 8px 12px; min-height: 0; }
.pane-row-helper .row-title { font-size: 0.875rem; }

/* Jumped to from the away summary line above the list (main.js's
   jumpToRow): the same brief highlight a chat search result gets, fading
   back to nothing on its own -- chat-entry-flash is defined once, further
   down, for both. */
.row-jump-highlight { animation: chat-entry-flash 1.6s ease-out; border-radius: var(--radius); }

/* The desktop's link state, folded into the header for the one phase -- Live
   -- with nothing more to say; every other phase keeps its fuller line below
   too (see .conn), with the countdown and the way to try again now. */
.header-live { display: inline-flex; vertical-align: middle; margin-left: 8px; }
.header-live .status { font-size: 0.6875rem; }

/* The offer of notifications, moved below the list and slimmed to one line:
   what matters first on this page is which agent needs you. Its own control
   (main.js's notifyControl) is unchanged; only how much room it takes here. */
.toolbar-compact { margin: 12px 0 0; }
.toolbar-compact .button { min-height: 36px; padding: 0 16px; font-size: 0.875rem; }

/* The project strip: one button per project that narrows the list to it (main.js's
   buildPaneList). One line that scrolls sideways, so many projects do not push
   the list down a 360px screen. overflow-x clips y as well, so the padding leaves
   room for the focus ring (2px outline, 2px offset); the negative margins keep
   the spacing as it was. */
.projects {
  list-style: none; display: flex; flex-wrap: nowrap; gap: 8px; padding: 4px; margin: -4px -4px 12px;
  overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin;
}
.projects[hidden] { display: none; }
.project-item { flex: none; }
.project {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  min-height: 40px;
  padding: 4px 12px;
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-xs);
  background: var(--raised);
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  touch-action: manipulation;
}
.project:hover { background: var(--hover); }
.project[aria-pressed="true"] { background: var(--accent-subtle); border-color: var(--accent); }
.project[aria-pressed="true"] .project-name { color: var(--accent-strong); }
.project-name { color: var(--text); font-weight: 500; }

/* "New agent": the button near the top of a desktop's page, and the sheet it
   opens over the page. See newagent.js. */
.new-agent { margin: 0 0 12px; }
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: flex-end; justify-content: center;
  padding: var(--gutter);
}
/* The dimming is a layer of its own so that its opacity can follow the sheet
   (sheetmotion.js) without fading the sheet. Flat, no blur. */
.sheet-scrim { position: absolute; inset: 0; background: var(--scrim); }
.sheet {
  position: relative;
  width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto;
  background: var(--overlay); border: 1px solid var(--edge);
  border-radius: var(--radius); padding: 0 20px 20px;
  box-shadow: var(--shadow-3);
  will-change: transform;
}
/* The handle: grabber and heading. Dragging starts here and nowhere else, so
   the form keeps its own touch scrolling. */
.sheet-grab { touch-action: none; cursor: grab; padding-top: 4px; }
.sheet-grab:active { cursor: grabbing; }
.sheet-grabber { display: block; width: 36px; height: 4px; margin: 4px auto 8px; border-radius: 2px; background: var(--edge-hard); }
.sheet h2 { margin: 0 0 16px; }
.new-agent-form textarea { min-height: 88px; resize: vertical; }

.conn { margin: 0 0 12px; min-height: 1.5em; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; }
/* Not live, the line stays in sight under the top bar: scrolled down a long
   list, someone never saw it change, only rows that had quietly stopped. */
.conn[data-phase="waiting"], .conn[data-phase="connecting"] {
  position: sticky;
  top: calc(56px + env(safe-area-inset-top));
  z-index: 5;
  padding: 6px 0;
  background: var(--surface);
}

/* ------------------------------------------------------------------- a pane */

/* The pane view fills what is in sight exactly: the terminal takes whatever
 * the heading and the dock leave, and the page itself does not scroll -- two
 * scrolling things stacked is one too many on a phone.
 *
 * What is in sight is the visual viewport, which main.js measures into --vvh
 * and --vvt: Safari on iOS draws its keyboard over the page rather than
 * making the page shorter, and scrolls what is in sight down the page to
 * show the box being typed into. Sized to the window instead, the view had
 * the prompt, the keys and the message box under the keyboard. */
/* min-height: 0 undoes the generic body rule's own min-height: 100dvh (above):
   left in place, it won a keyboard-shrunk --vvh outright on iOS Safari, where
   100dvh does not account for the keyboard the way --vvh (measured off
   visualViewport) does -- the body stayed a full, keyboard-covered screen
   tall regardless, and the dock pinned to its bottom sat behind the keyboard,
   exactly what --vvh was built to prevent. */
body[data-view="pane"] { height: 100vh; height: var(--vvh, 100dvh); min-height: 0; overflow: hidden; }
body[data-view="pane"] #app { transform: translateY(var(--vvt, 0px)); }
/* A page that does not scroll still reloads when dragged down from the top on
   Chrome for Android, and here a drag on the header or the keys would drop
   the terminal and where it was scrolled to. The lists keep it. */
body[data-view="pane"] { overscroll-behavior-y: none; }
/* The wide layout (main.js's renderWide, below) is the same fixed-height
   shell, one column wider: the list on the left keeps its own scroll
   (.wide-list), and the pane on the right keeps exactly this sizing, now one
   level deeper (#app > .wide-columns > .wide-pane > .view-pane) -- repeated
   rather than joined into the rules above by a selector list, so each stays
   matchable on its own. */
body[data-view="wide"] { height: 100vh; height: var(--vvh, 100dvh); min-height: 0; overflow: hidden; overscroll-behavior-y: none; }
body[data-view="wide"] #app { transform: translateY(var(--vvt, 0px)); }

/* ------------------------------------------------------------ wide layout
 * A tablet or a laptop's browser wastes the phone's single column, and
 * switching agents there means going back to the list every time. At
 * main.js's own WIDE_MIN (900px) and up, renderWide puts the list beside
 * whatever pane is open instead -- built from the very same buildPaneList
 * and pane renderers the phone layout uses, so nothing here duplicates a
 * single row, button or status line; it only lays the two out side by side.
 * Below that width nothing in this section ever applies: renderWide never
 * runs, so body never carries data-view="wide" at all. */
.wide-columns { flex: 1; display: flex; min-height: 0; }
.wide-list {
  flex: 0 0 340px;
  min-width: 260px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 16px 16px 24px;
  border-right: 1px solid var(--edge-soft);
}
.wide-pane { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
.wide-placeholder { margin: auto; padding: 24px; text-align: center; }
/* "N more waiting" (main.js's renderPaneChat/Terminal, .elsewhere) is a way
   back to the list to find the next one -- the list is right there already. */
.wide-pane .elsewhere { display: none; }
/* The away summary (.away-summary, above) wraps everywhere now; this says
   so again for the list column, under 300px with its own padding, where it
   was first found cut off. */
.wide-list .away-summary { white-space: normal; text-overflow: clip; }

.view-pane {
  max-width: none;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding-top: 8px;
  padding-bottom: 0;
  gap: 8px;
}
/* Only when it must: a phone held sideways, or its keyboard up, is too short
   for the header, a readable terminal and the dock together. The dock stays
   pinned at the bottom meanwhile (see the media query below, and
   body[data-keyboard] near the bottom of this file).
   Not made to scroll unconditionally: iOS Safari freezes a finger's drag on
   the terminal (.term, "-webkit-overflow-scrolling: touch") once it sits
   inside another element that scrolls too, even one with nothing to scroll
   yet -- so .view-pane only becomes that ancestor where it actually has to. */

/* The dock: the keys and the message box, one block at the bottom of the
   view, over whatever scrolls behind it. It carries the view's bottom padding
   and the home indicator's inset -- but not while a keyboard is drawn over
   the page, whose top edge is where the dock then sits. */
.dock {
  position: sticky;
  bottom: 0;
  z-index: 3;
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 2px 0 calc(10px + env(safe-area-inset-bottom));
  background: var(--surface);
}
body[data-keyboard] .dock { padding-bottom: 10px; }
body[data-keyboard] .view-pane { overflow-y: auto; overscroll-behavior: contain; }

/* The heading is a pane header, as the application and the site draw one. */
.pane-head {
  flex: none;
  padding: 8px 12px;
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
}
/* Chat's own header carries less than the terminal's: no third copy of the
   live activity line (chat.js's own status line, above the prompt box,
   already says it), so there is only ever a name and a line of agent/model/
   branch meta underneath -- tighter padding reads as a header, not a card. */
.pane-head-compact { padding: 6px 12px; }
.pane-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; }
.pane-title {
  margin: 0;
  font-family: var(--display); font-weight: 600;
  font-size: 1.125rem; letter-spacing: -.01em; line-height: 1.3;
}
.pane-meta { margin: 2px 0 0; color: var(--muted); font-family: var(--mono); font-size: 0.6875rem; overflow-wrap: anywhere; white-space: pre-wrap; }
/* Chat's own header is one line -- the name, its dot, then a muted
   "agent · model · branch" that gives way to an ellipsis, never to a second
   line, so a narrow phone still gets most of its height for the conversation
   rather than the header describing the pane. */
.pane-heading-oneline { flex-wrap: nowrap; }
.pane-heading-oneline .pane-title {
  flex: 0 1 auto; min-width: 0; max-width: 45%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pane-heading-oneline .pane-status { flex: none; }
.pane-heading-oneline .mode-toggle { flex: none; margin-left: auto; }
.pane-meta-inline {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--mono); font-size: 0.6875rem;
}
.pane-detail {
  margin: 2px 0 0;
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--accent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pane-detail.error { color: var(--exited); }
/* Waiting, the line is the request -- what the agent wants to be allowed to
   do -- and on a phone the terminal may be too small to read it in. Working,
   it stays one line: it changes all the time, and a header that changed
   height with it would keep resizing a fitted terminal. */
.pane-head[data-status="waiting"] .pane-detail { white-space: normal; overflow-wrap: anywhere; }
.pane-detail:empty, .pane-meta:empty { display: none; }

/* Another pane waiting on you, from inside this one: the amber, as a waiting
   pane is drawn everywhere, and a tap away. */
.elsewhere {
  flex: none;
  /* The gap is the space after the triangle: hidden from screen readers in a
     box of its own, the triangle's trailing space is dropped by the flex. */
  display: flex; align-items: center; gap: 6px; min-height: 36px;
  padding: 4px 12px;
  border: 1px solid var(--waiting-edge);
  border-radius: var(--radius-sm);
  background: var(--waiting-subtle);
  color: var(--waiting);
  font-family: var(--mono); font-size: 0.75rem;
  text-decoration: none;
}
.elsewhere:hover { color: var(--waiting); border-color: var(--waiting); }
/* The connection's status takes a line of its own under the controls when
   there is one: squeezed in beside them on a narrow phone, "Not sent:
   reconnecting…" broke "Fit to this screen" onto three lines. */
.term-bar { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
.spacer { flex: 1; }
/* The connection's line is in the header, on a line of its own while there
   is one; "Fit to this screen" and the text size are behind Aa beside it. */
.term-phase { order: 1; flex-basis: 100%; color: var(--muted); font-family: var(--mono); font-size: 0.75rem; }
.term-phase:empty { display: none; }
/* Whether this pane's terminal socket is end-to-end encrypted (terminal.js's
   onEncryption, e2e.js): shown beside the connection line, in the same quiet
   style, whichever it says -- this is about letting someone check, not about
   alarming them the moment a desktop has not registered a key of its own. */
.term-enc { order: 1; flex-basis: 100%; color: var(--muted); font-family: var(--mono); font-size: 0.75rem; }
.term-enc:empty { display: none; }
.view-toggle { margin-left: auto; min-width: 40px; padding: 0 8px; font-family: var(--display); }
.view-menu {
  flex: none;
  padding: 4px 12px 8px;
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-sm);
}
.switch span { white-space: nowrap; }
.fit-hint { flex: none; margin: 0; }
.term-note {
  flex: none;
  margin: 0;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  font-size: 0.8125rem;
  color: var(--muted);
}

.switch { display: inline-flex; align-items: center; gap: 8px; min-height: var(--tap); cursor: pointer; font-family: var(--display); font-weight: 500; font-size: 0.875rem; }
.switch input { width: 20px; height: 20px; accent-color: var(--accent); margin: 0; }

/* The terminal, and a choice offered over it. */
.term-stage { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 120px; }

/* Output arrived below where the reader is: a pill over the bottom of the
   terminal, under a choice sheet if there is one, in the accent as activity
   is everywhere here. */
.new-output {
  position: absolute;
  left: 50%; bottom: 12px;
  transform: translateX(-50%);
  z-index: 1;
  border-radius: var(--radius-sm);
  border-color: var(--accent-edge);
  background: var(--overlay);
  color: var(--accent);
  font-family: var(--mono);
  font-size: 0.75rem;
  box-shadow: var(--shadow-1);
}

/* A choice the agent is asking, over the bottom of the terminal, where the
   question is drawn. Over it and not beside it: taking room from the terminal,
   it made it jump each time a choice came and went, and resized a fitted pane
   on the desktop too. Taller than the terminal, it scrolls within itself. The
   amber edge is the pane waiting on you, as everywhere else. */
.choices {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--overlay);
  border: 1px solid var(--waiting-edge);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}
.choices-head { display: flex; align-items: flex-start; gap: 8px; }
.choices-q { flex: 1; min-width: 0; margin: 0; font: 600 16px/1.35 var(--display); overflow-wrap: anywhere; }
/* flex: none holds this to its own max-height rather than the column's: a
   flex item that scrolls its own overflow has an automatic minimum size of
   0, so with the question, the options and Esc together taller than the
   sheet, this -- the part saying what the command actually is -- was
   squeezed down to a sliver of its first line, the one place height was
   found from. The sheet already scrolls as a whole (.choices, above); this
   keeps its own share of that instead of giving it up first. */
.choices-context {
  flex: none;
  margin: 0; padding: 6px 8px;
  max-height: 6.5em; overflow: auto;
  font-size: 0.75rem; line-height: 1.35; color: var(--muted);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.choices-list { display: flex; flex-direction: column; gap: 8px; }
/* Each option a whole row, big enough for a thumb, its words wrapped rather
   than cut: the part of an option that says what it allows is often its end. */
.choice {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  width: 100%;
  min-height: 52px;
  padding: 12px 12px;
  border: 1px solid var(--edge-hard);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: 600 16px/1.35 var(--body);
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
  touch-action: manipulation;
  cursor: pointer;
}
.choice:active { background: var(--active); transform: translateY(1px); }
/* Where the pointer is in the terminal: what Enter alone would choose. */
.choice[data-current] { background: var(--accent-subtle); border-color: var(--accent-edge); box-shadow: inset 2px 0 0 var(--accent); }
.choice[aria-disabled="true"] { opacity: .45; cursor: default; }
.choice-num { flex: none; min-width: 1.6em; font: 500 14px/1.55 var(--mono); color: var(--accent); font-variant-numeric: tabular-nums; }
.choice-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.choice-detail { font-weight: 400; font-size: 0.875rem; color: var(--muted); }
.choice-esc { background: transparent; }
.choice-esc .choice-num { color: var(--muted); }
/* A box to tick: its state in the border as well as the glyph, and Submit
   filled, as the one thing the sheet is then for. */
.choice-tick { flex: none; width: 1.2em; text-align: center; color: var(--muted); }
.choice-toggle[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-subtle); }
.choice-toggle[aria-pressed="true"] .choice-tick { color: var(--accent); }
.choice-submit { justify-content: center; background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.choice-submit:active { background: var(--accent-strong); color: var(--on-accent); }
.choice-submit { font-weight: 600; }
/* Put away, it is one button in the corner that brings it back. */
.choices.choices-away { left: auto; right: 8px; bottom: 8px; padding: 0; border: 0; background: none; box-shadow: none; overflow: visible; }

/* The terminal sits in the sunken surface, framed like a pane on the deck. */
.term {
  position: relative;
  flex: 1;
  min-height: 120px;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--sunken);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
}
/* What the terminal is measured against: the frame's inside, less a margin,
   with no padding or border of its own for fitting to count. */
.term-body { position: absolute; inset: 4px 2px 2px 6px; }
.term.fitted { overflow: hidden; }
.term .xterm { width: max-content; }
/* Selecting with a finger (terminal.js's setSelecting): the browser's own
   selection and callout are left out of it, and the emulator's is the one. */
.term-body.selecting { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
/* Select is on (main.js's setSelect): a dot on the Aa button, and a chip over
   the terminal's top corner that turns it off. */
.view-toggle { position: relative; }
.view-toggle.on { border-color: var(--accent); }
.view-toggle.on::after {
  content: ""; position: absolute; top: 4px; right: 4px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
}
.select-chip {
  position: absolute; top: 8px; right: 8px; z-index: 1;
  border-radius: var(--radius-sm);
  border-color: var(--accent);
  background: var(--accent-subtle);
  color: var(--accent-strong);
  font-family: var(--mono); font-size: 12px;
  box-shadow: var(--shadow-1);
}
.select-chip[hidden] { display: none; }
.select-toggle[aria-pressed="true"] { background: var(--accent-subtle); border-color: var(--accent); color: var(--accent-strong); }

/* ---------------------------------------------------------------- the chat view */

/* The switch in a pane's header, beside the status badge: two small buttons
   rather than a single toggle, so which is current is said in words (pressed)
   as well as by which looks chosen -- a single toggle's own label says only
   what a tap would change to, not what is showing now. */
.mode-toggle { display: inline-flex; gap: 4px; }
.mode-toggle-btn[aria-pressed="true"] { background: var(--active); color: var(--text); font-weight: 600; }

/* The conversation fills the same space the terminal does, but scrolls as an
   ordinary page does -- there is no alternate screen here to hijack a wheel
   or a finger into arrow keys, which is the whole point. It carries no frame
   of its own (no border, no card shadow, the same background as the page) --
   a bordered box around it read as an oversized, mostly empty card for any
   conversation short of a full screen; a messaging app's own list of
   messages doesn't have one either. */
.chat-stage { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 120px; }
.chat-scroll {
  position: relative;
  flex: 1;
  min-height: 120px;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--surface);
  padding: 12px 12px 4px;
  display: flex;
  flex-direction: column;
  /* A short conversation sits at the bottom, next to the prompt box, as a
     messaging app's own history does -- not at the top with a bare gap
     underneath (see the ::before below for how). */
  gap: 14px;
}
/* The spacer that puts a short conversation at the bottom. This used to be
   justify-content: flex-end on the container, which does that but also makes
   a conversation taller than the view overflow *upward*, past the top edge of
   the scroll area, where no scrolling can reach: scrollHeight stayed equal to
   clientHeight and the earlier messages were unreadable on every browser --
   the phone only made it obvious, having no other way (a search, a
   find-in-page) to get at them. An auto-sized flex item takes up whatever
   room is spare and none when there is none, so a long conversation overflows
   downward, as an ordinary page does. Its negative bottom margin cancels the
   one gap it would otherwise add above the first entry. */
.chat-scroll::before { content: ""; flex: 1 0 0; margin-bottom: -14px; }
.chat-loading-more, .chat-empty { text-align: center; margin: 0; }
.chat-note { margin: 12px; }

/* A pill floating over the bottom of the scroll area -- pinned there by its
   own absolute position, not by the scroll position, so it stays put while
   the conversation underneath it scrolls -- offering to jump down to
   whatever arrived while the reader was looking at something earlier. */
.chat-new-messages {
  position: absolute;
  left: 50%; bottom: 10px;
  transform: translateX(-50%);
  z-index: 2;
  display: flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 6px 12px;
  border: 1px solid var(--accent-edge);
  border-radius: var(--radius-sm);
  background: var(--overlay);
  color: var(--accent);
  font: 600 12px/1 var(--body);
  box-shadow: var(--shadow-1);
}
.chat-new-messages:active { background: var(--active); transform: translate(-50%, 1px); }

.chat-timestamp { align-self: center; margin: 0; color: var(--muted); font-size: 0.75rem; }

.chat-entry { display: flex; flex-direction: column; }

/* The user's own words: a bubble, right-aligned, tinted with the accent --
   never wider than about 85% of the view, so even a one-word reply doesn't
   stretch edge to edge and a long one still reads as something *said*, not
   a document. */
.bubble-user {
  align-self: flex-end;
  max-width: 85%;
  padding: 8px 12px;
  border-radius: var(--radius);
  background: var(--accent-subtle);
  border: 1px solid var(--accent-edge);
  color: var(--text);
  font-size: 0.9375rem;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.bubble-text { margin: 0; white-space: pre-wrap; }

/* This device's own read on whether a just-sent message actually reached
   the agent -- calm and small, under the bubble it belongs to, never a
   banner: see chat.js's renderPromptEntry/renderPendingPrompt. */
.chat-prompt-wrap { align-self: flex-end; max-width: 85%; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.chat-prompt-wrap .bubble-user { align-self: stretch; max-width: none; }
.chat-prompt-status { margin: 0; padding: 0 2px; }
.chat-delivery-tick { padding: 0 2px; font-size: 0.75rem; }
.chat-pending-failed { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; width: 100%; }
.chat-pending-actions { display: flex; align-items: center; gap: 6px; }
.chat-pending-dismiss {
  min-height: var(--tap); min-width: var(--tap);
  border: none; background: none; color: var(--muted);
  font-size: 0.8125rem;
}

/* The agent's own reply: full width, unboxed prose -- the same shape
   Claude's own apps use -- so it reads as the point of the turn rather than
   one more card among the tool rows above it. */
.reply { align-self: stretch; width: 100%; font-size: 0.9375rem; line-height: 1.65; }
.reply .md { display: flex; flex-direction: column; gap: 12px; }
.reply .md > :first-child { margin-top: 0; }
.reply .md > :last-child { margin-bottom: 0; }
.reply .md p { margin: 0; }
.reply .md pre { max-width: 100%; }

/* A long reply, past about REPLY_COLLAPSE_RATIO screens (chat.js), folds to
   about one screen (--reply-collapsed-h, set from the real viewport where
   there is one) under a fade, with "Show more" below to open it back up in
   place -- see applyReplyCollapsing. */
.reply-clip.reply-collapsed { max-height: var(--reply-collapsed-h, 70vh); overflow: hidden; position: relative; }
.reply-clip.reply-collapsed::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 64px;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none;
}
.reply-more {
  display: block; width: 100%; margin: 8px 0 0;
  min-height: var(--tap); padding: 6px 8px;
  border: 1px solid var(--edge-soft); border-radius: var(--radius-sm);
  background: var(--raised); color: var(--accent);
  font: 600 13px/1.3 var(--body);
  text-align: center;
}
.reply-more:active { background: var(--active); }

/* A quiet control at the end of a reply, for copying its whole markdown
   source without a fiddly long-press selection on a phone -- one per reply,
   never per paragraph, and never on a prompt, a step row or a notice. */
.chat-reply-copy-row { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
.chat-reply-copy {
  min-height: var(--tap); padding: 4px 12px;
  border: 1px solid var(--edge-soft); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--muted);
  font: 600 12px/1 var(--body);
}
.chat-reply-copy:active { background: var(--active); color: var(--text); }

/* One turn's own work -- however many thinking blocks, tool calls and
   subagents it took -- collapsed to a single line until tapped open. */
.chat-steps { align-self: stretch; display: flex; flex-direction: column; gap: 6px; }
.chat-steps-summary {
  display: flex; align-items: center; gap: 8px;
  min-height: var(--tap); padding: 6px 8px;
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-sm);
  background: var(--raised);
  color: var(--muted);
  font: 600 13px/1.3 var(--body);
  text-align: left;
  cursor: pointer;
}
.chat-steps-summary:hover { background: var(--hover); color: var(--text); }
.chat-steps-icon { flex: none; font-family: var(--mono); }
.chat-steps-open .chat-steps-icon { color: var(--working); animation: pulse var(--pulse) var(--ease-in-out) infinite; }
/* Wraps to a second line rather than truncating: a summary cut with an
   ellipsis ("Ran 7 c…") hid whatever came after it, with no way to read the
   rest short of tapping the block open. */
.chat-steps-text { flex: 1; min-width: 0; }
.chat-steps-chevron { flex: none; color: var(--muted); }
/* One line with an ellipsis, unlike the summary above it: this is a live
   preview of whatever the agent is doing right now, not the only place its
   full text is reachable -- the same step, once the block is open, is its
   own row below, tappable for the rest (see .chat-row-label, .chat-detail). */
.chat-steps-live { margin: -2px 0 0 8px; font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-steps-body { display: flex; flex-direction: column; gap: 4px; padding-left: 4px; border-left: 2px solid var(--edge-soft); margin-left: 6px; }

/* A tool call, thinking, or a subagent: one row, collapsed, in the mono the
   rest of the pane's own detail line uses, with the diff or expanded detail
   underneath when there is one. */
.chat-collapsible { display: flex; flex-direction: column; gap: 4px; align-self: flex-start; max-width: 100%; }
.chat-row {
  display: flex; align-items: center; gap: 8px;
  min-height: 32px; padding: 4px 8px;
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-sm);
  background: var(--raised);
  color: var(--text);
  font-family: var(--mono); font-size: 0.75rem;
  text-align: left;
}
button.chat-row { cursor: pointer; }
button.chat-row:hover { background: var(--hover); }
.chat-tool-glyph { flex: none; color: var(--muted); }
.chat-tool-running .chat-tool-glyph { color: var(--working); animation: pulse var(--pulse) var(--ease-in-out) infinite; }
.chat-tool-error .chat-tool-glyph, .chat-tool-error .chat-row-label { color: var(--exited); }
/* min-width: 0 is what lets a flex child shrink below its content's own
   width at all -- without it, a label with nowhere to break (a long path,
   say) pushed the row wider than the screen instead of ellipsizing. */
.chat-row-label { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.chat-row-summary { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.chat-detail { padding: 8px 8px; border: 1px solid var(--edge-soft); border-radius: var(--radius-sm); background: var(--sunken); }
.chat-plain { margin: 0; font-family: var(--mono); font-size: 0.75rem; white-space: pre-wrap; overflow-wrap: anywhere; }
/* A row's own full, unshortened label, above whatever detail follows it --
   the tap target that keeps friendlyLabel's shortening from losing text. */
.chat-detail-label { margin: 0 0 6px; font-family: var(--mono); font-size: 0.75rem; color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; }
.chat-subagent-row { color: var(--accent); }
.chat-subagent-report { font-size: 0.8125rem; padding: 0 2px; }

.chat-divider {
  align-self: center;
  display: flex; align-items: center; gap: 8px;
  color: var(--muted); font-family: var(--mono); font-size: 0.6875rem;
}
.chat-divider::before, .chat-divider::after { content: ""; width: 24px; height: 1px; background: var(--edge); }
.chat-notice { align-self: center; margin: 0; font-style: italic; }

/* The status line, choices and prompt box are pinned above the bottom of the
   view exactly as the terminal dock's keys and message box are (the "dock"
   class, app.css above); chat-dock only adds what is chat's own. */
.chat-status { margin: 0; display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 0.75rem; color: var(--muted); }
.chat-status:empty { display: none; }
.chat-status-word { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-status[data-status="waiting"] { color: var(--waiting); }
.chat-status[data-status="working"] { color: var(--working); }
.chat-banner {
  margin: 0; padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--waiting-subtle);
  border: 1px solid var(--waiting-edge);
  color: var(--waiting);
  font-size: 0.8125rem;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
/* The link to the desktop, lost: calm, not alarming -- the same muted tone
   the rest of the app uses for "offline" (ui.js's connectionBadge,
   .status-offline), not the waiting amber, which already means something
   else here (the pane itself wants you). Cleared the moment the link is
   open again. */
.chat-conn-banner {
  margin: 0; padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  color: var(--muted);
  font-size: 0.8125rem;
}

/* The search bar and its results, over the conversation but inside
   chat-scroll (position: relative) alone -- never over the dock below it, so
   the prompt box, chips and status line all stay usable while this is open,
   per the design. */
.chat-search-toggle, .mute-toggle { flex: none; width: var(--tap); height: var(--tap); padding: 0; font-size: 1rem; }
.chat-search-panel {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--surface);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.chat-search-bar { flex: none; display: flex; align-items: center; gap: 8px; }
.chat-search-input {
  flex: 1; min-width: 0;
  min-height: var(--tap);
  padding: 8px 12px;
  border: 1px solid var(--edge-hard);
  border-radius: var(--radius-sm);
  background: var(--sunken);
  color: var(--text);
  font-family: var(--body);
  font-size: 1rem; /* anything smaller and iOS zooms the page on focus */
}
.chat-search-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.chat-search-input::placeholder { color: var(--muted); }
.chat-search-close { flex: none; width: var(--tap); height: var(--tap); padding: 0; }
.chat-search-results { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 6px; }
.chat-search-empty { text-align: center; margin: 12px 0 0; }
.chat-search-more { text-align: center; margin: 4px 0 0; }
.chat-search-result {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 8px;
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-sm);
  background: var(--raised);
  color: var(--text);
  text-align: left;
  font: 400 13px/1.4 var(--body);
}
.chat-search-result:hover { background: var(--hover); }
.chat-search-kind { font-family: var(--mono); font-size: 0.6875rem; }
.chat-search-snippet { overflow-wrap: anywhere; white-space: pre-wrap; }
.chat-search-snippet mark { background: var(--accent-subtle); color: var(--accent-strong); border-radius: var(--radius-xs); padding: 0 1px; }

/* Jumped to from a search result: a brief highlight so the entry that was
   tapped for is findable at a glance in a whole screen of chat rows, fading
   back to nothing on its own (removed after 1.6s in chat.js). */
.chat-entry-highlight { animation: chat-entry-flash 1.6s ease-out; border-radius: var(--radius-sm); }
@keyframes chat-entry-flash {
  0% { background: var(--accent-subtle); }
  100% { background: transparent; }
}

.chat-choices {
  display: flex; flex-direction: column; gap: 8px;
  padding: 8px; border: 1px solid var(--waiting-edge); border-radius: var(--radius-sm);
  background: var(--waiting-subtle);
  /* Grows to fit its question, every option and its own actions -- shrinking
     the conversation above it (chat-scroll's flex:1, down to its own
     min-height) -- up to this cap, past which chat-choices-list below is
     what scrolls: the header above it and the actions below (Submit,
     Yes/No, Cancel) never do, so answering never depends on having
     scrolled to find them first. */
  max-height: 58vh; max-height: 58dvh;
  overflow: hidden;
}
/* The card's own header: the one place a waiting pane says so while it is
   up, since the status line above the prompt box is hidden for as long
   (see chat.js's offerChoice) rather than repeating the same word beside it. */
.chat-choices-wait {
  display: flex; align-items: center; gap: 6px;
  margin: 0; font: 600 12px/1.3 var(--mono);
  color: var(--waiting);
}
.chat-choices-wait-glyph { flex: none; }
.chat-choices-q { margin: 0; font: 600 16px/1.35 var(--display); overflow-wrap: anywhere; }
/* The question, every option, and a permission's own command or diff, all
   scroll together here between the card's fixed header and its fixed
   actions. The layered background is the usual scroll-shadow trick: a fade
   at whichever edge still has more to show, gone once that edge is
   scrolled to -- a clear, passive cue, over and above the option a scroll
   would already leave visibly cut by the card's own edge. */
.chat-choices-list {
  display: flex; flex-direction: column; gap: 8px;
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background-color: var(--waiting-subtle);
  background-repeat: no-repeat;
  background-image:
    linear-gradient(to bottom, var(--waiting-subtle), transparent),
    linear-gradient(to top, var(--waiting-subtle), transparent);
  background-position: top, bottom;
  background-size: 100% 18px, 100% 18px;
  background-attachment: local, local;
}
/* A flex container that scrolls lets its own min-height default to 0
   instead of its content's, which otherwise lets flexbox shrink these below
   their natural size to fit rather than actually overflowing -- squashed
   options with their own text spilling out, not a working scrollbar. Kept
   at their real size, the list overflows (and scrolls) as intended instead. */
.chat-choices-list > * { flex-shrink: 0; }
.chat-choice {
  display: flex; align-items: center; gap: 8px;
  min-height: 52px; padding: 8px 12px;
  border: 1px solid var(--edge-hard);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  text-align: left;
}
.chat-choice:active { background: var(--active); transform: translateY(1px); }
/* The first option is where the pointer starts, what Enter alone would choose:
   the chosen row, as it is everywhere in Flockdeck. */
.chat-choices-list > .chat-choice:first-child { background: var(--accent-subtle); border-color: var(--accent-edge); box-shadow: inset 2px 0 0 var(--accent); }
.chat-choice-submit { justify-content: center; background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.chat-choice-esc { color: var(--muted); }
/* Submit, Yes/No, Cancel: outside chat-choices-list's own scrolling, so they
   are always in view -- a divider is all that marks where the scrollable
   question and options end and these begin. */
.chat-choices-actions {
  display: flex; flex-direction: column; gap: 8px; flex: none;
  padding-top: 8px; border-top: 1px solid var(--waiting-edge);
}
/* A structured ask/permission card (chat.js's drawAskCard/drawPermissionCard)
   once answered: greyed to say a tap already went to the pane, since it has
   no screen of its own repainting to say so the way a live-read choice does. */
.chat-choices-done { opacity: 0.55; }
.chat-choices-header { margin: -4px 0 0; }
.chat-choices-answered { margin: 0; }
.chat-choices-hint { margin: -4px 0 0; }
.chat-choices-more { margin: 0; }
/* A multi-question ask card's own option already picked (drawMultiAskCard),
   and its typed-answer row's input, sized to sit in a chat-choice like any
   other option's own text. */
.chat-choice-picked { background: var(--accent-subtle); border-color: var(--accent); }
/* The tick box a multiSelect question's own option draws ahead of its
   number, so more-than-one-allowed reads at a glance and a picked option
   still looks picked once a finger has lifted, not only highlighted while
   held (chat-choice-picked, above, still marks the whole row too). */
.chat-choice-check {
  flex: none; width: 18px; height: 18px; box-sizing: border-box;
  border: 1px solid var(--edge-hard); border-radius: var(--radius-xs); background: var(--surface);
}
.chat-choice-check-on {
  background: var(--accent); border-color: var(--accent);
  position: relative;
}
.chat-choice-check-on::after {
  content: ""; position: absolute; left: 3px; top: -1px;
  width: 5px; height: 10px;
  border: solid var(--on-accent); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.chat-choices-other-input {
  flex: 1; min-width: 0; min-height: var(--tap);
  border: none; background: transparent; color: var(--text); font: inherit;
}
.chat-choice[disabled] { opacity: 0.5; }

/* Stop sits in the prompt row itself now, not on a row of its own above it:
   display: contents so the row is only ever a wrapper for the hidden
   toggle -- the button it holds becomes a flex item of .chat-compose
   directly, beside (or, with the box empty, in place of) Send. */
.chat-stop-row { display: contents; }
.chat-stop-button { flex: none; }
.chat-stop-button[aria-busy="true"] { opacity: 0.6; }
/* The quick-reply chips (while idle, chat-actions) sit above the prompt box
   as before; Stop, now in the row itself, never competes with them since a
   pane is never both working and idle. */
.chat-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chat-chip {
  min-height: var(--tap); padding: 4px 12px;
  border: 1px solid var(--edge-hard); border-radius: var(--radius-sm);
  background: transparent; color: var(--text);
  font-size: 0.875rem;
}
.chat-chip:active { background: var(--active); transform: translateY(1px); }

/* ----------------------------------------------- markdown and diff rendering */

/* Headings, sized to sit inside a chat bubble rather than as page titles --
   the largest barely bigger than bold body text, so a reply with a "## " in
   it still reads as one message rather than a subheaded document. */
.md-h { margin: 16px 0 4px; font-family: var(--display); font-weight: 600; line-height: 1.3; }
.md-h1 { font-size: 1.125rem; }
.md-h2 { font-size: 1rem; }
.md-h3 { font-size: 0.9375rem; }
.md-h4, .md-h5, .md-h6 { font-size: 0.875rem; }

.md-p { margin: 6px 0; }

.md-hr { margin: 12px 0; border: none; border-top: 1px solid var(--edge); }

.md-list { margin: 6px 0; padding-left: 20px; display: flex; flex-direction: column; gap: 2px; }
.md-list-loose { gap: 8px; }
.md-list-loose > .md-li > .md-p { margin: 2px 0; }
.md-list-tight > .md-li > .md-p { margin: 0; }
.md-list .md-list { margin: 4px 0; }
.md-li { display: list-item; }
.md-task { list-style: none; margin-left: -20px; display: flex; align-items: baseline; gap: 6px; }
.md-task-box { flex: none; color: var(--accent); }

.md-quote {
  margin: 8px 0;
  padding: 2px 0 2px 12px;
  border-left: 3px solid var(--edge);
  color: var(--muted);
}
.md-quote .md-quote { border-left-color: var(--edge-soft); }
.md-quote > :first-child { margin-top: 0; }
.md-quote > :last-child { margin-bottom: 0; }

.md-table-scroll { margin: 8px 0; overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid var(--edge-soft); border-radius: var(--radius-sm); }
.md-table { border-collapse: collapse; font-size: 0.8125rem; width: 100%; }
.md-table th, .md-table td { padding: 6px 8px; border-bottom: 1px solid var(--edge-soft); text-align: left; white-space: nowrap; }
.md-table thead th { background: var(--raised); font-family: var(--display); font-weight: 600; }
.md-table tbody tr:last-child td { border-bottom: none; }
.md-align-center { text-align: center; }
.md-align-right { text-align: right; }

/* A fenced code block: a bar for the language and its actions, over a
   scrollable, non-wrapping <pre> -- .wrap flips that when the reader taps
   the toggle, for a snippet better read line by line on a narrow phone. */
.md-code-block { margin: 8px 0; border: 1px solid var(--edge-soft); border-radius: var(--radius); overflow: hidden; background: var(--sunken); }
.md-code-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 4px 8px;
  background: var(--raised);
  border-bottom: 1px solid var(--edge-soft);
  font-family: var(--mono); font-size: 0.75rem; color: var(--muted);
}
.md-code-lang { text-transform: lowercase; }
.md-code-actions { display: flex; gap: 4px; }
.md-code-wrap, .md-code-copy {
  min-height: 28px; padding: 2px 8px;
  border: 1px solid var(--edge-soft); border-radius: var(--radius-sm);
  background: transparent; color: var(--text);
  font-family: var(--mono); font-size: 0.75rem;
}
.md-code-wrap[aria-pressed="true"] { background: var(--active); font-weight: 600; }
.md-code-copy:active, .md-code-wrap:active { background: var(--active); }
.md-code {
  margin: 0; padding: 8px 12px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  font-family: var(--mono); font-size: 0.8125rem; line-height: 1.5;
  white-space: pre;
}
.md-code.wrap { overflow-x: hidden; white-space: pre-wrap; overflow-wrap: anywhere; }
/* The site's own narrow-phone rule below (`pre code { white-space: pre-wrap;
   overflow-wrap: anywhere }`) is meant for an install snippet's bare
   <pre><code>, but a fenced code block's <code> matches it too -- inherit
   pulls the property back from .md-code/.md-code.wrap above instead, which
   is what actually keeps the Wrap button the only thing that turns wrapping
   on, rather than the phone's width doing it regardless of the toggle. */
.md-code code { white-space: inherit; overflow-wrap: inherit; }

/* A code block (or, below, a diff) folded to its first dozen-odd lines
   (COLLAPSE_LINES/COLLAPSE_SHOW_LINES, markdown.js): a fade over the last
   line rather than a hard edge, and "Show all N lines" underneath to open
   it back up in place -- "Show less" folds it again. */
.md-clamped { position: relative; }
.md-clamped::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 32px;
  background: linear-gradient(to bottom, transparent, var(--sunken));
  pointer-events: none;
}
.md-code-more {
  display: block; margin: 8px 8px 8px;
  min-height: 28px; padding: 2px 8px;
  border: 1px solid var(--edge-soft); border-radius: var(--radius-sm);
  background: var(--raised); color: var(--accent);
  font-family: var(--mono); font-size: 0.75rem;
}
.md-code-more:active { background: var(--active); }

/* The tokenizer's own palette: kept small and desaturated, so a highlighted
   snippet still reads as part of the same dark chat rather than a jarring
   editor theme dropped into it. */
.tok-kw { color: var(--accent-strong); }
.tok-str { color: var(--ok); }
.tok-com { color: var(--muted); font-style: italic; }
.tok-num { color: var(--waiting); }
.tok-tag { color: var(--accent-strong); }
.tok-diff-add { color: var(--ok); }
.tok-diff-del { color: var(--exited); }
.tok-diff-hunk { color: var(--accent); }

/* A diff: a +/- gutter and, wherever a hunk header gave the parser something
   to count from, the old and new line numbers either side of it. Scrolls
   sideways on its own -- the page never does. */
.chat-diff-wrap { margin: 4px 0; display: flex; flex-direction: column; gap: 6px; }
.chat-diff-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid var(--edge-soft); border-radius: var(--radius); background: var(--sunken); }
.chat-diff { font-family: var(--mono); font-size: 0.75rem; width: max-content; min-width: 100%; }
.diff-row { display: flex; white-space: pre; }
.diff-num { flex: none; width: 3.5em; padding: 0 6px; text-align: right; color: var(--muted); user-select: none; }
.diff-num-old { border-right: 1px solid var(--edge-soft); }
.diff-mark { flex: none; width: 1.5em; text-align: center; user-select: none; }
.diff-text { padding-right: 12px; }
.diff-add { border-left: 2px solid var(--ok); background: var(--raised); }
.diff-add .diff-mark, .diff-add .diff-text { color: var(--ok); }
.diff-del { border-left: 2px solid var(--exited); background: var(--raised); }
.diff-del .diff-mark, .diff-del .diff-text { color: var(--exited); }
.diff-ctx, .diff-meta, .diff-hunk { border-left: 2px solid transparent; }
.diff-hunk { color: var(--accent); }
.diff-hunk .diff-text { padding-left: 6px; }
.diff-meta { color: var(--muted); }
.chat-diff-more {
  align-self: flex-start;
  min-height: 28px; padding: 2px 8px;
  border: 1px solid var(--edge-soft); border-radius: var(--radius-sm);
  background: var(--raised); color: var(--accent);
  font-size: 0.8125rem;
}

/* A picture in the conversation: a thumbnail row, fetched as soon as it is
   drawn, tapped open into a full-screen view. touch-action: pinch-zoom lets
   the browser's own two-finger gesture work inside the viewer without any
   script of this page's own. */
.chat-image-row { gap: 8px; }
.chat-image-thumb {
  flex: none; width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); background: var(--sunken); overflow: hidden;
  font-size: 1.25rem;
}
.chat-image-thumb-img { width: 100%; height: 100%; object-fit: cover; }
/* Sized off the same visual-viewport variables the pane view itself is
   (--vvh/--vvt, above) rather than inset: 0 -- on a phone whose browser
   chrome comes and goes, inset: 0 only ever covers the *layout* viewport,
   which can leave a band of the real, visible screen showing the page
   through above or below the backdrop. This is exactly the fix already used
   for body[data-view="pane"] itself. */
.chat-image-viewer {
  position: fixed; top: var(--vvt, 0px); left: 0;
  width: 100%; height: 100vh; height: var(--vvh, 100dvh);
  z-index: 50;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim-image);
  overflow: auto;
  touch-action: pinch-zoom;
}
.chat-image-viewer-img { width: 100%; height: 100%; object-fit: contain; touch-action: pinch-zoom; }
/* Stands in for the picture until it (or its row's own already-loaded
   thumbnail, used as a preload instead of this) arrives -- see chat.js's
   openImage -- in place of the plain black frame a tap used to open on. */
.chat-image-viewer-spinner {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  font-size: 2rem; color: var(--muted);
  animation: pulse var(--pulse) var(--ease-in-out) infinite;
}
.chat-image-viewer-close {
  position: absolute;
  top: calc(12px + env(safe-area-inset-top));
  right: calc(12px + env(safe-area-inset-right));
  width: var(--tap); height: var(--tap);
  border-radius: 50%; border: 1px solid var(--edge-soft);
  background: var(--raised); color: var(--text);
}

/* The attach and camera buttons, and the strip of queued pictures that shows
   above the prompt box, until each is sent or removed: one square thumbnail
   per picture, small enough that even MAX_ATTACHMENTS (chat.js) worth of
   them never costs much of the screen, in a single row that scrolls
   sideways rather than wrapping. Extra top/right padding on the strip, and
   overflow-y left visible, keep a tile's own remove button (below) from
   being clipped where it pokes out past the tile's own top-right corner. */
.chat-attach-button, .chat-camera-button { flex: none; width: var(--tap); height: var(--tap); padding: 0; font-size: 1.125rem; }
.chat-attach-chips {
  display: flex; flex-wrap: nowrap; gap: 16px;
  overflow-x: auto; overflow-y: visible;
  -webkit-overflow-scrolling: touch;
  padding: 10px 10px 2px 2px;
}
.chat-attach-chip {
  position: relative; flex: none;
  display: flex; flex-direction: column; align-items: center;
  width: 60px;
}
/* .chat-attach-tile is the 60px square itself -- the picture (or its
   pending/uploading/failed overlay), clipped to the same rounded corners the
   rest of the site uses for a thumbnail. */
.chat-attach-tile {
  position: relative; width: 60px; height: 60px;
  border-radius: var(--radius-sm); overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--edge-soft);
}
.chat-attach-chip-error .chat-attach-tile { border-color: var(--exited-edge); }
.chat-attach-thumb {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
}
.chat-attach-thumb-pending {
  display: flex; align-items: center; justify-content: center;
  color: var(--muted);
  animation: pulse var(--pulse) var(--ease-in-out) infinite;
}
/* The spinner overlays the whole tile (rather than sitting beside it, as it
   did in the old full-width row) so a picture still uploading reads clearly
   even at 60px, with the picture itself dimmed underneath through the
   translucent backdrop. */
.chat-attach-spinner {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgb(5 8 10 / .4); color: var(--text);
  font-size: 1.125rem;
  animation: pulse var(--pulse) var(--ease-in-out) infinite;
}
/* Retry replaces the tile's own contents entirely on a failed upload --
   still exactly one tap on the thumbnail, just no room left beside it for a
   separate button at this size. */
.chat-attach-retry {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: inherit;
  background: var(--exited-subtle); color: var(--exited);
  font-size: 0.6875rem; font-weight: 600;
}
/* The remove button's own hit area is the full --tap (44px) the site uses
   everywhere else, even though what is drawn -- .chat-attach-remove-dot -- is
   a small circle in the tile's top-right corner, offset to hang half off
   the tile itself into the gap the strip's own 14px gap leaves for exactly
   this. */
.chat-attach-remove {
  position: absolute; top: -10px; right: -10px;
  width: var(--tap); height: var(--tap); padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent;
}
.chat-attach-remove-dot {
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  border: 1px solid var(--edge-soft); background: var(--raised); color: var(--muted);
  font-size: 0.6875rem;
}
.chat-attach-note { margin: 0; }

/* The keys are drawn as keycaps, the way the site and the help draw a key.
   The first row shares the width out evenly, so its ten keys fit one row
   of any phone, as tall as a fingertip needs and no wider than the phone's
   own keys. The second row, opened with ⋯, may wrap: it is opened on
   purpose. Nothing scrolls sideways, which hid keys with nothing to say they
   were there. */
.softkeys {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.softkeys-row { display: flex; gap: 6px; }
.softkeys-more { flex-wrap: wrap; }
.softkey {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--tap);
  padding: 0 4px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--edge-hard);
  border-bottom-width: 2px;
  background: var(--raised);
  color: var(--text);
  font: 500 13px var(--mono);
  white-space: nowrap;
  touch-action: manipulation;
  cursor: pointer;
}
.softkeys-more .softkey { flex: 1 0 auto; min-width: var(--tap); padding: 0 8px; }
.softkey:active { background: var(--active); transform: translateY(1px); }
/* Control held, and the second row open, are shown on their keys. */
.softkey[aria-pressed="true"], .softkey[aria-expanded="true"] {
  background: var(--active); border-color: var(--accent); color: var(--text); font-weight: 600;
}
/* Under 360 pixels -- the smallest phones, and larger ones set to a large
   display size -- the keys are a little closer and their type a little
   smaller, and still as tall as a fingertip needs. */
@media (max-width: 359px) {
  .softkeys, .softkeys-row { gap: 4px; }
  .softkey { padding: 0 2px; font-size: 0.75rem; }
}
.softkey .turn-left, .softkey .turn-right { display: inline-block; }
.softkey .turn-left { transform: rotate(-90deg); }
.softkey .turn-right { transform: rotate(90deg); }

/* The messages last sent from this device, over the box they go back into. */
.recent {
  display: flex; flex-direction: column; gap: 8px;
  max-height: 40vh; max-height: 40dvh; overflow-y: auto; overscroll-behavior: contain;
  padding: 8px 12px;
  background: var(--raised);
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius);
}
.recent-head { display: flex; align-items: center; gap: 8px; }
.recent-title { flex: 1; margin: 0; }
.recent-list { display: flex; flex-direction: column; gap: 6px; }
.recent-empty { margin: 0; }
/* A message cut to two lines: enough to know it, not the whole of a pasted
   list. */
.recent-item {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  width: 100%;
  min-height: var(--tap);
  padding: 8px 12px;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: 0.9375rem/1.4 var(--body);
  text-align: left;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  touch-action: manipulation;
  cursor: pointer;
}
.recent-item:active { background: var(--active); transform: translateY(1px); }
.recent-toggle { min-width: var(--tap); min-height: var(--tap); padding: 0; font-size: 1.25rem; }

.send-note { flex: none; margin: 0; font-size: 0.8125rem; }
/* The box grows a line at a time with what is in it, and Send stays by its
   last line. Dragged taller by hand it would push the terminal away. */
.compose { flex: none; display: flex; align-items: flex-end; gap: 8px; }
.compose textarea { flex: 1; min-width: 0; resize: none; line-height: 1.4; }

@media (min-width: 720px) {
  .view { padding-top: 32px; }
  .view-pane { padding-top: 12px; }
}

/* A phone held sideways is too short for the header, the controls and the
   dock around a terminal worth reading: sized to what was left, the terminal
   shrank to its least. There the terminal is most of a screen tall and the
   view scrolls to it, with the dock pinned at the bottom throughout. */
@media (max-height: 520px) {
  .view-pane { overflow-y: auto; overscroll-behavior: contain; }
  .view-pane .term-stage { flex: none; height: 60vh; height: 60dvh; }
  .view-pane .chat-stage { flex: none; height: 60vh; height: 60dvh; }
}

/* On a narrow screen a command is wrapped rather than scrolled, as the site
   does: what is copied is the text, not the lines it was drawn on, and a
   sideways scroll inside a card is easy to miss. Inline code may break too, so
   `flockdeck remote pair` cannot push a sentence off the edge. */
@media (max-width: 660px) {
  pre code { white-space: pre-wrap; overflow-wrap: anywhere; }
  p code, li code { overflow-wrap: anywhere; }
}

/* ------------------------------------------------------------ icons, targets
 * The line icons (ui.js's icon): one stroke weight, drawn in the text colour
 * through a mask, so a control's glyph follows its state and never brings a
 * colour of its own. Colour emoji did, and an amber lock read as a status. */
.icon {
  display: inline-block; flex: none; width: 1.25em; height: 1.25em;
  background-color: currentColor;
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
  vertical-align: -.2em;
}
.icon-search { --icon: url("/app/icons/ui-search.svg"); }
.icon-attach { --icon: url("/app/icons/ui-attach.svg"); }
.icon-camera { --icon: url("/app/icons/ui-camera.svg"); }
.icon-bell { --icon: url("/app/icons/ui-bell.svg"); }
.icon-bell-off { --icon: url("/app/icons/ui-bell-off.svg"); }
.icon-lock { --icon: url("/app/icons/ui-lock.svg"); }
.icon-lock-open { --icon: url("/app/icons/ui-lock-open.svg"); }
.icon-image { --icon: url("/app/icons/ui-image.svg"); }
.button .icon, .chat-image-thumb .icon { vertical-align: 0; }
.chat-image-thumb { color: var(--muted); }
/* Windows High Contrast overrides a background, which would leave the masked
   icon blank. The icon keeps its own colour, which is the one the system has
   already chosen for the text around it (ButtonText, GrayText when a button is
   disabled, CanvasText in a line of text). */
@media (forced-colors: active) {
  .icon { forced-color-adjust: none; background-color: currentColor; }
}
.term-enc .icon { width: 1.1em; height: 1.1em; }
.term-enc-failed { color: var(--exited); }
.chat-attach-button, .chat-camera-button, .chat-search-toggle, .mute-toggle { font-size: 1.25rem; }

/* The sheet's heading takes focus when it opens, for a screen reader; a ring
   on it is noise. Fields and buttons keep theirs. */
:where(h1, h2, h3)[tabindex="-1"]:focus { outline: none; }
.sheet-backdrop { padding-bottom: max(var(--gutter), env(safe-area-inset-bottom)); }
.sheet { max-height: 90dvh; }

/* Targets: a fingertip, 44px, wherever the row was tighter than that. Inline
   links in a sentence keep the sentence's height. */
/* Inline, so the sentence keeps its lines; the padding makes the hit area a fingertip tall without moving them. */
.away-summary-link { display: inline; padding: 12px 2px; margin: -12px 0; }
.list.panes .row.group-toggle { min-height: var(--tap); }
.toolbar-compact .button, .elsewhere { min-height: var(--tap); }
.chat-new-messages { min-height: var(--tap); }
.back { min-width: var(--tap); }
.project { min-height: var(--tap); }
.foot { font-size: 0.8125rem; }
.foot a { display: inline-flex; align-items: center; min-height: var(--tap); }
.foot-links { gap: 0 16px; }

/* A device or desktop row on a phone: the name and its facts get the row's
   full width and the buttons go under them. Side by side they left the
   facts 150px, and an offline desktop's "last seen" ran under its Rename. */
.row.static .status { white-space: normal; flex-wrap: wrap; }
@media (max-width: 520px) {
  .row.static > .row-actions { flex-basis: 100%; }
}

/* ------------------------------------------------------------ press, prefs
 * Pressing answers on pointer-down, with no transition in (the release eases
 * back). The press is Quiet deck's own: a 1px drop and the active fill, never
 * a scale. Rows, chips and keys that had no :active of their own get one, and
 * touch-action: manipulation takes the tap delay off them. */
a.row, .chip, .project, .group-toggle, .navlink, .softkey, .button, .choice, .chat-chip, .chat-choice {
  touch-action: manipulation;
}
a.row, .chip, .project, .group-toggle, .navlink {
  transition: transform var(--dur-2) var(--ease-out), background-color var(--dur-1) linear, border-color var(--dur-1) linear, color var(--dur-1) linear;
}
a.row:active, .chip:active, .project:active, .group-toggle:active, .navlink:active { transform: translateY(1px); transition-duration: 0s; }
.chip:active, .project:active, .group-toggle:active, .navlink:active { background: var(--active); }
.button:active, .softkey:active, .choice:active, .chat-chip:active, .chat-choice:active, .recent-item:active { transition-duration: 0s; }

@media (prefers-reduced-transparency: reduce) {
  .sheet-scrim { background: var(--surface); }
}
@media (prefers-contrast: more) {
  :root { --muted: var(--text); --edge: var(--edge-hard); --edge-soft: var(--edge-hard); }
  .sheet-scrim { background: rgb(5 8 10 / .9); }
  .sheet { border-color: var(--text); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}
