/* =========================================================================
   Copilot Web - iPhone-first chat UI
   Layout uses dvh + safe-area insets; the composer is pinned with the
   visualViewport API (see app.js) because iOS does not shrink innerHeight
   when the software keyboard appears.
   ========================================================================= */

:root {
  --bg: #0d0f13;
  --bg-elev: #151920;
  --bg-elev-2: #1d222b;
  --line: #262c37;
  --text: #e8eaed;
  --text-dim: #9aa3af;
  --accent: #4f8cff;
  --accent-ink: #ffffff;
  --user-bubble: #2a3140;
  --ok: #3fb950;
  --warn: #d29922;
  --err: #f85149;
  --radius: 18px;
  --topbar-h: 52px;

  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  /* Updated from JS as the keyboard opens/closes. */
  --kb: 0px;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #ffffff;
    --bg-elev: #f4f6f9;
    --bg-elev-2: #e9edf3;
    --line: #dde2ea;
    --text: #14181f;
    --text-dim: #5c6673;
    --user-bubble: #e6ecf7;
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* Stop the whole page rubber-banding; only the timeline scrolls. */
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
}

body { touch-action: manipulation; }

/* Message content is meant to be selectable: long-press is now free for iOS
   text selection, since Rewind and Fork are always on screen rather than
   hidden behind a gesture. Stated explicitly rather than left to the default,
   so it cannot be lost to a future rule. */
.bubble, .md, .card-body, .term, .plan-text, pre, code {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

/* Chrome is not: long-pressing a button should offer its action, not select
   its label. */
.btn, .icon-btn, .msg-act, .file-act, .statcell, .sheet-item, .quick,
.crumb, .chat-item, .copy-btn, .card-head, .scroll-fab, .drawer-head, .topbar {
  -webkit-user-select: none;
  user-select: none;
}

.hidden { display: none !important; }
.muted { color: var(--text-dim); }
.tiny { font-size: 12px; }

svg {
  width: 22px; height: 22px;
  fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}

/* ------------------------------------------------------------------ login */

.login {
  position: fixed; inset: 0;
  display: grid; place-items: center;
  padding: 24px calc(24px + var(--safe-right)) 24px calc(24px + var(--safe-left));
  background: var(--bg);
  z-index: 100;
}
.login-card {
  width: 100%; max-width: 360px;
  display: flex; flex-direction: column; gap: 14px;
  text-align: center;
}
.login-logo, .empty-logo {
  font-size: 40px; color: var(--accent); line-height: 1;
}
.login h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; }
.login p { margin: 0; font-size: 14px; }
.login-error { color: var(--err); font-size: 14px; min-height: 20px; }

input[type=password], input[type=text], input[type=number], input[type=url], textarea {
  width: 100%;
  padding: 13px 15px;
  background: var(--bg-elev);
  border: 1px solid var(--line);
  border-radius: 14px;
  color: var(--text);
  /* 16px minimum, otherwise iOS zooms the viewport on focus. */
  font-size: 16px;
  font-family: inherit;
  outline: none;
}
input:focus, textarea:focus { border-color: var(--accent); }

/* Enable/disable switch on an MCP row. */
.tiny-btn { flex: none; padding: 7px 9px; border-radius: 10px; opacity: .45; }
.tiny-btn.on { opacity: 1; color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/* Monospace, but never below 16px: iOS zooms the viewport on smaller inputs. */
textarea.code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* ----------------------------------------------------------------- buttons */

.btn {
  appearance: none; border: 1px solid var(--line);
  background: var(--bg-elev); color: var(--text);
  padding: 12px 16px; border-radius: 14px;
  font: inherit; font-size: 15px; cursor: pointer;
  display: inline-block; text-align: center; text-decoration: none;
}
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.btn.ghost { background: transparent; }
.btn.block { width: 100%; display: block; }
.btn.danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 40%, transparent); }
.btn:active { transform: scale(0.98); }

.icon-btn {
  appearance: none; background: transparent; border: 0;
  color: var(--text); padding: 8px; border-radius: 12px;
  display: grid; place-items: center; cursor: pointer;
  /* 44px hit target per Apple HIG */
  min-width: 40px; min-height: 40px;
  -webkit-tap-highlight-color: transparent;
}
.icon-btn:active { background: var(--bg-elev-2); }
.icon-btn.round { border-radius: 999px; }
.icon-btn.send { background: var(--accent); color: var(--accent-ink); }
/* The same button, a different action: this one goes *into* a reply that is
   already running. Blue for both left the difference to a tooltip, which iOS
   has no way of showing. */
