/* Portal Workbench — tokens and layout lifted from the design doc.
   Fonts are local-first on purpose: the app must render identically with the
   network off, so no webfont fetch is allowed to block first paint. */

:root {
  --bg: #08090b;
  /* A pane holding no thread. One shade under --bg so an unassigned pane reads
     as a hole in the window rather than as a thread that has not loaded yet. */
  --bg-empty: #040507;
  --card: #0d1014;
  --chrome: #0b0e12;
  --line: #1e242c;
  --line-strong: #2a323d;
  --input: #141922;
  --text: #e6edf3;
  --dim: #8b949e;
  --faint: #4a5058;
  --body: #b9c2cc;
  --green: #7ee787;
  --amber: #e3b341;
  --red: #f85149;
  --violet: #d2a8ff;
  --blue: #79c0ff;
  --orange: #ffa657;
  --cyan: #56d4dd;
  --pink: #f778ba;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', monospace;
  --sans: 'Space Grotesk', system-ui, -apple-system, sans-serif;

  /* Selection and focus are neutral elevation, not colour. Green borders around
     grey text read as an alert rather than as "this one is chosen", and they
     competed with the status dots, which are the things that genuinely mean
     something. Colour is now reserved for meaning: status, diffs, links. */
  --sel-bg: #171d26;
  --sel-line: #3f4a59;
  --sel-ring: rgba(230, 237, 243, .09);

  /* The explorer's git colours, VS Code Dark Modern's gitDecoration.*. */
  --git-modified: #E2C08D;
  --git-untracked: #73C991;
  --git-added: #81B88B;
  --git-deleted: #C74E39;
  --git-ignored: #8C8C8C;
}

/* VS Code's Seti file icons, from this origin. `block` keeps the private-use
   characters from showing as boxes while the font loads. */
@font-face {
  font-family: 'seti';
  src: url('/seti.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  overscroll-behavior: none;
}

body { -webkit-tap-highlight-color: transparent; }

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes rise { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes rot { to { transform: rotate(360deg); } }

::-webkit-scrollbar { width: 3px; height: 0; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(139, 148, 158, .28); border-radius: 2px; }
[data-nobar]::-webkit-scrollbar { display: none; width: 0; height: 0; }
[data-nobar] { scrollbar-width: none; }

button, textarea, input { font-family: inherit; color: inherit; }
button { cursor: pointer; }

/* ---------- frame ---------- */

#frame {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100dvh;
  background: var(--bg);
  overflow: hidden;
  padding-bottom: env(safe-area-inset-bottom);
}

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

/* The chrome floats over the transcript, which scrolls underneath both bands and
   fades out into their scrims. The bands are pointer-transparent so the fade tail
   never eats a tap on the text showing through; every widget inside re-enables
   itself. Sheets (z 30) and the toast (z 40) still cover them. */
.overhead, .underfoot {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  pointer-events: none;
  /* Legibility without surfaces: every glyph on the band carries its own halo,
     so labels win against the text ghosting through — no local wash anywhere.
     A tight solid ring plus a wide soft one: the ring cuts the glyph out of any
     ghost line directly beneath it, the soft one dims the neighbourhood. */
  text-shadow: 0 0 2px rgba(8, 9, 11, 1), 0 0 8px rgba(8, 9, 11, .95), 0 1px 2px rgba(8, 9, 11, .95);
}

/* text-shadow never reaches SVG — icons (brand marks, steer wheel, send) need
   the same cut-out treatment as the labels beside them. */
.overhead svg, .underfoot svg {
  filter: drop-shadow(0 0 2px rgba(8, 9, 11, 1)) drop-shadow(0 0 5px rgba(8, 9, 11, .9));
}

/* …but only icons on the bare band ground. Widgets that carry their own surface
   opt out, or the armed send button smears a dark glow around its arrow on a
   light chip. */
.composer svg, .tobottom svg { filter: none; }

.overhead > *, .underfoot > * { pointer-events: auto; }

/* One continuous fade per band, by request: 75% at the transcript edge, where the
   innermost row sits (the todo strip up top, the subagents strip or status line
   below), climbing without a step to solid at the tabs and at the composer. No
   fade padding: a strip of half-faded transcript between that row and the
   transcript read as a gap. No row washes itself: a second layer would stack on
   this one and leave a step. */
.overhead {
  top: 0;
  background: linear-gradient(to top,
    rgba(8, 9, 11, .75) 0px, rgba(8, 9, 11, .9) 50%, rgba(8, 9, 11, 1) 100%);
}

.underfoot {
  bottom: 0;
  background: linear-gradient(to bottom,
    rgba(8, 9, 11, .75) 0px, rgba(8, 9, 11, .9) 50%, rgba(8, 9, 11, 1) 100%);
}

/* ---------- top bar ---------- */

.topbar {
  flex: 0 0 42px;
  height: 42px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  background: transparent;
}

.tabstrip {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: stretch;
  gap: 0;
  height: 38px;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The strip is a desktop tab bar at every width: the lane owns the tab height,
   square tabs meet on a hairline, and selection is a filled rectangle edged at the
   sides. The phone keeps the same shape. */
.tab {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 8px 0 12px;
  background: transparent;
  border: 0;
  border-left: 1px solid transparent;
  border-right: 1px solid var(--line-strong);
  border-radius: 0;
  color: var(--dim);
  font: 500 10.5px var(--mono);
  font-size: 12px;
  max-width: 240px;
}

.tab .label { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tab .tabicon { flex-basis: 13px; width: 13px; height: 13px; }

.tab:hover { color: var(--text); background: rgba(230, 237, 243, .05); }

.tab.on {
  color: var(--text);
  font-weight: 600;
  background: var(--sel-bg);
  border-left-color: var(--sel-line);
  border-right-color: var(--sel-line);
}

/* The selected tab's colour is the full-width bottom edge of the desktop shape. */
.tab.on::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 0;
  background: var(--tab-colour, var(--violet));
}

/* A tab's colour, from /colour or the cog. The name is the data attribute; the
   shade is here, shared by the tab's bar and the composer's edge. */
[data-colour="red"] { --tab-colour: var(--red); }
[data-colour="orange"] { --tab-colour: var(--orange); }
[data-colour="yellow"] { --tab-colour: var(--amber); }
[data-colour="green"] { --tab-colour: var(--green); }
[data-colour="cyan"] { --tab-colour: var(--cyan); }
[data-colour="blue"] { --tab-colour: var(--blue); }
[data-colour="purple"] { --tab-colour: var(--violet); }
[data-colour="pink"] { --tab-colour: var(--pink); }

.tab:active { color: var(--text); background: rgba(230, 237, 243, .07); border-radius: 0; }

/* The tab being dragged: lifted off the strip so it reads as picked up, and above
   the ones it is being pulled past. touch-action is what stops the strip scrolling
   under the finger for the rest of the gesture. */
.tab.dragging {
  z-index: 2;
  color: var(--text);
  background: rgba(230, 237, 243, .12);
  border-radius: 0;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
  touch-action: none;
}
.tab .dot, .dot {
  flex: 0 0 5px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #2b3138;
}
.dot.working { background: var(--amber); animation: pulse 1.4s ease-in-out infinite; }
/* Background agents still running in the tab: a hollow ring when the turn itself is
   over, a halo around the working dot when it is not. Outlines, so neither needs to
   know the colour it sits on — the strip, the drawer and a selected row all differ. */
.dot.orbit { background: transparent; outline: 1.5px solid var(--amber); outline-offset: -.5px; animation: pulse 1.4s ease-in-out infinite; }
.dot.working.ring { outline: 1.5px solid rgba(227, 179, 65, .55); outline-offset: 1.5px; }
.agents-n { flex: 0 0 auto; font: 600 9px var(--mono); color: var(--amber); }
.tab .agents-n { margin-left: -1px; }
.dot.waiting { background: var(--red); animation: pulse 1s ease-in-out infinite; }
.dot.idle { background: #2b3138; }
.dot.done { background: var(--green); }
.dot.failed, .dot.exited { background: var(--red); }

/* Borderless like the tabs it sits beside, and drawn rather than typed: a text + is
   whatever the mono face has at that codepoint. It keeps the full chip footprint —
   as a 22px sliver it was the smallest target on the screen — and --dim, because
   --faint on the chrome scrim was about 2:1. */
/* The cog is the same button on the other side of the badge — same slot, same mark
   size, same idle colour, same press — so it is the same rule, not a copy of it. */
.new-tab, .cog, .files, .update-ready, .install {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 34px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  padding: 0;
}

.new-tab svg, .cog svg, .files svg, .update-ready svg, .install svg { width: 18px; height: 18px; display: block; }

.new-tab:active, .cog:active, .files:active, .update-ready:active, .install:active { background: rgba(230, 237, 243, .07); color: var(--text); }

#update-ready[hidden] { display: none; }

/* The strip shrinks to its tabs, so the plus lands after the last one; this margin
   takes the rest of the row and keeps Install and the update arrow at the right. */
.topbar #new-tab { margin-right: auto; }

/* It had no flex-shrink, so the tab strip squeezed a 7px dot down to a sliver at the
   screen edge. Fixed size, a ring to lift it off the chrome, and no pulse while
   connected — a healthy state that blinks is noise, and dimming to 30% for half of
   every cycle is what made it hard to catch on a phone. */
.link-dot {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 0 2px rgba(126, 231, 135, .18), 0 0 6px rgba(126, 231, 135, .5);
}

/* Losing the connection is the state worth interrupting for, so that one is loud:
   amber, pulsing, and labelled rather than a dot you have to decode. */
.link-dot.off {
  width: auto;
  height: 18px;
  padding: 0 7px;
  border-radius: 9px;
  display: inline-flex;
  align-items: center;
  background: rgba(227, 179, 65, .16);
  border: 1px solid rgba(227, 179, 65, .5);
  color: var(--amber);
  font: 700 8.5px var(--mono);
  letter-spacing: .06em;
  box-shadow: none;
  animation: pulse 1.6s ease-in-out infinite;
}

/* ---------- agent meters ---------- */

/* No border of its own: the three header rows share one chrome surface and one edge
   at the bottom of the strip below, instead of reading as three stacked bands. */
.agents {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
  padding: 3px 10px 4px;
  background: transparent;
}

/* Top-aligned, with a floor of two meter rows: codex reports one usage window and
   claude reports two, so centring a lone meter left the two cards' first rows on
   different baselines and the pair looked broken rather than merely different. */
.agent {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  /* Two 8px rows + 1px gap + 4px padding either side: the meter stack matches the
     17px brand mark, so the floor follows it down. */
  min-height: 25px;
  /* Room under the meters for the bar, which is why the padding is no longer even. */
  padding: 4px 6px 6px;
  border-radius: 8px;
  /* Flat on the scrim — widgets stopped wearing card boxes, and selection is a bar
     underneath rather than an edge around. */
  position: relative;
  background: transparent;
  border: 0;
  text-align: left;
}

/* The rail's bar again, and the mark going from .55 to full opacity beside it: two
   signals, one of which survives sunlight. Sized to the brand mark rather than to the
   card — a 2px rule across 190px of meters reads as a divider between the rows, not
   as a selection. */
.agent.on::after {
  content: '';
  position: absolute;
  left: 6px;
  width: 22px;
  bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--violet);
}

.agent:active { background: rgba(230, 237, 243, .07); }

.agent .icon {
  /* Optical, not geometric: the mark lines up with the first meter row rather than
     with the top of the card. */
  margin-top: 1px;
  flex: 0 0 17px;
  width: 17px;
  height: 17px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  opacity: .55;
}

.agent.on .icon { opacity: 1; }

/* The brand marks are SVG symbols painted with currentColor, so the icon box
   sets the size and the inline colour tints them. Padding keeps the Claude
   starburst from touching the selected-state background. */
.icon svg { width: 100%; height: 100%; display: block; }
.icon img, .tabicon img, .wsicon img, .smark img, .brandmark img, .agentmark img {
  width: 100%; height: 100%; display: block; object-fit: contain;
}
.agent .icon svg { padding: 1px; box-sizing: border-box; }
.agent .icon img { padding: 1px; box-sizing: border-box; border-radius: 3px; }
/* 8px rows and a 1px gap: together the two rows stand exactly as tall as the 17px
   brand mark beside them, instead of overshooting it by half again. */
.agent .meters { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.agent .meter { display: flex; align-items: center; gap: 5px; height: 8px; }
.agent .meter .name { flex: 0 0 14px; white-space: nowrap; font: 600 7.5px var(--mono); color: var(--dim); }
.agent .meter .name.weekly { color: var(--faint); }
.agent .meter .pct { flex: 0 0 auto; min-width: 3ch; font: 600 7.5px var(--mono); color: var(--dim); white-space: nowrap; }
/* flex:1 with no cap, as the portal does it: the bar was pinned to 54px and left most
   of the row empty, which wastes the whole point of a meter. */
.agent .meter .bar { flex: 1 1 auto; min-width: 0; height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.agent .meter .bar span { display: block; height: 100%; border-radius: 3px; }
.agent .meter .reset { flex: 0 0 auto; min-width: 36px; font: 500 7.5px var(--mono); color: var(--faint); text-align: right; white-space: nowrap; }

/* ---------- mode + tray strip ---------- */

.modestrip {
  flex: 0 0 34px;
  height: 34px;
  display: flex;
  align-items: center;
  gap: 0;
  padding: 0 6px;
  background: transparent;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The cog closes the rail at the right edge. */
.modestrip .rail-end { margin-left: auto; }

/* No track: in a rail the mode icons are two more icons, and the divider after them
   is what says the first two are a pair. A well around them would put a box back into
   the one row that just lost all of its boxes. */
.segmented {
  flex: 0 0 auto;
  display: flex;
  gap: 0;
  padding: 0;
  background: transparent;
  border: 0;
}

/* An icon rail, the way an editor does it: no labels anywhere, and the active control
   marked by a bar under it rather than a box around it. The names live in title and
   aria-label — the only places a collapsed label can still be read, by a pointer and
   by a screen reader.
   The bar alone is not enough on a phone in daylight, so the active icon also goes
   from grey to full white: two signals, and one of them survives sunlight. */
.chip {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 34px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #6b7480;
  font: 600 10px var(--mono);
  white-space: nowrap;
}

.chip .cg {
  flex: 0 0 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* Every mark is drawn on the shared 24x24 grid with ~2 units of padding, so 20px here
   is about 16.7px of ink and a 1.7px stroke - the same two numbers for every icon in
   the rail, which is what stops one from reading larger or heavier than the next. */
.chip .cg svg { width: 20px; height: 20px; display: block; }

/* Uniform stroke weight is handled per symbol in index.html, not here: `vector-effect`
   is not an inherited property, and outer CSS cannot reach the shadow content a <use>
   clones, so a rule at this level would silently do nothing. */

/* The agent mark is a filled starburst, not a 2/24 outline, so it carries far more ink
   at the same size. Four pixels down is the optical correction. */
.chip .cg.brandmark svg { width: 16px; height: 16px; }

/* Never shown in the rail. Kept in the DOM, and kept out of the accessibility tree by
   aria-label on the button, so nothing reads the name twice. */
.chip .cl { display: none; }
.chip.dim { color: #6b7480; }
.chip.on { color: var(--text); }

.chip.on::after {
  content: '';
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--violet);
}

.chip:active { color: var(--text); background: rgba(230, 237, 243, .07); border-radius: 9px; }

/* Hidden until the browser says the app is installable — a button that cannot work is
   worse than no button, the same rule the steer button follows. Blue because it is the
   one control here that is an offer rather than a switch. */
/* Same rule as install: a bell that cannot ring — plain http, or iOS before the app
   is on the home screen — is worse than no bell. On, it is an ordinary switch. */
.chip.notify { display: none; }
.chip.notify.show { display: inline-flex; }
/* In the top bar beside the plus, not in the mode strip: the composer's strip is
   hidden while a tab runs, and this is the one control whose whole audience is
   someone who has not installed the app yet. Blue because it is an offer rather
   than a switch. */
.install { display: none; }
.install.show { display: inline-flex; color: var(--blue); }
/* Separates the mode you work in from the utilities beside it. Shorter than the
   controls and dimmer than a border, so it groups without becoming a third element
   competing with the glyphs. */
.divider { flex: 0 0 1px; height: 16px; background: var(--line); margin: 0 6px; }

/* Keyboard focus was invisible on every control except the composer. One rule, on
   :focus-visible only, so a thumb tap never draws a ring it did not ask for. */
button:focus-visible, select:focus-visible {
  outline: 2px solid var(--sel-line);
  outline-offset: 1px;
}

/* ---------- plan strip ---------- */

.planstrip {
  flex: 0 0 auto;
  display: none;
  background: transparent;
}

/* Bare on the band: its fade is 75% where this row starts. */
.planstrip.show { display: block; }
.plan-summary,
.subs-summary {
  width: 100%;
  border: 0;
  border-radius: 0;
  /* Bare content on the band's one faded surface; the halo up on the band
     rule is what keeps the label legible. */
  background: transparent;
  color: inherit;
  display: flex;
  align-items: center;
  gap: 7px;
  text-align: left;
}
.plan-summary { min-height: 29px; padding: 5px 10px; }
.planstrip .dots { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
.planstrip .pd { font: 600 10px var(--mono); color: var(--faint); }
.planstrip .pd.done { color: var(--green); }
.planstrip .pd.now { color: var(--amber); animation: pulse 1.4s ease-in-out infinite; }
/* Past twenty dots the row runs out of width, so the rest are counted instead. */
.planstrip .pd.more { color: var(--faint); }
.planstrip .now-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 500 10px var(--mono);
  color: var(--dim);
}
.planstrip .count { flex: 0 0 auto; font: 600 9px var(--mono); color: var(--faint); }
.disclosure {
  flex: 0 0 auto;
  color: var(--faint);
  font: 700 13px/1 var(--mono);
  transform: rotate(0);
  transition: transform 140ms ease;
}
.expanded .disclosure { transform: rotate(180deg); }

/* Expanded details are more content on the same faded surface — the band grows
   and the fade grows with it. */
.plan-details,
.subs-details {
  max-height: min(22vh, 200px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.plan-details[hidden],
.subs-details[hidden] { display: none; }
.plan-details {
  padding: 2px 10px 8px;
  /* Room for the inspectors: a step that opens its detail and note needs more
     than the four rows the old height left. It still scrolls inside itself. */
  max-height: min(40vh, 360px);
}
/* A row with something to inspect is a <button>, one with nothing is a <div>.
   Both draw the same, so the reset comes first and the grid applies to either. */
.plan-detail {
  width: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
  text-align: left;
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) auto auto;
  align-items: start;
  gap: 6px;
  padding: 5px 0;
  color: var(--dim);
  font: 500 10px/1.4 var(--mono);
}
/* Nothing to the right of the text on a row that does not open, and the grid's
   gaps for those two tracks would otherwise wrap the text 12px early. */
.plan-detail.bare { grid-template-columns: 14px minmax(0, 1fr); }
.plan-detail .detail-state { color: var(--faint); text-align: center; }
.plan-detail.done .detail-state { color: var(--green); }
.plan-detail.now { color: var(--text); }
.plan-detail.now .detail-state { color: var(--amber); }
/* The clock on the right: a duration once the step is finished, the elapsed while
   it runs, nothing before it starts. Tabular figures so a column of them is still
   a column when the minutes tick over. */
.plan-detail .detail-meta {
  color: var(--faint);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.plan-row.open .plan-detail .detail-meta { color: var(--dim); }
/* The only thing on the row that says it opens. Same turn and the same 140ms as
   the strip's own disclosure, a quarter turn instead of a half because it starts
   on its side. */
.plan-detail .detail-cue {
  color: var(--faint);
  font: 700 11px/1.2 var(--mono);
  transform: rotate(0);
  transition: transform 140ms ease;
}
.plan-row.open .plan-detail .detail-cue { transform: rotate(90deg); }

/* What the row opens: the step's times and whatever the agent wrote about it.
   Indented under the glyph column, with the rule tying it back to its row. */
.plan-inspect {
  margin: 0 0 6px 6px;
  padding: 1px 0 1px 13px;
  border-left: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 3px;
  font: 500 10px/1.45 var(--mono);
}
.plan-inspect .ins-line { display: flex; gap: 6px; align-items: baseline; }
.plan-inspect .ins-label { flex: 0 0 auto; min-width: 52px; color: var(--faint); }
.plan-inspect .ins-value {
  flex: 1;
  min-width: 0;
  color: var(--dim);
  /* A note is written by an agent and may carry its own line breaks. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---------- thread ---------- */

.thread {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Headroom measured by app.js: the bands are overlays, so the scroller keeps
     its own clearance and the last message always rises above the composer. */
  padding: var(--overhead-h, 150px) 10px var(--underfoot-h, 130px);
  display: flex;
  flex-direction: column;
  gap: 18px;
  -webkit-overflow-scrolling: touch;
  /* Vertical stays the browser's; horizontal belongs to the tab-swipe tracker,
     which can then follow the finger without a single preventDefault. */
  touch-action: pan-y;
}

/* Content is just text on the page: the head row and the thread's spacing do
   the separating that box edges used to. Only actionable alerts keep a box. */
.item {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 1px;
}

/* An approval is a widget you act on, not content you read past — it keeps a
   full box of its own now that the base item has none. */
.item.approval {
  padding: 10px 11px;
  background: var(--card);
  border: 1px solid rgba(248, 81, 73, .27);
  border-radius: 12px;
}
.item.diff { cursor: pointer; }

.item .head { display: flex; align-items: center; gap: 7px; }

.tag {
  flex: 0 0 auto;
  padding: 3px 6px;
  border-radius: 5px;
  font: 700 9px var(--mono);
  letter-spacing: .06em;
  background: var(--input);
  color: var(--dim);
}

.tag.user { background: rgba(126, 231, 135, .13); color: var(--green); }
.tag.assistant { background: rgba(126, 231, 135, .13); color: var(--green); }
.tag.reasoning { background: rgba(210, 168, 255, .13); color: var(--violet); }
.tag.diff { background: rgba(227, 179, 65, .13); color: var(--amber); }
.tag.approval { background: rgba(248, 81, 73, .13); color: var(--red); }
.tag.done { background: rgba(126, 231, 135, .13); color: var(--green); }
.tag.error { background: rgba(248, 81, 73, .13); color: var(--red); }
.tag.queued { background: rgba(227, 179, 65, .13); color: var(--amber); }
.tag.subagent { background: rgba(210, 168, 255, .13); color: var(--violet); }
.tag.tool { background: rgba(121, 192, 255, .13); color: var(--blue); }

.item .meta {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 500 10px var(--mono);
  color: var(--faint);
}

.item .count { flex: 0 0 auto; font: 500 9.5px var(--mono); color: var(--faint); }

/* Actions in a card's header row — steer, and drop. Both used to inherit chrome
   meant for something else: the dismiss ✕ was styled with `.count`, which assumes
   a text span, so as a button it kept the browser's default face and became a grey
   glyph on a grey box. They are grey on purpose — colour in a card means status —
   but grey has to mean legible, and pressed has to be a shade you can see. */
.item .rowbtn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 24px;
  min-width: 24px;
  padding: 0 8px;
  border-radius: 7px;
  background: rgba(230, 237, 243, .05);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font: 600 9.5px var(--mono);
}

.item .rowbtn:active { background: rgba(230, 237, 243, .16); border-color: var(--sel-line); color: var(--text); }
/* Square, and a mark big enough to aim at with a thumb. */
.item .rowbtn.drop { padding: 0; }
.item .rowbtn.drop svg { width: 13px; height: 13px; }
.item .rowbtn.rowedit svg { width: 12px; height: 12px; margin-right: 4px; }
/* The promote button wears the same wheel as the composer's steer control. */
.item .rowbtn.rowsteer svg { width: 12px; height: 12px; margin-right: 4px; }

/* Prose in the proportional face, code in mono. Everything used to be mono, which
   is the single thing that made a conversation read as terminal output rather than
   as a conversation — the tags, timestamps and chrome stay mono on purpose. */
.item .body,
.streaming .body {
  font: 400 13.5px/1.55 var(--sans);
  color: var(--body);
  white-space: pre-wrap;
  word-break: break-word;
}

.item.user .body { font-size: 13.5px; color: var(--text); }

/* Anything that is literally code or machine output keeps the monospace face. */
.item .body.output, .item .body .code, .item .body code.inline, .item .body .dl { font-family: var(--mono); }
/* Sits at the right end of the item header. 30px keeps it thumb-sized without
   crowding the timestamp on a narrow screen.
   --dim, not --faint: #4a5058 on --input is 1.96:1, the same grey-on-grey the
   queued ✕ had. Matches .item .rowbtn, so every button that lives in a card
   header reads at the same weight. */
.item .head .copy {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 24px;
  border-radius: 7px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font: 400 12px var(--mono);
}

.item .head .copy svg { width: 14px; height: 14px; display: block; }

.item .head .copy:active { color: var(--text); border-color: var(--sel-line); background: var(--sel-bg); }

/* The diff button's page is grey like the copy button's; the + and − inside it
   take the diff line colours. */
#i-diff .ig-add { stroke: var(--green); }
#i-diff .ig-del { stroke: var(--red); }

.item .body.reasoning { color: var(--dim); }
.item .body.output { color: var(--faint); font-size: 11px; }

/* Only long cards are clamped, and only while collapsed. The fade says there is
   more without needing a label, and the button says what a tap will do. */
.item.clampable:not(.expanded) .body,
.item.clampable:not(.expanded) .difflines {
  max-height: 12em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, #000 68%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 68%, transparent 100%);
}

.item.clampable:not(.expanded) .body.output { max-height: 8.5em; }
.item.expanded .body, .item.expanded .difflines { max-height: none; }

/* The row above the windowed history: a chip, centred, because it stands alone
   between nothing and the oldest visible card. */
.more.earlier {
  align-self: center;
  height: 28px;
  padding: 0 14px;
  margin: 2px auto 8px;
  display: block;
  border-radius: 9px;
  background: var(--input);
  border: 1px solid var(--line);
  color: var(--dim);
  font: 600 10px var(--mono);
}

/* ---------- code + syntax ---------- */

/* Tabs are 8 columns by default, which wrecks any indented block on a phone. */
.item .body, .streaming .body, .prose, .code, .tl, .chipfile .fname { tab-size: 4; }

.prose { white-space: pre-wrap; word-break: break-word; }
.prose + .mdlist, .prose + .mdtable-wrap, .prose + .mdquote,
.mdlist + .prose, .mdtable-wrap + .prose { margin-top: 7px; }
.prose + .codeblock, .codeblock + .prose { margin-top: 7px; }

code.inline {
  font: 400 11.5px var(--mono);
  padding: 1px 4px;
  border-radius: 5px;
  background: var(--input);
  color: var(--blue);
  word-break: break-all;
}

.codeblock {
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--bg);
  overflow: hidden;
}

.codeblock .clang {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 4px 3px 9px;
  background: var(--input);
  border-bottom: 1px solid var(--line);
}

.codeblock .clang .name { flex: 1; font: 600 9px var(--mono); color: var(--faint); letter-spacing: .07em; text-transform: uppercase; }
.codeblock .clang .copy { width: 28px; height: 22px; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; background: transparent; border: 0; color: var(--faint); }
.codeblock .clang .copy svg { width: 13px; height: 13px; display: block; }
.codeblock .clang .run { width: 28px; height: 22px; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; background: transparent; border: 0; color: var(--faint); }
.codeblock .clang .run svg { width: 13px; height: 13px; display: block; }
.codeblock .clang .run:active { color: var(--text); }

.code {
  margin: 0;
  padding: 8px 9px;
  font: 400 11.5px/1.55 var(--mono);
  color: var(--text);
  /* Wrap rather than scroll: a clipped line on a phone is invisible, and there is no
     horizontal scrollbar to hint more exists. No hanging indent — text-indent applies
     once per block, not per line, so it left every line after the first pushed in. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Token colours. Same family as the terminal palette so a code block and the shell
   pane do not look like two different apps. */
/* Line numbers in a Read listing: present but receding, like every gutter. */
.output .gutter { color: var(--faint); }

.t.c { color: var(--faint); font-style: italic; }
.t.s { color: #a5d6ff; }
.t.n { color: #79c0ff; }
.t.k { color: #ff7b72; }
.t.b { color: var(--green); }
.t.fn { color: var(--violet); }
.t.v { color: #ffa657; }
.t.fl { color: var(--amber); }
.t.ky { color: #7ee7d0; }
.t.p { color: var(--dim); }
.t.add { color: var(--green); }
.t.del { color: var(--red); }

.plus { color: var(--green); }
.minus { color: var(--red); }
.counts { display: inline-flex; gap: 4px; }

/* What the turn changed, under its last reply. */
/* A button, because the footer opens the files popup — and it still has to look
   like the line of text it was. */
.item button.changes { width: 100%; padding: 0; background: none; border: 0; text-align: left; cursor: pointer; }
.item .changes { display: flex; align-items: center; gap: 6px; margin-top: 8px; font: 500 9.5px var(--mono); color: var(--faint); }
.item .changes svg { width: 11px; height: 11px; }
.t.hunk { color: var(--violet); }

/* Links need a tap target that survives a thumb without breaking the line box. */
a.link { color: var(--blue); text-decoration: underline; text-underline-offset: 2px; word-break: break-all; padding: 2px 0; }
a.link:active { color: var(--green); }

/* Sits right after a URL in terminal output. Small, because it repeats wherever a
   URL does, but with a 28px tap target hanging off the glyph — dragging selection
   handles across a URL that wraps three times is not a copy path on a phone. */
.linkcopy {
  position: relative;
  display: inline-block;
  margin-left: 3px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--blue);
  font: 400 11px/1 var(--mono);
  /* The mark is a block box inside an inline-block button, so the button's baseline
     is its bottom edge and the icon would ride above the line. -3px drops its centre
     onto the x-height of the URL beside it. */
  vertical-align: -3px;
  cursor: pointer;
}
.linkcopy::after {
  content: '';
  position: absolute;
  inset: -9px -8px;
}
.linkcopy svg { width: 12px; height: 12px; display: block; }
.linkcopy:active { color: var(--green); }

.difflines {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg);
}

/* Three columns: line number, marker, content. The marker having its own column is
   what lets the content be syntax-highlighted instead of read as +/- prefixed text,
   and it stops a wrapped line looking like an added one. */
.dl {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  padding: 2px 8px;
  font: 400 11px/1.55 var(--mono);
  color: var(--body);
}

.dl .ln {
  flex: 0 0 auto;
  min-width: 2.2em;
  text-align: right;
  color: var(--faint);
  user-select: none;
}

.dl .mk { flex: 0 0 auto; width: .8em; color: var(--faint); user-select: none; }
.dl .dtext { flex: 1; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* Tint the row, and let the tokens carry the colour of the code itself — the way a
   real diff view reads. */
.dl.add { background: rgba(126, 231, 135, .07); }
.dl.del { background: rgba(248, 81, 73, .07); }
.dl.add .mk { color: var(--green); }
.dl.del .mk { color: var(--red); }
.dl.hunk, .dl.meta { color: var(--faint); background: rgba(210, 168, 255, .05); }
.dl.hunk .dtext { color: var(--violet); }
.dl.more-lines { color: var(--faint); font-style: italic; }
/* Deleted content reads as past, not as a second kind of code. */
.dl.del .dtext { opacity: .75; }

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

.btn {
  height: 34px;
  padding: 0 13px;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  background: var(--input);
  color: var(--text);
  font: 600 11.5px var(--mono);
}

.btn.primary { border: 0; background: var(--green); color: #07080a; font-weight: 700; }
.btn.danger { color: var(--red); }

/* streaming card */

/* Bare like the reply it is about to become. */
.streaming {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 1px;
}

/* Fault card — the surface for a stopped/errored/early-ended turn. Reads as a
   problem (left rule + tinted head) without shouting; the whole card is one
   readable block with a single clear action. */
.fault {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 10px 11px;
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-left: 3px solid var(--red);
  border-radius: 12px;
}
.fault.warn { border-left-color: var(--amber); }
.fault .head { display: flex; align-items: center; gap: 7px; }
.fault .tag { font: 700 10px var(--mono); letter-spacing: .04em; color: var(--red); }
.fault.warn .tag { color: var(--amber); }
.fault .text { font: 400 12px/1.5 var(--mono); color: var(--text); white-space: pre-wrap; word-break: break-word; }
.fault .acts { display: flex; gap: 8px; }
.fault .act {
  height: 30px;
  min-height: 30px;
  padding: 0 14px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--text);
  font: 600 11px var(--mono);
  cursor: pointer;
}
.fault .act:active { background: var(--sel-bg); }

.streaming .head { display: flex; align-items: center; gap: 7px; }
.streaming .elapsed { flex: 1; font: 500 10px var(--mono); color: var(--faint); }
/* This is the only way to interrupt a running turn, and it was --faint on --card:
   #4a5058 on #0d1014, about 1.9:1. Bordered, legible and a real hit target — grey
   like the other card actions, since red here competed with the status dots that
   genuinely mean something. The contrast, not the hue, was the problem. */
.streaming .stopturn {
  flex: 0 0 auto;
  height: 24px;
  padding: 0 10px;
  border-radius: 7px;
  background: rgba(230, 237, 243, .05);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font: 600 10px var(--mono);
}

.streaming .stopturn:active { background: rgba(230, 237, 243, .16); border-color: var(--sel-line); color: var(--text); }

/* A running command's live stdout tail: the tool voice, not the prose one, and
   clamped — the durable tool card above carries the full output when it lands. */
.streaming pre.stream-raw {
  margin: 0;
  font: 400 10.5px/1.55 var(--mono);
  color: var(--dim);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 11em;
  overflow: hidden;
}

.cursor { animation: blink 1s step-end infinite; color: var(--green); }

/* Animated in CSS rather than by re-rendering: the render tick is 1s and delta
   events fire many times a second, so JS-driven frames would either crawl or
   never advance. The phase is set from the wall clock in app.js so a rebuilt
   node picks the animation up where the old one left off. */
.spinner {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  box-sizing: border-box;
  border: 1.5px solid var(--line-strong);
  border-top-color: var(--violet);
  border-radius: 50%;
  animation: rot .7s linear infinite;
}

.streaming.assistant .spinner { border-top-color: var(--green); }
.streaming.tool .spinner { border-top-color: var(--blue); }
.streaming.assistant { border-color: rgba(126, 231, 135, .22); }
.streaming.tool { border-color: rgba(121, 192, 255, .2); }
.streaming.reasoning { border-color: rgba(210, 168, 255, .2); }

/* RUNNING BASH, THINKING — a single line of status with nothing in it that the row
   above does not already say. It reads as a strip, like the plan and status bars,
   rather than as another message in the transcript. Must come after the tone rules
   above, which set the border it is dropping. Border kept at transparent rather than
   removed so the box does not shift by 2px when the first token turns it into a card.
   The stop button keeps its own chrome — it is the only way to interrupt a turn. */
.streaming.bare {
  /* Horizontal padding matched to a card's, so the tag pill stays on the same
     vertical line as every tag above it — losing the box should not cost the rhythm. */
  padding: 2px 11px;
  background: transparent;
  border-color: transparent;
}

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: pulse 1.4s ease-in-out infinite; }
  /* The chrome's blinkers stop. Every one of them says what it means in colour or in
     words as well — amber is working, the offline pill is labelled — so the motion was
     the emphasis, not the message.
     Listed at the same specificity as the rules they override, since a media query
     adds none: a bare `.dot` would lose to `.dot.working` and quietly do nothing. */
  .dot.working, .dot.waiting, .link-dot.off, .offline-banner .dot,
  .planstrip .pd.now, .subs .label .dot, .subs-details .dot:not(.done):not(.failed) { animation: none; }
  /* The drawer still opens and closes; it just arrives rather than slides. */
  .drawer.settling .drawer-panel, .drawer.settling .drawer-scrim { transition: none; }
}

.empty { margin: auto; text-align: center; color: var(--faint); font: 400 12px/1.7 var(--mono); padding: 20px; }

/* ---------- offline banner ---------- */

.offline-banner {
  flex: 0 0 auto;
  display: none;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  background: #1d1608;
  border-top: 1px solid rgba(227, 179, 65, .2);
}

.offline-banner.show { display: flex; }
.offline-banner .dot { background: var(--amber); animation: pulse 1.4s ease-in-out infinite; width: 6px; height: 6px; flex: 0 0 6px; }
.offline-banner .msg { flex: 1; font: 600 10px var(--mono); color: var(--amber); }
.offline-banner .q { font: 400 10px var(--mono); color: #8a7434; }

/* ---------- trays ---------- */

.tray {
  flex: 0 0 auto;
  display: none;
  padding: 7px 10px 1px;
  background: var(--card);
  border-top: 1px solid var(--line);
  animation: rise .14s ease-out;
  max-height: 168px;
  overflow-y: auto;
}

.tray.show { display: block; }
.tray.keys { display: none; }
.tray.keys.show { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; }

.key {
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  color: var(--text);
  font: 500 11.5px var(--mono);
}

/* ---------- subagents strip ---------- */

.subs {
  flex: 0 0 auto;
  display: none;
  background: transparent;
}

/* Bare on the band, like the todo strip: its fade is 75% where this row starts. */
.subs.show { display: block; }
.subs-summary { min-height: 29px; padding: 4px 10px; }
.subs .label { flex: 0 0 auto; display: flex; align-items: center; gap: 5px; font: 700 8.5px var(--mono); color: var(--faint); letter-spacing: .08em; white-space: nowrap; }
.subs .label .dot { background: var(--violet); animation: pulse 1.4s ease-in-out infinite; }
.subs-status {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  color: var(--faint);
  font: 500 9px var(--mono);
}
.subs-details {
  max-height: min(42vh, 360px);
  padding: 2px 10px 8px;
}

/* The list: one line per subagent, the whole row is the tap target. */
.sub-row {
  width: 100%;
  min-height: 36px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  color: inherit;
  text-align: left;
}
.sub-row:last-child { border-bottom: 0; }
.sub-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.sub-row .name, .sub-heading .name { color: #c9b8e8; font: 650 10px var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub-row .task { color: var(--text); font: 500 11px/1.35 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub-row.done .name, .sub-detail.done .name { color: var(--dim); }
.sub-row.done .task { color: var(--dim); }
.sub-row .time, .sub-heading .time { flex: 0 0 auto; color: var(--dim); font: 500 9.5px var(--mono); }
.sub-row .chev { flex: 0 0 auto; color: var(--faint); font: 600 14px/1 var(--mono); }
.subs-details .dot:not(.done):not(.failed) { background: var(--violet); animation: pulse 1.4s ease-in-out infinite; }
.subs .dot.failed { background: var(--red); }

/* One subagent: its task, what it is doing, then the prompt it was given. */
.sub-detail { display: flex; flex-direction: column; gap: 5px; padding: 4px 0 2px; }
.sub-back {
  align-self: flex-start;
  min-height: 30px;
  padding: 4px 8px 4px 0;
  border: 0;
  background: transparent;
  color: var(--violet);
  font: 600 10px var(--mono);
}
.sub-heading { display: flex; align-items: center; gap: 6px; min-width: 0; }
.sub-heading .name { flex: 1; min-width: 0; }
.sub-task { color: var(--text); font: 500 11.5px/1.4 var(--mono); overflow-wrap: anywhere; }
.sub-label { margin-top: 6px; color: var(--faint); font: 700 8.5px var(--mono); letter-spacing: .08em; }
.sub-activity { display: flex; gap: 8px; color: var(--dim); font: 500 10.5px/1.45 var(--mono); overflow-wrap: anywhere; }
.sub-activity.latest { color: var(--violet); }
.sub-activity .clock { flex: 0 0 auto; color: var(--faint); }
.sub-activity .what { min-width: 0; }
.sub-prompt { color: var(--dim); font: 400 10.5px/1.5 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }

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

.statusbar {
  flex: 0 0 22px;
  height: 22px;
  display: flex;
  align-items: center;
  padding: 0 4px;
  background: transparent;
  overflow-x: auto;
  overflow-y: hidden;
}

.seg {
  flex: 0 0 auto;
  white-space: nowrap;
  height: 22px;
  padding: 0 6px;
  border: 0;
  background: transparent;
  color: var(--dim);
  font: 500 9px var(--mono);
  display: flex;
  align-items: center;
  gap: 4px;
}

/* Clamped: a 30-character hostname would push every other segment off the bar. */
.seg.node {
  display: block;
  max-width: 12ch;
  line-height: 22px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Pushed to the right end of the bar. */
.seg.changes { margin-left: auto; }
.seg.changes svg { width: 11px; height: 11px; }

.seg.green { color: var(--green); }
.seg.amber { color: var(--amber); }
.seg.red { color: var(--red); }

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

/* One rounded box holding the entry and its controls, floating clear of the edges
   the way the reference apps do it. The box is the widget now; nothing inside it
   wears a box of its own. Translucent over the scrim, blurred where supported. */
/* The box's own measurements, read by the box and by the stub that stands in for
   it in a pane without it, so the two are the same size and cannot drift apart.
   `--box-controls` is the height of the row under the entry — see `.send`. */
:root {
  --box-margin: 0 10px calc(10px + env(safe-area-inset-bottom));
  --box-pad: 2px 8px 6px;
  --box-gap: 4px;
  --box-controls: 34px;
  /* The row's tracks: attach · agent · model · effort · the space between ·
     cancel · mic · steer · send. Named here because the stub in a pane without
     the box draws the same keys in the same places — see `.composer`. */
  --box-cols: 38px auto auto auto 1fr auto auto auto 38px;
  /* The two lines `#draft` opens at (`rows="2"`) in its own font and padding —
     see `.composer textarea`. */
  --box-lines: calc(2 * 1.45 * 14px + 15px);
}

.composer {
  flex: 0 0 auto;
  margin: var(--box-margin);
  padding: var(--box-pad);
  background: rgba(13, 16, 20, .88);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border: 1px solid var(--line-strong);
  border-radius: 22px;
  /* The one boxed widget: it opts back out of the band-wide glyph halo. */
  text-shadow: none;
  display: grid;
  /* The nine tracks of --box-cols. The entry spans all nine on row 1, so only the
     flexible track is ever empty. Steer stays against send, the other action on
     the message; the mic sits on its far side. Its two tracks are content-sized
     rather than 38px: hidden — on a box with no transcription service, and cancel
     on every box while nothing is being recorded — they cost the row nothing but
     their gap. */
  grid-template-columns: var(--box-cols);
  gap: var(--box-gap);
  align-items: center;
}

/* The focus ring belongs to the box, since the entry inside it has no edge. */
.composer:focus-within { border-color: var(--sel-line); }

/* A coloured tab's composer: the edge in the tab's colour, dimmer until focused. */
.composer[data-colour] { border-color: color-mix(in srgb, var(--tab-colour) 55%, transparent); }
.composer[data-colour]:focus-within { border-color: var(--tab-colour); }

/* The words take the whole first row, in the box and in the stub that stands for
   it, so a line typed in one falls where the other drew it. */
.composer textarea,
.pane-entry-text { grid-column: 1 / -1; grid-row: 1; }

.composer textarea {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  resize: none;
  background: transparent;
  border: 0;
  padding: 10px 8px 5px;
  color: var(--text);
  font: 400 14px/1.45 var(--mono);
  outline: none;
  max-height: 40dvh;
}

/* The version is a row of its own below the box's outline, with the connection dot
   right of it. The box gives its old bottom clearance to this last row. */
.underfoot > .composer { margin-bottom: 0; }

.app-version {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  margin: 3px 16px calc(6px + env(safe-area-inset-bottom));
  color: var(--faint);
  font: 500 8.5px var(--mono);
  letter-spacing: .02em;
  white-space: nowrap;
  pointer-events: none;
}

.send {
  grid-column: 9;
  grid-row: 2;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 38px;
  height: 34px;
  border-radius: 10px;
  /* A real button chip, matching steer: the two actions on the entry row must
     read as the same kind of thing. */
  border: 1px solid var(--line);
  background: var(--input);
  color: var(--faint);
  font: 700 15px var(--mono);
}

.send svg { display: block; width: 18px; height: 18px; }

/* Armed is a lighter grey, not a colour. A green send button read as a status
   light next to the real ones; what it actually says is "there is something to
   send", which a change of shade says just as loudly. Offline keeps a shade of its
   own — the ◷ glyph, the banner and the status bar carry that warning. */
.send.ready { background: var(--dim); color: #07080a; }
.send.offline { background: #5b636d; color: #07080a; }

/* The agent chip: the same flat picker as the model beside it, but wearing the
   agent's own mark rather than a word, because that is how this agent is named
   everywhere else in the UI — tab strip, meters, assistant cards. */
.agentpick {
  grid-column: 2;
  grid-row: 2;
  display: grid;
  grid-auto-flow: column;
  align-items: center;
  gap: 5px;
  height: 34px;
  padding: 0 8px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 10px var(--mono);
  letter-spacing: .02em;
  white-space: nowrap;
}

.agentpick svg { display: block; width: 13px; height: 13px; }
.agentpick:active { background: var(--sel-bg); border-color: var(--sel-line); color: var(--text); }
.agentpick:disabled { opacity: .4; }

/* Beside the attach key on the controls row, in a track of its own: it costs the entry
   nothing, since that has row 1 to itself. It used to sit in the flexible track and be
   held in place by justify-self, which meant the row could not size it.
   The cap ellipsises `gpt-5.6-sol` rather than letting one long model name set the width
   of a row that also has to hold a send button. */
.modelpick {
  grid-column: 3;
  grid-row: 2;
  max-width: 96px;
  height: 34px;
  padding: 0 8px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 10px var(--mono);
  letter-spacing: .02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* No model set means the CLI decides; that is a weaker statement than a named model
   and reads as one. */
.modelpick.unset { color: var(--faint); }
.modelpick:active { background: var(--sel-bg); border-color: var(--sel-line); color: var(--text); }
.modelpick:disabled { opacity: .4; }

/* The effort dial rides beside the model it modifies: same box, shorter cap, since
   its longest value is "minimal". */
.effortpick {
  grid-column: 4;
  grid-row: 2;
  max-width: 72px;
  height: 34px;
  padding: 0 8px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 10px var(--mono);
  letter-spacing: .02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.effortpick.unset { color: var(--faint); }
.effortpick:active { background: var(--sel-bg); border-color: var(--sel-line); color: var(--text); }
.effortpick:disabled { opacity: .4; }

/* The status line rides in the row's flexible track, right of the effort dial, in
   the box and in the stub that stands in for it. It is the track that was empty,
   so nothing else on the row moves; `min-width: 0` is what lets it be the part
   that gives when a pane is narrow, scrolling sideways instead of pushing the send
   button off the end. `.seg.changes` still has `margin-left: auto`, so the last
   turn's edits sit against the mic. */
.composer .statusbar,
.pane-entry .statusbar {
  grid-column: 5;
  grid-row: 2;
  min-width: 0;
  height: var(--box-controls);
}
/* The phone's line stays under the box, where the band has the room for it: the
   box's own row is nine tracks in 466px and the flexible one is a few characters
   wide. Only the direct child, or this hides the line a pane's stub draws. */
.composer > .statusbar { display: none; }
#frame.wide .composer > .statusbar { display: flex; }

/* In the box the segments are keys on the key row, so they are the row's height
   and the model and effort chips' size — 8.5px, not the band's 9px. At the band's
   size they stood 4px taller than the row they sit in and the line ran out of
   track before `◷`, `● needs you` and the last turn's changes, which are the
   segments worth the room. */
#frame.wide .composer .seg,
#frame.wide .pane-entry .seg {
  height: var(--box-controls);
  padding: 0 5px;
  font-size: 10.5px;
}
#frame.wide .composer .seg.node,
#frame.wide .pane-entry .seg.node { line-height: var(--box-controls); }
#frame.wide .composer .seg.changes svg,
#frame.wide .pane-entry .seg.changes svg { width: 12.5px; height: 12.5px; }

/* The track holds five segments. Six — a paused thread that also has a queue —
   is more than it has, and the sixth is cut off at the mic. The machine name is
   the one that gives up its room first: it already ellipsises at 12ch, so it
   shrinks and the segments after it keep more of theirs. Four characters is the
   floor — a name down to `work…` still says which box this is, where the bare
   `w` it shrank to without one says nothing. The floor is the border box, so it
   carries the 4px of padding either side: 4ch of glyphs is `4ch + 8px`. It buys
   about 40px, which is not the 50 the sixth segment wants; `⏸ resumes 15:46`
   still loses its clock. The bar scrolls, but only in the box — in the stub it
   is inside a button, where a drag takes the focus. */
#frame.wide .composer .seg.node,
#frame.wide .pane-entry .seg.node { flex: 0 1 auto; min-width: calc(4ch + 10px); }

.attach {
  grid-column: 1;
  grid-row: 2;
  flex: 0 0 auto;
  width: 38px;
  height: 34px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 400 18px var(--mono);
  /* Holding + opens the file browser; stop the phone selecting the glyph instead. */
  -webkit-touch-callout: none;
  user-select: none;
}

.attach:active { background: var(--sel-bg); color: var(--text); }

/* ---------- dictation ---------- */

/* The attach key's twin at the other end of the row, beside send: same
   footprint, same radius, same absent border, same grey. Hidden until /health says this box has a transcription service, so a box
   without voice has the composer it always had. */
.mic {
  grid-column: 7;
  grid-row: 2;
  display: none;
  place-items: center;
  width: 38px;
  height: 34px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  /* The same guard the attach key carries, for the same reason: a press-and-hold
     on a control must not start a text selection on it. */
  -webkit-touch-callout: none;
  user-select: none;
}

.mic.show { display: grid; }
.mic svg { display: block; width: 18px; height: 18px; }
.mic:active { background: var(--sel-bg); color: var(--text); }
/* Recording is a status, and status is what colour on this surface is for. The
   red is the palette's own — the stopped marker and the offline dot wear it too. */
.mic.rec { color: var(--red); }
/* After the stop: the clip going up, or the phone transcribing it. Disabled like
   the permission prompt, but at full strength and breathing, so it reads as work
   under way rather than a control switched off. */
.mic.busy:disabled,
.mic.loading:disabled { opacity: 1; color: var(--text); }
.mic.busy svg { animation: mic-busy 1.2s ease-in-out infinite; }
@keyframes mic-busy { 50% { opacity: .35; } }
/* A ring round the icon, filled by --progress (0 to 1, set by app.js): while
   recording, how much of the 120 s cap has gone, in the stop's red; while the
   model comes down, the share that has arrived. */
.mic.rec,
.mic.loading { position: relative; }
.mic.rec::before,
.mic.loading::before {
  content: '';
  position: absolute;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: conic-gradient(var(--text) calc(var(--progress, 0) * 1turn), var(--line-strong) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
}
.mic.rec::before { background: conic-gradient(var(--red) calc(var(--progress, 0) * 1turn), var(--line-strong) 0); }
.mic.rec svg,
.mic.loading svg { width: 14px; height: 14px; }
@media (prefers-reduced-motion: reduce) { .mic.busy svg { animation: none; } }
/* Opening the microphone, and waiting for the transcript: the same dimming every
   disabled control here uses. */
.mic:disabled { opacity: .4; }

/* Beside the stop it undoes, and mounted only while there is a recording to throw
   away. Narrower than the stop button on purpose: discarding is the lesser of the
   two actions, and the row has a send button to fit. */
.miccancel {
  grid-column: 6;
  grid-row: 2;
  display: none;
  place-items: center;
  width: 28px;
  height: 34px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--faint);
}

.miccancel.show { display: grid; }
.miccancel svg { display: block; width: 14px; height: 14px; }
.miccancel:active { color: var(--text); }

/* The number beside the mic while the work has a known share. It takes cancel's
   track and cancel's width: cancel is up only while recording, the number only
   after, so the row is never wider than it is mid-recording. Fixed width and
   digits of one width, so 9% to 10% to 100% moves nothing. */
.micpct {
  grid-column: 6;
  grid-row: 2;
  display: none;
  width: 28px;
  font: 600 10px/34px var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  color: var(--dim);
  user-select: none;
}
.micpct.show { display: block; }

/* ---------- attachments ---------- */

/* ---------- slash menu ---------- */

.slashmenu {
  flex: 0 0 auto;
  display: none;
  flex-direction: column;
  max-height: 46dvh;
  overflow-y: auto;
  padding: 6px;
  gap: 3px;
  background: var(--card);
  border-top: 1px solid var(--line-strong);
}

.slashmenu.show { display: flex; }
.slashmenu .slabel { font: 700 9px var(--mono); color: var(--faint); letter-spacing: .08em; text-transform: uppercase; padding: 2px 5px 4px; }

.slashmenu .srow {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 9px;
  border-radius: 9px;
  background: var(--bg);
  border: 1px solid var(--line);
  text-align: left;
}

.slashmenu .srow:active { border-color: var(--sel-line); background: var(--sel-bg); }
.slashmenu .srow.portal { border-color: rgba(121, 192, 255, .3); }
/* The row the arrows are on, drawn the way the folder box draws the folder you
   are already in. */
.slashmenu .srow.sel { border-color: var(--sel-line); background: var(--sel-bg); }
.slashmenu .sname { font: 600 12px var(--mono); color: var(--text); }
.slashmenu .srow.portal .sname { color: var(--text); }
/* Two lines of description: enough to tell two skills apart, not enough to scroll. */
.slashmenu .sdesc {
  font: 400 10.5px/1.4 var(--mono);
  color: var(--faint);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.drafts { flex: 0 0 auto; display: none; gap: 6px; padding: 7px 10px 0; overflow-x: auto; }
.drafts.show { display: flex; }

.chipfile {
  flex: 0 0 auto;
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  height: 44px;
  padding: 0 8px 0 0;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  overflow: hidden;
}

.chipfile img { width: 44px; height: 44px; object-fit: cover; display: block; }
.chipfile .glyph { width: 34px; text-align: center; font: 400 15px var(--mono); color: var(--dim); }
.chipfile .meta { display: flex; flex-direction: column; gap: 2px; max-width: 118px; }
.chipfile .fname { font: 500 10.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chipfile .fsize { font: 400 9.5px var(--mono); color: var(--faint); }
/* Amber while the bytes are still only on this device. */
.chipfile.local { border-color: rgba(227, 179, 65, .4); }
.chipfile .drop {
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 7px;
  background: var(--input);
  border: 0;
  color: var(--faint);
}
.chipfile .drop svg { flex: 0 0 auto; width: 12px; height: 12px; display: block; }

/* In a sent message: a strip of thumbnails under the text. */
.attached { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.attached .chipfile { height: 40px; }
.attached .chipfile img { width: 40px; height: 40px; }

/* ---------- pull-up panel ---------- */

/* The shell replaces the transcript rather than squeezing it, so it takes the same
   slot in the column and only one of the two is ever mounted. A drawer left both
   half-visible and neither usable on a phone. */
.panel {
  display: none;
  position: absolute;
  inset: 0;
  /* The same slot as the transcript, so the same measured headroom. */
  padding: var(--overhead-h, 150px) 0 var(--underfoot-h, 130px);
  flex-direction: column;
  background: var(--card);
  overflow: hidden;
}

.panel.show { display: flex; }
.thread.swapped { display: none; }

/* The page past either end of the strip. It swipes like a tab, so it takes the
   transcript's slot and its pan-y; the composer hides while it shows, because a
   draft typed here would go to a thread nobody can see. */
.endpage {
  display: none;
  position: absolute;
  inset: 0;
  padding: var(--overhead-h, 150px) 10px var(--underfoot-h, 130px);
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  touch-action: pan-y;
}
.endpage.show { display: flex; cursor: pointer; background: var(--bg-empty); }
/* Hidden, not unmounted: display:none would drop the transcript's scroll position
   and a swipe back would land at the top of the thread. */
.thread.behind { visibility: hidden; }
.endpage-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  padding: 0;
}
.endpage-plus svg { width: 40px; height: 40px; display: block; }
/* The VCode lockup over the plus: the V in the brand's own gradients, so it does
   not take the page's colour, and the word in white rather than the page's --dim.
   Capped but able to shrink, since a stacked pane can be narrow; the ratio is the
   asset's viewBox. No glow: the site's drop-shadow
   is sized for a 64px V beside live text, and one blurred copy of the whole lockup
   is a purple slab behind white letters. */
.endpage-logo {
  flex: 0 0 auto;
  width: min(180px, 55%);
  aspect-ratio: 751 / 160;
  height: auto;
  display: block;
  /* Well back. The page's subject is the plus under it, not the logo: at full
     strength the white word and the saturated V are the brightest things in a
     window of dark panes, and the eye lands on the wrong one. */
  opacity: .28;
}
.endpage-plus:active { background: var(--input); color: var(--text); }
.endpage-hint { font: 400 12px var(--mono); color: var(--faint); }
#frame.at-end .tobottom, #frame.at-end .underfoot, #frame.at-end .planstrip { display: none; }

.panel .phead {
  flex: 0 0 26px;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 6px 0 10px;
  border-bottom: 1px solid var(--line);
}

.panel .ptitle { font: 700 9.5px var(--mono); color: var(--dim); letter-spacing: .06em; }
.panel .psub { flex: 1; min-width: 0; font: 400 9.5px var(--mono); color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel .picon { width: 26px; height: 22px; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; background: transparent; border: 0; color: var(--faint); }
.panel .picon svg { width: 14px; height: 14px; display: block; }
.panel .pbody { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }

.tailbox {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 8px 10px;
  background: var(--bg);
  display: flex;
  flex-direction: column;
}

/* Keep short output at the prompt edge. Once it overflows, the auto margin gives
   way and the whole capture stays inside the scroll range. */
.shell-lines {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  margin-top: auto;
}

/* tmux has already wrapped this at the pane width, so breaking mid-word wraps it
   a second time and splits identifiers. Break at spaces where one exists. */
.tl { flex: 0 0 auto; font: 400 11.5px/1.5 var(--mono); color: var(--dim); white-space: pre-wrap; overflow-wrap: break-word; }
.tl.ok { color: var(--green); }
.tl.warn { color: var(--amber); }
.tl.err { color: var(--red); }

/* The heading over a list, wherever one is: the drawer, the popup card and both
   sheets. Only whether it stretches along its row differs, so only that is said
   again where it does. */
.stitle { font: 700 10px var(--mono); color: var(--faint); letter-spacing: .08em; }

/* ---------- tab drawer ---------- */

/* The open tabs, pulled in from the left edge over the transcript. Above the
   floating bands and the jump button, below the sheets — a sheet opened from the
   drawer has to cover it. */
.drawer { position: absolute; inset: 0; z-index: 25; display: none; }
.drawer.show { display: block; }
.drawer-scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, .45); }

/* Parked off-screen by the stylesheet, moved by the class once it is open. Only
   the gesture writes px, and only while a finger is down, so a render mid-drag
   cannot snap the panel home under it. */
.drawer-panel {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 78%;
  max-width: 320px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: calc(env(safe-area-inset-top) + 12px) 10px 12px;
  background: #05070b;
  border-right: 1px solid var(--line);
  overflow-y: auto;
  transform: translateX(-100%);
  touch-action: pan-y;
}
.drawer.open .drawer-panel { transform: none; }
/* Only while it settles after the release: a transition during the drag would
   leave the panel trailing the finger. */
.drawer.settling .drawer-panel { transition: transform .18s ease; }
.drawer.settling .drawer-scrim { transition: opacity .18s ease; }
.drawer-list { display: flex; flex-direction: column; gap: 6px; }

/* Rows are the manager's, two lines instead of three: this one picks a tab rather
   than describing it, and the status is already a dot. */
.drawer .ws { padding: 4px 4px 4px 9px; }
.drawer .ws .wsmain { gap: 8px; }

.drawer-new {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin-top: auto;
  padding: 11px;
  background: transparent;
  border: 1px dashed var(--line);
  border-radius: 12px;
  color: var(--dim);
  font: 600 12px var(--mono);
}
.drawer-new svg { width: 14px; height: 14px; display: block; }
.drawer-new:active { background: var(--input); color: var(--text); }

/* ---------- file explorer ---------- */

/* The same drawer on the other edge. Only where it is parked, which side it is cut
   off on and what it holds differ — `.drawer.show`, `.drawer.open` and
   `.drawer.settling` move this panel too, so the two sides can never drift apart.
   The panel itself does not scroll: the list inside it does, or the path line and
   the filter box would leave the screen on the way down a long folder. */
.drawer.explorer .drawer-panel {
  left: auto;
  right: 0;
  gap: 7px;
  padding: calc(env(safe-area-inset-top) + 12px) 0 0;
  background: var(--card);
  border-right: 0;
  border-left: 1px solid var(--line);
  overflow: hidden;
  transform: translateX(100%);
}

/* Said again rather than left to `.drawer.open .drawer-panel`: that rule and the
   parked transform above are both two classes deep, and this section comes later in
   the file, so the panel would stay off-screen with `open` on it. */
.drawer.explorer.open .drawer-panel { transform: none; }

.xhead { display: flex; align-items: center; gap: 8px; padding: 0 10px; }
.xhead .icon-btn:disabled { opacity: .35; }

/* Clipped at the start, not the end: the folder you are in is the last segment, and
   it is the one that has to survive. app.js wraps the text in U+200E (left-to-right
   mark) because rtl direction otherwise drags a leading or trailing neutral
   character, such as the dot in `.claude`, to the wrong end of the string. */
.xpath {
  flex: 1;
  min-width: 0;
  direction: rtl;
  text-align: left;
  font: 600 11.5px var(--mono);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.xfilter {
  flex: 0 0 auto;
  height: 34px;
  margin: 0 10px;
  padding: 0 9px;
  background: var(--input);
  border: 1px solid var(--line);
  border-radius: 9px;
  color: var(--text);
  font: 400 12px var(--mono);
}
.xfilter[hidden] { display: none; }

.xlist { flex: 1; min-height: 0; overflow-y: auto; padding: 0 6px calc(env(safe-area-inset-bottom) + 12px); }
.xlist .dl.meta { padding: 8px 8px 0; }

/* The panel's own way out. It sits after the flexible list, so it stays at the
   bottom-left without covering the last file row or stealing the list's scroll. */
.explorer-toggle {
  flex: 0 0 auto;
  align-self: flex-start;
  width: 30px;
  height: 30px;
  margin: 0 0 calc(env(safe-area-inset-bottom) + 6px) 6px;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: 6px;
  color: var(--faint);
}
.explorer-toggle svg { width: 16px; height: 16px; display: block; margin: auto; }
.explorer-toggle:active { color: var(--text); background: rgba(230, 237, 243, .08); }

.xrow {
  display: flex;
  align-items: center;
  gap: 0;
  width: 100%;
  height: 36px;
  padding: 0 8px 0 calc(4px + var(--depth, 0) * 14px);
  background: transparent;
  border: 0;
  border-radius: 8px;
  color: var(--text);
  text-align: left;
}
.xrow svg { flex: 0 0 auto; width: 20px; height: 20px; display: block; color: var(--dim); }
.xrow .name { flex: 1; min-width: 0; font: 400 12px var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xrow .to { flex: 0 0 auto; margin-left: 4px; font: 400 11px var(--mono); color: var(--faint); }
.xrow .size { flex: 0 0 auto; margin-left: 6px; font: 400 10.5px var(--mono); color: var(--faint); }
.xrow:active { background: var(--sel-bg); }

/* Laid out like VS Code with the Seti theme: a twistie column on every row,
   empty on a file, then a file's glyph. A folder has no icon, so its name starts
   where a file's glyph does. The glyph is the theme's 150% of the row's text. */
.xchev { flex: 0 0 auto; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-right: 4px; }
.xrow .xchev svg { transition: transform .12s ease; }
.xrow.open .xchev svg { transform: rotate(90deg); }
.xglyph { flex: 0 0 auto; width: 20px; margin-right: 6px; font: 18px/1 'seti'; text-align: center; -webkit-font-smoothing: antialiased; }

/* VS Code's git decorations: the name in the colour of what happened to it and a
   letter at the right end in the same colour; a folder over changes gets a dot,
   a little dimmer. What git ignores is grey. The header keeps the counts. */
.xbadge { flex: 0 0 auto; min-width: 14px; margin-left: auto; padding-left: 6px; font: 600 11px var(--mono); text-align: center; }
.xrow[aria-expanded] .xbadge { opacity: .8; }
.xrow.t-modified .name, .xrow.t-modified .xbadge { color: var(--git-modified); }
.xrow.t-untracked .name, .xrow.t-untracked .xbadge { color: var(--git-untracked); }
.xrow.t-added .name, .xrow.t-added .xbadge { color: var(--git-added); }
.xrow.t-deleted .name, .xrow.t-deleted .xbadge { color: var(--git-deleted); }
.xrow.t-ignored .name { color: var(--git-ignored); }

/* What changed since the last commit, in the header beside the root's name. */
.xdelta { flex: 0 0 auto; display: inline-flex; gap: 5px; font: 500 10.5px var(--mono); }
.xdelta[hidden] { display: none; }
.xdelta .add { color: var(--green); }
.xdelta .del { color: var(--red); }
.xdelta .mod { color: var(--amber); }

/* 'reading…', 'empty' and a failed read stand under the folder they are about. */
.xlist .dl.meta.xstatus { padding: 4px 8px 6px calc(28px + var(--depth, 0) * 14px); }

/* The rest of a long folder, on a row that looks like the drawer's own new-thread
   button rather than like a file. */
.xmore {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(100% - var(--depth, 0) * 14px);
  margin: 6px 0 6px calc(var(--depth, 0) * 14px);
  padding: 9px;
  background: transparent;
  border: 1px dashed var(--line);
  border-radius: 10px;
  color: var(--dim);
  font: 600 11px var(--mono);
}
.xmore:active { background: var(--input); color: var(--text); }

/* ---------- new-thread popup ---------- */

/* One form, two hosts: the Threads sheet's section and this card. Above the drawer
   and the sheets, because either of them can open it. */
.popup { position: absolute; inset: 0; z-index: 35; display: none; align-items: center; justify-content: center; }
.popup.show { display: flex; }
.popup-scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, .55); }
.popup-card {
  position: relative;
  width: 88%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  background: #05070b;
  border: 1px solid var(--line);
  border-radius: 14px;
  animation: rise .18s ease-out;
}
.popup-card .shead { display: flex; align-items: center; gap: 8px; }
.popup-card .stitle { flex: 1; }
/* The way into the Threads sheet from the card the + opens. Worn like the title
   beside it, so it reads as part of the header rather than a second action. */
.popup-card .headlink {
  min-height: 32px;
  padding: 0 6px;
  background: none;
  border: 0;
  font: 700 10px var(--mono);
  letter-spacing: .08em;
  color: var(--dim);
  cursor: pointer;
}
.popup-card .headlink:hover { color: var(--text); }

/* The files a turn changed, one row each, straight into the review sheet. */
.changes-list { display: flex; flex-direction: column; max-height: 50vh; overflow: auto; }
.changefile {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  padding: 0 4px;
  background: none;
  border: 0;
  border-top: 1px solid var(--line);
  font: 500 11px var(--mono);
  color: var(--dim);
  text-align: left;
  cursor: pointer;
}
.changefile:first-child { border-top: 0; }
.changefile .pathlabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Accepted in the review sheet: on a file row, and on the diff icon once every file is. */
svg.accepted { color: var(--green); }
.changefile svg.accepted { flex: 0 0 auto; width: 13px; height: 13px; }
/* The sheet's heading and its form host, kept in a column of their own so the
   heading sizes to its text rather than to the sheet's spare height. */
.newthread-section { display: flex; flex-direction: column; gap: 8px; }

/* ---------- sheets ---------- */

.sheet { position: absolute; inset: 0; z-index: 30; display: none; flex-direction: column; animation: rise .18s ease-out; }
.sheet.show { display: flex; }

.sheet.review, .sheet.viewer { background: var(--card); }
.sheet.review .rhead, .sheet.viewer .rhead { flex: 0 0 auto; display: flex; align-items: center; gap: 9px; padding: 12px 12px 10px; border-bottom: 1px solid var(--line); }
.sheet.review .rpath, .sheet.viewer .rpath { flex: 1; min-width: 0; font: 600 11.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet.review .plus { font: 600 10.5px var(--mono); color: var(--green); }
.sheet.review .minus { font: 600 10.5px var(--mono); color: var(--red); }
.sheet.review .rchunk { display: block; }
.sheet.review .rbody, .sheet.viewer .rbody { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; background: var(--bg); }
.sheet.review .rfoot { flex: 0 0 auto; display: flex; justify-content: flex-end; gap: 7px; padding: 10px 12px 14px; border-top: 1px solid var(--line); }
.sheet.review .rfoot .btn { height: 44px; }

/* Long lines wrap: on a phone a sideways scroll loses the start of every line the
   moment it moves. Leading spaces survive (pre-wrap), and each line is a block with
   a hanging indent the width of the gutter, so what wraps continues under the text
   and the numbers stay a clean column. */
.sheet.viewer .code { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0; }
.sheet.viewer .vline { display: block; padding-left: calc(3.4em + 10px); text-indent: calc(-3.4em - 10px); }
.sheet.viewer .gutter {
  display: inline-block;
  text-indent: 0;
  width: 3.4em;
  padding-right: 10px;
  text-align: right;
  color: var(--faint);
  user-select: none;
}
.sheet.viewer img { max-width: 100%; display: block; }
.sheet.shotview .rbody { display: flex; align-items: flex-start; justify-content: center; }

/* ---------- changes since the last commit, in the viewer ---------- */

/* Before the comment rules on purpose: a commented line that is also new shows as
   commented, which is what the reader is acting on. */
.sheet.viewer .vline.added { background: rgba(126, 231, 135, .08); box-shadow: inset 2px 0 0 var(--green); }
.sheet.viewer .vline.removed { background: rgba(248, 81, 73, .09); box-shadow: inset 2px 0 0 var(--red); cursor: default; }
.sheet.viewer .vline.removed .gutter { color: var(--red); }
.rbody.nodiff .vline.removed { display: none; }
.rbody.nodiff .vline.added { background: none; box-shadow: none; }
#viewer-diff[aria-pressed="true"] { color: var(--amber); border-color: rgba(227, 179, 65, .45); }

/* ---------- comments on lines ---------- */

/* A line is a thing to tap. One that carries a review comment is marked down its
   left edge, and the comment stands under it. */
.sheet.viewer .vline { cursor: pointer; }
.sheet.viewer .vline:active { background: var(--sel-bg); }
.vline.noted { background: rgba(210, 168, 255, .08); box-shadow: inset 2px 0 0 var(--violet); }

/* Inside a <pre>, so both say for themselves that their own text wraps like prose. */
.lcnote {
  display: block;
  width: calc(100% - 3.4em - 10px);
  margin: 3px 0 6px calc(3.4em + 10px);
  padding: 7px 9px;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 2px solid var(--violet);
  border-radius: 8px;
  color: var(--text);
  font: 400 12.5px/1.45 var(--sans, system-ui, sans-serif);
  text-align: left;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.lcnote.stale { border-left-color: var(--amber); color: var(--dim); }
.lcbox {
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin: 4px 0 8px;
  padding: 8px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  white-space: normal;
}
.lctext {
  width: 100%;
  min-height: 64px;
  padding: 8px 9px;
  background: var(--input);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font: 400 14px/1.4 var(--sans, system-ui, sans-serif);
  resize: vertical;
}
.lcactions { display: flex; justify-content: flex-end; gap: 7px; flex-wrap: wrap; }
.lcactions .btn { height: 36px; }
.lcactions .btn:disabled, .rvbar .btn:disabled { opacity: .4; }

/* The review in progress, on top of the viewer and of the explorer. */
.rvbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 8px 12px;
  background: rgba(210, 168, 255, .07);
  border-bottom: 1px solid var(--line);
}
.rvbar[hidden] { display: none; }
.rvbar .count { flex: 1; min-width: 0; font: 600 11px var(--mono); color: var(--violet); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rvbar .btn { height: 34px; }
.drawer.explorer .rvbar { margin: 0 10px; padding: 7px 8px; border: 1px solid var(--line); border-radius: 9px; }

.icon-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border-radius: 9px; background: var(--input); border: 1px solid var(--line); color: var(--dim); }
.icon-btn svg { width: 15px; height: 15px; display: block; }
/* An author display beats the UA's [hidden], so the toggled buttons need this or
   they show on every file (#viewer-diff) with nothing behind the tap. */
.icon-btn[hidden] { display: none; }

/* Keep both scrollable sheets opaque. Settings has its own surface and spacing so
   the grouped form reads clearly above the app chrome. */
.sheet.sessions { background: #05070b; padding: 12px; gap: 8px; overflow-y: auto; }
.sheet.settings { background: #080b10; padding: 16px 20px 22px; gap: 10px; overflow-y: auto; }
.sheet.sessions .shead, .sheet.settings .shead { display: flex; align-items: center; gap: 8px; }
.sheet.sessions .stitle { flex: 1; }

.ws {
  display: flex;
  align-items: stretch;
  gap: 4px;
  padding: 6px 6px 6px 11px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
  width: 100%;
}

/* The selectable part of a manager row. Its own button, with rename and close as
   siblings rather than children: as nested buttons the row was one target with
   three meanings, and any tap that missed a 28px glyph selected the thread and
   dismissed the sheet instead — which is exactly what "close does not work" looked
   like from the outside. */
.ws .wsmain {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  background: transparent;
  border: 0;
  text-align: left;
}

.ws.on { background: var(--sel-bg); border-color: var(--sel-line); }
/* A coloured tab's row: the edge in its colour, full strength on the tab in front. */
.ws[data-colour] { border-color: color-mix(in srgb, var(--tab-colour) 45%, transparent); }
.ws.on[data-colour] { border-color: var(--tab-colour); }

/* A row being dragged in the sheet: lifted above the rows it is pulled past, the
   way .tab.dragging is in the strip. touch-action stops the sheet scrolling under
   the finger for the rest of the gesture. */
.ws.dragging {
  position: relative;
  z-index: 3;
  border-color: var(--sel-line);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .5);
  touch-action: none;
}
/* A long press is how a finger picks up a row, so it must not open the callout. */
.ws .wsmain { -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
.ws .dot { width: 8px; height: 8px; flex: 0 0 8px; }
.ws .info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; text-align: left; }
.ws .name { font: 600 12.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* One line: a long scratchpad path wrapped to three and made every row a
   different height. The title already identifies the thread. */
.ws .meta { font: 400 10.5px var(--mono); color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ws .state { flex: 0 0 auto; font: 600 12px var(--mono); color: var(--faint); }
/* The adopt mark is drawn, not typed, so it matches the reopen and delete buttons
   two rows above it. The status markers beside it stay typographic: they sit in a
   line of text, not on a button. */
.ws .state.resume { display: inline-flex; align-items: center; }
.ws .state.resume svg { width: 14px; height: 14px; display: block; }
.ws .state.working { color: var(--amber); }
.ws .state.done { color: var(--green); }
.ws .icon { flex: 0 0 16px; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; }

.resume { display: flex; flex-direction: column; gap: 8px; padding-top: 6px; border-top: 1px solid var(--line); margin-top: 4px; }
.resume .shint, .sheet.settings .shint { color: var(--faint); font-weight: 400; letter-spacing: 0; }
.resume .empty { font: 400 11px var(--mono); color: var(--faint); padding: 6px 2px; }

/* A form column in a sheet: the new-thread form and the settings sheet's group are
   the same stack, so they are the same rule. */
.newthread, .sheet.settings .group { display: flex; flex-direction: column; gap: 11px; }
/* Settings read as three deliberate cards, with a small semantic accent per group. */
.sheet.settings .group { position: relative; padding: 15px; background: linear-gradient(150deg, #0e131a, #0a0e14); border: 1px solid #242c36; border-radius: 16px; }
.sheet.settings .group + .group { margin-top: 0; }
/* Same reason as .icon-btn[hidden]: without it the dictation group showed its
   controls on a box with no transcription service. */
.sheet.settings .group[hidden] { display: none; }
/* And a single setting that shows only for one engine: the live correction switch. */
.sheet.settings .setting[hidden] { display: none; }
.newthread .row { display: flex; gap: 6px; align-items: center; }

/* The new-thread controls and settings fields share the same dark well, but the
   settings sheet gets larger touch targets and a slightly roomier type size. */
.newthread input, .newthread .newagentpick,
.sheet.settings input[type="number"], .sheet.settings input[type="email"], .sheet.settings input[type="text"], .sheet.settings textarea, .sheet.settings select {
  min-width: 0;
  background: var(--input);
  color: var(--text);
}
.newthread input, .newthread .newagentpick {
  height: 38px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  font: 500 11.5px var(--mono);
}
.sheet.settings input[type="number"], .sheet.settings input[type="email"], .sheet.settings input[type="text"], .sheet.settings textarea, .sheet.settings select {
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  font: 500 12px var(--mono);
}
.newthread input, .newthread .newagentpick { flex: 1; }

/* The folder field: the chevron lives inside the well rather than flush against
   its edge, so the glyph is inset the same 10px as the text, and the text stops
   short of it instead of running underneath. */
.newthread .cwdbox { position: relative; flex: 1; display: flex; min-width: 0; }
.newthread .new-cwd { padding-right: 34px; }
.newthread .cwdchev { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); color: var(--dim); background: none; border: 0; }
.newthread .newagentpick { display: inline-flex; align-items: center; gap: 7px; text-align: left; }
.newthread .newagentpick:disabled { color: var(--faint); }

/* Settings keep the same dark, mono-first visual language while giving the form
   clearer hierarchy and more room around its touch targets. */
.sheet.settings .shead {
  position: sticky;
  top: -16px;
  z-index: 3;
  gap: 12px;
  padding: 16px 0 15px;
  border-bottom: 1px solid #242c36;
  background: #080b10;
}
.sheet.settings .settings-heading { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.sheet.settings .settings-title { color: var(--text); font: 600 21px/1.15 var(--sans); letter-spacing: -.025em; }
.sheet.settings .shead .icon-btn { width: 42px; height: 42px; border-radius: 13px; background: var(--input); border-color: var(--line-strong); color: var(--text); }
.sheet.settings .shead .icon-btn svg { width: 17px; height: 17px; }
.sheet.settings .group > .stitle {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #aeb8c4;
  font: 650 10px/1.2 var(--mono);
  letter-spacing: .11em;
}
.sheet.settings .group > .stitle::before {
  content: '';
  width: 6px;
  height: 6px;
  flex: 0 0 6px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 12px rgba(126, 231, 135, .22);
}
.sheet.settings #settings-tabs > .stitle::before { background: var(--blue); box-shadow: 0 0 12px rgba(121, 192, 255, .2); }
.sheet.settings #settings-voice > .stitle::before { background: var(--violet); box-shadow: 0 0 12px rgba(210, 168, 255, .2); }
.sheet.settings #settings-remote > .stitle::before { background: var(--amber); box-shadow: 0 0 12px rgba(227, 179, 65, .2); }
/* Remote access: the auth sheet's remote pieces (fingerprint well, device rows)
   inside a settings card, one part shown at a time. */
.sheet.settings .remote-part { display: flex; flex-direction: column; gap: 11px; }
.sheet.settings .remote-part[hidden], .sheet.settings .remote-message[hidden], .sheet.settings .auth-err[hidden] { display: none; }
.sheet.settings .remote-part > .note { font: 400 10.5px/1.45 var(--mono); color: var(--dim); }
.sheet.settings .remote-value { font: 500 12px var(--mono); color: var(--text); overflow-wrap: anywhere; text-align: right; }
.sheet.settings .group .btn { height: 44px; }
.sheet.settings .remote-box .btn { height: 34px; flex: 0 0 auto; }
.sheet.settings .remote-box .btn.armed, .sheet.settings #remote-unlink.armed { border-color: rgba(248, 81, 73, .5); }
.sheet.settings .remote-device { display: flex; flex-direction: column; gap: 3px; min-width: 0; padding: 8px 0; }
.sheet.settings .group .shint { margin-left: auto; color: var(--green); font: 500 10px var(--mono); letter-spacing: 0; }
.sheet.settings .help { margin: -5px 0 0; font: 400 11px/1.4 var(--mono); color: var(--dim); }
.sheet.settings .help code { color: var(--text); }
.sheet.settings .setting { display: flex; flex-direction: column; gap: 7px; }
.sheet.settings .setting.row { flex-direction: row; align-items: center; justify-content: space-between; min-height: 44px; }
.sheet.settings .setting .name { font: 550 13px/1.3 var(--sans); color: var(--text); display: flex; flex-direction: column; gap: 2px; }
.sheet.settings .setting .note { font: 400 10.5px/1.45 var(--mono); color: var(--dim); }
/* The same box, grown: only what a multi-line field has to say for itself. */
.sheet.settings textarea { height: auto; min-height: 76px; padding: 10px 12px; line-height: 1.45; resize: vertical; }
/* A native select keeps its keyboard and platform picker behavior; the inset endcap
   gives its disclosure a deliberate home and keeps it away from the field edge. */
.sheet.settings .select-control { position: relative; display: block; min-width: 0; width: 100%; }
.sheet.settings select {
  display: block;
  width: 100%;
  padding-right: 58px;
  font: 500 11.5px/1.2 var(--sans);
  text-overflow: ellipsis;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  transition: background .12s ease-out, border-color .12s ease-out;
}
.sheet.settings select::-ms-expand { display: none; }
.sheet.settings select:hover:not(:disabled) { border-color: #455363; background: #171e28; }
.sheet.settings select:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.sheet.settings select:active:not(:disabled) { border-color: #576779; }
.sheet.settings select:disabled { color: var(--dim); opacity: 1; cursor: default; }
.sheet.settings select option { color: var(--text); background: #0d1014; }
.sheet.settings .select-chevron {
  position: absolute;
  top: 50%;
  right: 9px;
  width: 34px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #303b48;
  border-radius: 9px;
  background: #1a222d;
  color: #c9d1d9;
  pointer-events: none;
  transform: translateY(-50%);
  transition: background .12s ease-out, border-color .12s ease-out, color .12s ease-out;
}
.sheet.settings .select-chevron svg { display: block; width: 16px; height: 16px; }
.sheet.settings .select-control:hover .select-chevron { border-color: #455363; background: #202a36; color: var(--text); }
.sheet.settings select:disabled + .select-chevron { border-color: var(--line); background: #11161d; color: var(--dim); opacity: .72; }
.sheet.settings select:focus-visible + .select-chevron { border-color: var(--blue); background: #222d39; color: var(--text); }

/* A switch drawn from the checkbox it labels: the input stays for the keyboard and
   the screen reader, the track and knob are what a thumb sees. On wears the app's
   selected state — the --sel-bg ground and --sel-line edge of .ws.on and the
   new-thread button — with the status --green on the knob. */
.sheet.settings input[type="checkbox"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sheet.settings .switch {
  flex: 0 0 auto;
  width: 40px;
  height: 22px;
  border-radius: 11px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  position: relative;
  transition: background .12s ease-out;
}
.sheet.settings .switch::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--dim);
  transition: transform .12s ease-out, background .12s ease-out;
}
.sheet.settings input[type="checkbox"]:checked + .switch { background: var(--sel-bg); border-color: var(--sel-line); }
.sheet.settings input[type="checkbox"]:checked + .switch::after { transform: translateX(18px); background: var(--green); }
.sheet.settings input[type="checkbox"]:focus-visible + .switch { outline: 2px solid var(--sel-line); outline-offset: 2px; }

@media (max-height: 850px) {
  .sheet.settings { padding: 12px 16px 16px; gap: 8px; }
  .sheet.settings .group { gap: 8px; padding: 12px; }
  .sheet.settings .shead { top: -12px; padding: 12px 0; }
}
@media (min-width: 720px) {
  .sheet.settings { padding-left: max(24px, calc((100% - 640px) / 2)); padding-right: max(24px, calc((100% - 640px) / 2)); }
}
@media (prefers-reduced-motion: reduce) {
  .sheet.settings select, .sheet.settings .select-chevron { transition: none; }
}

.newagentmenu, .cwdmenu { display: none; flex-direction: column; gap: 4px; }
.newagentmenu.show, .cwdmenu.show { display: flex; }
.cwdmenu .srow, .newagentmenu .srow {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 9px 8px 34px;
  border-radius: 9px;
  background: var(--bg);
  border: 1px solid rgba(121, 192, 255, .3);
  color: var(--text);
  text-align: left;
}
.cwdmenu .srow:active, .newagentmenu .srow:active { border-color: var(--sel-line); background: var(--sel-bg); }
.cwdmenu .sname, .newagentmenu .sname { font: 600 12px var(--mono); }
/* Long enough to scroll on a phone: completion over a directory like /usr/lib
   returns far more rows than the agent menu ever has. */
.cwdmenu { max-height: 40vh; overflow-y: auto; }
/* No mark to leave room for, so the row's text starts where the box's does. */
.cwdmenu .srow { flex-direction: row; align-items: baseline; padding: 8px 9px; }
.cwdmenu .sname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cwdmenu .branch { color: var(--dim); margin-left: 6px; font: 400 11px var(--mono); }
/* Logged-out agents wear the amber dot and open the login sheet. */
.newagentmenu .agent-option.needs-login .sname { color: var(--amber); }
/* The in-card login panel: ask, link, status. Same type scale as the menus. */
.agentlogin { display: flex; flex-direction: column; gap: 8px; padding: 8px 2px 0; }
.agent-login-text { font: 400 12px var(--mono); color: var(--text); }
.agent-login-row { display: flex; gap: 8px; }
.agent-login-link { overflow-wrap: anywhere; }
/* The folder the box already holds, so a full list still says where you are. */
.cwdmenu .srow.on { border-color: var(--sel-line); background: var(--sel-bg); }
.newagentmenu .agent-option, .slashmenu .agent-option { color: var(--dim); }
.newagentmenu .srow.agent-option .sname, .slashmenu .srow.portal.agent-option .sname { color: inherit; }
.newagentmenu .agent-option.on, .slashmenu .agent-option.on { color: var(--text); }
.smark { flex: 0 0 16px; width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; }
.smark svg { width: 100%; height: 100%; display: block; }
.newagentmenu .srow > .smark, .slashmenu .srow.agent-option > .smark,
.slashmenu .srow.in-use > .smark { position: absolute; left: 10px; top: 10px; }
.slashmenu .srow.agent-option, .slashmenu .srow.in-use { position: relative; padding-left: 34px; }
/* The model or effort this tab is already on. Green, because it is a state and
   not a thing to choose again. */
.slashmenu .srow.in-use > .smark { color: var(--green); }

/* Centred under the floating chrome, a clear gap below the icons: over the composer
   it sat on whatever was being typed. --overhead-h already follows the band as trays
   and the plan strip open, so the toast never covers a chip. max-content because
   left: 50% alone would cap the shrink-to-fit width at half the frame. Hidden by
   visibility rather than display so it can fade out as well as in. */
.toast {
  position: absolute;
  top: calc(var(--overhead-h, 150px) + 18px);
  left: 50%;
  z-index: 40;
  width: max-content;
  max-width: min(88%, 360px);
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 9px 12px 9px 11px;
  border-radius: 10px;
  background: var(--card);
  border: 1px solid var(--line-strong);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .5), 0 2px 6px rgba(0, 0, 0, .35);
  color: var(--text);
  font: 500 11.5px/1.45 var(--mono);
  overflow-wrap: anywhere;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, -6px);
  transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s;
}

.toast.show { opacity: 1; visibility: visible; transform: translateX(-50%); transition: opacity .16s ease, transform .16s ease; }
/* Grey either way: the check or the cross says which it was, not a colour. */
.toast > svg { flex: 0 0 13px; width: 13px; height: 13px; margin-top: 2px; color: var(--dim); }

/* The Access-expiry variant of the offline banner reads as an action, not a
   passive state, because tapping it re-runs the sign-in. */
.offline-banner.access { cursor: pointer; }
.offline-banner.access .msg,
.offline-banner.access { color: var(--accent, #7aa2ff); }

/* Closed-threads manager section: dimmer than the live list, with reopen and a
   two-tap delete. */
.closed { padding: 10px 12px 4px; }
.ws.closed { opacity: .78; }
.ws.closed .name { font: 600 12px var(--mono); color: var(--text); }
.ws.closed .meta { font: 400 10px var(--mono); color: var(--faint); }
.wsclose.danger { color: var(--faint); }
.wsclose.danger.armed { color: var(--red); border-color: rgba(248, 81, 73, .5); width: auto; padding: 0 10px; font: 700 10px var(--mono); }

/* ---------- auth (only shown when AUTH_TOKEN is set) ---------- */

.auth {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg);
}

.auth.show { display: flex; }

.auth-card {
  width: 100%;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
}

.auth-title { font: 700 11px var(--mono); color: var(--green); letter-spacing: .1em; }
.auth-hint { font: 400 11.5px/1.5 var(--mono); color: var(--dim); }

.auth input {
  height: 44px;
  padding: 0 12px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  color: var(--text);
  font: 400 13px var(--mono);
  outline: none;
}

.auth input:focus { border-color: var(--green); }
.auth .btn { height: 44px; }
.auth-err { font: 500 11px var(--mono); color: var(--red); min-height: 14px; }

/* The remote screens (public/remote.mjs) are the auth sheet's card, one step at a time. */
.remote-step { display: flex; flex-direction: column; gap: 10px; }
.remote-step[hidden] { display: none; }
.remote-message { font: 600 13px/1.5 var(--mono); color: var(--text); }
.remote-row { display: flex; gap: 10px; }
.remote-row[hidden] { display: none; }
.remote-row .btn { flex: 1; }
.remote .btn { height: 44px; }
.remote a.btn { display: flex; align-items: center; justify-content: center; text-decoration: none; }
.remote input:disabled, .remote .btn:disabled { opacity: .45; }
.remote .auth-hint code { color: var(--text); }
.remote-fingerprint {
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  font: 700 15px var(--mono);
  letter-spacing: .12em;
  color: var(--green);
  text-align: center;
  word-break: break-all;
}
.remote-list { display: flex; flex-direction: column; gap: 8px; }
.remote-box {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 44px;
  padding: 0 12px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  color: var(--text);
  font: 600 13px var(--mono);
  text-align: left;
}
.remote-box-note { font: 400 11px var(--mono); color: var(--dim); }

/* reload probe */

/* Matched characters in the slash menu. `em` so it still reads as emphasis with
   styles off, rather than being invisible. */
.slashmenu .sname em.hit { font-style: normal; color: var(--text); background: rgba(230, 237, 243, .14); border-radius: 3px; }
.slashmenu .srow.portal .sname em.hit { background: rgba(121, 192, 255, .2); }

/* Matched characters in the slash menu. `em` so the emphasis survives with styles
   off, rather than the match becoming invisible. */
.slashmenu .sname em.hit { font-style: normal; color: var(--text); background: rgba(230, 237, 243, .14); border-radius: 3px; }
.slashmenu .srow.portal .sname em.hit { background: rgba(121, 192, 255, .2); }

/* ---------- steer ---------- */

/* Beside send, and only mounted while a turn is running — a button that is visible
   but would fail on tap is worse than no button.
   Grey, like send: an action becoming available is a change of shade, not a change
   of hue. Colour on this surface means status, and steer is not a status. */
.steer {
  grid-column: 8;
  grid-row: 2;
  flex: 0 0 auto;
  display: none;
  place-items: center;
  width: 38px;
  height: 34px;
  border-radius: 10px;
  background: var(--input);
  border: 1px solid var(--line);
  color: var(--faint);
}

.steer.show { display: grid; }
/* Ready steer wears exactly the armed-send chip: same fill, same dark glyph —
   the two actions must be visually indistinguishable in kind. */
.steer.ready { background: var(--dim); color: #07080a; border-color: transparent; }
.steer svg { display: block; width: 18px; height: 18px; }

/* ---------- markdown ---------- */

/* Screenshots served from the shots endpoint: never wider than the card, capped
   so a tall phone capture cannot swallow the transcript, tappable to full size. */
.body img.shot {
  display: block;
  max-width: 100%;
  max-height: 55vh;
  border-radius: 10px;
  border: 1px solid var(--line);
  margin: 4px 0;
}

/* Sized to sit inside a card, so a heading reads as structure without shouting.
   The last-child rules stop a block adding trailing space inside the bubble. */
.mdh { margin: 6px 0 2px; font-family: var(--sans); font-weight: 650; color: var(--text); line-height: 1.3; }
.mdh:first-child { margin-top: 0; }
.mdh.h1 { font-size: 15px; }
.mdh.h2 { font-size: 14px; }
.mdh.h3 { font-size: 13px; }
.mdh.h4, .mdh.h5, .mdh.h6 { font-size: 12.5px; color: var(--dim); }

.item .body > *:last-child,
.streaming .body > *:last-child { margin-bottom: 0; }

.mdlist { margin: 2px 0 4px; padding-left: 18px; }
.mdlist li { margin: 2px 0; line-height: 1.55; }
.mdlist .mdlist { margin: 2px 0 0; }
/* A dash reads better than a bullet at this size on a dark background. */
ul.mdlist { list-style: none; padding-left: 14px; }
ul.mdlist > li::before { content: '–'; color: var(--dim); position: absolute; margin-left: -14px; }
ul.mdlist > li { position: relative; }

.mdquote {
  margin: 4px 0;
  padding: 2px 0 2px 10px;
  border-left: 2px solid var(--line-strong);
  color: var(--dim);
}

.mdrule { margin: 8px 0; border: 0; border-top: 1px solid var(--line); }

.mdtable-wrap { margin: 4px 0; overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; }
/* Sized to content, with a floor so a narrow table still fills the card. */
.mdtable { border-collapse: collapse; width: auto; min-width: 100%; font: 400 12px var(--sans); }
.mdtable th, .mdtable td { padding: 5px 8px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
.mdtable th { background: var(--input); color: var(--dim); font-weight: 600; }
.mdtable tbody tr:last-child td { border-bottom: 0; }

.item .body strong,
.streaming .body strong { font-weight: 650; color: var(--text); }
.item .body em,
.streaming .body em { font-style: italic; color: var(--body); }
.item .body s,
.streaming .body s { color: var(--faint); }

/* ---------- scroll to bottom ---------- */

/* Every chat app has this: once you scroll up, streaming must not yank you back, and
   there has to be one tap to return. */
.tobottom {
  position: absolute;
  /* Centred: an arrow is a direction, and the middle is where the thumb lives. */
  left: 50%;
  transform: translateX(-50%);
  /* Above the floating bands (z 20), below sheets (z 30). */
  z-index: 22;
  display: none;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 34px;
  min-width: 34px;
  padding: 0 9px;
  border-radius: 17px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font: 600 11px var(--mono);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
  /* Its own chip surface — the band glyph halo would just muddy it. */
  text-shadow: none;
}

.tobottom.show { display: flex; }
.tobottom .n { color: var(--green); }
.tobottom .n:empty { display: none; }
.tobottom .arrow { font-size: 15px; line-height: 1; }

/* Agent mark in a tab: small enough that the label still gets the room, dimmed on
   an inactive tab so the current thread reads first. */
.tab .tabicon { flex: 0 0 13px; width: 13px; height: 13px; display: flex; align-items: center; justify-content: center; opacity: .5; }
.tab.on .tabicon { opacity: 1; }

/* The sheet scrolls as one column, so the header has to stay put or you lose the
   close button once you are a few threads down. */
/* Fully opaque, not .97: at that alpha the row scrolling underneath ghosted
   through the title. */
.sheet.sessions .shead { position: sticky; top: -12px; z-index: 2; padding: 12px 0 8px; background: #05070b; }
.ws .wsicon { flex: 0 0 13px; width: 13px; height: 13px; display: flex; align-items: center; justify-content: center; opacity: .75; }
.ws.on .wsicon { opacity: 1; }
.ws .state.failed, .ws .state.exited { color: var(--red); }

/* Softer than a filled slab: in the threads sheet the main act is picking an existing
   thread, so starting a new one should not be the brightest thing on screen. */
.newthread .btn.primary {
  background: var(--sel-bg);
  border: 1px solid var(--sel-line);
  color: var(--text);
  font-weight: 600;
}

/* Filename first: the directory is dim and gives way under pressure, the basename
   never truncates, so you can always see which file you are looking at. */
.pdir { color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.pbase { color: var(--text); flex: 0 0 auto; white-space: nowrap; }
/* The flex container is the wrapper, never the host: making .meta itself flex turned
   each syntax token into a flex item and collapsed the spaces between them. */
.pathlabel { display: inline-flex; align-items: baseline; min-width: 0; max-width: 100%; vertical-align: bottom; }
.pathlabel .pdir { flex: 0 1 auto; }

/* Hidden scrollbar plus a dozen tabs meant nothing indicated the strip scrolls. The
   fade only appears when there is something past the edge to reach. */
.tabstrip.overflowing {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent);
}

/* Tab manager rows: name, then the folder it works in, then what it is doing. */
.ws .path { display: flex; align-items: baseline; gap: 5px; min-width: 0; font: 400 11px var(--mono); color: var(--blue); }
.ws .path .fmark { flex: 0 0 auto; color: var(--faint); }
.ws .path .ftext { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The folder picker, open in place of the path line. Sized like the line it
   replaces so the row does not jump. */
.ws .wspick {
  width: 100%;
  min-width: 0;
  margin: -2px 0;
  padding: 1px 4px;
  background: var(--bg);
  border: 1px solid var(--sel-line);
  border-radius: 6px;
  color: var(--text);
  font: 400 11px var(--mono);
  outline: none;
}

/* Up and down, stacked into one column the width of half a button: five 40px
   controls beside the name would leave a phone row with nothing to read. The strip
   has the drag; this is the press that needs no steady hand. */
.ws .wsorder {
  flex: 0 0 auto;
  width: 26px;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ws .wsup,
.ws .wsdown {
  flex: 1;
  min-height: 0;
  border-radius: 8px;
  background: rgba(230, 237, 243, .05);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.ws .wsup svg,
.ws .wsdown svg { width: 14px; height: 14px; display: block; }

.ws .wsup:active,
.ws .wsdown:active { background: rgba(230, 237, 243, .16); border-color: var(--sel-line); color: var(--text); }

/* An end of the strip has nowhere to go. Dimmed rather than removed, so the two
   arrows stay in the same place on every row. */
.ws .wsup:disabled,
.ws .wsdown:disabled { opacity: .3; }

/* Move, rename and close. Rename and close were unreachable in practice before: a 28px glyph in
   --faint on --card is 1.9:1 — you cannot aim at what you cannot see — and it left
   50px of dead row above and below it. Now they are visible, and they stretch to the
   full row height, so a thumb cannot miss vertically at all. */
.ws .wsmove,
.ws .wsedit,
.ws .wsclose {
  flex: 0 0 auto;
  width: 40px;
  align-self: stretch;
  border-radius: 8px;
  background: rgba(230, 237, 243, .05);
  border: 1px solid var(--line-strong);
  color: var(--dim);
  /* Still set, because the armed delete replaces its icon with the word "Delete?". */
  font: 400 11px var(--mono);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* 16px of drawing in a 40px box: the rail's proportion, one size down for a row
   control. Unsized, a <use> renders at the SVG default of 300x150. */
.ws .wsmove svg,
.ws .wsedit svg,
.ws .wsclose svg { width: 16px; height: 16px; display: block; }

.ws .wsmove:active,
.ws .wsedit:active { background: rgba(230, 237, 243, .16); border-color: var(--sel-line); color: var(--text); }
.ws .wsclose:active { background: rgba(248, 81, 73, .16); border-color: rgba(248, 81, 73, .45); color: var(--red); }

/* The name, being edited in place. Sized and spaced like the .name it replaces so
   the row does not jump when the editor opens. */
.ws .wsname {
  width: 100%;
  min-width: 0;
  padding: 2px 6px;
  margin: -3px 0;
  background: var(--bg);
  border: 1px solid var(--sel-line);
  border-radius: 6px;
  color: var(--text);
  font: 600 12.5px var(--mono);
  outline: none;
}

/* The agent mark sits where its name used to, matching the pill's height so the
   header baseline does not shift between an agent card and a tool card. No pill
   behind it though — a brand mark in a grey box read as a button, so it floats
   on the card and only text tags keep the .tag background. */
.tag.agentmark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 3px;
  border-radius: 6px;
  background: transparent;
}

.tag.agentmark svg { width: 100%; height: 100%; display: block; }

/* The agent's name beside its mark, quieter than the reply it labels. A long model
   id shrinks to an ellipsis rather than pushing the copy button off the card. */
.agentname {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--dim);
  font: 600 12px var(--mono);
  white-space: nowrap;
}

/* ---------- tablet and desktop ---------- */

/* The frame fills the window at every width, with the file explorer parked off
   its right edge. Given a wide window the column has more than width to gain, and from a
   laptop's width up the explorer is better as furniture. Nothing here is a media
   query: public/wide-layout.js owns the two widths and writes `wide` and
   `dock-files` onto #frame, so the numbers live in one file and the CSS and the JS
   cannot disagree about which layout is on screen.

   Everything inside #frame is absolutely positioned against it with inset:0, so
   the whole of the work is pulling each layer off the explorer's column. --dock-r
   is how wide that column is, or 0 where there is none. */

/* The type is a phone's: 7.5 to 13.5px, set in px throughout. At a desktop's one
   device pixel per px that is small and soft, so the whole frame is zoomed rather
   than each size restated. The zoom lives on its own class because app.js takes it
   back off a browser that zooms the layout but still measures rects unzoomed. Zoom
   scales dvh like any other length, so the height is divided back down or the
   frame runs 20% past the bottom of the window.

   Every px below is the frame's own, a tenth short of the screen's where the zoom
   holds: the explorer is 308 screen px and the composer 792. app.js reads the
   zoom back (frameZoom) for the places it mixes screen px with the frame's. */
#frame.wide {
  --zoom: 1.1;
  max-width: none;
  --dock-r: 0px;
  /* Firefox has no ::-webkit-scrollbar. `thin` plus the pair is the whole of what
     it offers, and both inherit, so the frame sets them once for everything in it
     — everything but the strips that declare `scrollbar-width: none` on
     themselves, which a declared value wins against an inherited one. */
  scrollbar-width: thin;
  scrollbar-color: rgba(139, 148, 158, .34) transparent;
  --compose-w: 466px;
  /* #4a5058 is about 2.3:1 on this ground. Dense phone pixels carry it; one device
     pixel per px does not, and the unset model or a meter's reset time blurs into
     the black. Lifted, and still well under --dim, so the greys keep their order. */
  --faint: #626a74;
}

/* A 3px hairline is a phone's: a finger flings the list and never grabs the bar,
   so it is only ever a position mark. A pointer has to catch it, so on a wide
   frame it is 10px of track carrying a 6px thumb — the 2px of transparent border
   either side is what leaves the thumb clear of the content it sits against — and
   the thumb takes a hover like every other control here.

   `:not([data-nobar])` is doing real work: the rule that hides the bar on the tab
   strip, the mode strip, the pane tabs, the subagents strip and the status line
   carries one attribute of specificity, and an id plus a class would walk over it
   and put a channel through the chrome. */
#frame.wide :not([data-nobar])::-webkit-scrollbar { width: 10px; }

#frame.wide :not([data-nobar])::-webkit-scrollbar-thumb {
  background: rgba(139, 148, 158, .34);
  background-clip: padding-box;
  border: 2px solid transparent;
  border-radius: 5px;
}

#frame.wide :not([data-nobar])::-webkit-scrollbar-thumb:hover {
  background: rgba(139, 148, 158, .6);
  background-clip: padding-box;
}

#frame.zoomed {
  zoom: var(--zoom);
  height: calc(100dvh / var(--zoom));
}

/* After #frame.wide, which zeroes it: equal specificity, so source order is what
   makes this win. */
#frame.dock-files { --dock-r: 280px; }

/* Hidden while it could dock, the column leaves its inset standing as a slim
   rail rather than nothing: the files stay one tap away. */
#frame.files-rail { --dock-r: 28px; }

/* Every layer that floats over the transcript, moved off the explorer's column.
   Leaving one out puts it under the docked panel. */
#frame.wide .thread,
#frame.wide .panel,
#frame.wide .endpage,
#frame.wide .overhead,
#frame.wide .underfoot,
#frame.wide .sheet,
#frame.wide .popup {
  right: var(--dock-r);
}

/* These two centre themselves with left:50% and translateX(-50%) rather than
   stretching, so they need the column's middle instead of the frame's. The
   percentages resolve against #frame, which is what they are positioned in. */
#frame.wide .toast,
#frame.wide .tobottom {
  left: calc((100% - var(--dock-r)) / 2);
}

/* ---------- the column ---------- */

/* The transcript uses the whole column. The bottom band is capped and centred,
   every row of it: a text box 2000px wide puts the send button a screen away from
   the words, and a status line, a picker's menu or a skills list that stretched
   past the entry it belongs to read as a separate piece of furniture. One rule
   for the band, so a row added later lands in the column without being told.

   The composer has 10px of its own either side, which a narrow column still
   needs, so each row takes whichever is larger. Left and right only: the
   composer's bottom margin clears the safe area. */
#frame.wide .underfoot > * {
  margin-left: max(10px, (100% - var(--compose-w)) / 2);
  margin-right: max(10px, (100% - var(--compose-w)) / 2);
}

/* A row carrying a surface drew its top edge across the window. At the column's
   width that line stops in mid-air, so those rows take the composer's edge and a
   corner instead, and sit clear of the row below. The bare rows — the status
   line, the subagents strip, the drafts — have no surface to round. */
#frame.wide .underfoot > .slashmenu,
#frame.wide .underfoot > .tray {
  border: 1px solid var(--line-strong);
  /* The box's own corner, which is smaller here than on a phone — see the
     composer's block at the foot of this file. */
  border-radius: 11px;
  margin-bottom: 6px;
}

#frame.wide .underfoot > .offline-banner {
  border: 1px solid rgba(227, 179, 65, .2);
  border-radius: 11px;
  margin-bottom: 6px;
}

/* The three stacked rows the phone has become one 48px row: the meters take the width
   left over, and the chrome the phone spreads over the rows below sits at the right end
   of the same line. No DOM moves — the rows are still three elements, so the phone keeps
   them exactly as they are — so each one is placed explicitly. #planstrip is the fourth
   child and is hidden while the panes are up, which on a wide frame is always. */
#frame.wide .overhead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
}

/* Two meters sharing a 2000px column were each 1000px of bar, and the percentage
   sat a screen's width from the name. Each keeps a phone's width at most, and the
   pair sits at the left, where the phone has them. */
#frame.wide .agents {
  grid-column: 1;
  grid-row: 1;
  grid-template-columns: repeat(2, minmax(0, 260px));
  justify-content: start;
}

/* What is left of the mode strip on a wide frame: the notify bell, the folder and the cog, at the
   frame's right edge. The modes are each pane header's. */
#frame.wide .modestrip {
  grid-column: 3;
  grid-row: 1;
  align-self: end;
  width: auto;
}

/* The version and the connection dot are the app's, not a tab's, so on a wide frame
   they sit once in the band's top right corner, over the cog. The icons sit at the
   foot of the cell to make the room. */
#frame.wide .overhead > .app-version {
  grid-column: 3;
  grid-row: 1;
  align-self: start;
  justify-self: end;
  margin: 3px 14px 0;
}

/* The phone's copies of the per-tab-group controls. Each pane header carries its own —
   the plus, agent or shell and the two splitters — and a second set on a
   band that belongs to no pane had nothing to act on. They stay in the DOM: below the
   wide width they are the only ones there are, and the keyboard clicks two of them. */
#frame.wide #tabstrip,
#frame.wide #new-tab,
#frame.wide #split-right,
#frame.wide #split-down,
#frame.wide #send-modes,
#frame.wide .modestrip .divider {
  display: none;
}

/* The top bar is the middle of the one row: the tabs are each pane's now, so what
   is left is the install and update buttons, just before the bell and the cog. The shell panel is not capped either — a
   terminal wants its columns, and tmux has already wrapped its lines to the pane.

   The glyph halo is for the rows transcript text fades through. The top bar sits
   where the band is solid, and at one device pixel per px the halo only blurs the
   edge of every glyph and icon on it. The composer already opts out. */
#frame.wide .topbar {
  grid-column: 2;
  grid-row: 1;
  flex-basis: 48px;
  height: 48px;
  gap: 2px;
  padding: 0;
  text-shadow: none;
}
#frame.wide .topbar svg { filter: none; }

/* The install and update buttons live in the band's middle cell, the bell and the
   cog in the cell after it. Padding between the cells was an 18px hole in a row whose
   other gaps are 2px, so only the frame edge keeps any. */
#frame.wide .modestrip { padding: 0 10px 0 2px; gap: 2px; }

/* The same button as a pane header's, one row up: a 30px slot, a 17px mark and a 5px
   corner. The three of them were a 38x34 slot carrying 20px of bell, 18px of cog and
   17px of folder, which is what made one row of icons read as three toolbars' ends. */
#frame.wide .install,
#frame.wide .update-ready,
#frame.wide .cog,
#frame.wide .files,
#frame.wide .chip.notify {
  width: 30px;
  height: 30px;
  border-radius: 5px;
}

#frame.wide .install svg,
#frame.wide .update-ready svg,
#frame.wide .cog svg,
#frame.wide .files svg,
#frame.wide .chip.notify .cg svg { width: 17px; height: 17px; }

#frame.wide .chip.notify .cg { flex-basis: 17px; }

/* ---------- the tab bar ---------- */

/* On a phone the strip is a row of labels a thumb swipes along. With a mouse it is
   the only list of tabs on screen, so it takes the look of a desktop app's tab bar:
   a taller strip, labels at reading size with room for a real title, a hover, and
   the active tab filled the way a selected row is everywhere else, with its colour
   bar underneath. The rules below it are the shape; the pane's lane wears them. */
#frame.wide .tabstrip {
  height: 38px;
  gap: 3px;
}

/* Square, and the lane's own height. A rounded tab floating inside a rectangular
   header read as a chip dropped on a toolbar, and the colour bar ran out past its
   corner. The shape is a desktop editor's: a rectangle in a rectangle, and the
   next one starts where this one ends. The two side borders are
   here, transparent, rather than on the active tab alone: a border that arrives
   with the selection moves every label a px sideways. */
#frame.wide .tab {
  gap: 7px;
  padding: 0 8px 0 12px;
  border: 0;
  border-left: 1px solid transparent;
  /* The divider between two tabs, in place of the gap they used to sit apart by, so
     the stronger hairline: --line between two fills that already touch is a line
     you have to look for, and the lane read as one bar. The active tab paints over
     it with its own edge. */
  border-right: 1px solid var(--line-strong);
  border-radius: 0;
  font-size: 12px;
  max-width: 240px;
}

#frame.wide .tab .label { max-width: 180px; }
#frame.wide .tab .tabicon { flex-basis: 13px; width: 13px; height: 13px; }

/* One hairline between two tabs, not two: the active tab draws its own edge, so
   the tab in front of it drops the divider. Nor is there one at the end of the
   lane. */
#frame.wide .tab:has(+ .tab.on) { border-right-color: transparent; }
/* Except the tab you are in, whose own edge that is: this rule is the more
   specific of the two, so it has to leave the active tab out by name. */
#frame.wide .pane-tabs .tab:last-child:not(.on) { border-right-color: transparent; }
/* And none at the start of a lane, where the pane's own border is that edge — the
   active tab included, which is why this rule outranks the one that colours it.
   The px is still there, transparent, so no label moves with the selection.
   Drawn, the lane began with two lines: the pane's hairline and the tab's own
   edge beside it. In the pane you are in, its ring makes that three, and the
   hairline caught between the ring and the tab read as a gap down the seam.
   The lane in a header, not the copy under the pointer: that one carries
   .pane-tabs too, and it is over a transcript with no border of its own. */
#frame.wide .pane-head .pane-tabs .tab:first-child { border-left-color: transparent; }

#frame.wide .tab:hover { color: var(--text); background: rgba(230, 237, 243, .05); }

/* The selected surface everywhere else is --sel-bg inside --sel-line. Here the
   line is on the two sides only: a rule along the top or the bottom of a
   full-height tab would draw a second header rule inside the header. A border
   rather than an inset shadow, because box-shadow replaces rather than merges:
   drawn that way, the edges took the lift off the one tab anybody drags. */
#frame.wide .tab.on {
  background: var(--sel-bg);
  border-left-color: var(--sel-line);
  border-right-color: var(--sel-line);
}

/* The bar is the tab's bottom edge now, the whole width of it: an inset bar under
   a square tab reads as an underlined word. */
#frame.wide .tab.on::after { left: 0; right: 0; bottom: 0; border-radius: 0; }

/* The cross that closes a tab. A phone has no hover, so its strip shows the cross
   on the tab you are in and on an armed one, and takes it out of the rest: a cross
   on every 148px tab would eat their labels. On a phone its hit area reaches past
   the 18px glyph to the tab's right edge and 9px above and below, so a thumb finds
   it; a wide frame's pointer keeps the glyph's own. */
.tab .tabclose {
  display: none;
  position: relative;
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  align-items: center;
  justify-content: center;
  border-radius: 5px;
  color: var(--dim);
}

#frame:not(.wide) .tab .tabclose::before { content: ''; position: absolute; inset: -9px -8px -9px -4px; }

.tab.on .tabclose,
.tab.armed .tabclose { display: inline-flex; }

.tab.armed .tabclose { color: var(--red); background: rgba(248, 81, 73, .14); }
.tab .tabclose svg { width: 11px; height: 11px; display: block; }

/* On a wide frame every tab keeps its cross in place, hidden rather than removed,
   so a hover does not shift the label the pointer is on. */
#frame.wide .tab .tabclose { display: inline-flex; visibility: hidden; }

/* Up on the tab you are in, the way a browser's is, and on whichever tab the
   pointer is over. What keeps a stray click from stopping a running turn is the
   two presses `requestClose` asks for, not a hidden cross. An armed cross — a
   running tab's first press — stays up and goes red until the second press or the
   three seconds are out.
   The active tab's takes `.hovers` — a pointer, not a width. A hidden cross is not
   hit-tested, so on a wide touch frame it has never taken a tap; showing it there
   would put an 18px close under the thumb that drags tabs, and `[data-nodrag]`
   would stop that hold from picking the tab up at all. */
#frame.wide .tab:hover .tabclose,
#frame.wide.hovers .tab.on .tabclose,
#frame.wide .tab.armed .tabclose { visibility: visible; }

#frame.wide .tab .tabclose:hover { color: var(--text); background: rgba(230, 237, 243, .1); }
#frame.wide .tab.armed .tabclose:hover { color: var(--red); background: rgba(248, 81, 73, .14); }

/* ---------- the docked explorer ---------- */

/* A drawer shows itself with `.show` and moves with `.open`; docked it gets
   neither, because the gesture that would add them is refused while it is a
   column. So the host is displayed from the dock class, cut down to the column's
   width, and its scrim — a black sheet over the whole frame — goes. The explorer
   keeps its own padding and background from `.drawer.explorer .drawer-panel`;
   only where it sits changes. */
#frame.dock-files > #explorer {
  display: block;
  left: auto;
  width: var(--dock-r);
}
#frame.dock-files > #explorer .drawer-scrim { display: none; }

/* Two ids and two classes, so it beats `.drawer.explorer .drawer-panel`'s parked
   translateX(100%) and `.drawer.settling`'s transition without either of them
   having to know. The panel is the column, so it fills the host. */
#frame.dock-files > #explorer .drawer-panel {
  transform: none;
  transition: none;
  width: 100%;
  max-width: none;
  padding-top: 0;
}

/* Docked, the header shares its row with the top bar, and the panel's 12px of top
   padding sat its button 3px below the cog beside it. The padding goes and the
   header takes the bar's own height, so the two rows end on the same line — the one
   the pane headers below start from. */
#frame.dock-files > #explorer .xhead { height: 48px; }

/* The hidden column's way back. Nowhere until the column is hidden while it
   could dock; then a slim rail down the frame's right edge, carrying the whole
   height so any tap on it brings the files back. */
#explorer-rail { display: none; }
#frame.files-rail > #explorer-rail {
  display: flex;
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: var(--dock-r);
  align-items: flex-start;
  justify-content: center;
  padding: calc(env(safe-area-inset-top) + 58px) 0 0;
  background: transparent;
  border: 0;
  border-left: 1px solid var(--line);
  border-radius: 0;
  color: var(--faint);
}
#frame.files-rail > #explorer-rail svg { width: 14px; height: 14px; display: block; }
#frame.files-rail > #explorer-rail:hover { color: var(--text); background: rgba(230, 237, 243, .08); }
#frame.files-rail > #explorer-rail:active { color: var(--text); background: rgba(230, 237, 243, .08); }

/* On a tablet the explorer is not docked, so it still arrives as an overlay from
   the folder button or the right edge, and the tab drawer from the left edge. */

/* A pointer has no :active to tell it a row is a row. Only where there is a mouse
   worth the assumption, which is the same place the explorer docks. */
#frame.wide .xrow:hover { background: var(--input); }

/* The folder button is a switch while the explorer is a column: lit is the column
   showing, the same selected colour .chip.on carries. */
#files-open[aria-pressed="true"] { color: var(--text); }

/* The phone's tree is built for a thumb: a 36px row, 20px columns, 14px of indent
   per level, a 34px filter box. A pointer hits a 22px row, and beside a
   transcript the tree is
   read rather than tapped, so every one of those numbers comes down to an
   editor's. Only on a wide frame — the same rules on a phone would leave rows a
   finger cannot land on. */
#frame.wide .xhead { gap: 6px; padding: 0 6px; }
/* Docked, the header is the top bar's other half on the same row, so its button is
   the same flat 30px slot with a 17px mark. The filled, bordered well it wears on a
   phone read as a control belonging to some other toolbar beside four bare marks.
   The hover is what the box used to say: this is a button. */
#frame.wide .xhead .icon-btn { background: transparent; border: 0; border-radius: 5px; }
#frame.wide .xhead .icon-btn svg { width: 17px; height: 17px; }
#frame.wide .xhead .icon-btn:hover { color: var(--text); background: rgba(230, 237, 243, .08); }
#frame.wide .explorer-toggle {
  width: 24px;
  height: 24px;
  margin: 0 0 3px 3px;
  border-radius: 5px;
}
#frame.wide .explorer-toggle svg { width: 14px; height: 14px; }
#frame.wide .explorer-toggle:hover { color: var(--text); background: rgba(230, 237, 243, .08); }
#frame.wide .xfilter { height: 26px; margin: 0 6px; border-radius: 6px; }
#frame.wide .xlist { padding: 0 4px calc(env(safe-area-inset-bottom) + 8px); }
#frame.wide .xlist .dl.meta { padding: 5px 6px 0; }
/* VS Code's own tree: a 22px row, 8px of indent a level, a 16px twistie column
   and a 16px icon column, each with 6px after it. */
#frame.wide .xrow {
  height: 22px;
  padding: 0 8px 0 calc(var(--depth, 0) * 8px);
  border-radius: 4px;
}
#frame.wide .xrow .xchev { width: 16px; height: 16px; margin-right: 6px; }
#frame.wide .xrow .xchev svg { width: 16px; height: 16px; }
#frame.wide .xglyph { width: 16px; }
/* The twistie column and its 6px: the text starts where a file's glyph does. */
#frame.wide .xlist .dl.meta.xstatus { padding: 2px 6px 4px calc(22px + var(--depth, 0) * 8px); }
#frame.wide .xmore {
  width: calc(100% - var(--depth, 0) * 8px);
  margin: 4px 0 4px calc(var(--depth, 0) * 8px);
  padding: 4px;
  border-radius: 6px;
}

/* ---------- split panes ---------- */

/* Every wide frame is on panes, one of them included: the pane header is where the
   per-tab-group controls live, so a single pane needs one too. The phone's layout —
   #thread under the two floating bands, the plan strip in the top band, the subagents
   strip in the bottom one — steps aside for #panes, which sits between the bands and
   stops at the explorer's column. The one row on top, the status line and the one
   composer stay where they are and belong to the focused pane.

   Each pane is placed from public/split-layout.js's rects() as percentages of this
   box, rather than nested flex boxes mirroring the tree: a pane's element then
   never moves in the DOM when the tree changes shape, so its transcript keeps its
   scroll position and its nodes through every split, close and zoom. */
/* The header's height and, under its own rule, the lane's: the tabs, the copy of
   a tab under the pointer and the seam's lift are all that one number. */
.panes { display: none; --head-h: 38px; --lane-h: calc(var(--head-h) - 1px); }

#frame.wide.split .panes {
  display: block;
  position: absolute;
  left: 0;
  right: var(--dock-r);
  top: var(--overhead-h, 150px);
  /* The band's height, which in split mode is whatever is left in it: the
     composer, the drafts and the slash menu live in the focused pane, so an empty
     band is zero and the panes take the floor. A band with the offline banner or
     the key tray in it keeps its room, or it would sit on a pane's composer and
     swallow the taps meant for it. */
  bottom: var(--underfoot-h, 0px);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

#frame.wide.split #thread,
#frame.wide.split #planstrip,
#frame.wide.split #subs,
#frame.wide.split #statusbar,
#frame.wide.split #tobottom,
#frame.wide.split #endpage { display: none; }

/* In split mode the band holds only the offline banner and the key tray, and it
   keeps its place at the foot of the window: the pane area stops above whatever
   it has (below), so neither sits on a pane's composer or takes its taps. Its
   fade goes, since no transcript runs under it any more. */
#frame.wide.split .underfoot { background: none; }

/* Dividers are each pane's right and bottom edge, dropped where the pane meets the
   area's own edge, so every line between two panes is one px. The stronger of the
   two hairlines: the lanes either side of it are flush with it, and at --line the
   boundary between two headers of tabs read as a seam that had failed to close. */
.pane {
  position: absolute;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--bg);
  border-right: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
  /* Each pane keeps its layers to itself. Without it an unfocused pane's floating
     footer (z-index 20) stood in the pane area's own stack, over the dragged tab's
     ghost and the drop tint that hang off #panes. */
  isolation: isolate;
}
.pane.edge-r { border-right: 0; }
.pane.edge-b { border-bottom: 0; }
.pane[hidden] { display: none; }

/* The focused pane is the one the composer, the status line and the explorer
   follow: the selected edge every other selected thing here wears, two px of it
   rather than one, because it stands for a whole pane and not a control.

   On a layer of its own. The header below paints an opaque background the width of
   the pane, so a ring on .pane itself is painted over along the top and the header
   row — the part with the pane's name on it — reads as unselected. */
.pane.focus { z-index: 1; }
.pane.focus::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Over the floating footer (20) and the jump button (22): the ring is the pane's
     whole edge, and it takes no taps. */
  z-index: 23;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--sel-line);
}

/* One pane: there is nothing to move the focus away to, so the ring says nothing, and
   nothing to zoom it against or close it in favour of. */
#frame.wide .panes.solo .pane.focus::after { display: none; }
/* A pane's footer would stretch the text box across the pane: the send button a
   screen away from the words. Every row of the footer keeps the entry column the
   bottom band has, centred, whether it is the only pane or one of four — a box is
   read the way a paragraph is, and the width it is read at does not depend on how
   many panes are open. The same rule the phone's band uses, so a row added later
   lands in the column without being told, and a pane narrower than the column
   still takes its own width. */
#frame.wide .pane-foot > * {
  margin-left: max(10px, (100% - var(--compose-w)) / 2);
  margin-right: max(10px, (100% - var(--compose-w)) / 2);
  pointer-events: auto;
}
#frame.wide .panes.solo .pane-btn[data-act="zoom"],
#frame.wide .panes.solo .pane-btn[data-act="close"] { display: none; }

.pane-head {
  flex: 0 0 var(--head-h);
  height: var(--head-h);
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  padding: 0 6px 0 0;
  border-bottom: 1px solid var(--line);
  color: var(--dim);
  font: 600 11px var(--mono);
  white-space: nowrap;
  cursor: default;
}
.pane.focus .pane-head { color: var(--text); background: #0c1017; }
/* The focused header paints an opaque lift, so an empty pane kept a light strip
   over a dark body and the step across the tab strip doubled. Same lift, measured
   from --bg-empty rather than from --bg. */
.pane.no-tab.focus .pane-head { background: #080c13; }

/* The pane's tab strip: the top strip's tabs, at the top strip's size — a pane is
   a desktop window, and four panes wearing 11px tabs read as four phones side by
   side. It scrolls sideways the same way. */
.pane-tabs {
  flex: 1 1 auto;
  min-width: 0;
  height: 32px;
  display: flex;
  align-items: stretch;
  gap: 2px;
  overflow-x: auto;
  overflow-y: hidden;
}
/* The line between two panes, dragged to move it. The line itself is the pane's
   own border; this is the hit area over it — a few px, wider than the one px it
   moves, and blank until the pointer arrives. Above the panes, under the tab
   drag's wash and the copy of the tab it carries. */
.pane-seam {
  position: absolute;
  z-index: 2;
  touch-action: none;
  background: transparent;
}
/* Stacked, the handle leans off what is under it: it sits wholly above the line,
   because the pane below carries its tabs from its own top edge down and a px of
   handle over one of them is a px where a press starts a resize rather than a drag.

   Sideways there is no free side — a pane's scrollbar is the ten px inside its
   right border, and the lane of the pane on the right starts at that pane's edge —
   so it straddles instead, two px either way. It used to lean right over a band
   the next header held open for it, which left that pane's tabs six px off the
   edge every other lane was flush with.

   The handle is placed at the boundary and pulled back by its margin, and
   box-sizing: border-box puts the line in the last px of the pane on the left. So
   five px pulled back three is two of scrollbar, the line, and two of the first
   tab. */
.pane-seam.row { width: 5px; margin-left: -3px; cursor: col-resize; }
.pane-seam.col { height: 7px; margin-top: -7px; cursor: row-resize; }
.pane-seam:hover, .pane-seam.dragging { background: var(--sel-line); }
/* Lined up with another pane's line: brighter, still neutral, since colour here
   means status and this is only where the seam is. */
.pane-seam.dragging.snapped { background: var(--dim); }

/* While a seam is dragged the whole area wears its cursor and selects nothing: the
   pointer runs well past the handle and over the transcripts either side of it. */
.panes.seam-row, .panes.seam-row * { cursor: col-resize; }
.panes.seam-col, .panes.seam-col * { cursor: row-resize; }
.panes.seam-row *, .panes.seam-col * { user-select: none; }

/* Where a tab dragged out of its strip would land: over the whole pane it would
   join, over the half an edge drop would split off. Above the focused pane's ring,
   and never in the pointer's way. */
.pane-drop {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  border: 2px solid var(--blue);
  border-radius: 6px;
  background: rgba(88, 166, 255, .16);
}
.pane-drop[hidden] { display: none; }

/* On a pane's tab lane the same element is the caret between two tabs: where the
   tab lands, the way a browser's tab bar shows it. */
.pane-drop.caret {
  /* Above the copy of the tab, which rides under the pointer and would otherwise
     cover the seam it is aimed at. */
  z-index: 5;
  width: 3px;
  margin-left: -1.5px;
  border: 0;
  border-radius: 2px;
  background: var(--blue);
  box-shadow: 0 0 6px rgba(121, 192, 255, .7);
}

/* A tab sliding aside for the one being dropped into its strip. Not the tab under
   the finger: its transform is the drag itself, and it has to keep up. */
.pane-tabs .tab { transition: transform .12s ease; }
.pane-tabs .tab.dragging { transition: none; }

/* The dragged tab's copy, under the pointer. It carries .pane-tabs as well, so the
   clone inside it is sized by the same rules as the strip it came from. */
.pane-ghost {
  position: absolute;
  z-index: 4;
  height: auto;
  overflow: visible;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.pane-ghost[hidden] { display: none; }
/* Opaque, unlike the tab in the strip: this one is over a transcript. */
.pane-ghost .tab { background: var(--sel-bg); }
/* #pane-ghost hangs off #panes, not off a header, so the lane's stretch never
   reaches the clone inside it. The header's height less its own rule: a copy the
   size of the tabs it is being dragged past. */
/* The copy under the pointer is a lane of one, so the rule above already takes
   its divider off — and leaves the edges on where the tab is the active one. */
#frame.wide .pane-ghost .tab { height: var(--lane-h); }

.pane-empty { align-self: center; padding: 0 6px; color: var(--faint); font-weight: 400; }

/* The strip does not stretch, so the plus sits against the last tab the way a browser
   puts it and the gap after it is the spacer .pane-acts pushes off. It takes the
   header's full height rather than the 32px the phone's strip has: the tabs in it
   are rectangles, and they meet the header's edges. */
#frame.wide .pane-head .pane-tabs { flex: 0 1 auto; align-self: stretch; height: auto; gap: 0; }

.pane-acts { margin-left: auto; flex: 0 0 auto; display: flex; align-items: center; gap: 2px; }

/* Agent or shell for this pane. A rule after them rather than a box around them, the
   same pair the composer's own strip offers on a phone. */
#frame.wide .pane-modes {
  display: flex;
  gap: 2px;
  margin-right: 4px;
  padding-right: 5px;
  border-right: 1px solid var(--line);
}
#frame.wide .pane-btn.on { color: var(--text); background: rgba(230, 237, 243, .12); }
#frame.wide .pane-btn .brandmark { display: block; width: 17px; height: 17px; }
.pane-btn {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--faint);
}
.pane-btn:hover { color: var(--text); background: rgba(230, 237, 243, .08); }
.pane-btn svg { width: 17px; height: 17px; display: block; }

/* A pane's plan and its subagents sit in its own column, not on a band: no fade to
   cut glyphs out of, and a rule under each so the two read as the pane's header
   rows. Both give way before the box does — in a pane too short for everything,
   the plan and the subagents lose their rows and the message box keeps its own. */
.pane .planstrip.show,
.pane .subs.show { border-bottom: 1px solid var(--line); }
.pane .planstrip, .pane .subs { flex: 0 1 auto; min-height: 0; overflow: hidden; }

/* A pane's transcript is not the floating one: it scrolls in the space its pane
   leaves it, with no band headroom. `right: auto` undoes #frame.wide .thread, which
   would pull it off the explorer's column a second time. */
#frame.wide.split .pane .thread {
  position: relative;
  inset: auto;
  right: auto;
  /* Basis zero, so the transcript is the part that gives when the pane is short:
     it takes what is left after the strips and the footer, and shrinks to nothing
     before the footer has to clip the send row. */
  flex: 1 1 0;
  min-height: 0;
  /* No bottom padding: padding does not shrink, so those 18px were 18px the
     footer could not have in a pane too short for both. The composer's own
     margin is the gap under the last card. The footer floats over the transcript
   the way the phone's band does, so the bottom padding is the footer's own
   height, written per pane by renderToBottom as --pane-foot-h, plus the gap the
   last card keeps from whatever row the footer starts with. */
  padding: 14px 10px calc(var(--pane-foot-h, 0px) + 8px);
}

/* The jump button rides above the pane's own composer, centred on the pane;
   renderToBottom measures the foot and sets the offset. */
#frame.wide.split .pane .tobottom { left: 50%; bottom: 12px; }

/* Attachments stand over the box, on the transcript, so the box keeps its own
   bottom edge. moveComposer appends the row before the box; the order is the
   footer's to set. */
.pane-foot .drafts { order: -1; }

/* A menu opens over the transcript and above every other row: the status line,
   the subagents strip and the attachments stay against the box, where they were,
   instead of riding up on the menu and landing on the text. app.js keeps the
   menu's height out of --pane-foot-h, so the transcript does not move either. */
.pane-foot .slashmenu { order: -2; }

/* The pane's footer never takes the whole pane: a slash menu capped against the
   window (46dvh) plus a textarea grown to 40dvh is taller than a pane half the
   window, and the pane would clip the composer — the one thing that has to stay
   reachable. The footer stops at the pane's height and the menu is what gives
   way, scrolling inside whatever is left. */
.pane-foot {
  flex: 0 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The footer is the phone's bottom band, in a pane: it floats over the pane's own
   transcript rather than standing in the column beside it, so the text uses the
   whole pane and scrolls under the box instead of leaving a dead margin either
   side of it. Out of the pane's flex flow, so the transcript is the whole of it;
   the pane's head still bounds the footer's growth, and the menu is what gives way
   inside it.

   No fade and no glyph halo. Both were for the rows that stood on the transcript
   bare — the status line and the subagents strip — and neither is here any more:
   the strips are header rows and the status line is a key of the box. What the
   fade did instead was black out the transcript either side of a box that already
   carries its own surface, which is the text you were reading. */
#frame.wide.split .pane-foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  max-height: calc(100% - var(--head-h));
  pointer-events: none;
}
/* The menu is what gives way first, and by a lot: shrinking is weighted by size,
   so an even factor takes a slice out of the box as well. */
.pane-foot .slashmenu.show { flex: 1 100 auto; min-height: 0; }

/* In a pane short enough that even the box does not fit, the entry is what gives
   and the row of controls under it stays: the textarea's row takes what is left
   (minmax(0, 1fr)) instead of the grid growing past the pane and the send button
   being the part clipped away. */
.pane-foot .composer {
  flex: 0 1 auto;
  min-height: 0;
  grid-template-rows: minmax(0, 1fr) auto;
}
.pane-foot .composer textarea { min-height: 0; }

/* The stub: what a pane without the composer shows in its place. Same height and
   same edge as the box it stands for, so the footer does not jump when the focus
   moves; the text is whatever was typed into that pane's thread, or the invitation
   to type. */
.pane-entry {
  display: grid;
  grid-template-columns: var(--box-cols);
  grid-template-rows: minmax(0, var(--box-lines)) var(--box-controls);
  gap: var(--box-gap);
  align-content: start;
  margin: var(--box-margin);
  padding: var(--box-pad);
  min-height: 0;
  background: transparent;
  border: 1px dashed var(--line-strong);
  border-radius: 22px;
  color: var(--faint);
  text-align: left;
  cursor: text;
}
/* Said, not left to the browser: the `display` above beats the UA stylesheet's
   rule for [hidden], so the focused pane kept a dashed empty box above the
   composer that had just landed in the same slot. */
.pane-entry[hidden] { display: none; }

/* A pane with no tab is the phone's new-thread page: the plus in the middle, the
   whole body the target. Nothing it would stand over or under is drawn — no
   strips, no jump pill, and no footer, so no stub and no box to type into a
   thread that is not there. */
.pane-newpage { display: none; }
.pane.no-tab .pane-newpage {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  cursor: pointer;
}
/* Darker than the panes either side of it: the window's one pane with nothing in
   it, said in the background rather than only in the words. The header inherits it
   while the pane is unfocused; focused, it takes the darker lift above. */
.pane.no-tab { background: var(--bg-empty); }
.pane.no-tab .thread,
.pane.no-tab .planstrip,
.pane.no-tab .subs,
.pane.no-tab .tobottom,
#frame.wide .pane.no-tab > .panel,
.pane.no-tab .pane-foot { display: none; }
/* A short stacked pane, or a phone in landscape, would otherwise squash the circle
   into an ellipse: the mark above it does not shrink, so the circle took the whole
   loss. Both pages, because both carry the mark. */
.endpage .endpage-plus,
.pane-newpage .endpage-plus { flex: 0 0 96px; }
.pane-entry:hover { border-color: var(--sel-line); color: var(--dim); }
/* The stub stands on the pane's own transcript with nothing between them: the
   footer's fade is gone, and unlike the box it stands for the stub has no surface.
   It takes the box's — the same fill and the same blur — so a pane you are not
   typing into reads its own draft, its keys and its status line rather than the
   text scrolling under them. Only in a pane: on the phone the stub is never
   drawn. */
#frame.wide.split .pane-entry {
  background: rgba(13, 16, 20, .88);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}

/* A pane with a message half written in it shows the words, at the composer's
   own colour: it is text waiting to be sent, not a placeholder. */
.pane-entry.typed { color: var(--text); border-style: solid; }
.pane-entry[data-colour] { border-color: color-mix(in srgb, var(--tab-colour) 40%, transparent); }

/* The keys the stub draws where the box draws its own. They wear the box's
   classes, so every rule that sizes and places a key there sizes and places it
   here — on the phone, on a wide frame, and in a pane too narrow for the row. The
   wrapper draws no box of its own: with `display: contents` the keys are the
   stub's grid items, and one `aria-hidden` covers all seven. */
.pane-entry-keys { display: contents; }
/* A span is not a button, so the three keys the browser centred because they were
   buttons are given their line box. `--box-controls` is the height of a key. */
.pane-entry .attach,
.pane-entry .modelpick,
.pane-entry .effortpick {
  display: block;
  line-height: var(--box-controls);
  text-align: center;
}
/* The weight `:disabled` gives a key in the box, for the keys the box would have
   disabled here: the agent while a turn runs, the model and the effort without a
   thread. `renderStubKeys()` decides which. */
.pane-entry .off { opacity: .4; }
/* The words sit where the textarea's do: its font, its padding, and the two lines
   it opens at (`rows="2"`), so the focus moving does not shift the text a pixel.
   A longer message is clamped to those two lines. */
.pane-entry-text {
  min-width: 0;
  /* The row above holds the two lines; the text takes that row and no more, so a
     pane too short for the footer shrinks the stub instead of the words spilling
     past the pane, which clips them. The second cap ends the box at the foot of
     line two rather than at the foot of the row: clipping happens at the padding
     edge, so the textarea's 5px of bottom padding was a strip the top of line
     three showed through. */
  min-height: 0;
  max-height: min(100%, calc(var(--box-lines) - 5px));
  padding: 10px 8px 5px;
  font: 400 14px/1.45 var(--mono);
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  /* A line break typed into the box is a line break in the stub, and a word with
     nowhere to break is broken rather than run out of the box. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Shell mode is one pane's, so renderPanel mounts the panel in that pane, in the
   transcript's slot: ahead of .thread, which steps aside, and above the pane's
   footer, which keeps the composer the shell is typed into. A pane has no band
   above it and is already clear of the explorer's column, so the headroom and the
   inset go. */
#frame.wide .pane > .panel {
  position: relative;
  inset: auto;
  right: auto;
  flex: 1 1 0;
  min-height: 0;
  padding: 0;
}
#frame.wide .pane > .panel.show + .thread { display: none;
}
/* The footer floats, so the panel no longer stands on it by being next in the
   column. The shell's last line is the prompt: the panel stops where the footer
   starts, by the same measured height the transcript pads itself with. */
#frame.wide.split .pane > .panel { margin-bottom: var(--pane-foot-h, 0px); }

/* The splitters moved into the pane headers, which are the only place there is a pane
   to split. The top bar's pair stays in the markup for Ctrl+D and Ctrl+Shift+D, which
   click nothing, so it is shown at no width. */
.split-btn { display: none; }

/* ---------- the composer on a wide frame ---------- */

/* The box is built for a thumb: 14px type, a 34px row of keys, a 38px send, and
   two lines held open because a thumb writes into a box it cannot see past. With
   a mouse on a 27" screen that was the largest thing on the page, and with four
   panes open four copies of it were most of the floor. Everything here is the
   phone's box scaled down — same grid, same tracks, same radii in proportion —
   so nothing moves between the two, it only gets smaller.

   Every length is read against the frame's own zoom: #frame.wide renders at
   --zoom, so 11px of type here is 12 on the screen, and a number that merely
   looks smaller than the phone's in the stylesheet can still be the bigger of
   the two in front of the reader. lib/wide-composer-size.test.js compares them
   after the zoom for that reason. */
#frame.wide {
  /* The keys at the ends of the row sit in the box's rounded corners, and at 4px
     beside the send chip and 2px under it the chip read as tucked into the curve
     rather than standing in the box. These are a little over the phone's room
     round its own bigger key — its share of a 20px key is 5px and 4px — rounded
     up until the corner looked right. What is pinned is the floor, not these two
     numbers: lib/wide-composer-size.test.js measures what is left between the
     chip's corner and the inside of the box's border. */
  --box-pad: 3px 8px 6px;
  --box-gap: 5px;
  /* The height of the keys. They sit beside the entry here rather than under it,
     so this is a row only in a pane too narrow for that — see the @container at
     the end of this file, which is also what the stub's second row reads. */
  --box-controls: 25px;
  /* The phone's nine tracks at the smaller keys. */
  --box-cols: 30px auto auto auto 1fr auto auto auto 30px;
  /* The one line the entry opens at, in its own font and padding. The stub in a
     pane without the composer is sized from this, so the two name one number. */
  --box-lines: calc(1 * 1.45 * 13.75px + 8px);
  /* And how far it grows once it is typed into: six of those lines, fixed. The
     phone caps the entry at a share of what it stands in (40dvh here, a third of
     the pane in app.js's resizeDraft), which on a desktop window is a box the
     height of a transcript. A keyboard writes a long message without needing to
     see all of it at once, and the pane under the box is what is being read. */
  --box-grow: calc(6 * 1.45 * 13.75px + 8px);
}