.icon-btn.send.steer { background: var(--warn); color: #1b1200; }
.icon-btn.send:disabled { background: var(--bg-elev-2); color: var(--text-dim); }
.icon-btn.stop { background: var(--err); color: #fff; }
/* Neither sent nor still yours: the message has reached the runtime and is
   waiting for the running reply to take it in. White so it reads as "nothing
   is happening yet" beside the blue Send it replaced, and it spins because the
   wait is real -- measured at about three seconds. Tapping takes it back.

   The ring is not decoration. The composer bar is `--bg`, which is white in
   light mode, so a white disc on it left nothing on screen but the spinning
   arc -- and the one thing this button has to say is that it *is* a button,
   since tapping it is how the message comes back. */
.icon-btn.wait {
  background: #fff; color: #111;
  box-shadow: 0 0 0 1px var(--line);
}
.icon-btn.wait .spinner { animation: spin 900ms linear infinite; transform-origin: 50% 50%; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Speaking a message. The button beside the box is a switch between typing and
   the bar below, so it is quiet; it dims while something is being recorded and
   cannot be switched. */
.icon-btn.mic { color: var(--text-dim); }
.icon-btn.mic:disabled { opacity: .45; pointer-events: none; }
.voice-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 0 0 8px; font-size: 12.5px; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.voice-bar .voice-hint { flex: 1; text-align: center; }
.voice-bar .voice-hint.cancel { color: var(--err); }
/* The box itself while it holds a spoken message, or is taking one: the same
   pill the text sits in, so it reads as the message rather than as something
   attached to one. Edged red while it listens, like the button that ends it.
   Nothing under it may select, scroll or open a menu: pressing it is the
   whole gesture, and `touch-action` is what keeps a slide up from being a
   scroll that cancels the press. */
.voice-field {
  flex: 1; min-width: 0; min-height: 44px;
  display: flex; align-items: center; gap: 10px;
  padding: 0 6px 0 14px; border-radius: 22px;
  background: var(--bg-elev); border: 1px solid var(--line);
  font-size: 15px; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  touch-action: none; -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;
}
.voice-field.recording { border-color: var(--err); }
.voice-field.cancelling { border-color: var(--err); background: color-mix(in srgb, var(--err) 14%, var(--bg-elev)); }
/* "Hold to talk": a bar waiting to be pressed, centred like a button's label. */
.voice-field.hold { justify-content: center; padding: 0 14px; cursor: pointer; color: var(--text); }
.voice-field.hold.pressed { background: var(--bg-elev-2); }
.voice-field.hold:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.voice-field .voice-held { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; color: var(--text); }
.voice-field .voice-held > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.voice-field .voice-drop {
  flex: none; background: none; border: 0; color: var(--text-dim);
  cursor: pointer; font-size: 20px; line-height: 1; padding: 6px 8px;
}
/* The live trace: recent loudness, newest on the right. Heights are set from
   the page, so the only motion the stylesheet owns is the easing. */
.voice-bar .wave, .voice-field .wave { display: flex; align-items: center; gap: 3px; height: 28px; flex: 1; min-width: 0; }
.voice-bar .wave i, .voice-field .wave i {
  display: block; flex: none; width: 3px; height: 3px; border-radius: 2px;
  background: var(--accent); transition: height 90ms linear;
}
.voice-field .voice-time { flex: none; padding-right: 8px; }
/* A spoken message in the transcript: a waveform and nothing else. */
.voice-clip { display: flex; align-items: center; min-height: 24px; }
/* The switch in the answer row: a bare microphone between the box and Answer,
   and a keyboard in the same place while the row is a Hold to talk bar. It
   stretches to the row's height like the button beside it, so only the width
   is set. */
.ask-mic {
  flex: none; width: 44px; padding: 0;
  display: inline-grid; place-items: center; color: var(--text-dim);
}
.ask-mic:disabled { opacity: .45; pointer-events: none; }
/* The same bar as the composer's, at the size of the box it replaces: its edge
   and corners are the answer box's, not the composer's pill. */
.ask-freeform .voice-field {
  min-height: 49px; border-radius: 10px; background: var(--bg);
  padding: 0 14px; justify-content: center;
}
.ask-freeform .voice-field.cancelling { background: color-mix(in srgb, var(--err) 14%, var(--bg)); }
.ask-freeform .voice-field .wave { flex: 1; }
/* What letting go will do, under the row, while it is held. */
.ask-hint { margin-top: 6px; text-align: center; min-height: 16px; }
.ask-hint.cancel { color: var(--err); }
/* The model's cleaned-up text. Plain text on purpose -- see buildTool. */
.voice-prompt { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.5; }

.linkish {
  background: none; border: 0; color: var(--accent);
  font: inherit; font-size: 13px; cursor: pointer; padding: 4px;
}

/* --------------------------------------------------------------- app shell */

.app {
  position: fixed; inset: 0;
  display: flex; flex-direction: column;
  padding-left: var(--safe-left);
  padding-right: var(--safe-right);
}

.topbar {
  flex: none;
  height: calc(var(--topbar-h) + var(--safe-top));
  padding-top: var(--safe-top);
  display: flex; align-items: center; gap: 2px;
  padding-left: 4px; padding-right: 4px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--line);
  z-index: 20;
}
.topbar-title {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 8px; justify-content: center;
  font-weight: 600; font-size: 16px;
}
#chat-title {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 52vw;
}

.pill {
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 3px 7px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--text-dim);
  flex: none;
}
.pill[data-state="live"]       { color: var(--ok);   border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.pill[data-state="connecting"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
.pill[data-state="offline"]    { color: var(--err);  border-color: color-mix(in srgb, var(--err) 45%, transparent); }
.pill[data-state="polling"]    { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

.banner {
  flex: none;
  padding: 10px 14px; font-size: 14px;
  background: color-mix(in srgb, var(--warn) 16%, var(--bg));
  border-bottom: 1px solid var(--line);
}
.banner.error { background: color-mix(in srgb, var(--err) 16%, var(--bg)); }
/* A confirmation, not a failure. */
.banner.note { background: color-mix(in srgb, var(--accent) 14%, var(--bg)); }
.banner code {
  background: rgba(127,127,127,.22); padding: 1px 5px; border-radius: 5px;
  font-size: 12px; word-break: break-all;
}

/* ---------------------------------------------------------------- timeline */

.timeline {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  padding: 14px 12px 8px;
  display: flex; flex-direction: column; gap: 12px;
}

/* The timeline is a flex column, so its children are flex items and shrink
   once the content overflows. `.card` sets `overflow: hidden`, which forces
   `min-height: 0` on a flex item -- collapsing tool cards to a 2px sliver in
   any conversation long enough to scroll. Nothing here may shrink. */
.timeline > * { flex: 0 0 auto; }

/* Only the most recent stretch of a long chat is kept in the document; this
   brings the stretch before it back. */
.load-earlier {
  align-self: center;
  padding: 7px 14px; border-radius: 999px;
  background: var(--bg-elev); border: 1px solid var(--line);
  color: var(--text-dim); font: inherit; font-size: 12.5px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.load-earlier:active { background: var(--bg-elev-2); color: var(--text); }

.empty {
  margin: auto; text-align: center; max-width: 460px; padding: 20px;
  display: flex; flex-direction: column; gap: 10px; align-items: center;
}
.empty h2 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
.empty p { margin: 0; font-size: 14px; }
.starter-grid {
  margin-top: 10px; width: 100%;
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.starter {
  text-align: left; font-size: 13px; line-height: 1.35;
  padding: 12px; border-radius: 14px;
  background: var(--bg-elev); border: 1px solid var(--line);
  color: var(--text); cursor: pointer;
}
@media (max-width: 380px) { .starter-grid { grid-template-columns: 1fr; } }

.row { display: flex; width: 100%; }
.row.user { justify-content: flex-end; }

.bubble {
  max-width: min(86%, 680px);
  padding: 10px 14px;
  border-radius: var(--radius);
  overflow-wrap: anywhere;
}
.row.user .bubble {
  background: var(--user-bubble);
  border-bottom-right-radius: 6px;
}
.row.agent .bubble {
  background: transparent;
  padding-left: 2px; padding-right: 2px;
  max-width: 100%;
}

/* --------------------------------------------------------- markdown output */

.md > *:first-child { margin-top: 0; }
.md > *:last-child { margin-bottom: 0; }
/*
 * A message cannot reach outside itself. `contain: paint` makes each rendered
 * block the containing block for anything fixed or absolute inside it, its own
 * stacking context, and the edge everything inside is clipped to -- so a
 * `position: fixed; inset: 0` laid over the app from a message's markup covers
 * that message and nothing else. The sanitiser removes what it can name; this
 * is for whatever it cannot. A sub-agent's view and a task's page draw the
 * same model-written markup outside the transcript, so they are held to it
 * too.
 */
#timeline .md, #agent-timeline .md, .task-detail .md { contain: paint; }
.card-body > .preview-page { margin: 0 0 8px; }
.md p { margin: 0 0 10px; }
.md ul, .md ol { margin: 0 0 10px; padding-left: 22px; }
.md li { margin: 3px 0; }
.md h1, .md h2, .md h3, .md h4 { margin: 16px 0 8px; line-height: 1.25; letter-spacing: -0.01em; }
.md h1 { font-size: 21px; } .md h2 { font-size: 19px; }
.md h3 { font-size: 17px; } .md h4 { font-size: 15px; }
.md a { color: var(--accent); }
.md blockquote {
  margin: 0 0 10px; padding: 2px 0 2px 12px;
  border-left: 3px solid var(--line); color: var(--text-dim);
}
.md code {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: 13.5px;
  background: rgba(127,127,127,.20);
  padding: 1.5px 5px; border-radius: 6px;
}
.md pre {
  margin: 0 0 10px;
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.md pre code { background: none; padding: 0; font-size: 13px; line-height: 1.5; }
/* A table keeps its natural width and scrolls, rather than being crushed into
   the bubble. `.scrollable` is set from JS once overflow is measurable. */
.tablewrap {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  max-width: 100%; margin: 0 0 10px;
}
.tablewrap.scrollable {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
}
.md table {
  border-collapse: collapse; margin: 0; font-size: 14px;
  display: table; width: auto; min-width: 100%;
}
.md th, .md td {
  border: 1px solid var(--line); padding: 6px 9px; text-align: left;
  /* The balance: wrap inside a cell, but give every column a floor so it is
     not crushed to a couple of characters, and a ceiling so one prose cell
     cannot stretch the table across several screens. The floor is what
     actually drives the layout -- auto table layout balances columns to the
     available width, so raising it trades width for shorter rows. */
  white-space: normal;
  overflow-wrap: break-word;
  min-width: 10ch;
  max-width: 28ch;
}
.md th { font-weight: 600; }
.md img { max-width: 100%; border-radius: 10px; }
.md hr { border: 0; border-top: 1px solid var(--line); margin: 14px 0; }

.codewrap { position: relative; }
.copy-btn {
  position: absolute; top: 8px; right: 8px;
  font-size: 11px; padding: 5px 9px; border-radius: 8px;
  background: var(--bg-elev-2); border: 1px solid var(--line);
  color: var(--text-dim); cursor: pointer;
}

.cursor::after {
  content: '▍'; margin-left: 1px;
  animation: blink 1.05s steps(2, start) infinite;
  color: var(--accent);
}
@keyframes blink { to { visibility: hidden; } }

/* Said in words instead, for anyone who reads a blinking block as a fault.
   The class stays on the element either way -- the script finds the open
   bubble by it -- so the choice is made here. */
body[data-waiting="thinking"] .cursor::after { content: none; }
.thinking {
  color: var(--text-dim); font-size: 14px;
  animation: fade-pulse 1.6s ease-in-out infinite;
}
@keyframes fade-pulse { 50% { opacity: .45; } }
/* The fade is decoration; the word carries the meaning without it. The cursor
   above is deliberately left alone -- take its blink away and a static block
   at the end of a transcript reads as stray text rather than as a reply on its
   way, and that block is the only sign the send worked. Anyone who does not
   want a blinking cursor now has a setting that removes it outright. */
@media (prefers-reduced-motion: reduce) {
  .thinking { animation: none; }
}

/* --------------------------------------------------- tools / shell / notice */

.card {
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--bg-elev); overflow: hidden;
  max-width: min(94%, 680px);
}
.card-head {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; cursor: pointer;
  /* iOS wants at least 44px for anything you tap. */
  min-height: 44px; box-sizing: border-box;
  font-size: 13.5px; font-weight: 500;
}
.card-head .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-head .chev { transition: transform .15s ease; color: var(--text-dim); }
/* "background" on a shell card. Quiet, and `flex: none` so it never competes
   with the command for the width -- the command is the part worth reading,
   and the head is the one line of it most people ever see. */
.card-head .tag {
  flex: none;
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-dim);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 1px 6px;
}
/* The permission decision, on the card it was about. Quiet by design: it is a
   record of something already settled, not something to act on -- so the head
   gets the mark on its own and the words wait below. */
.card-head .approval-mark { flex: none; display: inline-flex; color: var(--text-dim); }
.card-head .approval-mark.denied { color: var(--warn); }
/* Two marks in one 24-unit box. The tick is `check` verbatim so the two kinds
   of approval read as the same decision; the badge is about 6x8 units, and a
   hollow bolt that small closes into a dot at 14px -- rendered at true size
   and compared before filling it. The hairline stroke keeps its points from
   vanishing at 14px without thickening the body. */
.card-head .approval-mark.auto svg path:last-child {
  fill: currentColor; stroke-width: .6;
}
/* Outside `.card-body` on purpose: the body scrolls at 340px, so a decision
   put inside it would be scrolled out of sight by any long tool output. */
.card-foot {
  display: flex; justify-content: flex-end;
  border-top: 1px solid var(--line);
  padding: 6px 12px; font-size: 11.5px; color: var(--text-dim);
}
.card:not(.open) .card-foot { display: none; }
.card-foot.denied { color: var(--warn); }

/* The question a card was about, and what was picked. The chosen option is
   marked rather than being the only one shown: which options were *offered*
   is half of what the exchange said. */
.ask-choices { margin: 8px 0 0; padding-left: 18px; color: var(--text-dim); }
.ask-choices li { margin: 2px 0; }
.ask-choices li.picked { color: var(--text); font-weight: 600; list-style: disclosure-closed; }
.ask-typed { margin-top: 8px; color: var(--text); }

/* The server's own log for one chat. Scrolls on its own so a long tail cannot
   push the Copy button and the note below it off the sheet. */
.log-note { margin: 10px 0; line-height: 1.5; }
.log-box {
  max-height: 46vh; overflow: auto; -webkit-overflow-scrolling: touch;
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: 12px; padding: 10px 12px; margin-bottom: 10px;
}
.log-box pre.term { font-size: 11.5px; }
.card.open .card-head .chev { transform: rotate(180deg); }
.card-body {
  border-top: 1px solid var(--line);
  padding: 10px 12px; font-size: 13px;
  max-height: 340px; overflow: auto; -webkit-overflow-scrolling: touch;
}
.card:not(.open) .card-body { display: none; }

/* A picture a tool produced. Height is capped below the card body's own
   340px so the image is never the thing you have to scroll inside a scroll. */
.tool-images { display: flex; flex-wrap: wrap; gap: 8px; }
.tool-images:not(:last-child) { margin-bottom: 8px; }
.tool-image { display: block; max-width: 100%; line-height: 0; }
.tool-image img {
  display: block; max-width: 100%; max-height: 260px;
  width: auto; height: auto; object-fit: contain;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--bg);
}

/* Reasoning is easy to miss among tool cards, so give it an accent edge and
   set the text apart from the answer itself. */
.card.thought { border-left: 3px solid color-mix(in srgb, var(--accent) 50%, transparent); }
.card.thought .card-head .ico { color: var(--accent); opacity: .95; }
.card.thought .card-body .md { color: var(--text-dim); font-style: italic; }

/* How a card went, on its own left edge.
   The Thinking card has carried its colour this way from the start, and it is
   the better place for it: a dot in the head is a fixed 8px plus a gap taken
   out of the title, which is the only part of that row that shrinks, and a
   column of cards each opening with a small coloured circle reads as a
   bulleted list rather than as a stack of pills. */
.card[data-status] { border-left: 3px solid var(--text-dim); }
.card[data-status="in_progress"], .card[data-status="running"] { border-left-color: var(--warn); }
.card[data-status="completed"], .card[data-status="ok"] { border-left-color: var(--ok); }
.card[data-status="failed"], .card[data-status="error"] { border-left-color: var(--err); }
/* Stopped and waiting for a person. The edge says so as loudly as the head
   tint does, so a chat that has stopped is findable by eye down a long
   transcript -- which is the whole reason the colour is on the edge. */
.card.asking[data-status] { border-left-color: var(--warn); }

/* The chat list's status used to be a dot here; it is the colour of the mode
   glyph now -- see `.chat-mode[data-status]`. The transcript's cards wear
   theirs on the edge instead -- see above. The pulse is shared by both. */
@keyframes pulse { 50% { opacity: .35; } }

/* The command a run card ran, leading its output. Bold and monospace so it
   reads as the thing that was typed rather than as part of what came back. */
.run-cmd {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px; font-weight: 600; line-height: 1.45;
  white-space: pre-wrap; word-break: break-word;
  padding-bottom: 8px; margin-bottom: 8px;
  border-bottom: 1px solid var(--line);
}

/* A permission, inside the card it is about: the head stays the tool's and
   this is the body until someone answers. Marked so a chat that has stopped
   is findable by eye on a long transcript. */
.card.asking { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }
.card.asking > .card-head { background: color-mix(in srgb, var(--warn) 9%, transparent); }
.perm-lede { font-size: 13px; margin-bottom: 8px; }
.perm-ask .term { margin-bottom: 10px; }

/* The agent's own question, in the card it was asked from. The question leads
   the body the way a command leads a run card's, and for the same reason: the
   head is a flex row where the title is the only thing that shrinks, so a
   sentence put there is an ellipsis. */
.ask-ask > .md > :first-child { margin-top: 0; }
.ask-ask > .md > :last-child { margin-bottom: 0; }
/* And it does not scroll while the question is on it. The body caps at 340px,
   which is right for output nobody has to act on -- but the one control that
   moves this chat forward must not be buried in a scroll inside the
   transcript's own scroll. */
.card-body:has(> .ask-ask) { max-height: none; }

/* An edit, drawn from its arguments: the result carries no diff at all, so
   this is the replacement the model asked for rather than a diff of the file.
   Marked as well as tinted, because colour is the first thing a phone in
   bright sun loses. */
.diff { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
.diff-line { display: flex; gap: 8px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.diff-mark { flex: none; width: 1ch; opacity: .8; }
.diff-line.del { background: color-mix(in srgb, var(--err) 12%, transparent); }
.diff-line.add { background: color-mix(in srgb, var(--ok) 12%, transparent); }
.diff-line.del .diff-mark { color: var(--err); }
.diff-line.add .diff-mark { color: var(--ok); }
.diff-text { flex: 1; min-width: 0; }

pre.term {
  margin: 0; white-space: pre-wrap; word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12.5px; line-height: 1.45; color: var(--text);
}

/* ------------------------------------------------- the six kinds of shell run

   One card for all of them; what differs is where it sits. The card is
   wrapped the way an agent bubble is, so a closing line can sit outside the
   pill the way a reply footer sits outside a bubble.

   `.pinned` is ordered by `syncRunPills`, in bands of two per turn, so a run
   still going sits at the foot of *its own* exchange and a later message
   lands below it rather than above. The rule here is only what a freshly
   appended block wears until that pass runs; the inline order wins. */
.run-block { display: flex; flex-direction: column; gap: 5px; }
.timeline > .run-block.pinned { order: 1; }
/* Always last, whatever band the rows above are in. */
#pending-cursor { order: 99999; }
.timeline > .load-earlier { order: -1; }

/* A run that finished after its answer had already been stamped. It stays
   below that footer rather than folding back into a reply that is over, so it
   is set apart -- quietly, by its edge.

   Three sides, not four: the left edge says how the run *went*, which is a
   different question from where the card sits, and `border-color` would take
   that colour away. */
.run-block.closed-out > .run-pill {
  border-top-color: color-mix(in srgb, var(--accent) 32%, var(--line));
  border-right-color: color-mix(in srgb, var(--accent) 32%, var(--line));
  border-bottom-color: color-mix(in srgb, var(--accent) 32%, var(--line));
}

/* The closing line, in the reply footer's own voice: the answer above has
   been closed off already, and this run is a second thing that ended later. */
.run-reply-foot { padding-left: 2px; }

/* Still going. The whole head is swept rather than a hairline above it: on a
   phone at arm's length a 2px bar is invisible, and this is the one thing on
   the card that says the work outlived the answer it came from.

   An overlay rather than an animated background on the head itself, so the
   tint passes over the title without the title having to be repainted -- and
   `pointer-events: none` because the head is the tap target that opens the
   card. */
.run-pill.running .card-head { position: relative; overflow: hidden; }
.run-pill.running .card-head > * { position: relative; z-index: 1; }
.run-pill.running .card-head::before,
.card.running > .card-head::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background-image: linear-gradient(90deg,
    transparent 0%,
    color-mix(in srgb, var(--accent) 7%, transparent) 25%,
    color-mix(in srgb, var(--accent) 24%, transparent) 50%,
    color-mix(in srgb, var(--accent) 7%, transparent) 75%,
    transparent 100%);
  background-size: 60% 100%;
  background-repeat: no-repeat;
  animation: runflow 1.8s linear infinite;
}
/* Every card that is still working wears it, not only the run cards. */
.card.running > .card-head { position: relative; overflow: hidden; }
.card.running > .card-head > * { position: relative; z-index: 1; }
@keyframes runflow {
  from { background-position: -60% 0; }
  to   { background-position: 160% 0; }
}

/* A sub-agent's own working-out, inside the card that dispatched it. Set in
   from the edge so the nesting is visible at a glance rather than having to be
   worked out from the titles. */
.run-children {
  display: flex; flex-direction: column; gap: 8px;
  margin-bottom: 10px;
  padding-left: 10px;
  border-left: 2px solid var(--line);
}
.run-children .card { max-width: 100%; }
.run-said { font-size: 12.5px; }
.run-said > :first-child { margin-top: 0; }
.run-said > :last-child { margin-bottom: 0; }
/* A nested card scrolls on its own; capping it again inside a body that also
   scrolls is the scroll-inside-a-scroll the tool images rule already avoids. */
.run-children .run-body { max-height: 160px; }

/* A sub-agent's card holds a transcript rather than one command's output, so
   it is given more room before it starts scrolling. */
.run-pill[data-source="task"] > .run-body { max-height: 420px; }

/* The output is the point of the card, so it gets more room than a tool
   card's 340px -- and scrolls rather than growing, or one `npm test` pushes
   the reply that explains it off the top of the screen. */
.run-pill .run-body { max-height: 260px; overflow: auto; }

/* The footer says how long it ran and nothing else: which kind of run it is,
   which folder it ran in and what it was approved under are the same words on
   every card of that kind, and a line repeated down the whole transcript is
   read once and then costs space for ever.

   It follows the card open and shut, like every other foot. A collapsed card
   is one line by choice, and a duration under it makes that two. */
.run-pill .run-foot { display: flex; justify-content: flex-end; }
.run-elapsed { font-variant-numeric: tabular-nums; }


.notice {
  font-size: 13.5px; padding: 10px 13px; border-radius: 12px;
  background: var(--bg-elev); border: 1px solid var(--line);
  max-width: min(94%, 680px);
}
.notice.warn  { border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
.notice.error { border-color: color-mix(in srgb, var(--err) 45%, transparent); }

.plan { list-style: none; margin: 0; padding: 0; font-size: 13.5px; }
.plan li { display: flex; gap: 8px; align-items: flex-start; padding: 4px 0; }
.plan li.completed .plan-text { color: var(--text-dim); text-decoration: line-through; }

/* ------------------------------------------------------------- permissions */

.perm {
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, var(--bg-elev));
  border-radius: 14px; padding: 12px; font-size: 14px;
  max-width: min(94%, 680px);
}
.perm h4 { margin: 0 0 4px; font-size: 14px; }
.perm-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.perm-actions .btn { flex: 1 1 auto; padding: 10px 12px; font-size: 14px; }
/* The agent's own choices, which are sentences as often as they are words. One
   per line, left-aligned and free to wrap: two of them sharing a line means one
   wraps and the other does not, which stops reading as a list of options. */
.perm-actions.stacked { flex-direction: column; align-items: stretch; }
.perm-actions.stacked .btn {
  flex: 0 0 auto; text-align: left; white-space: normal; line-height: 1.35;
}
/* A standing allowance answers two questions at once -- for how long, and how
   widely -- so the breadth is a heading over its own pair rather than a fifth
   and sixth pill in the same row. Four pills reading "This chat / This folder"
   twice cannot be told apart by anyone. */
.perm-group-label {
  display: block; margin-top: 10px;
  font-size: 12px; color: var(--text-dim);
}
.perm-group .perm-actions { margin-top: 4px; }
/* A rule about one file, one tool or one command sits in a panel joined to
   the row that covers every one of them: turning the parent on makes the
   child moot, which is only legible if the two read as one thing. They used
   to be rows of the same size, the same width and the same type, indented
   only inside their own boxes -- so nothing said which belonged to which. */
.perm-family {
  margin-bottom: 6px; border-radius: 12px; background: var(--bg-elev-2);
}
.perm-family > .sheet-item { margin-bottom: 0; }
.perm-family.open > .sheet-item { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.perm-kids {
  display: none;
  background: color-mix(in srgb, var(--bg-elev-2) 45%, var(--bg));
  border: 1px solid var(--bg-elev-2); border-top: 0;
  border-radius: 0 0 12px 12px;
}
.perm-family.open > .perm-kids { display: block; }
/* Inside the panel a row is a line of it, not a card of its own: no box of
   its own, smaller type, and a hairline between it and the next. */
.perm-kids .sheet-item,
.perm-kids .perm-family {
  background: transparent; border-radius: 0; margin-bottom: 0;
}
.perm-kids .sheet-item {
  font-size: 14px; padding-top: 9px; padding-bottom: 9px;
  padding-left: calc(10px + var(--perm-depth, 1) * 14px);
}
.perm-kids .sheet-item .desc { font-size: 12px; }
.perm-kids > * + * { border-top: 1px solid var(--line); }
/* A panel inside a panel -- a tool in its server, in Tools -- steps in again
   and drops the fill, so the depth reads from the indent and the rule. */
.perm-kids .perm-kids {
  background: transparent; border: 0;
  border-top: 1px solid var(--line);
}
/* The answers are buttons inside the row, so the row is a div and wraps: on a
   narrow phone the segments drop under the label rather than squeezing it. */
.sheet-item.perm-row { flex-wrap: wrap; row-gap: 8px; cursor: default; }
.sheet-item.perm-row.perm-parent { cursor: pointer; }
.sheet-item.perm-row .grow { flex: 1 1 130px; }
.sheet-item.perm-row .seg { margin-left: auto; }
/* The chevron is part of the row now -- the whole row opens what is under
   it -- so it only needs to be seen, at a thumb's width so the label lines up
   with rows that have none. */
.sheet-item .perm-chev {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 20px; color: var(--text-dim);
}
.sheet-item .perm-chev > svg { transform: rotate(-90deg); transition: transform .15s ease; }
.perm-family.open > .sheet-item .perm-chev > svg { transform: none; }
.sheet-item.perm-heading { font-weight: 600; }
.perm-resolved { margin-top: 8px; font-size: 13px; color: var(--text-dim); }

/* The iOS segmented control: one of a few answers, chosen in place. */
.seg {
  display: inline-flex; flex: none; padding: 2px; gap: 2px;
  border-radius: 9px; background: color-mix(in srgb, var(--text) 9%, transparent);
}
.seg-opt {
  appearance: none; border: 0; background: transparent; color: var(--text);
  font: inherit; font-size: 13px; line-height: 1; padding: 7px 10px;
  border-radius: 7px; cursor: pointer; min-width: 44px;
}
.seg-opt.on {
  background: var(--bg-elev); font-weight: 600;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .18);
}
.seg-opt:disabled { cursor: default; }
.seg.locked { opacity: .6; }
/* A disabled segment that is not the chosen one: offered, and not available. */
.seg-opt:disabled:not(.on) { opacity: .45; }
/* The Mode sheet's Work | Life tabs run the full width, as iOS tabs do. */
.mode-tabs .seg, .speech-speed .seg { display: flex; width: 100%; box-sizing: border-box; }
.mode-tabs .seg-opt, .speech-speed .seg-opt { flex: 1; padding: 8px 10px; }
.speech-speed { margin-bottom: 8px; }
.mode-tabs p { margin: 6px 2px 0; }
/* A switch row: the label never changes, the switch says where it stands. */
.sheet-item.switch-row.pending { opacity: .6; }

/* The agent's own question. Shares the permission card's frame: both block the
   turn, so both should read as "nothing moves until you answer this". */
.ask-freeform { display: flex; gap: 8px; margin-top: 10px; }
/* A card that gives up by itself says when. Tabular figures, so the count does
   not make the line shuffle sideways every second. */
.ask-deadline { margin-top: 10px; font-variant-numeric: tabular-nums; }
.ask-input {
  flex: 1 1 auto; min-width: 0;
  padding: 10px 12px; font-size: 16px; /* 16px: anything smaller zooms on iOS */
  border-radius: 10px; border: 1px solid var(--border);
  background: var(--bg); color: var(--text);
}


/* A held message keeps its place at the end of the transcript rather than
   being buried under the reply it is waiting for. The timeline is a flex
   column, so `order` moves it without touching the DOM -- and the bubble drops
   back into its real position the moment the queue picks it up and the class
   goes. Several held messages keep the order they were typed in, flex being
   stable within one `order` group. */


/* When something happened, on every message and every card. Tabular figures
   so a column of them does not jitter. */
.stamp {
  font-size: 11.5px; color: var(--text-dim);
  font-variant-numeric: tabular-nums; flex: none;
}
.card-head .stamp { margin-left: 4px; }
.notice .stamp, .perm .stamp { display: block; margin-top: 6px; }
.row.agent .stamp { margin-top: 5px; padding-left: 2px; }
/* The reply footer ends in the same time, so the row's own stamp stands down
   rather than being removed -- a footer comes and goes with the turn. */
.row.agent.has-foot .stamp { display: none; }
/* The same goes for a notice that carries the footer of a reply with no answer. */
.notice.has-foot .stamp { display: none; }
.notice .notice-foot { margin-top: 6px; padding-left: 0; }

/* ---------------------------------------------------------------- composer */

.scroll-fab {
  position: absolute; right: 14px;
  width: 38px; height: 38px; border-radius: 999px;
  background: var(--bg-elev-2); border: 1px solid var(--line);
  display: grid; place-items: center; z-index: 15; cursor: pointer;
}
.scroll-bottom { bottom: calc(var(--composer-h, 120px) + 12px); }
/* Sits above the jump-to-latest button, which is the more common action. */
.scroll-response { bottom: calc(var(--composer-h, 120px) + 58px); }
.scroll-fab:active { background: var(--bg-elev); }

.composer {
  flex: none;
  border-top: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  /*
   * The home indicator's safe area, less whatever of it the layout already
   * stops short of -- see `layoutShortfall` in app.js. Without that, an iOS
   * whose Home Screen viewport ends above the indicator gets the gap twice.
   */
  padding: 8px 10px calc(6px + max(var(--safe-bottom) - var(--bottom-shortfall, 0px), 4px));
  /* --kb is set from visualViewport so the bar rides above the iOS keyboard. */
  transform: translateY(calc(-1 * var(--kb)));
  transition: transform .12s ease-out;
}
.composer-row { display: flex; align-items: flex-end; gap: 8px; }

/*
 * What the chat is waiting on, above the message box. It scrolls inside itself
 * rather than growing past a third of the screen, so the box underneath stays
 * reachable with a long request or several at once. Warn-tinted like the card
 * edge it stands in for: both mean "nothing moves until you answer".
 */
.ask-dock {
  max-height: 38vh; overflow-y: auto; overscroll-behavior: contain;
  margin-bottom: 8px;
  border: 1px solid color-mix(in srgb, var(--warn) 55%, var(--line));
  border-radius: 12px;
  background: color-mix(in srgb, var(--warn) 9%, var(--bg-elev));
}
.ask-dock-item { padding: 10px 12px; }
.ask-dock-item + .ask-dock-item { border-top: 1px solid var(--line); }
.ask-dock-head { display: flex; flex-direction: column; gap: 1px; margin-bottom: 8px; }
.ask-dock-title { font-size: 13px; font-weight: 600; }
.ask-dock-where {
  font-size: 12px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ask-dock-where:empty { display: none; }
/* In the transcript, where the buttons used to be. */
.ask-pinned { font-size: 13px; }
.ask-ask > .md + .ask-pinned, .perm > .ask-pinned { margin-top: 8px; }

#input {
  flex: 1; resize: none;
  max-height: 34vh; min-height: 44px;
  padding: 11px 14px; border-radius: 22px;
  line-height: 1.4;
}

.attachments { display: flex; gap: 8px; flex-wrap: wrap; padding: 0 0 8px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--bg-elev-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 5px 10px; font-size: 12.5px;
}
.chip button { background: none; border: 0; color: var(--text-dim); cursor: pointer; font-size: 15px; padding: 0 2px; }
.chip img { width: 22px; height: 22px; border-radius: 5px; object-fit: cover; }
/* An upload on its way: the chip fills from the left as it goes, so how far
   it has got is visible at a glance as well as in the words. */
.chip.upload-chip {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 28%, var(--bg-elev-2)) var(--progress, 0%),
    var(--bg-elev-2) var(--progress, 0%));
  font-variant-numeric: tabular-nums;
}

/* Status line: fixed columns with dividers, sits above the composer */
.quickbar {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--bg-elev);
  overflow: hidden;
  margin-bottom: 8px;
}
.statcell {
  display: flex; align-items: center; gap: 6px;
  min-width: 0; padding: 8px 6px;
  background: transparent; border: 0;
  border-right: 1px solid var(--line);
  color: var(--text); font: inherit; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.quickbar .statcell:last-child { border-right: 0; }
.statcell:active { background: var(--bg-elev-2); }
.statcell.on { color: var(--ok); }
.statcell .ico { flex: none; opacity: .75; }
.statcell.on .ico { opacity: 1; }
.statcell-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }
.statcell-label {
  font-size: 12.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.statcell-sub {
  font-size: 10.5px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A mark after the value -- the struck-through eye of an anonymous chat. */
.statcell .statcell-mark {
  width: 12px; height: 12px; margin-left: 3px; vertical-align: -2px;
  flex: none;
}
@media (max-width: 360px) {
  .statcell { padding: 8px 4px; gap: 4px; }
  .statcell .ico { width: 14px; height: 14px; }
  .statcell .statcell-mark { width: 12px; height: 12px; }
}

svg.ico { width: 18px; height: 18px; stroke-width: 1.5; }
/* Iconoir fills a few subpaths as well as stroking them: the dots of `more`
   and `chat`, the centre of `pin`. */
svg path.f { fill: currentColor; }

/* The mini globe a browser tool's glyph wears. The base glyph is scaled into
   the top-left three quarters, which thins its stroke with it, so the badge is
   drawn thinner than the default to keep the two reading as one mark rather
   than as a glyph with something stuck to it. */
svg.ico .ico-badge { stroke-width: 1.25; }

/* Per-message actions (rewind / fork). Always visible, on your own messages
   only -- hiding them behind hover or a gesture made them undiscoverable on a
   phone, so they are kept small and dim instead. */
.msg-actions {
  display: flex; gap: 4px; align-items: center;
  margin-top: 4px;
}
.row.user .msg-actions { justify-content: flex-end; }
.msg-act {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: 8px; padding: 4px 9px;
  color: var(--text-dim); font: inherit; font-size: 11.5px; cursor: pointer;
}
.msg-act .ico { opacity: .8; }
.msg-act:active { background: var(--bg-elev-2); color: var(--text); }
.msg-act.on { color: var(--accent); border-color: var(--accent); }

/* Stands in for a streaming bubble while the turn is between chunks, so the
   cursor never disappears part-way through an answer. */
#pending-cursor .md { min-height: 1.25em; padding: 2px 2px 4px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

.agent-col {
  display: flex; flex-direction: column; align-items: flex-start;
  min-width: 0; max-width: min(86%, 680px);
}
.agent-col .bubble { max-width: 100%; }
.reply-foot {
  /* No wrapping: with it, a long model name takes a whole line of its own and
     pushes the buttons onto a second one instead of shortening. */
  display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
  margin-top: 5px; padding-left: 2px; max-width: 100%;
}
.reply-meta {
  font-size: 11.5px; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  margin-right: auto;
  /* A long model name should shorten, not push the buttons onto a second row. */
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* On a phone the model name, the timings and two labelled buttons will not
   share a line, so the buttons drop their text and keep their icons. */
@media (max-width: 480px) {
  .reply-foot .msg-act span { display: none; }
  .reply-foot .msg-act { padding: 5px 9px; }
  .reply-foot .msg-act .ico { opacity: 1; }
}

/* Three-column picker (model / thinking / context). The context column is
   present but disabled: Copilot ignores the tier in ACP mode, so it is shown
   ready for the CLI fix rather than wired up to something that does nothing. */
.col-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr;
  gap: 8px;
  min-height: 0;
}
.col { display: flex; flex-direction: column; min-width: 0; }
.col-head {
  font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-dim); padding: 0 2px 6px;
}
.col-list {
  flex: 1; min-height: 0;
  /* What is left of the sheet once everything else on it has its room, rather
     than a share of the screen: the sheet is capped at 88dvh and the rest of
     it (note, switch, usage, OK) needs about 360px, with some to spare for a
     note or a second line, so a fixed 46dvh pushed OK below the fold on
     every phone. The lists scroll inside themselves. */
  max-height: clamp(140px, calc(88dvh - 430px), 46dvh);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--bg-elev-2);
  padding: 4px;
}
.col-list::-webkit-scrollbar { width: 0; }
.col-item {
  display: flex; align-items: center; gap: 5px; justify-content: space-between;
  width: 100%; text-align: left;
  padding: 9px 8px; margin-bottom: 3px;
  border: 1px solid transparent; border-radius: 9px;
  background: transparent; color: var(--text);
  font: inherit; font-size: 13px; line-height: 1.25;
  cursor: pointer;
}
.col-item > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.col-item.sel { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
/* A disabled option still has to read as the current setting, so `.sel` keeps
   its fill and the whole column is dimmed instead. */
.col-item:disabled { opacity: .5; cursor: default; }
.col-tag { font-size: 10px; opacity: .65; flex: none; }
.col-list .muted { padding: 8px; margin: 0; }
/* A model family: a heading that opens to its members, the same shape as a
   chat folder so it reads as one without being explained. */
.col-fam {
  display: flex; align-items: center; gap: 5px; width: 100%;
  padding: 8px 6px 6px; margin: 2px 0;
  background: none; border: 0; border-radius: 8px;
  color: var(--text-dim); font: inherit; font-size: 11px;
  letter-spacing: .04em; text-transform: uppercase; cursor: pointer;
}
.col-fam > svg:first-child {
  transform: rotate(-90deg); transition: transform .15s ease; flex: none;
}
.col-fam.open > svg:first-child { transform: none; }
.col-fam-name { flex: 1; min-width: 0; text-align: left; overflow-wrap: anywhere; }
.col-fam .col-cost { text-transform: none; }
.col-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; overflow-wrap: anywhere; }
.col-purpose { font-size: 10.5px; opacity: .7; }
.col-cost { letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
/* The member Auto select is on right now: the model's choice, not the
   person's, so it is outlined where a choice somebody made is filled. */
.col-item.running { border-color: var(--accent); }

/* Slash-command autocomplete above the input */
.suggest {
  max-height: 46vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--bg-elev); border: 1px solid var(--line);
  border-radius: 14px; margin-bottom: 8px;
}
.suggest-item { padding: 11px 13px; cursor: pointer; border-bottom: 1px solid var(--line); }
.suggest-item:last-child { border-bottom: 0; }
.suggest-item.sel { background: var(--bg-elev-2); }
.suggest-item .cmd { font-weight: 600; font-size: 14px; }
.suggest-item .desc { font-size: 12.5px; color: var(--text-dim); }

/* ------------------------------------------------------------------ drawer */

.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 40; }