/* The phone's two rows at the smaller sizes: the entry across the top, the keys
   under it. They were tried on one row, which came in at the height of a status
   line, and the row a box needs for nine tracks is one a pane does not always
   have — a box that narrow put every character on a line of its own. What the
   frame changes is the sizes, not the shape. */
#frame.wide .composer { border-radius: 11px; }

#frame.wide .composer textarea,
#frame.wide .pane-entry-text {
  padding: 4px 5px;
  font-size: 13.75px;
}

/* One line to open at, not the phone's two: `rows="2"` is the opening height of
   a box typed into with a thumb. The autosize in app.js grows it from here on
   every keystroke and clears the inline height when the box empties, so this is
   what an empty box comes back to. */
#frame.wide .composer textarea {
  height: var(--box-lines);
  max-height: var(--box-grow);
}

/* The stub stands for that box, so it carries its corner. Its rows and its text
   are the box's own — the lines the entry opens at, then the keys. */
#frame.wide .pane-entry { border-radius: 11px; }

/* One box for every key on the row: attach, mic, steer and send read as the same
   kind of thing, so they shrink as one. The glyph in attach is text, not an svg. */
#frame.wide .attach,
#frame.wide .mic,
#frame.wide .steer,
#frame.wide .send {
  width: 30px;
  height: 25px;
  border-radius: 7.5px;
  font-size: 16px;
}

#frame.wide .mic svg,
#frame.wide .steer svg,
#frame.wide .send svg { width: 15px; height: 15px; }

/* Cancel is the narrow key beside the mic, and the share that takes its track
   after the stop carries the row's height as its line box. */
#frame.wide .miccancel { width: 25px; height: 25px; border-radius: 7.5px; }
#frame.wide .miccancel svg { width: 12.5px; height: 12.5px; }
#frame.wide .micpct { width: 25px; font: 600 10px/25px var(--mono); }

#frame.wide .agentpick,
#frame.wide .modelpick,
#frame.wide .effortpick {
  height: 25px;
  padding: 0 5px;
  border-radius: 7.5px;
  font-size: 10.5px;
}

#frame.wide .agentpick { gap: 5px; }
#frame.wide .agentpick svg { width: 14px; height: 14px; }
#frame.wide .modelpick { max-width: 98px; }
#frame.wide .effortpick { max-width: 72px; }

/* The skills and commands under the entry: a row of them is read with a mouse on
   it, so the name drops to the size of the words it completes and the two lines
   of description under it follow. */
#frame.wide .slashmenu { padding: 4px; gap: 2px; }
#frame.wide .slashmenu .slabel { font-size: 8px; padding: 1px 4px 2px; }
#frame.wide .slashmenu .srow { padding: 4px 6px; border-radius: 6px; gap: 1px; }
#frame.wide .slashmenu .sname { font-size: 10px; }
#frame.wide .slashmenu .sdesc { font-size: 9px; }
/* The agent rows keep their mark clear of the name at the shorter padding. */
#frame.wide .slashmenu .srow.agent-option, #frame.wide .slashmenu .srow.in-use { padding-left: 25px; }
#frame.wide .slashmenu .srow.agent-option > .smark,
#frame.wide .slashmenu .srow.in-use > .smark { left: 6px; top: 5px; }
#frame.wide .slashmenu .smark { flex-basis: 12px; width: 12px; height: 12px; }