.drawer {
  position: fixed; top: 0; bottom: 0; left: 0;
  width: min(84vw, 330px);
  background: var(--bg-elev);
  border-right: 1px solid var(--line);
  z-index: 50;
  transform: translateX(-102%);
  transition: transform .22s cubic-bezier(.32,.72,0,1);
  display: flex; flex-direction: column;
  padding: calc(10px + var(--safe-top)) 12px calc(12px + var(--safe-bottom)) calc(12px + var(--safe-left));
  overflow: hidden;
}
.drawer.open { transform: translateX(0); }
.drawer-head { display: flex; align-items: center; justify-content: space-between; }
.drawer-head h2 { margin: 0; font-size: 19px; }
/* Search box above the chat list. 16px or iOS zooms the whole drawer in. */
.drawer-search {
  display: flex; align-items: center; gap: 6px;
  margin-top: 8px; padding: 0 10px;
  background: var(--bg-elev); border: 1px solid var(--line); border-radius: 12px;
  color: var(--text-dim);
}
.drawer-search input {
  flex: 1; min-width: 0;
  background: none; border: 0; border-radius: 0;
  padding: 10px 0; font-size: 16px;
}
.drawer-search input:focus { outline: none; }
.drawer-search:focus-within { border-color: var(--accent); }
.drawer-search .icon-btn { padding: 4px; color: var(--text-dim); }