/* The ring dictation draws round the mic key while it records, and while the
   model comes down, is the key's own size less its border: on the phone's 34px
   key a 30px ring, on this one a 22px ring on a 26px key. Left at 30 it was
   drawn over the entry above it and out through the box's bottom padding. The
   icon gives the ring its room the same way it does on the phone. */
#frame.wide .mic.rec::before,
#frame.wide .mic.loading::before { width: 20px; height: 20px; }
#frame.wide .mic.rec svg,
#frame.wide .mic.loading svg { width: 11px; height: 11px; }

/* The clamp ends the stub's text on the foot of its last line — one here, two on
   a phone. The row is those lines plus the entry's padding, and clipping happens
   at the padding edge, so what comes off is this box's own bottom padding rather
   than the phone's. */
#frame.wide .pane-entry-text {
  max-height: min(100%, calc(var(--box-lines) - 4px));
  -webkit-line-clamp: 1;
}

/* The attachment row stacks on the box inside the same cap. A chip built around
   a thumb-sized thumbnail stood a third taller than the whole row of controls
   under it, so the thumbnail comes down to the row's height and the two lines of
   name and size with it. */
#frame.wide .drafts { gap: 4px; padding: 4px 10px 0; }
#frame.wide .chipfile { height: 28px; gap: 4px; border-radius: 7px; }
#frame.wide .chipfile img { width: 28px; height: 28px; }
#frame.wide .chipfile .glyph { width: 22px; font-size: 11px; }
#frame.wide .chipfile .meta { max-width: 92px; gap: 0; }
#frame.wide .chipfile .fname { font-size: 9px; }
#frame.wide .chipfile .fsize { font-size: 8px; }
#frame.wide .chipfile .drop { width: 16px; height: 16px; border-radius: 5px; }
#frame.wide .chipfile .drop svg { width: 9px; height: 9px; }