.drawer-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; margin: 8px -4px; padding: 0 4px; }
.drawer-foot {
  border-top: 1px solid var(--line);
  padding-top: 10px; margin: 4px 0 0;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}

.chat-item {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 11px; border-radius: 12px; cursor: pointer;
  margin-bottom: 4px;
}
.chat-item.active { background: var(--bg-elev-2); }
/*
 * The mode glyph before a chat's name. Dimmed like the other leading icons so
 * the name is still what the eye lands on, and `flex: none` because the meta
 * column beside it is the only thing that should shrink.
 */
.chat-item .chat-mode { flex: none; opacity: .55; }
.chat-item.active .chat-mode { opacity: .8; }
/* And it carries the chat's status, which used to be a dot at the end of the
   row: green and pulsing while it answers, amber and steady while it is
   stopped waiting for you, green and steady while a reply waits to be read.
   Full strength, because a status at .55 is a status nobody sees. The two
   greens differ only by the pulse; the amber is the one that needs a tap. */
.chat-item .chat-mode[data-status] { opacity: 1; }
.chat-item .chat-mode[data-status="answering"] {
  color: var(--ok); animation: pulse 1.1s ease-in-out infinite;
}
.chat-item .chat-mode[data-status="asking"] { color: var(--warn); }
.chat-item .chat-mode[data-status="unread"] { color: var(--ok); }
.chat-item .meta { flex: 1; min-width: 0; }
.chat-item .name {
  font-size: 14.5px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-item .sub {
  font-size: 12px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-item .kebab { color: var(--text-dim); padding: 4px; }
.chat-item .name.unread { font-weight: 650; color: var(--text); }

/* Standing in for the chat rows an empty devbox folder does not have yet, so
   it takes their shape. A <button> brings its own background, border and
   font, all of which have to go for it to sit level with them. */
.chat-item.new-here {
  width: 100%; text-align: left;
  background: none; border: 0; font: inherit; color: var(--text-dim);
}
.chat-item.new-here .ico { flex: none; opacity: .7; }
.chat-item.new-here:hover { background: var(--bg-elev-2); }

/* A line of a search result: where it was said, and the words around it. */
.chat-item .hit {
  display: flex; gap: 6px; align-items: baseline;
  font-size: 12px; color: var(--text-dim); margin-top: 3px;
}
.chat-item .hit-role { flex: none; opacity: .7; }
.chat-item .hit-text {
  min-width: 0;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.chat-item mark {
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--text); border-radius: 3px; padding: 0 1px;
}
/* A result is several lines tall, so the row must not centre them. */
.chat-item:has(.hit) { align-items: flex-start; }
.chat-item:has(.hit) .kebab, .chat-item:has(.hit) .chat-mode { margin-top: 2px; }

/* Chat folders: a heading you can collapse, with its own summary on the right. */
.folder-head {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 7px 9px; margin: 6px 0 2px;
  background: none; border: 0; border-radius: 10px;
  color: var(--text-dim); font: inherit; font-size: 12.5px;
  letter-spacing: .02em; text-transform: uppercase; cursor: pointer;
}
.folder-head:hover { background: var(--bg-elev-2); }
/* Only the chevron turns. A devbox folder also carries a machine icon, and
   rotating that as well made the whole heading look broken when collapsed. */
.folder-head > svg:first-child {
  transform: rotate(-90deg); transition: transform .15s ease; flex: none;
}
.folder-head > svg { flex: none; }
.folder-head.open > svg:first-child { transform: none; }
.folder-name { flex: 1; min-width: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.folder-count { font-variant-numeric: tabular-nums; text-transform: none; }
.folder-body { padding-left: 8px; border-left: 2px solid var(--line); margin-left: 6px; }
.folder-sep {
  padding: 10px 9px 4px; font-size: 12.5px; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: .02em;
}
/* Name a new folder without leaving the picker. */
/* An input with its action button beside it. */
.folder-new, .input-row { display: flex; gap: 8px; align-items: center; }
.folder-new input, .input-row input { flex: 1; }
.folder-new .btn, .input-row .btn { flex: none; }
/* A remembered shell command reads as one, and wraps instead of running off
   the side of a phone -- these are long far more often than they are short. */
.shell-cmd {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px; overflow-wrap: anywhere;
}
.shell-history-item {
  display: flex; align-items: center; margin-bottom: 6px;
  border-radius: 12px; background: var(--bg-elev-2);
  border: 1px solid transparent;
}
.shell-history-item .sheet-item {
  flex: 1; min-width: 0; width: auto; margin: 0;
  background: transparent; border: 0;
}
.shell-history-delete {
  flex: none; margin: 6px 6px 6px 0; color: var(--text-dim);
}
.shell-history-delete:disabled { opacity: .45; }
/* Restore/delete sit at the end of an archived row, with room for a thumb. */
#sheet-body .chat-item { cursor: default; }
#sheet-body .chat-item .meta { cursor: pointer; }
#sheet-body .chat-item .icon-btn { padding: 6px; color: var(--text-dim); }
#sheet-body .chat-item .icon-btn.danger { color: var(--err); }

/* ------------------------------------------------------------------- sheet */

.sheet { position: fixed; inset: 0; z-index: 60; display: flex; align-items: flex-end; }
.sheet::before { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.5); }
.sheet-panel {
  position: relative; width: 100%;
  max-height: 88dvh;
  background: var(--bg-elev);
  border-radius: 20px 20px 0 0;
  border-top: 1px solid var(--line);
  padding: 8px 16px calc(18px + var(--safe-bottom));
  display: flex; flex-direction: column;
  animation: sheet-up .24s cubic-bezier(.32,.72,0,1);
}
@keyframes sheet-up { from { transform: translateY(100%); } }
.sheet-grip { width: 38px; height: 4px; border-radius: 999px; background: var(--line); margin: 4px auto 6px; }
.sheet-head { display: flex; align-items: center; justify-content: space-between; }
.sheet-head h3 { margin: 0; font-size: 17px; }
.sheet-body { overflow-y: auto; -webkit-overflow-scrolling: touch; padding-top: 8px; }
/* The Model sheet's OK: it stays in view over whatever scrolls beneath it. */
.sheet-ok-bar {
  position: sticky; bottom: 0; z-index: 1;
  padding-top: 12px; background: var(--bg-elev);
}

.sheet-group { margin-bottom: 16px; }.sheet-group > h4 {
  margin: 0 0 8px; font-size: 12px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-dim);
}
/*
 * Heading for one run of built-in MCP tools. Inside the list rather than
 * above it, because the switch beside it acts on that run only.
 */
.file-row.mcp-group {
  padding-top: 12px; padding-bottom: 4px;
  font-size: 11px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-dim);
}
.sheet-item {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 13px 12px; border-radius: 12px;
  background: var(--bg-elev-2); border: 1px solid transparent;
  color: var(--text); font: inherit; font-size: 15px;
  text-align: left; cursor: pointer; margin-bottom: 6px;
}

/* A command to copy. Wraps rather than scrolls: on a phone a long URL that
   runs off the side cannot be read, and this is meant to be read as well as
   copied. */
.code-copy {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 10px 10px 10px 12px; margin-bottom: 8px;
  border-radius: 12px; background: var(--bg-elev-2);
  border: 1px solid var(--border);
}
.code-copy .code-line {
  flex: 1; margin: 0; font-size: 12.5px; line-height: 1.5;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre-wrap; overflow-wrap: anywhere; user-select: all;
}
.code-copy .icon-btn { flex: none; margin-top: -2px; }
.sheet-item.sel { border-color: var(--accent); }
/* A row the chat's mode has already decided. Dimmed rather than hidden -- the
   answer is still worth reading, it is just not this sheet's to change -- and
   the cursor says so before a tap finds out. */
.sheet-item.locked { opacity: .62; cursor: default; }
.sheet-item.locked > .ico:last-child { flex: none; opacity: .8; }
.sheet-item .grow { flex: 1; min-width: 0; }
.sheet-item > .ico { flex: none; opacity: .75; }
.sheet-item.sel > .ico { opacity: 1; color: var(--accent); }

/* Chat / session identity in the manage sheet. */
.id-box {
  border: 1px solid var(--line); border-radius: 12px;
  padding: 8px 10px; margin-bottom: 10px; background: var(--bg-elev);
}
.id-row { display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; }
.id-row + .id-row { margin-top: 4px; }
.id-label { color: var(--text-dim); flex: none; min-width: 52px; }
.id-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  cursor: pointer; -webkit-user-select: all; user-select: all;
}
.id-hint { color: var(--warn); flex: none; font-size: 11.5px; }
.sheet-item .desc { font-size: 12.5px; color: var(--text-dim); margin-top: 2px; }
/* The Model sheet's one switch: a label and a line, in as little height as a
   switch needs -- the sheet is already taller than a phone. */
.model-switch-group { margin-bottom: 0; }
.model-switch-group .sheet-item { padding: 8px 12px; margin-bottom: 0; font-size: 14px; }
.model-switch-group .sheet-item .desc { font-size: 12px; margin-top: 1px; }
.sheet-item.danger { color: var(--err); }

.switch { width: 44px; height: 26px; border-radius: 999px; background: var(--line); position: relative; flex: none; transition: background .15s; }
.switch::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%; background: #fff;
  transition: transform .15s;
}
.switch.on { background: var(--ok); }
.switch.on::after { transform: translateX(18px); }

.file-row { display: flex; align-items: center; gap: 10px; padding: 11px 8px; border-bottom: 1px solid var(--line); font-size: 14px; }
.file-row .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-row .size { font-size: 12px; color: var(--text-dim); flex: none; }
.file-row .ico { flex: none; opacity: .7; }
/* What a server would run is the thing being allowed, so it wraps rather than ending in an ellipsis. */
.file-row.secret-server .grow { white-space: normal; }
.file-row .secret-command { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; }
.file-row .secret-warn { color: var(--warn); }

/* Four icon-only actions still have to fit beside a long filename. */
.file-acts { display: flex; align-items: center; gap: 0; flex: none; }
.file-act { min-width: 34px; min-height: 34px; padding: 6px; border-radius: 10px; color: var(--text-dim); }
.file-act:active { background: var(--bg-elev-2); color: var(--text); }
.file-act.danger { color: var(--err); }

/* ------------------------------------------------------------------ tasks */

/* A row in the Tasks sheet opens a page of its own, which the chevron says
   before the tap does. Turned to point right, the way a folder's points down. */
.task-row .sheet-item > .task-chev { flex: none; color: var(--text-dim); opacity: .6; transform: rotate(-90deg); }

/* A task's own page. The run card is the same one the chat draws, open and
   with nothing left to fold: its title wraps rather than ending in an
   ellipsis, which is the point of opening it, and it gets more of the screen. */
.task-detail .run-block { margin-bottom: 12px; }
.task-detail .run-pill { max-width: 100%; }
.task-detail .run-pill > .card-head { cursor: default; }
.task-detail .run-pill > .card-head .title { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
.task-detail .run-pill > .card-head .chev { display: none; }
.task-detail .run-pill > .run-body { max-height: 46vh; }
.task-name { font-size: 16px; font-weight: 600; line-height: 1.35; margin: 2px 0 10px; overflow-wrap: anywhere; }
.details-toggle > .task-chev { flex: none; color: var(--text-dim); transform: rotate(-90deg); transition: transform .15s ease; }
.details-toggle.open > .task-chev { transform: none; }
/* Facts about a task: the identity box's shape, in ordinary type and allowed
   to wrap -- these are sentences, not ids to copy. */
.id-box.facts .id-label { min-width: 96px; }
.id-box.facts .id-row { align-items: flex-start; }
.id-box.facts .id-value { font-family: inherit; font-size: 12.5px; white-space: normal; overflow-wrap: anywhere; cursor: default; }
.task-text {
  background: var(--bg-elev-2); border-radius: 12px; padding: 10px 12px; margin-bottom: 10px;
  font-size: 13.5px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere;
}

/* Sub-agents still at work, under the chat in the list. Green, the colour
   the list already uses for "answering". */
.chat-item .agents-toggle {
  flex: none; display: inline-flex; align-items: center; gap: 1px;
  padding: 3px 4px 3px 5px; border: 0; border-radius: 999px;
  font: inherit; font-size: 12px; font-variant-numeric: tabular-nums; color: var(--ok);
  background: color-mix(in srgb, var(--ok) 13%, transparent); cursor: pointer;
}
.chat-item .agents-toggle > svg:last-child { transform: rotate(-90deg); transition: transform .15s ease; }
.chat-item .agents-toggle.open > svg:last-child { transform: none; }
.agent-rows {
  margin: -2px 0 6px 18px; padding-left: 8px;
  border-left: 2px solid color-mix(in srgb, var(--ok) 35%, var(--line));
}
.agent-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: 12px; margin-bottom: 2px;
  background: none; color: var(--text); font: inherit; text-align: left; cursor: pointer;
}
.agent-row:hover { background: var(--bg-elev-2); }
.agent-row > .ico { flex: none; color: var(--ok); }
.agent-row .meta { flex: 1; min-width: 0; }
.agent-row .name { font-size: 13.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.agent-row .sub { font-size: 12px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A sub-agent, drawn as a chat. Over the chat it belongs to and under the
   drawer and the sheets, so both still open over it. */
.agent-view { z-index: 30; background: var(--bg); }
#agent-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 52vw; }
.pill.subagent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.agent-view .topbar-spacer { width: 40px; flex: none; }
.agent-view .banner .linkish { padding: 0; font-size: 14px; }
.asked-by { font-size: 11.5px; color: var(--text-dim); text-align: right; margin: 0 4px 4px 0; }
.readonly-bar { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 2px 2px 2px 4px; }
.readonly-bar .muted { flex: 1; font-size: 13px; line-height: 1.4; }
.readonly-bar .agent-config { color: var(--text); font-weight: 600; }
.run-open-agent { display: block; margin: -2px 0 8px -4px; }

/* Workspace files Copilot mentions become one-tap links.
   `stretch` matters: with `center` the two halves size to their own content
   and the download pill ends up shorter than the name pill. */
.filelink-wrap {
  display: inline-flex; align-items: stretch;
  vertical-align: -0.15em; max-width: 100%;
}
.filelink, .filelink-dl {
  display: inline-flex; align-items: center;
  background: var(--bg-elev); border: 1px solid var(--line);
  padding: 1px 6px; line-height: 1.45;
  text-decoration: none;
}
.filelink {
  gap: 4px; border-radius: 7px 0 0 7px; border-right: 0;
  color: var(--accent); font-size: .93em;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filelink-dl { border-radius: 0 7px 7px 0; color: var(--text-dim); }
.filelink:active, .filelink-dl:active { background: var(--bg-elev-2); }
.filelink .ico, .filelink-dl .ico { flex: none; }
.file-list { max-height: 52dvh; overflow-y: auto; -webkit-overflow-scrolling: touch; }

.crumbs { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; padding-bottom: 8px; font-size: 13px; }
/* Downloads the folder the crumbs name. Pushed right so it never crowds a
   deep path, which wraps onto a second line rather than shrinking. */
.crumbs .file-act { margin-left: auto; }
.crumb {
  background: none; border: 0; color: var(--accent);
  font: inherit; font-size: 13px; cursor: pointer; padding: 3px 4px; border-radius: 6px;
}
.crumb.sel { color: var(--text); font-weight: 600; }
.crumb-sep { color: var(--text-dim); }

.file-actions { display: flex; gap: 8px; padding-bottom: 10px; }
.file-actions .btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 9px 10px; font-size: 13.5px; }
/* Three buttons have to survive a 393px screen without wrapping. */
.file-actions .btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Files or a whole folder, asked under the row rather than as a fourth button. */
.upload-choice { display: flex; gap: 8px; padding-bottom: 10px; }
.upload-choice .btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 9px 10px; font-size: 13.5px; }
.upload-progress { padding-bottom: 10px; }
.upload-progress .chip { width: 100%; justify-content: space-between; box-sizing: border-box; }
.cwd-note { margin-top: -4px; }

.usage-bar { height: 6px; border-radius: 999px; background: var(--bg-elev-2); overflow: hidden; margin-top: 6px; }
.usage-bar > i { display: block; height: 100%; background: var(--accent); }

/* Device-code display for Copilot sign-in */
.code-box {
  display: block; width: 100%;
  margin: 12px 0 6px;
  padding: 16px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 30px; font-weight: 600; letter-spacing: .14em;
  text-align: center;
  color: var(--text);
  background: var(--bg-elev-2);
  border: 2px dashed var(--line);
  border-radius: 14px;
  cursor: pointer;
  -webkit-user-select: all; user-select: all;
}
.code-box.copied { border-color: var(--ok); color: var(--ok); letter-spacing: normal; }
@media (max-width: 380px) { .code-box { font-size: 24px; } }

/* --------------------------------------------------------- preview window */
/*
 * A file or a forwarded server used to open in a new tab. Installed on the
 * Home Screen, iOS gives that tab no address bar and no back button, so a
 * previewed web app that navigates once cannot be left at all. The preview
 * runs in here instead, with a bar this app owns: back, reload, the real
 * browser, and a way out.
 */
.viewer {
  position: fixed; inset: 0; z-index: 70;
  display: flex; flex-direction: column;
  background: var(--bg);
}
.viewer-bar {
  display: flex; align-items: center; gap: 2px;
  padding: calc(var(--safe-top) + 6px) calc(var(--safe-right) + 6px) 6px calc(var(--safe-left) + 6px);
  background: var(--bg-elev);
  border-bottom: 1px solid var(--line);
}
.viewer-title {
  flex: 1; min-width: 0; text-align: center;
  font-size: 12.5px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.viewer .icon-btn[disabled] { opacity: .35; pointer-events: none; }
.screen-metrics { display: grid; gap: 6px; font-size: 13px; }
.screen-metric { display: flex; justify-content: space-between; gap: 12px; }
.screen-metric > span:last-child { text-align: right; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

/* The Usage sheet: two controls, three numbers, a chart and its legend. */
.usage-controls { display: grid; gap: 8px; margin-bottom: 12px; }
.usage-head { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
.usage-stat { background: var(--bg-elev); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; }
.usage-stat-value { font-weight: 600; font-variant-numeric: tabular-nums; }
/*
 * The chart is Chart.js, which sizes itself from its container, so the box has
 * to have a height of its own and be the positioned parent it asks for.
 * `touch-action` lets a finger that starts on the chart still scroll the sheet.
 */
.usage-chart { margin-bottom: 14px; }
.usage-unit { margin-bottom: 2px; }
.usage-chart-wrap { position: relative; height: 200px; }
.usage-canvas { touch-action: pan-y; }
.usage-chart-note { padding: 80px 0; text-align: center; }
.usage-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: 14px; }
.usage-swatch { flex: none; width: 10px; height: 10px; border-radius: 3px; }
.usage-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.usage-value { flex: none; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* Words rather than a glyph: "source" has no icon anybody reads at a glance. */
.viewer-text-btn {
  flex: none;
  background: transparent; border: 1px solid var(--line); border-radius: 8px;
  color: var(--text-dim); font: inherit; font-size: 12.5px;
  padding: 5px 10px; margin: 0 4px; cursor: pointer;
}
.viewer-text-btn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
/*
 * `Canvas`, not white, and not the app's background either.
 *
 * A text file is served as text/plain and drawn by the browser itself, which
 * gives it a *transparent* canvas and takes its text colour from the colour
 * scheme in force -- white on a phone in dark mode. Painting this element
 * white therefore put white text on white, which is the one thing a file
 * preview must not do. `--bg` is wrong in the other direction: the light
 * palette's white is right, but in light mode the dark half is never used, so
 * it cannot be hardcoded either.
 *
 * `Canvas` is the system background for whichever scheme is in force
 * (inherited here from the root's `color-scheme: dark light`), so it always
 * matches the text colour the browser chose. A previewed dev server is
 * unaffected: an HTML document paints its own opaque canvas -- measured, a
 * page setting no colours at all still renders black on white behind this.
 */
.viewer-frame { flex: 1; width: 100%; border: 0; background: Canvas; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* A note shortened to one line, and the bubble its info button opens. The
   button sits in the text's own line, so it wraps with it rather than taking a
   column of its own beside a paragraph that is no longer there. */
.info-note .info-short { vertical-align: middle; }
.info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  vertical-align: middle;
  /* A 15px glyph in a 28px target: small to look at, big enough to hit. */
  width: 28px; height: 28px; margin: -7px -6px -7px -4px; padding: 0;
  border: 0; background: none; color: var(--accent); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.info-btn[aria-expanded="true"] { color: var(--text); }
.info-bubble {
  position: fixed; z-index: 1000;
  max-width: 320px; padding: 10px 12px;
  background: var(--bg-elev-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, .28);
  font-size: 13px; line-height: 1.45; text-align: left;
  white-space: normal; overflow-wrap: anywhere;
}
/* Scrolled inside, so the arrow above is not clipped with it. */
.info-bubble .info-scroll { max-height: 60vh; overflow-y: auto; }
.info-bubble .info-text { white-space: pre-line; }
.info-bubble::before {
  content: ''; position: absolute; top: -6px; left: calc(var(--arrow-x, 50%) - 6px);
  width: 10px; height: 10px; background: inherit;
  border-left: 1px solid var(--line); border-top: 1px solid var(--line);
  transform: rotate(45deg);
}
.info-bubble.above::before {
  top: auto; bottom: -6px; transform: rotate(225deg);
}
.info-bubble .usage-row { display: flex; gap: 12px; justify-content: space-between; }
.info-bubble .usage-row + .usage-row { margin-top: 4px; }
.info-bubble .usage-row .usage-cost { font-variant-numeric: tabular-nums; white-space: nowrap; }
.info-bubble .usage-kind { color: var(--text-dim); }

/* The footer's model and its breakdown button, kept together so the button
   is not pushed to the far end of the row beside Copy. */
.reply-meta-box {
  display: flex; align-items: center; gap: 2px;
  flex: 1 1 auto; min-width: 0; margin-right: auto;
}
.reply-meta-box > .reply-meta { flex: 0 1 auto; margin-right: 0; }
.reply-meta-box > .info-btn { flex: none; margin: -7px 0; }
