/* VCode — tokens and layout lifted from the design doc.
   The two faces ship from this origin (public/fonts/) and the service worker
   precaches them, so the app renders the same with the network off. Each role
   falls back to a system face, and `swap` means no font fetch blocks 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;
  --bg-chrome: #0b0e12;
  --line: #1e242c;
  --line-strong: #2a323d;
  --input: #141922;
  --text: #e6edf3;
  --dim: #8b949e;
  /* A toggle that is on: a shade under --text, which its hover turns it to. */
  --lit: #c2c9d0;
  /* A tab's folder line: a step under --dim, so on a tab you are not on the folder
     still sits behind the name, which is --dim there. */
  --subtle: #6b7480;
  --faint: #4a5058;
  --body: #b9c2cc;
  --green: #7ee787;
  --amber: #e3b341;
  --red: #f85149;
  --violet: #d2a8ff;
  --blue: #79c0ff;
  /* The Agent Commander's purple, from VCode's mark. */
  --conductor: #b36bff;
  --orange: #ffa657;
  --cyan: #56d4dd;
  --pink: #f778ba;
  /* Two faces. --font-mono is for what reads as code: code, diffs, shell and
     tool output, commands, file names and paths, slash commands, env names and
     keycaps. --font-sans is everything else, numbers in meters included, with
     tabular figures where they line up. */
  --font-sans: 'Noto Sans', 'VCode Symbols', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'VCode Symbols', ui-monospace, monospace;
  /* The transcript's text size; the terminal reads it (termFontSize). */
  --transcript-size: 13.5px;

  /* 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);

  /* Buttons: GitHub's raised buttons in this palette. Neutral, primary and danger,
     each lifted by an inner top highlight and a two-step drop shadow; the filled
     two carry a brighter highlight. */
  --btn-bg: #212830;
  --btn-line: #3d444d;
  --btn-text: #f0f6fc;
  --btn-primary: #238636;
  --btn-danger: #da3633;
  --btn-lift: inset 0 1px 0 rgba(255, 255, 255, .06), 0 1px 2px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .28);
  --btn-lift-strong: inset 0 1px 0 rgba(255, 255, 255, .14), 0 1px 2px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .28);

  /* 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;
}

/* The type roles' faces, Latin subset, from Google Fonts (SIL OFL 1.1). See
   public/fonts/README.md. Every file is in sw.js SHELL. */
@font-face {
  font-family: 'Noto Sans';
  src: url('/fonts/noto-sans.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('/fonts/jetbrains-mono.woff2') format('woff2');
  font-weight: 400 600;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* The shell's symbols the two faces lack (● ✓ ⑂ ⏎ …), from Noto's symbol and
   math fonts, so every OS draws the same ones at the same size. Second in both
   stacks. Built by scripts/build-symbol-font.py; see public/fonts/README.md. */
@font-face {
  font-family: 'VCode Symbols';
  src: url('/fonts/symbols.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+2190-2193, U+21AA, U+21AF, U+21E7, U+22EF, U+2303-2304, U+2318, U+2325, U+23CE, U+2442, U+258C, U+25A0, U+25A3-25A4, U+25B4, U+25B6, U+25B8, U+25BE, U+25CB, U+25CF, U+25F7, U+26A0, U+2713, U+2715, U+2717, U+275D, U+27F3;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-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); } }
@keyframes breathe { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(.78); opacity: .7; } }

::-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;
  /* --room: the height above a keyboard laid over the page (keyboard-room.js). */
  height: var(--room, 100dvh);
  background: var(--bg);
  overflow: hidden;
  padding-bottom: env(safe-area-inset-bottom);
  /* Landscape on a notched phone: viewport-fit=cover lets the page run under the
     cutout, so the frame, and every band and sheet placed inside it, stops short
     of it. The strip beside the cutout shows the page's background. */
  margin: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left);
}

/* ---------- 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, and a solid mark on the selected tab sits in a dark disc. */
.composer svg, .tobottom svg, .tab 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;
  padding-top: env(safe-area-inset-top);
  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%);
}

/* On the phone the transcript's 3px scrollbar runs the frame's right edge, under
   both bands, and the fade dimmed its thumb to nothing near the top and bottom.
   The fade stops short of it, so the thumb shows the whole way down. */
#frame:not(.wide) .overhead,
#frame:not(.wide) .underfoot {
  background-size: calc(100% - 3px) 100%;
  background-repeat: no-repeat;
}

/* On the phone the meters, the mode strip and the tab strip are solid, as on a
   wide frame: through the band's fade the transcript's text showed between their
   icons and around the tabs as it scrolled under them. The fade is left to the
   todo strip, the band's one row over the text. */
#frame:not(.wide) .overhead {
  /* Keep transcript text out of the status bar above the solid header rows. */
  background: linear-gradient(var(--bg), var(--bg)) top / 100% env(safe-area-inset-top) no-repeat;
}
#frame:not(.wide) .agents,
#frame:not(.wide) .modestrip,
#frame:not(.wide) .topbar {
  background: linear-gradient(var(--bg), var(--bg)) no-repeat 0 0 / calc(100% - 3px) 100%;
}
#frame:not(.wide) .planstrip {
  background: linear-gradient(to bottom, rgba(8, 9, 11, .9), rgba(8, 9, 11, .75)) no-repeat 0 0 / calc(100% - 3px) 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(--font-sans);
  font-size: 12px;
  max-width: 240px;
}

.tab .tabcopy { display: flex; flex: 1 1 auto; flex-direction: column; justify-content: center; min-width: 0; max-width: 180px; text-align: left; }
.tab .label { display: block; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 14px; }
.tab .folder { display: flex; align-items: center; gap: 3px; max-width: 180px; min-width: 0; color: var(--subtle); font-size: 9px; font-weight: 400; line-height: 10px; margin-top: 3px; white-space: nowrap; -webkit-text-stroke: 0; }
.tab .folder svg, .ws .folder svg { flex: none; width: 10px; height: 10px; }
.tab .folder .ftext, .ws .folder .ftext { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The tab's machine after its folder: the folder gives way first. */
/* An unselected tab's name sits well above its folder line (--subtle): at --dim
   the two greys read as one. The selected tab keeps --text and its stroke. */
.tab:not(.on) .label, .drawer .ws:not(.on) .label { color: #c2cad3; }
.tab .folder .fmachine, .ws .folder .fmachine { flex: none; max-width: 60%; overflow: hidden; text-overflow: ellipsis; color: inherit; }
.tab .tabicon { flex-basis: 13px; width: 13px; height: 13px; }
/* The name being edited in place, after a click on the current tab's name. */
.tab .wsname {
  width: 140px;
  min-width: 0;
  padding: 1px 5px;
  background: var(--bg);
  border: 1px solid var(--sel-line);
  border-radius: 4px;
  color: var(--text);
  font: inherit;
  caret-color: var(--violet);
  -webkit-text-stroke: 0;
}
.tab .wsname:focus { outline: none; }
.tab .wsname::selection, .ws .wsname::selection {
  background: #3768a0;
  color: #fff;
  -webkit-text-fill-color: #fff;
  -webkit-text-stroke: 0;
}

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

/* On the phone the strip sits in the top fade, and a transparent tab was a label
   over the transcript. Each tab is an opaque rectangle, the way the desktop pane
   header's lane reads: a fill a step below the selected one, and a hairline at the
   start and along the bottom that the selected tab breaks with its colour. The
   press, the selection and the drag's fill are restated here, or the #frame rule
   outranks them. A border, not an inset shadow, so the drag keeps its lift. */
#frame:not(.wide) .tab {
  background: var(--bg-chrome);
}
#frame:not(.wide) .tab:first-child { border-left-color: var(--line-strong); }
#frame:not(.wide) .tab:active { background: var(--input); }
#frame:not(.wide) .tab.on { background: var(--sel-bg); border-left-color: var(--sel-line); }
#frame:not(.wide) .tab.dragging { background: #1c232d; }

/* The selected title is thickened with a stroke, not font-weight: a heavier
   weight is wider, so a swipe's landing resized the tab and moved its ellipsis. */
.tab.on {
  color: var(--text);
  -webkit-text-stroke: .35px currentColor;
  background: var(--sel-bg);
  border-left-color: var(--sel-line);
  border-right-color: var(--sel-line);
}

/* The selected tab's colour is a thin edge flush with the top of the shape. */
.tab.on::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
  border-radius: 0;
  background: var(--tab-colour, var(--violet));
}

/* Inactive tabs are underlined in the selected tab's colour, the way Chrome's
   strip runs into the tab in front; the selected tab has no underline. The top
   bar (the pane's strip when wide) names that colour as --strip-colour, and
   while the selection glides the colour it is moving through (--glide-colour, public/strip-motion.js). */
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="red"], > .pane-tabs > .tab.on[data-colour="red"]) { --strip-colour: var(--red); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="orange"], > .pane-tabs > .tab.on[data-colour="orange"]) { --strip-colour: var(--orange); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="yellow"], > .pane-tabs > .tab.on[data-colour="yellow"]) { --strip-colour: var(--amber); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="green"], > .pane-tabs > .tab.on[data-colour="green"]) { --strip-colour: var(--green); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="cyan"], > .pane-tabs > .tab.on[data-colour="cyan"]) { --strip-colour: var(--cyan); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="blue"], > .pane-tabs > .tab.on[data-colour="blue"]) { --strip-colour: var(--blue); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="purple"], > .pane-tabs > .tab.on[data-colour="purple"]) { --strip-colour: var(--violet); }
:where(#frame:not(.wide) .topbar, #frame.wide .pane-head):has(> .tabstrip > .tab.on[data-colour="pink"], > .pane-tabs > .tab.on[data-colour="pink"]) { --strip-colour: var(--pink); }
#frame:not(.wide) .tabstrip > .tab:not(.on)::before,
#frame.wide .pane-head .pane-tabs > .tab:not(.on)::before,
#frame:not(.wide) .tabstrip.gliding > .tab::before,
#frame.wide .pane-head .pane-tabs.gliding > .tab::before {
  content: '';
  position: absolute;
  /* Include the side borders so the rail meets the selected tab's sides. */
  left: -1px;
  right: -1px;
  bottom: 0;
  height: 1px;
  background: var(--strip-colour, var(--violet));
  pointer-events: none;
}
/* The underline runs on past the last tab, under the + and to the row's right
   edge, at the strip's bottom so it lines up with the tabs'. The tabs are drawn
   over it, and the selected + covers it with the bar's own fill, so the
   selection breaks it. */
#frame:not(.wide) .topbar { position: relative; }
#frame:not(.wide) .topbar::before {
  content: '';
  position: absolute;
  left: 10px;
  right: 3px;
  bottom: 2px;
  height: 1px;
  background: var(--strip-colour, var(--violet));
  pointer-events: none;
}
#frame:not(.wide) .topbar:has(> .tabstrip.gliding)::before {
  background: var(--glide-colour, var(--strip-colour, var(--violet)));
}
#frame:not(.wide) .topbar #new-tab.on:not(.gliding) { border-radius: 0; background: var(--bg); }
/* On the new-thread page the selected + ends the underline: the tabs' own
   underlines run up to it and nothing is drawn past it. With the + at the left
   end the line runs on under the tabs as usual. */
#frame:not(.wide):not(.end-left) .topbar:has(> #new-tab.on:not(.gliding))::before { display: none; }
/* A pane's header does the same, from its left edge to its right one, past the +
   and under the pane's buttons. */
#frame.wide .pane-head { position: relative; }
#frame.wide .pane-head::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--strip-colour, var(--violet));
  pointer-events: none;
}
#frame.wide .pane-head:has(> .pane-tabs.gliding)::before {
  background: var(--glide-colour, var(--strip-colour, var(--violet)));
}
/* While the selection glides the tab it left is still .on, so every tab draws the
   underline and the gliding fill covers it where the selection is. */
#frame:not(.wide) .tabstrip.gliding > .tab::before,
#frame.wide .pane-head .pane-tabs.gliding > .tab::before {
  background: var(--glide-colour, var(--strip-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;
}

/* The selection moving: public/strip-motion.js. While it slides between two tabs
   or follows a swipe, the strip draws the selected tab's fill, side edges and
   top edge itself, as its ::after, at --glide-x and --glide-s. It is drawn over
   the tabs' backgrounds, and their contents are lifted above it. The selected
   tab drops its own selection for that time, so there is one fill, not two. */
.tabstrip, .pane-head .pane-tabs { position: relative; isolation: isolate; }
/* How much the gliding selection is the + ring rather than a tab, 0..1 (app.js
   plusGlide): the selection's opacity is 1 less it and the ring's is it, so a
   swipe to or from the new-thread page fades one into the other. 0 everywhere
   but there. Not transitioned itself: the opacities it sets are, on the
   compositor. Not inherited: set on the strip every frame, an inherited one
   restyled every tab in it, a working tab's spinner included. Only the glide's
   own boxes take it. */
@property --glide-m {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
/* Only the glide layers need the position and stretch changed on every touch.
   Keeping them off the tab contents avoids restyling each label and spinner. */
@property --glide-x {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}
@property --glide-s {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}
/* The ring's outer size: .topbar .new-tab.on svg below is 14px + 2 x 4px padding
   + 2 x 1.5px border. app.js reads it to aim the glide at the ring. */
.topbar { --plus-ring: 25px; }
/* The gliding selection: one box 100px wide (GLIDE_BOX in lib/tab-motion.js),
   moved by --glide-x and stretched by --glide-s to the tab it stands for. Only
   its transform and opacity change while it moves, which the phone draws without
   painting the strip again; a width, a colour and a shape changing every frame
   painted it every frame, and swipes stalled on it. The bar takes its new colour
   at once rather than blending. */
.tabstrip.gliding::after, .pane-head .pane-tabs.gliding::after, .topbar .new-tab.gliding::before {
  --glide-m: inherit;
  --glide-x: inherit;
  --glide-s: inherit;
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: 0;
  width: 100px;
  box-sizing: border-box;
  transform-origin: 0 0;
  /* Keep the drag and finish on the same layers. Without this the finger's
     zero-duration moves repaint the strip; lifting creates new layers. */
  will-change: transform, opacity;
  transform: translateX(var(--glide-x, 0px)) scaleX(var(--glide-s, 0));
  opacity: calc(1 - var(--glide-m));
  transition:
    transform var(--glide-ms, 0ms) var(--glide-ease, ease),
    opacity var(--glide-ms, 0ms) var(--glide-ease, ease);
  background: var(--sel-bg);
  border-left: 1px solid var(--glide-colour, var(--violet));
  border-right: 1px solid var(--glide-colour, var(--violet));
  box-shadow: inset 0 1px 0 0 var(--glide-colour, var(--violet));
  pointer-events: none;
}
.tabstrip.gliding::after, .pane-head .pane-tabs.gliding::after { z-index: 2; }
.gliding > .tab > * { position: relative; z-index: 3; }
.gliding > .tab.on::after { display: none; }
.gliding > .tab.on { background: transparent; border-left-color: transparent; border-right-color: var(--line-strong); }
#frame:not(.wide) .gliding > .tab.on { background: var(--bg-chrome); border-left-color: transparent; }
#frame:not(.wide) .gliding > .tab.on:first-child { border-left-color: var(--line-strong); }
#frame.wide .gliding > .tab.on { background: transparent; border-left-color: transparent; border-right-color: var(--line-strong); }

/* A tab growing in, collapsing out or changing width clips what it holds for the
   moment it is narrower than that, rather than holding the strip open. A tab on its
   way out takes no touch. */
.tab.sizing { overflow: hidden; min-width: 0; }
.tab[data-leaving] { pointer-events: none; }
.dot {
  flex: 0 0 14px;
  width: 14px;
  height: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.dot svg { width: 12px; height: 12px; display: block; overflow: visible; }
.dot svg.ring { width: 14px; height: 14px; }
/* A tab is tight: the whole set scales down, the ring and the glyphs alike. */
.tab .dot svg { width: 10px; height: 10px; }
.tab .dot svg.ring { width: 12px; height: 12px; }
.dot .arc { transform-origin: 7px 7px; animation: rot 1s linear infinite; }
.dot .core { transform-origin: 7px 7px; animation: breathe 1.8s ease-in-out infinite; }
.dot .halo { animation: pulse 1s ease-in-out infinite; }
.agents-n { flex: 0 0 auto; font: 500 9px var(--font-sans); color: var(--amber); font-variant-numeric: tabular-nums; }
.tab .agents-n { margin-left: -1px; }
/* A pinned tab's small pin, after its name; its drawer row's too. */
.pinmark { flex: 0 0 auto; display: inline-flex; align-items: center; color: var(--faint); }
.pinmark svg { width: 10px; height: 10px; display: block; }

/* A tab in Shell mode leads with the prompt in place of the agent's mark: green,
   glowing, a size up, and at full strength whether the tab is on or not. */
.tab .tabicon.termicon, .drawer .ws .tabicon.termicon,
#frame.wide .tab .tabicon.termicon {
  flex: 0 0 18px; width: 18px; height: 18px; opacity: 1; color: var(--green);
  display: flex; align-items: center; justify-content: center;
}
.tabicon.termicon svg {
  width: 16px; height: 16px; display: block; stroke-width: 2.6;
  filter: drop-shadow(0 0 3px rgba(126, 231, 135, .7));
}

/* In the app, a tab whose box is refusing, connecting or offline is faded; it
   still opens, from the cache. */
.tab.unavailable .label, .tab.unavailable .tabicon, .tab.unavailable.on .tabicon { opacity: .45; }
.ws.unavailable .info { opacity: .55; }
.ws .wsbox { font: 400 10.5px var(--font-sans); color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* 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, .report-open {
  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, .report-open svg { width: 18px; height: 18px; display: block; }
.report-open svg { width: 21px; height: 21px; }

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

#update-ready[hidden] { display: none; }
/* Disabled while the tap waits for the newest build to install. */
#update-ready:disabled svg { animation: rot 1s linear infinite; }

/* How many tabs want you, on the Tabs button's corner: orange while they wait on
   an answer, green when they only finished, yellow for a mix. */
#tabs-open { position: relative; }
#tabs-open .attn {
  position: absolute;
  top: 2px;
  right: 3px;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  border-radius: 8px;
  font: 600 10px/15px var(--font-sans);
  font-variant-numeric: tabular-nums;
  color: var(--bg, #0d1014);
  background: var(--green);
  box-sizing: border-box;
  text-align: center;
  pointer-events: none;
}
#tabs-open .attn[data-kind="waiting"] { background: var(--orange); }
#tabs-open .attn[data-kind="mixed"] { background: var(--amber); }

/* 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.
   It meets the strip with no gap, the way tabs meet each other: the negative
   margin takes back the top bar's gap. Flush at rest as well as selected, so
   landing on the new-thread page does not widen the strip and shift every tab. */
.topbar #new-tab { margin-left: -6px; margin-right: auto; }

/* On the new-thread page the + is selected: no tab fill, edges or bar, only the
   + in a ring in the selection's colour, at the strip's height. Past the left end
   it stands in front of the first tab, and the strip takes the spare room instead. */
.topbar #new-tab.on {
  position: relative;
  height: 38px;
  color: var(--text);
}
.topbar #new-tab.on svg {
  box-sizing: content-box;
  width: 14px;
  height: 14px;
  padding: 4px;
  border: 1.5px solid var(--tab-colour, var(--violet));
  border-radius: 50%;
}
/* Past the left end it faces the strip from the other side. */
#frame.end-left .topbar #new-tab.on { order: -1; margin-left: 0; margin-right: -6px; }
#frame.end-left .topbar .tabstrip { margin-right: auto; }
/* A swipe to or from the right end: the + draws the strip's gliding selection
   clipped to itself (app.js plusGlide), so the selection slides out of the strip
   into it in one piece. It keeps its place in the row until the page lands, or
   every tab would shift under the finger; it takes the strip's height, which moves
   nothing sideways. Its own selected look waits under the glide. The strip's
   fade at its right edge goes for as long, or the selection dims on its way out. */
.topbar #new-tab.gliding {
  position: relative;
  height: 38px;
  border-radius: 0;
  overflow: hidden;
  isolation: isolate;
  background: transparent;
  border-left: 0;
  border-right: 0;
}
/* The ring, faded in as the selection fades out (--glide-m), where the selected
   +'s own ring is drawn. That one waits under it until the page lands; the +
   keeps the ring's room. */
.topbar .new-tab.gliding::after {
  --glide-m: inherit;
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 0;
  width: var(--plus-ring, 25px);
  height: var(--plus-ring, 25px);
  box-sizing: border-box;
  transform: translate(-50%, -50%);
  border: 1.5px solid var(--ring-colour, var(--violet));
  border-radius: 50%;
  will-change: opacity;
  opacity: var(--glide-m);
  transition: opacity var(--glide-ms, 0ms) var(--glide-ease, ease);
  pointer-events: none;
}
.topbar .new-tab.gliding.on svg { border-color: transparent; }
/* Past the left end the + is not in the row until the page lands. For the swipe
   it stands in front of the strip and grows into the row, or out of it, with the
   finger (app.js plusAtEnd), so the strip slides over rather than jumping.
   The + floats over the row's left edge and the strip keeps its width and slides
   by a transform, so a frame of the swipe repaints only the +. */
.topbar:has(#new-tab.growing) { position: relative; }
.topbar #new-tab.growing {
  position: absolute;
  left: 10px;
  top: 0;
  bottom: 0;
  margin: auto 0;
  min-width: 0;
  width: var(--plus-w, 38px);
  transition: width var(--glide-ms, 0ms) var(--glide-ease, ease);
}
.topbar:has(#new-tab.growing) .tabstrip {
  flex: none;
  width: var(--strip-w);
  margin-right: auto;
  transform: translateX(var(--strip-x, 0px));
  will-change: transform;
  transition: transform var(--shift-ms, 0ms) var(--shift-ease, ease);
}
.topbar .new-tab.gliding > svg { position: relative; z-index: 1; }
.topbar:has(.new-tab.gliding) .tabstrip.overflowing { -webkit-mask-image: none; mask-image: none; }
/* The right page scrolls the strip to its end, so nothing is past its right edge
   and the fade would only dim the last tab against the +. */
#frame.at-end:not(.end-left) .tabstrip.overflowing { -webkit-mask-image: none; mask-image: none; }

/* 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: 500 8.5px var(--font-sans);
  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;
}
.agents[hidden] { display: none; }

/* 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: 500 7.5px var(--font-sans); color: var(--dim); }
.agent .meter .name.weekly { color: var(--faint); }
.agent .meter .pct { flex: 0 0 5ch; font: 500 7.5px var(--font-sans); color: var(--dim); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* flex:1 with no cap, as the retired terminal app did 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; background: var(--line); overflow: hidden; }
/* The fill is cut into 5px blocks with 2px gaps, a pixel-style meter. */
.agent .meter .bar span {
  display: block;
  height: 100%;
  -webkit-mask-image: repeating-linear-gradient(90deg, #000 0 5px, transparent 5px 7px);
  mask-image: repeating-linear-gradient(90deg, #000 0 5px, transparent 5px 7px);
}
.agent .meter .reset { flex: 0 0 auto; min-width: 36px; font: 500 7.5px var(--font-sans); color: var(--faint); text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }

/* A third agent (Muse) wrapped onto a second row and made the phone's header
   taller. With three cards or more they share the one row: the reset times go,
   which is the only way three bars fit in 390px, and the padding tightens. */
#frame:not(.wide) .agents:has(> .agent:nth-child(3)) {
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 2px;
  padding-left: 6px;
  padding-right: 6px;
}
#frame:not(.wide) .agents:has(> .agent:nth-child(3)) .agent { gap: 4px; padding-left: 3px; padding-right: 3px; }
#frame:not(.wide) .agents:has(> .agent:nth-child(3)) .meter .reset { display: none; }

/* A meter card's popover: the tab menu's raised box, under the card. app.js sets
   `left` and `top`. The card's own text is 7.5px; this is where it reads. */
.agent { cursor: default; }
.agent:focus-visible { outline: 2px solid var(--sel-line); outline-offset: -2px; }
.usage-tip {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 60;
  max-width: min(340px, calc(100vw - 16px));
  padding: 8px 10px;
  background: var(--btn-bg);
  border: 1px solid var(--sel-line);
  border-radius: 10px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 12px 32px rgba(0, 0, 0, .85);
  font: 400 12px var(--font-sans);
  color: var(--text);
  pointer-events: none;
}
.usage-tip[hidden] { display: none; }
.usage-tip .tip-head { font-weight: 500; margin-bottom: 6px; }
.usage-tip .tip-rows { display: grid; grid-template-columns: auto auto auto; column-gap: 12px; row-gap: 4px; }
.usage-tip .tip-name { color: var(--dim); white-space: nowrap; }
.usage-tip .tip-used { white-space: nowrap; font-variant-numeric: tabular-nums; }
.usage-tip .tip-reset { color: var(--dim); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* ---------- 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; }

/* Keep Tabs, Files and Changes on the left of the mobile rail. Changes takes the free
   space, so whichever right-side controls are available stay together at the right edge. */
#frame:not(.wide) .modestrip #tabs-open { order: 1; }
#frame:not(.wide) .modestrip #files-open { order: 2; }
#frame:not(.wide) .modestrip #changes-open { order: 2; margin-right: auto; }
#frame:not(.wide) .modestrip .install { order: 2; margin-left: 0; }
#frame:not(.wide) .modestrip .chip.notify { order: 3; }
#frame:not(.wide) .modestrip .rail-end { order: 4; margin-left: 0; }
#frame:not(.wide) .modestrip .report-open { order: 4; margin-left: 0; }
/* Eleven buttons and the two mode chips share a phone rail since Recently closed
   came in. They start at 33px (the chips at 38px) and shrink together to 24px,
   so the cog stays on a 320px screen with the install offer showing (#82). */
#frame:not(.wide) .modestrip > button { flex: 0 1 33px; width: auto; min-width: 24px; }
#frame:not(.wide) .modestrip .segmented { flex: 0 1 auto; min-width: 0; }
#frame:not(.wide) .modestrip .segmented .chip { flex-shrink: 1; min-width: 24px; }

/* 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(--font-sans);
  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); }
/* The agent mark keeps its brand colour, so an off chip dims it with opacity instead. */
.chip:not(.on) .cg.brandmark { opacity: .55; }
.chip.notify.on { color: var(--lit); }

.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 rail beside the cog. Blue because it is an offer rather than a switch.
   While it shows, it takes the push to the right edge and the cog follows it. */
.install { display: none; }
.install.show { display: inline-flex; color: var(--blue); }
.modestrip .install.show { margin-left: auto; }
.modestrip .install.show + .rail-end { margin-left: 0; }
/* The report button sits just left of the cog and takes the push with it; after
   a shown install, the install has the push. */
.modestrip .report-open { margin-left: auto; }
.modestrip .report-open + .rail-end, .modestrip .install.show + .report-open { margin-left: 0; }
/* Recently closed sits left of the report button and takes the push from it. */
.modestrip .recent-open + .report-open { margin-left: 0; }
/* The one offer gets a bigger mark than the switches beside it, and nothing else:
   no chip, no edge, no label. */
.install svg { width: 24px; height: 24px; }
/* The Agent Commander: VCode's icon in the same slot as the rail's other
   buttons, just before the bell. */
.conductor-open {
  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(--conductor);
  padding: 0;
}
.conductor-open[hidden] { display: none; }
.conductor-open img {
  width: 22px;
  height: 22px;
  display: block;
  border-radius: 50%;
  transition: transform .2s cubic-bezier(.2, .8, .2, 1), box-shadow .2s ease;
}
.conductor-open { transition: background-color .2s ease; }
.conductor-open:active img { transform: scale(.9); }
/* Pressed while the Commander is in front (renderCommanderIcon): a tint of its
   colour behind the icon and a ring round it. */
.conductor-open[aria-pressed="true"] { background: rgba(179, 107, 255, .16); }
.conductor-open[aria-pressed="true"] img { box-shadow: 0 0 0 2px var(--conductor); }
#frame:not(.wide) .modestrip .conductor-open { order: 3; }
/* The Commander tab's own ! or tick on the icon's corner (renderCommanderIcon),
   on a disc of the page colour so it stands off the icon. */
.conductor-open { position: relative; }
.conductor-open .dot {
  position: absolute;
  top: 1px;
  right: 2px;
  border-radius: 50%;
  background: var(--bg);
  pointer-events: none;
}

/* VCode's own mark, drawn as a background (commanderIcon in app.js). */
.vmark { background: url(/icon.svg) center / contain no-repeat; }

@media (prefers-reduced-motion: reduce) {
  .conductor-open, .conductor-open img { transition: none; }
}

/* 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(--font-sans); 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 12.5px var(--font-sans);
  color: var(--body);
}
.planstrip .count { flex: 0 0 auto; font: 600 9px var(--font-sans); color: var(--faint); }
.disclosure {
  flex: 0 0 auto;
  color: var(--faint);
  font: 700 13px/1 var(--font-sans);
  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(--font-sans);
}
/* 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(--font-sans);
  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(--font-sans);
}
.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;
  /* A swipe moves the pages by a transform set on each touch move (app.js
     movePage). Each page on its own layer moves without being painted again;
     without it the phone redrew the whole transcript every frame. */
  will-change: transform;
  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; }

/* A row off screen skips style, layout and paint. Opening a long tab, the swipe's
   preview of one and the window filling in above it each laid out every row, a
   quarter to half a second on a phone. The last 16 rows are always drawn: a
   transcript opens at its end, and rows there held at the 160px guess put the
   end short of where the render scrolled to. Only where the browser anchors the
   scroll: a row drawn for the first time as you scroll up takes its real height,
   and without anchoring what you are reading would jump by the difference. */
@supports (overflow-anchor: auto) {
  .thread > .item:nth-last-child(n+17) {
    content-visibility: auto;
    contain-intrinsic-size: auto 160px;
  }
}

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

.tag {
  flex: 0 0 auto;
  color: var(--text);
  font: 500 12.5px var(--font-sans);
  text-transform: lowercase;
}
.tag::first-letter { text-transform: uppercase; }
.tag.diff { color: var(--amber); }
.tag.error { color: var(--red); }
.tag.approval { color: var(--orange); }

/* A row's icon before its word (rowLabel): the colour is on the icon, the word
   stays in the text colour, as a reply's mark and name do. */
.rowicon { flex: 0 0 auto; display: grid; place-items: center; width: 14px; height: 14px; color: var(--dim); }
.rowicon svg { width: 14px; height: 14px; display: block; }
.rowicon .dot svg { width: 12px; height: 12px; }
.rowicon.user { color: var(--green); }
.rowicon.queued { color: var(--amber); }
.rowicon + .tag { font-weight: 600; }
/* The kind of a panel before its path (rowIcon): the icon alone. */
.rkind { flex: 0 0 auto; width: 14px; height: 14px; color: var(--dim); }
.rkind.diff { color: var(--amber); }

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

.item .count { flex: 0 0 auto; font: 500 9.5px var(--font-sans); 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(--font-sans);
}

.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 sans face, code in mono. */
.item .body,
.streaming .body {
  font: 400 var(--transcript-size)/1.55 var(--font-sans);
  color: var(--body);
  white-space: pre-wrap;
  word-break: break-word;
}

.item.user .body { font-size: var(--transcript-size); color: var(--text); }
.item.user.unread .meta { color: var(--amber); }
.item.user.unread .body { color: var(--dim); }

/* 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(--font-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(--font-sans);
}

.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); }

/* Read aloud while its reply plays: lit like a pressed button, the stop square inside. */
.item .head .copy.speak.on { 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; }

/* A rewind's row: a rule either side of what it took away. Tapped, the removed
   items follow it greyed out ([data-removed]), copy their only control. */
.item.rewound {
  display: flex; align-items: center; gap: 10px;
  margin: 14px 0; color: var(--dim); font-size: 14px; cursor: pointer;
}
.item.rewound .rewound-rule { flex: 1; height: 1px; background: var(--faint); }
.item.rewound .rewound-label { display: inline-flex; align-items: center; gap: 6px; }
.item.rewound .rewound-label svg { width: 14px; height: 14px; }
.item[data-removed] { opacity: 0.45; }

/* Stream detail levels (the cog's TRANSCRIPT group). A group row is a head row
   standing for a run of steps, with no tag colour of its own: it is not any one
   kind of step. Open, its steps sit indented under it on a rule, each its head
   row until a tap opens it. */
.item.oneline > .body { display: none; }
.item.oneline, .item.stepgroup { cursor: pointer; }
.item.stepgroup .count { font-size: 12px; margin-left: auto; }
.item[data-member] {
  margin-left: 10px;
  padding-left: 10px;
  border-left: 2px solid var(--line-strong);
}

/* 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: 500 12px var(--font-sans);
}

/* ---------- 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(--font-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(--font-sans); 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(--font-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;
}

/* Line numbers in a Read listing: present but receding, like every gutter. */
.output .gutter { color: var(--faint); }

/* Code colours are Shiki's github-dark-default, set on each run by app.js. These
   are a diff's own: its file headers, added and removed lines, and hunks. */
.t.c { color: var(--faint); font-style: italic; }
.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(--font-sans); color: var(--faint); }
.item .changes svg { width: 11px; height: 11px; }
.item .changes .noproof { margin-left: auto; color: var(--amber); }

/* The proof under a turn's last reply: what the agent showed for its work. A
   hairline sets it off from the reply's text; its files line is the footer's. */
.item .proof { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line); }
.item .proof-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 6px; }
.item .proof-label { font: 700 8.5px var(--font-sans); letter-spacing: .08em; color: var(--faint); }
/* Where the proof's branch stands: the line review's outdated pill, in the
   colour of its answer. */
.item .proof-merge { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; overflow: hidden; white-space: nowrap; font: 500 12px var(--font-sans); }
.item .proof-merge code { font: 500 11px var(--font-mono); color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.item .proof-merge.ok { color: var(--green); }
.item .proof-merge.warn { color: var(--orange); }
.item .proof-merge.bad { color: var(--red); }
.item .proof-shots { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
.item .proof-shots img.shot { display: block; max-width: 100%; max-height: 40vh; border-radius: 10px; border: 1px solid var(--line); }
/* A check: its mark in a column of its own, so a wrapped line keeps under the
   command, then the command in mono and what it printed dimmed. */
.item .proof-check { display: grid; grid-template-columns: 14px minmax(0, 1fr); column-gap: 6px; margin: 3px 0; font: 400 11.5px/1.45 var(--font-sans); color: var(--dim); overflow-wrap: anywhere; }
.item .proof-mark { font-weight: 700; color: var(--faint); }
.item .proof-check.pass .proof-mark { color: var(--green); }
.item .proof-check.fail .proof-mark { color: var(--red); }
.item .proof-command { margin-right: 5px; font: 600 11px var(--font-mono); color: var(--text); }
.item .proof-verdict { font-weight: 600; }
.item .proof-verdict.pass { color: var(--green); }
.item .proof-verdict.fail { color: var(--red); }
.item .proof .changes { margin-top: 6px; }
.t.hunk { color: var(--violet); }

/* Links need a tap target that survives a thumb without breaking the line box. */
a.link { cursor: pointer; color: var(--blue); text-decoration: underline; text-underline-offset: 2px; word-break: break-all; padding: 2px 0; }
a.link:active { color: var(--green); }
/* A tool card's path that opens the file: the name keeps its face, underlined. */
a.link .pathlabel .pbase { text-decoration: underline; text-underline-offset: 2px; }

/* 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(--font-sans);
  /* 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(--font-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; }

/* Three looks and no more. Primary is the act the surface is for (Accept, Allow
   once, Save, Unlock); danger is a destructive one, and an armed confirm (Revert,
   Deny, Delete?). Every other text button is neutral. The padding is for buttons
   32px tall and up; one of 30px or less takes `0 12px`. */
.btn {
  height: 34px;
  padding: 0 16px;
  border-radius: 6px;
  border: 1px solid var(--btn-line);
  background: var(--btn-bg);
  color: var(--btn-text);
  font: 600 13px var(--font-sans);
  box-shadow: var(--btn-lift);
}

.btn:active { background: #1b2129; }
.btn:disabled { opacity: .5; }

.btn.primary { background: var(--btn-primary); border-color: rgba(240, 246, 252, .1); color: #fff; box-shadow: var(--btn-lift-strong); }
.btn.primary:active { background: #1f7a30; }

/* `.armed` is a neutral button on its first tap, asking to be tapped again. */
.btn.danger,
.btn.armed { background: var(--btn-danger); border-color: rgba(240, 246, 252, .1); color: #fff; box-shadow: var(--btn-lift-strong); }
.btn.danger:active,
.btn.armed:active { background: #c42f2c; }

/* Hover only where there is a pointer to hover with: on a phone it sticks after
   the tap. */
@media (hover: hover) {
  .btn:hover { background: #262e38; }
  .btn.primary:hover { background: #29903b; }
  .btn.danger:hover,
  .btn.armed:hover { background: #e03e3b; }
}

/* streaming card */

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

/* Fault line — a stopped agent, an errored turn, or a usage pause. A transcript
   line between two hairlines, no box: the icon says which it is (a red stop
   square, an amber ring for the pause), the sentence says what happened, and the
   one action or the time left sits on the right. */
/* In a narrow pane the buttons wrap under the words, which would otherwise be
   squeezed to a letter a line beside them. */
.fault {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.fault .ficon { flex: 0 0 18px; height: 20px; display: grid; place-items: center; color: var(--red); }
.fault .ficon svg { width: 16px; height: 16px; }
.fault .ficon .track { stroke: var(--line-strong); }
.fault .ficon .arc { stroke: var(--amber); }
.fault .fbody { flex: 1 1 100px; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.fault .text { font: 500 13.5px/1.45 var(--font-sans); color: var(--text); white-space: pre-wrap; word-break: break-word; }
.fault .detail { font: 400 12px/1.45 var(--font-sans); color: var(--dim); white-space: pre-wrap; word-break: break-word; }
.fault .fside { flex: 0 0 auto; margin-left: auto; display: flex; align-items: center; gap: 10px; }
.fault .count { font: 500 12px/20px var(--font-sans); color: var(--amber); font-variant-numeric: tabular-nums; }
.fault .act {
  height: 28px;
  min-height: 28px;
  padding: 0 12px;
  border-radius: 6px;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--text);
  font: 600 12px var(--font-sans);
  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(--font-sans); color: var(--faint); }
/* 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(--font-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); }

/* The activity spinner, a canvas public/logo-spinner.js draws: 13 x 13 blocks on whole
   device pixels, about 14 CSS px like the agent marks beside it (logo-spinner.js
   sets the exact size), in its own greys, so no tone rule colours it. The script puts
   one bitmap pixel on each device pixel; at 1.25x Chrome still blends neighbouring
   pixels unless told to take the nearest, which is then the bitmap's own. */
.spinner {
  display: block;
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  image-rendering: pixelated;
}
.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); }

/* The status word (Thinking, Writing, Running bash, Compacting) is grey text with a
   white head, the same for every label, that sweeps left to right across it in 2.4s
   with a pale trail fading back to grey behind it. Each 2.9s turn first holds still
   for 0.5s while the spinner's logo changes, then sweeps. At the start and end of a
   sweep the head and its trail are off the word, so the restart happens out of
   sight. The phase is set from the wall clock in app.js. */
.tag.shimmer {
  --shimmer-head: #ffffff;
  --shimmer-trail: color-mix(in srgb, var(--shimmer-head) 30%, var(--text));
  --shimmer-lr: linear-gradient(90deg, var(--dim) 28%, var(--shimmer-trail) 45%, var(--shimmer-head) 50%, var(--dim) 54%);
  background-image: var(--shimmer-lr);
  background-size: 400% 100%;
  background-position: 0% 0;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: status-shimmer 2.9s linear infinite;
}
/* TURN_MS in public/logo-spinner.js is this animation's length, and MORPH_MS its still
   start (0.5s of 2.9s); change both together. */
@keyframes status-shimmer {
  0%, 17.2414% { background-position: 100% 0; }
  100% { background-position: 0% 0; }
}

/* 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 selector would lose to the rule it means to stop. */
  .link-dot.off, .planstrip .pd.now { animation: none; }
  /* The status glyphs keep their shapes and lose the spin, the breath and the pulse. */
  .dot .arc, .dot .core, .dot .halo { animation: none; }
  /* The status word stays grey: with no animation the head sits off the word. */
  .tag.shimmer { 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(--font-sans); padding: 20px; }
/* The line an empty tab opens with (app.js renderWelcome): the V mark, faded the
   way the end page's logo is, over one short sentence. Centred in the pane by the
   transcript's flex column; it is the only child while it shows. */
.welcome { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px; text-align: center; }
/* The mark is a background, not an <img>: app.js writes an image src only in
   showImage (lib/store-transport.test.js), and this one is the page's own icon. */
.welcome-mark { width: 36px; height: 36px; opacity: .55; background: url(/icon.svg) center / contain no-repeat; }
.welcome-line { font: 600 24px/1.25 var(--font-sans); letter-spacing: -.01em; color: var(--text); }
/* One tip on getting more out of VCode, under the line in the quieter body type. */
.welcome-tip { max-width: 36ch; font: 400 14px/1.5 var(--font-sans); color: var(--dim); }
/* The faint code rain behind an empty tab's welcome (public/code-rain.js): the
   homepage's rain, quieter and slower, over the pane's own background. */
.welcome-rain { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity .8s ease; }
.welcome-rain.on { opacity: 1; }
.welcome { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) { .welcome-rain { display: none; } }
/* The same rain at 35% of the strength behind a tab with messages, on a
   founder's box (renderWelcome). A sibling before the transcript, which scrolls:
   it stays put while the rows move, under the bands. */
.thread-rain { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity .8s ease; }
.thread-rain.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .thread-rain { display: none; } }

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

.offline-banner {
  flex: 0 0 auto;
  display: none;
  align-items: center;
  gap: 8px;
  margin: 0 10px 6px;
  padding: 7px 11px;
  /* The band floats over the transcript, so the tint sits on an opaque base. */
  background: linear-gradient(rgba(227, 179, 65, .13), rgba(227, 179, 65, .13)), var(--bg);
  border-radius: 8px;
}

.offline-banner.show { display: flex; }
.offline-banner .msg { flex: 1; font: 500 12.5px/1.4 var(--font-sans); color: var(--amber); }
.offline-banner .q { font: 500 12px var(--font-sans); color: #c9a03a; }

/* ---------- 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; }
/* The keypad is for keys a touch keyboard lacks. A machine whose main pointer is
   a mouse or trackpad has a real keyboard, so it gets none; a phone, an Android
   tablet and an iPad keep it. */
@media (pointer: fine) {
  .tray.keys.show { display: none; }
}

/* Keycaps: a lit face, a darker lip along the bottom edge and a small drop shadow.
   The padding lifts the label off the lip. A press sinks the cap: the lip shrinks
   to 1px and the face moves down by the 2px it lost. */
.key {
  height: 44px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 0 3px;
  background: linear-gradient(#1c222c, #161b23);
  border: 1px solid #2f3742;
  border-bottom-color: #0a0c10;
  border-radius: 8px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), inset 0 -3px 0 #0f1318, 0 1px 0 rgba(0, 0, 0, .6), 0 2px 4px rgba(0, 0, 0, .4);
  color: var(--text);
  font: 500 11.5px var(--font-mono);
}
.key:active {
  padding-bottom: 1px;
  transform: translateY(2px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), inset 0 -1px 0 #0f1318, 0 1px 1px rgba(0, 0, 0, .5);
}
/* ⏎ ↑ ↓ are one glyph each and read small at the word keys' size. */
.key.glyph { font-size: 15px; }

/* ---------- side questions (/btw) ---------- */

/* A card over the box, never a transcript row: its own fill and edge, the
   transcript's prose inside. Blue is the tool tag's, the colour of something the
   agent did aside from the conversation. */
/* Several cards scroll inside the stack rather than push the strip and the
   transcript off a phone's screen. */
.sides { flex: 0 0 auto; max-height: min(45vh, 420px); overflow-y: auto; overscroll-behavior: contain; padding: 0 10px 6px; display: flex; flex-direction: column; gap: 6px; }
.sides .item.side { flex: 0 0 auto; }
.sides[hidden] { display: none; }
.sides .item.side {
  max-height: min(40vh, 340px);
  overflow-y: auto;
  padding: 0 10px 10px;
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .05), 0 6px 16px rgba(0, 0, 0, .45);
}
/* The head stays at the top while a long answer scrolls under it. */
.sides .item.side > .head {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 8px 0 4px;
  background: var(--card);
}
/* The × keeps the top right with no meta line beside the label. */
.sides .item.side > .head .drop { margin-left: auto; }
.sides .side-mark { flex: 0 0 auto; color: var(--blue); font: 600 13px var(--font-sans); }
.tag.side { color: var(--blue); }
.sides .item.side .meta { flex: 1; min-width: 0; color: var(--faint); font: 500 11px var(--font-sans); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sides .side-q { font: 500 12.5px/1.45 var(--font-sans); color: var(--dim); white-space: pre-wrap; word-break: break-word; }
.sides .side-err { font: 500 12.5px/1.45 var(--font-sans); color: var(--red); white-space: pre-wrap; word-break: break-word; }
.sides .side-a + .side-q { margin-top: 10px; }
.sides .side-reply { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
.sides .side-reply input {
  flex: 1;
  min-width: 0;
  padding: 7px 10px;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 9px;
  font: 500 13px var(--font-sans);
}
.sides .side-reply input:focus { outline: none; border-color: var(--sel-line); }
/* An svg with no size draws at 300×150 and stretches the button across the row. */
.sides .side-reply .rowbtn { width: 32px; height: 32px; padding: 0; border-radius: 9px; }
.sides .side-reply .rowbtn svg { width: 15px; height: 15px; }

/* ---------- 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(--font-sans); color: var(--faint); letter-spacing: .08em; white-space: nowrap; }
.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(--font-sans);
}
.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(--font-sans); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub-row .task { color: var(--text); font: 500 11px/1.35 var(--font-sans); 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(--font-sans); }
.sub-row .chev { flex: 0 0 auto; color: var(--faint); font: 600 14px/1 var(--font-sans); }

/* 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(--font-sans);
}
.sub-heading { display: flex; align-items: center; gap: 6px; min-width: 0; }
.sub-heading .name { flex: 1; min-width: 0; }
.sub-heading .kind { flex: 0 1 auto; min-width: 0; color: var(--dim); font: 500 9.5px var(--font-sans); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub-task { color: var(--text); font: 500 11.5px/1.4 var(--font-sans); overflow-wrap: anywhere; }
.sub-label { margin-top: 6px; color: var(--faint); font: 700 8.5px var(--font-sans); letter-spacing: .08em; }
.sub-activity { display: flex; gap: 8px; color: var(--dim); font: 500 10.5px/1.45 var(--font-sans); 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(--font-sans); 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(--body);
  font: 500 11px var(--font-sans);
  display: flex;
  align-items: center;
  gap: 4px;
  font-variant-numeric: tabular-nums;
}

/* 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 12px 10px;
  --box-gap: 4px;
  --box-controls: 34px;
  /* The row of facts under the keys — the machine, the branch, the context — the
     height of a status segment. See `.composer .facts`. */
  --box-facts: 22px;
  /* Which grid row each part of the box takes: the words, the keys, the facts.
     A phone has no facts row; a wide frame puts it on top — see #frame.wide. */
  --row-text: 1;
  --row-keys: 2;
  --row-facts: 3;
  /* The row's tracks: attach · agent · model · effort · permission · 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 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 * 13px + 15px);
}

.composer {
  flex: 0 0 auto;
  margin: var(--box-margin);
  padding: var(--box-pad);
  /* Nearly opaque and no backdrop blur: a blur has to be drawn again on every
     frame the transcript moves under it, a swipe or a scroll, and on a phone's
     GPU it was the largest cost of a frame. */
  background: rgba(13, 16, 20, .96);
  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 ten tracks of --box-cols. The entry spans all ten 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;
}

/* Cites take a row of their own above the words while there are any. */
.composer[data-cites] { --row-cites: 1; --row-text: 2; --row-keys: 3; --row-facts: 4; }
#frame.wide .composer[data-cites] { --row-facts: 1; --row-cites: 2; --row-text: 3; --row-keys: 4; }
.composer > .cites { display: none; }
.composer[data-cites] > .cites {
  grid-column: 1 / -1;
  grid-row: var(--row-cites);
  display: flex;
  gap: 6px;
  padding: 8px 0 0;
  overflow-x: auto;
  scrollbar-width: none;
}
/* A pill keeps its width and the row scrolls: shrunk to fit, six pills lost
   their words and the × sat on the ❝. Under half the row each, so on a phone
   two show and the edge of a third says there are more. */
.citepill {
  flex: 0 0 auto;
  min-width: 0;
  max-width: min(220px, 45%);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 28px;
  padding: 0 4px 0 10px;
  background: rgba(121, 192, 255, .1);
  border: 1px solid rgba(121, 192, 255, .35);
  border-radius: 14px;
  color: var(--text);
  font: 400 12.5px var(--font-sans);
}
.citepill .mark { flex: 0 0 auto; color: var(--blue); font-size: 15px; line-height: 1; }
.citepill .words { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.citepill .drop {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 11px;
  background: transparent;
  color: var(--faint);
}
.citepill .drop svg { width: 11px; height: 11px; display: block; }
.citepill .edit {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 11px;
  background: transparent;
  color: var(--faint);
}
.citepill .edit svg { width: 13px; height: 13px; display: block; }
.citepill.commented .edit { color: var(--blue); }
/* 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: var(--row-text); }

.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 13px/1.45 var(--font-sans);
  outline: none;
  max-height: 40dvh;
}

/* The slash menu's prediction, grey after the caret. It lies in the entry's own
   cell, in its font and padding, so the invisible copy of the draft wraps where
   the textarea does and the grey part starts at the caret. */
.dghost {
  grid-column: 1 / -1;
  grid-row: var(--row-text);
  align-self: stretch;
  min-width: 0;
  box-sizing: border-box;
  padding: 10px 8px 5px;
  font: 400 13px/1.45 var(--font-sans);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  overflow: hidden;
  pointer-events: none;
}
.dghost[hidden] { display: none; }
.dghost-typed { color: transparent; }
.dghost-rest { color: var(--faint); pointer-events: auto; cursor: pointer; }
/* A fuzzy pick is named in full behind an arrow, fainter than a rest that
   carries on from the typed letters. */
.dghost-rest.fuzzy { opacity: .7; }

/* 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(--font-sans);
  letter-spacing: .02em;
  white-space: nowrap;
  pointer-events: none;
}

.send {
  grid-column: 10;
  grid-row: var(--row-keys);
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 38px;
  height: 34px;
  border-radius: 6px;
  /* 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(--font-sans);
}

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

/* Send after dictating: over the wait before the draft goes, the key turns round
   like the stop key, and a ring fills along its edge from the top, the way the
   mic's ring fills while it records. --auto-send-wait comes from
   AUTO_SEND_WAIT_MS in voice.js. */
@property --auto-send-sweep {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 0%;
}
.send { position: relative; }
.send[data-countdown] { width: 38px; height: 38px; border-radius: 50%; }
.send[data-countdown]::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  background: conic-gradient(var(--text) var(--auto-send-sweep), 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));
  pointer-events: none;
  animation: auto-send-sweep var(--auto-send-wait, 2s) linear forwards;
}
@keyframes auto-send-sweep { to { --auto-send-sweep: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .send[data-countdown]::after { animation: none; --auto-send-sweep: 100%; }
}

/* 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; }
/* Stop is a round key with a solid black square: the one control that ends a
   turn, shaped unlike every other key on the row. */
/* --hover-radius keeps the round keys round under the hover wash. */
.send.stop, .send[data-countdown] { --hover-radius: 50%; }
.send.stop { width: 38px; height: 38px; border-radius: 50%; }
.send.stop svg { width: 21px; height: 21px; }

/* 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: var(--row-keys);
  display: grid;
  grid-auto-flow: column;
  align-items: center;
  gap: 5px;
  height: 34px;
  padding: 0 5px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 11px var(--font-sans);
  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; }
/* A row too tight for the model and effort labels keeps the mark alone; app.js
   measures the labels and sets the class (fitPicks). */
.composer.tight .agentpick .pick-name { display: none; }

/* 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: var(--row-keys);
  max-width: 96px;
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 5px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 11px var(--font-sans);
  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: var(--row-keys);
  max-width: 72px;
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 5px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 11px var(--font-sans);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.effortpick.unset { color: var(--faint); }
.effortpick[hidden] { display: none; }

/* The label is a span because a flex container does not ellipsise its own text. */
.modelpick .pick-label,
.effortpick .pick-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.effortpick:active { background: var(--sel-bg); border-color: var(--sel-line); color: var(--text); }
.effortpick:disabled { opacity: .4; }
/* Fast mode on beside the model's name, outside the cut; grey while claude
   refuses it or cools down. */
.modelpick .pick-fast { flex: 0 0 auto; display: inline-flex; margin-left: 4px; color: var(--text); }
.modelpick .pick-fast svg { width: 12px; height: 12px; }
.modelpick .pick-fast.off { color: var(--faint); }

/* The permission level, after the effort it sits beside and in its box. Hidden
   until the box reports a level, and for an agent with one to choose from. */
.permpick {
  grid-column: 5;
  grid-row: var(--row-keys);
  display: inline-flex;
  align-items: center;
  height: 34px;
  padding: 0 5px;
  border-radius: 10px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 600 11px var(--font-sans);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.permpick[hidden] { display: none; }
/* A VCode tab has no model to pick (Jev picks it), in the composer and a pane's stub. */
.modelpick[hidden], .pane-entry .modelpick[hidden] { display: none; }
/* The key reads Plan while the tab is in Plan, green, the colour the design
   gives a switch that is on. */
.permpick.plan { color: var(--green); }
.permpick:active { background: var(--sel-bg); border-color: var(--sel-line); color: var(--text); }
.permpick: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. */
.composer .statusbar,
.pane-entry .statusbar {
  grid-column: 6;
  grid-row: var(--row-keys);
  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 ten 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; }

/* The facts row: the branch, the context and the ping on a line of their own
   above the words, spanning the box, on a wide frame only. The last turn's
   changes sit at its right end (`.seg.changes` has `margin-left: auto`). In the
   stub too, so the two stay the same height. Its segments are the band's size, not a
   key's. A phone keeps them on its one line above the box. */
.composer .facts,
.pane-entry .facts {
  grid-column: 1 / -1;
  grid-row: var(--row-facts);
  height: var(--box-facts);
  padding: 0;
}
.composer > .facts { display: none; }
#frame.wide .composer > .facts { 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;
  line-height: var(--box-controls);
  font-size: 10.5px;
}
#frame.wide .composer .seg.changes svg,
#frame.wide .pane-entry .seg.changes svg { width: 12.5px; height: 12.5px; }
/* The facts row's segments keep the band's height on a wide frame too. */
#frame.wide .composer .facts .seg,
#frame.wide .pane-entry .facts .seg { height: var(--box-facts); line-height: var(--box-facts); }
/* A row narrower than its segments cuts each to "…" rather than scrolling the
   end of the row out of sight. A segment with an icon in it, the last turn's
   changes among them, keeps its width. A block, because a flex box does not
   ellipsise its own text. On the facts row the branch gives way first. */
#frame.wide .composer .seg:not(:has(svg)),
#frame.wide .pane-entry .seg:not(:has(svg)) {
  display: block;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
#frame.wide .composer .facts .seg:first-child,
#frame.wide .pane-entry .facts .seg:first-child { flex-shrink: 100; min-width: 3.5em; }

.attach {
  grid-column: 1;
  grid-row: var(--row-keys);
  flex: 0 0 auto;
  width: 38px;
  height: 34px;
  border-radius: 6px;
  background: transparent;
  border: 0;
  color: var(--dim);
  font: 400 18px var(--font-sans);
  /* 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: 8;
  grid-row: var(--row-keys);
  display: none;
  place-items: center;
  width: 38px;
  height: 34px;
  border-radius: 6px;
  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: 7;
  grid-row: var(--row-keys);
  display: none;
  place-items: center;
  width: 28px;
  height: 34px;
  border-radius: 6px;
  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: 7;
  grid-row: var(--row-keys);
  display: none;
  width: 28px;
  font: 600 10px/34px var(--font-sans);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  color: var(--dim);
  user-select: none;
}
.micpct.show { display: block; }

/* The waveform: a clip recording, a 48 px strip drawn in the entry's own cell.
   Over an empty entry, which stays under it at full size and keeps the focus, only
   see-through, so the composer does not change height and the caret the words land
   at survives. Hiding the textarea outright would blur it and shut the phone's
   keyboard. The strip is absolute, so it adds nothing to the row's height. */
.wave {
  grid-column: 1 / -1;
  grid-row: var(--row-text);
  display: none;
  position: relative;
  align-self: stretch;
  min-height: 0;
}
.composer[data-wave] .wave { display: block; }
.composer[data-wave] textarea { opacity: 0; pointer-events: none; }
.wave canvas {
  position: absolute;
  left: 8px;
  width: calc(100% - 16px);
  top: 50%;
  height: 48px;
  /* A pane squeezed shorter than the strip keeps its height; the strip gives. */
  max-height: 100%;
  transform: translateY(-50%);
  /* The bars' colour, read by app.js's drawWave. */
  color: var(--text);
}
/* An entry with text: the words stay in view, dimmed, and the entry leaves the
   strip's height free under them, so no bar covers a word. The composer grows by
   the strip while it shows. */
.composer[data-wave-place="below"] textarea { opacity: .45; margin-bottom: 48px; }
.composer[data-wave-place="below"] .wave canvas { top: auto; bottom: 0; transform: none; }
/* After the stop: the bars hold still and dim until the words land. The mic's
   ring and number say how the clip is getting on. */
.composer[data-wave="transcribing"] .wave canvas { opacity: .25; }

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

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

.slashmenu {
  flex: 0 0 auto;
  display: none;
  flex-direction: column;
  /* --room, not dvh: with an iPhone's keyboard up 100dvh is still the whole
     screen, and 60dvh ran the menu off the top (issue #71). */
  max-height: calc(var(--room, 100dvh) * .6);
  overflow-y: auto;
  padding: 6px;
  gap: 1px;
  background: var(--card);
  border-top: 1px solid var(--line-strong);
}

.slashmenu.show {
  display: flex;
  /* Closed by its own line, then a strip of page under it: the composer's rounded
     top sits on the page, not flush against the menu's square corners. */
  border-bottom: 1px solid var(--line-strong);
  margin-bottom: 8px;
  box-shadow: 0 8px 0 var(--bg);
}
/* On the phone the menu stands in the bottom band, so it gets what the band and
   the top bar leave: above a keyboard 60% of the room reached under the top bar. */
#frame:not(.wide) .underfoot > .slashmenu {
  max-height: min(calc(var(--room, 100dvh) * .6), calc(var(--room, 100dvh) - var(--overhead-h, 150px) - var(--underfoot-h, 130px)));
}
/* The panel: the search bar across the top, the rail down the left and the list
   beside it. Only the list scrolls. */
.slashmenu.railed.show {
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  padding: 0;
  gap: 0;
  overflow: hidden;
}
.slashmenu .sbar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 8px 8px 6px;
  padding: 0 10px;
  height: 34px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 9px;
  font: 400 12.5px var(--font-sans);
  color: var(--text);
  min-width: 0;
}
.slashmenu .sbar > svg { flex: 0 0 15px; width: 15px; height: 15px; color: var(--subtle); }
.slashmenu .squery { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.slashmenu .squery .shint { color: var(--faint); margin-left: 6px; }
.slashmenu .sinput { flex: 1 1 auto; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font: inherit; padding: 0; }
.slashmenu .sinput::placeholder { color: var(--faint); }
.slashmenu .sinput::-webkit-search-cancel-button { display: none; }
.slashmenu .sscope { flex: 0 0 auto; margin-left: auto; font: 500 10px var(--font-sans); color: var(--subtle); white-space: nowrap; }

/* The effort slider over the Models search bar: auto, then the model's levels,
   each stop centred in an equal cell so a label never runs off the end. */
.slashmenu.railed.show.effortful { grid-template-rows: auto auto minmax(0, 1fr); }
.slashmenu .seffort { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; margin: 8px 8px 0; padding: 0 2px 0 4px; }
.slashmenu .selabel { flex: 0 0 auto; font: 600 9px var(--font-sans); letter-spacing: .1em; text-transform: uppercase; color: var(--subtle); }
.slashmenu .setrack { position: relative; flex: 1 1 auto; display: flex; height: 38px; touch-action: none; cursor: pointer; outline: none; border-radius: 8px; }
.slashmenu .setrack:focus-visible { box-shadow: 0 0 0 1px var(--sel-line); }
.slashmenu .setrack::before, .slashmenu .sefill { content: ''; position: absolute; top: 11px; height: 2px; border-radius: 2px; left: calc(50% / var(--stops)); }
.slashmenu .setrack::before { right: calc(50% / var(--stops)); background: var(--line-strong); }
.slashmenu .sefill { background: var(--violet); }
.slashmenu .sestop { position: relative; flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 6px; padding-top: 7px; }
.slashmenu .sedot { width: 10px; height: 10px; border-radius: 50%; background: var(--card); border: 2px solid var(--line-strong); box-sizing: border-box; }
.slashmenu .sename { font: 500 10.5px var(--font-sans); color: var(--subtle); white-space: nowrap; }
.slashmenu .sestop.on .sedot { width: 14px; height: 14px; margin-top: -2px; margin-bottom: -2px; background: var(--text); border-color: var(--violet); }
.slashmenu .sestop.on .sename { color: var(--text); }

/* Fusion over the Models list on a founder's claude tab: Single model | Fusion,
   and on a pair the Orchestrator › Executor slots, the lit one filled by the
   list and the effort slider under them. */
.slashmenu.railed.show.fusionful { grid-template-rows: auto auto minmax(0, 1fr); }
.slashmenu.railed.show.fusionful.effortful, .slashmenu.railed.show.fusionful.fastful { grid-template-rows: auto auto auto minmax(0, 1fr); }
.slashmenu.railed.show.fusionful.effortful.fastful { grid-template-rows: auto auto auto auto minmax(0, 1fr); }
.slashmenu .sfusion { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 6px; margin: 8px 8px 0; }
.slashmenu .sfseg { display: flex; padding: 3px; background: var(--bg); border: 1px solid var(--line-strong); border-radius: 9px; }
.slashmenu .sfopt { flex: 1 1 0; padding: 6px 0; border: 0; border-radius: 6px; background: transparent; color: var(--dim); font: 500 12px var(--font-sans); cursor: pointer; }
.slashmenu .sfopt.on { background: var(--btn-bg); color: var(--text); box-shadow: inset 0 0 0 1px var(--btn-line); }
.slashmenu .sfslots { display: flex; align-items: stretch; gap: 6px; }
.slashmenu .sfarrow { align-self: center; color: var(--subtle); font-size: 14px; }
.slashmenu .sfslot { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 2px; padding: 6px 10px; background: var(--card); border: 1px solid var(--line-strong); border-radius: 10px; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
.slashmenu .sfslot.on { border-color: var(--violet); background: color-mix(in srgb, var(--violet) 9%, var(--card)); }
.slashmenu .sfrole { font: 600 9px var(--font-sans); letter-spacing: .1em; text-transform: uppercase; color: var(--subtle); }
.slashmenu .sfvalue { display: flex; align-items: center; gap: 6px; min-width: 0; }
.slashmenu .sfvalue .sicon, .slashmenu .sfvalue .sicon svg { width: 14px; height: 14px; display: block; flex: 0 0 auto; }
.slashmenu .sfname { font: 600 13px var(--font-sans); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.slashmenu .sfeffort { margin-left: auto; font: 500 11px var(--font-sans); color: var(--dim); }
.slashmenu .sfslots .sdefault { align-self: center; margin: 0 -4px 0 0; }
/* A pair's row: the Orchestrator box over the Executor box, each with its word. */
.slashmenu .sfboxes { flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px; }
.slashmenu .sfbox { display: flex; align-items: center; gap: 7px; padding: 1px 0; background: none; border: 0; color: var(--dim); font: 400 12px var(--font-sans); text-align: left; cursor: pointer; }
.slashmenu .sfbox.on { color: var(--text); }
.slashmenu .sfbox:disabled { opacity: .3; cursor: default; }
.slashmenu .sfcheck { flex: 0 0 auto; width: 16px; height: 16px; display: grid; place-items: center; border: 1.5px solid var(--sel-line); border-radius: 4px; background: var(--bg); color: var(--bg); box-sizing: border-box; }
.slashmenu .sfbox:not(:disabled):hover .sfcheck { border-color: var(--dim); }
.slashmenu .sfbox.on .sfcheck { background: var(--green); border-color: var(--green); }
.slashmenu .sfcheck svg { width: 12px; height: 12px; display: block; stroke-width: 3; }
#frame.wide .slashmenu .sfboxes { gap: 2px; }
#frame.wide .slashmenu .sfbox { font-size: 10.5px; gap: 6px; }
#frame.wide .slashmenu .sfcheck { width: 13px; height: 13px; }
#frame.wide .slashmenu .sfcheck svg { width: 10px; height: 10px; }
#frame.wide .slashmenu .sfusion { margin: 6px 6px 0; gap: 4px; }

/* Fusion categories (renderRoster): the list's rows with a dot, a chip and a go
   arrow, the presets, and each page's header, "When to use" box and buttons. */
.slashmenu .rrow .stx { flex: 1 1 auto; }
.slashmenu .rgo { flex: 0 0 auto; color: var(--faint); font: 400 16px var(--font-sans); margin-left: -2px; }
.slashmenu .rtag { font-weight: 400; color: var(--subtle); }
.slashmenu .rchip { flex: 0 0 auto; max-width: 46%; display: inline-flex; align-items: center; gap: 5px; padding: 3px 7px; border: 1px solid var(--line-strong); border-radius: 7px; background: var(--input); font: 500 11.5px var(--font-sans); color: var(--text); white-space: nowrap; }
.slashmenu .rchip .sicon, .slashmenu .rchip .sicon svg { width: 12px; height: 12px; display: block; flex: 0 0 auto; }
.slashmenu .rchip .rmodel { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.slashmenu .rchip .reffort { color: var(--dim); font-weight: 400; }
.slashmenu .rchip.orch { border-color: color-mix(in srgb, var(--violet) 45%, var(--line-strong)); color: var(--violet); }
.slashmenu .rdot { width: 8px; height: 8px; border-radius: 50%; background: var(--violet); }
.slashmenu .rdot.r0 { background: var(--green); }
.slashmenu .rdot.r1 { background: var(--amber); }
.slashmenu .rdot.r2 { background: var(--orange); }
.slashmenu .rdot.r3 { background: var(--cyan); }
.slashmenu .rdot.r4 { background: var(--violet); }
.slashmenu .rdot.r5 { background: var(--pink); }
.slashmenu .rdot.r6 { background: var(--blue); }
.slashmenu .rdot.r7 { background: var(--red); }
.slashmenu .rorch .sname { color: var(--violet); }
.slashmenu .radd .sname { color: var(--blue); font-family: var(--font-sans); }
.slashmenu .rpresets { margin: 0 2px; }
.slashmenu .rnote { padding: 8px 6px 4px; font: 400 11px var(--font-sans); color: var(--subtle); line-height: 1.45; }
.slashmenu .rlink { padding: 0; border: 0; background: none; color: var(--blue); font: 500 11.5px var(--font-sans); cursor: pointer; }
.slashmenu .rhead { flex-direction: row; align-items: center; gap: 8px; }
.slashmenu .rback { width: 28px; height: 28px; margin-left: -4px; border: 0; border-radius: 7px; background: none; color: var(--dim); font: 400 22px/1 var(--font-sans); cursor: pointer; }
.slashmenu .rback:hover { background: var(--sel-bg); color: var(--text); }
.slashmenu .rtitle { min-width: 0; display: flex; flex-direction: column; }
.slashmenu .rname { font: 600 14px var(--font-sans); color: var(--text); }
.slashmenu .rsub { font: 400 11px var(--font-sans); color: var(--dim); }
.slashmenu .rwhen { display: flex; flex-direction: column; gap: 5px; margin: 8px 2px 2px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg); }
.slashmenu .rwtop { display: flex; align-items: center; gap: 7px; font: 600 9px var(--font-sans); letter-spacing: .1em; text-transform: uppercase; color: var(--subtle); }
.slashmenu .rwtop .rlink { margin-left: auto; letter-spacing: 0; text-transform: none; }
.slashmenu .rwtag { padding: 0 5px; border: 1px solid var(--line-strong); border-radius: 5px; font: 500 10px var(--font-sans); letter-spacing: 0; text-transform: none; color: var(--dim); }
.slashmenu .rwtext { margin: 0; font: 400 12px/1.45 var(--font-sans); color: var(--body); }
.slashmenu .rwinput { width: 100%; box-sizing: border-box; min-height: 60px; padding: 7px 9px; border: 1px solid var(--line-strong); border-radius: 7px; background: var(--input); color: var(--text); font: 400 12.5px/1.4 var(--font-sans); resize: vertical; }
.slashmenu .rwinput.rnameinput { min-height: 0; }
.slashmenu .rwinput:focus { outline: none; border-color: var(--sel-line); }
.slashmenu .rwactions { display: flex; gap: 14px; justify-content: flex-end; }
.slashmenu .rdelete { margin: 8px 2px 2px; padding: 8px; border: 1px solid var(--btn-line); border-radius: 8px; background: var(--btn-bg); box-shadow: var(--btn-lift); color: var(--red); font: 600 12.5px var(--font-sans); cursor: pointer; }
.slashmenu .rdelete:disabled { opacity: .4; cursor: default; }
.slashmenu .rbottom { gap: 8px; position: sticky; bottom: -6px; z-index: 1; margin: 6px -6px -6px; padding: 6px 8px 8px; background: var(--card); border-top: 1px solid var(--line); }
.slashmenu .rbottom .rdelete { margin: 0; }
.slashmenu .rbottom .rdelete { flex: 1 1 0; }
.slashmenu .rplain { color: var(--text); }
.slashmenu .radd-go { background: var(--btn-primary); color: #fff; box-shadow: var(--btn-lift-strong); }

/* The Fast row under the slider: title and the model's note on the left, the
   settings sheet's switch on the right, and claude's refusal in amber. */
.slashmenu.railed.show.fastful { grid-template-rows: auto auto minmax(0, 1fr); }
.slashmenu.railed.show.effortful.fastful { grid-template-rows: auto auto auto minmax(0, 1fr); }
.slashmenu .sfast { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; margin: 6px 8px 0; padding: 7px 10px; background: var(--bg); border: 1px solid var(--line); border-radius: 9px; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
.slashmenu .sfast:focus-visible { outline: 2px solid var(--sel-line); outline-offset: 1px; }
.slashmenu .sftext { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.slashmenu .sftitle { display: flex; align-items: center; gap: 5px; font: 600 12.5px var(--font-sans); color: var(--text); }
.slashmenu .sfbolt { display: inline-flex; }
.slashmenu .sfbolt svg { width: 13px; height: 13px; }
.slashmenu .sfnote { font: 400 11px var(--font-sans); color: var(--subtle); }
.slashmenu .sfwhy { font: 500 11px var(--font-sans); color: var(--amber); }
.slashmenu .sfastmark { flex: 0 0 auto; display: inline-flex; color: var(--dim); }
.slashmenu .sfastmark svg { width: 11px; height: 11px; }

.slashmenu .srail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 2px 0 6px;
  border-right: 1px solid var(--line);
  overflow-y: auto;
  scrollbar-width: none;
}
.slashmenu .sslot {
  position: relative;
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  color: var(--subtle);
  background: none;
  border: 0;
  padding: 0;
}
.slashmenu .sslot > svg, .slashmenu .sslot .sicon, .slashmenu .sslot .sicon svg { width: 17px; height: 17px; display: block; }
.slashmenu .sslot.on { background: var(--sel-bg); box-shadow: inset 0 0 0 1px var(--sel-line); color: var(--text); }
/* The violet edge says which slot the list is, the way a tab's edge does. */
.slashmenu .sslot.on::before { content: ''; position: absolute; left: -6px; top: 9px; bottom: 9px; width: 2px; border-radius: 2px; background: var(--violet); }
.slashmenu .sbadge { position: absolute; right: -2px; bottom: -1px; font: 600 8px var(--font-sans); color: var(--dim); background: var(--card); padding: 0 2px; border-radius: 4px; }
.slashmenu .sdiv { flex: 0 0 1px; width: 22px; background: var(--line); margin: 2px 0; }
.slashmenu .slist { overflow-y: auto; padding: 2px 6px 6px; display: flex; flex-direction: column; gap: 1px; min-height: 0; }

.slashmenu .sgrp, .slashmenu .slabel { padding: 8px 6px 4px; font: 600 9px var(--font-sans); letter-spacing: .1em; color: var(--subtle); text-transform: uppercase; }
.slashmenu .sempty { padding: 14px 8px; font: 400 12px var(--font-sans); color: var(--subtle); }

.slashmenu .srow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 8px;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  color: var(--text);
}
.slashmenu .srow:active { background: var(--sel-bg); }
/* The row the arrows are on, drawn the way the folder box draws the folder you
   are already in. */
.slashmenu .srow.sel { background: var(--sel-bg); box-shadow: inset 0 0 0 1px var(--sel-line); }
.slashmenu .slead { flex: 0 0 20px; display: grid; place-items: center; }
.slashmenu .slead > svg, .slashmenu .slead .sicon, .slashmenu .slead .sicon svg { width: 15px; height: 15px; display: block; }
.slashmenu .stx { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.slashmenu .sname { font: 500 12.5px var(--font-mono); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.slashmenu .sname.sans { font: 500 13px var(--font-sans); }
.slashmenu .sdesc { font: 400 10.5px var(--font-sans); color: var(--subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A favourite's effort: the word, then five bars rising 4px to 12px, lit grey to
   red like the usage meters' pixel blocks. */
.slashmenu .smeter { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
.slashmenu .smeter-word { font: 600 10px var(--font-sans); color: var(--faint); }
.slashmenu .smeter-bars { display: flex; align-items: flex-end; gap: 2px; height: 12px; }
.slashmenu .smeter-bars span { width: 4px; background: var(--line); }
.slashmenu .smeter-bars span:nth-child(1) { height: 4px; }
.slashmenu .smeter-bars span:nth-child(2) { height: 6px; }
.slashmenu .smeter-bars span:nth-child(3) { height: 8px; }
.slashmenu .smeter-bars span:nth-child(4) { height: 10px; }
.slashmenu .smeter-bars span:nth-child(5) { height: 12px; }
.slashmenu .smeter.grey .on { background: var(--dim); }
.slashmenu .smeter.yellow .on { background: var(--amber); }
.slashmenu .smeter.green .on { background: var(--green); }
.slashmenu .smeter.orange .on { background: var(--orange); }
.slashmenu .smeter.red .on { background: var(--red); }
.slashmenu .stick { flex: 0 0 14px; width: 14px; height: 14px; color: var(--green); }
.slashmenu .stick svg { width: 100%; height: 100%; display: block; }
.slashmenu .sstar, .slashmenu .sdefault { flex: 0 0 auto; width: 28px; height: 28px; margin: -6px -4px -6px 0; display: grid; place-items: center; background: none; border: 0; padding: 0; color: var(--faint); }
.slashmenu .sstar svg, .slashmenu .sdefault svg { width: 15px; height: 15px; display: block; }
.slashmenu .sstar.on { color: var(--amber); }
/* The house beside it: lit on the favourite new tabs open on. */
.slashmenu .sdefault.on { color: var(--blue); }
/* The agent rows: the one running this tab in white, the rest dim. */
/* A favourite being dragged down the list: lifted over the rows it passes, the
   way .ws.dragging is in the drawer; touch-action keeps the list from scrolling
   under the finger for the rest of the gesture. */
.slashmenu .srow.dragging { position: relative; z-index: 3; background: var(--sel-bg); box-shadow: 0 6px 18px rgba(0, 0, 0, .5); touch-action: none; }
.slashmenu .srow.agent-option { color: var(--dim); }
.slashmenu .srow.agent-option .sname { color: inherit; }
.slashmenu .srow.agent-option.on { color: var(--text); }

.drafts { flex: 0 0 auto; display: none; gap: 6px; padding: 7px 10px 8px; 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; }
/* Blank while the picture comes from the box, not the browser's broken-picture mark. */
.chipfile img:not([src]) { visibility: hidden; }
.chipfile .glyph { width: 34px; text-align: center; font: 400 15px var(--font-sans); color: var(--dim); }
.chipfile .meta { display: flex; flex-direction: column; gap: 2px; max-width: 118px; }
.chipfile .fname { font: 500 12.5px var(--font-sans); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chipfile .fsize { font: 400 12px var(--font-sans); color: var(--faint); }
/* An agent's recording in a message: a chip in the line of text, not a block. */
.chipfile.shotvideo { display: inline-flex; vertical-align: middle; margin: 2px 0; max-width: 100%; color: inherit; text-decoration: none; cursor: pointer; }
.chipfile.shotvideo .meta { max-width: 260px; }
/* 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; }
/* Up and down scroll the shell; sideways is the tab swipe's, as on .thread. */
.panel { touch-action: pan-y; }
/* The neighbour's shell a swipe pulls in (app.js drawShellPeek): shown only
   while it is pulled, and never touched; the finger is on the shell. */
.panel.peek { pointer-events: none; }
.panel.peek[data-pulled] { display: flex; }
/* A tab whose shell this screen has not opened yet: an empty terminal. */
.panel.peek .pbody { background: var(--bg); }

/* 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;
  will-change: transform; /* see .thread */
  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); }
/* A swipe pulls in the page beside the one under the finger: the neighbour tab's,
   drawn into a preview (`.thread.peek`), the new-thread page, or from that page
   the transcript it was reached from. A data attribute, since renderPanel
   rewrites these classes on every render. The page being pulled takes no touch;
   the finger is on the other. A preview waits off the screen rather than taken
   out of the layout, so the neighbour drawn into it ahead of the swipe is already
   laid out when the finger pulls it in; the swipe's transform brings it on. Not
   `visibility`: showing a hidden page restyled every row in it, in the frame the
   finger set off. It is `inert` instead (app.js tradeHosts). */
.thread.peek { transform: translateX(-300%); pointer-events: none; }
#frame.wide.split .thread.peek { display: none; }
.endpage[data-pulled] { display: flex; background: var(--bg-empty); }
.thread.behind[data-pulled] { visibility: visible; }
/* 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(--font-sans); color: var(--faint); }
/* The page past the end hides the transcript's parts; the Commander's popup is a
   tab of its own over that page, so its parts stay. */
#frame.at-end .tobottom:not(.commander-pop *), #frame.at-end:not(.drafting) .underfoot, #frame.at-end .planstrip:not(.commander-pop *), #frame.at-end .subs:not(.commander-pop *) { display: none; }
/* A project's page past the end is the next tab's draft: the welcome line an
   empty tab opens with, over the composer, and no plus. */
.endpage.drafting { cursor: default; background: var(--bg); }
.endpage.drafting > .endpage-logo,
.endpage.drafting > .endpage-plus,
.endpage.drafting > .endpage-hint,
.endpage:not(.drafting) > .endpage-welcome { 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(--font-sans); color: var(--dim); letter-spacing: .06em; }
.panel .psub { flex: 1; min-width: 0; font: 400 9.5px var(--font-sans); 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; }

/* Shell mode's terminal (public/term-view.js): xterm.js fills the panel body
   on the transcript's background, a few px in from its edges. */
.term-view {
  position: relative;
  flex: 1;
  min-height: 0;
  padding: 6px 4px 4px 8px;
  background: var(--bg);
  overflow: hidden;
}
.term-mount { width: 100%; height: 100%; }
.term-mount .xterm { height: 100%; }
/* A held finger is the copy sheet's, not the browser's text menu. */
.term-mount .xterm-screen { -webkit-touch-callout: none; }
/* The line being typed, laid over the shell's input cells in xterm's font and
   cell size; term-view.js sets the place, the indent, the letter-spacing and
   the colours. Wrapped as the terminal wraps it: at the cell, not the word. */
.term-input {
  position: absolute;
  z-index: 5;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  outline: none;
  resize: none;
  overflow: hidden;
  white-space: pre-wrap;
  word-break: break-all;
  overflow-wrap: anywhere;
  font-variant-ligatures: none;
  font-feature-settings: "liga" 0, "calt" 0;
  -webkit-text-size-adjust: none;
}
.term-input[hidden] { display: none; }
/* Sent, but the shell's line does not show it yet. */
.term-input.behind { text-decoration: underline dotted var(--dim); text-decoration-thickness: 2px; text-underline-offset: 4px; }
/* Measures one glyph's advance in the terminal's font. */
.term-probe { position: absolute; visibility: hidden; white-space: pre; left: 0; top: 0; pointer-events: none; }
.term-note { padding: 12px 10px; font: 400 11.5px/1.5 var(--font-mono); color: var(--dim); }

/* The scrollback sheet: plain text the phone's own selection works on. */
.sheet.viewer.termcopy .rpath { font: 400 11.5px var(--font-sans); color: var(--dim); }
.termcopy-text {
  margin: 0;
  font: 400 11.5px/1.5 var(--font-mono);
  color: var(--body);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  user-select: text;
  -webkit-user-select: text;
}
.term-link-text { font-family: var(--font-mono); font-size: 12px; color: var(--blue); }
.confirm-actions a.btn { display: inline-flex; align-items: center; text-decoration: none; }
.confirm-actions a.btn[hidden] { display: none; }

/* The composer steps aside in Shell mode: keys go to the terminal, and the
   keypad stays. */
#frame.shell-on .composer,
#frame.shell-on #drafts,
#frame.shell-on #slashmenu { display: none; }

/* 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(--font-sans); 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; }
/* An edge touch shows the drawer before the finger has a direction. Until it is
   open the drawer takes no touch: Chrome scrolls whatever is under the finger at
   touchstart, and the invisible scrim would take a vertical drag from the
   transcript. A drag in progress keeps its own target, so pulling it in works. */
.drawer.show:not(.open) { pointer-events: none; }
.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: 0; }
/* The drawer's search sits on the panel's own padding. */
.drawer .tabsearch { margin: 0; }
.drawer-list .wsnone { padding: 10px 12px; color: var(--subtle); font: 400 12px var(--font-sans); }

/* The drawer uses the strip tab's fill and dividers, with a full outline on
   each row. Each row keeps the taller two-line label, with the strip's cross at
   the right edge. */
.drawer .ws {
  position: relative;
  gap: 0;
  padding: 0;
  background: var(--bg-chrome);
  border: 1px solid var(--line-strong);
  border-top: 0;
  border-radius: 0;
}
.drawer .ws:first-child { border-top: 1px solid var(--line-strong); }
.drawer .ws.on {
  background: var(--sel-bg);
  border-color: var(--sel-line);
}
.drawer .ws .wsmain { gap: 8px; padding: 8px 10px 8px 12px; color: var(--dim); font: 500 12px var(--font-sans); }
.drawer .ws.on .wsmain { color: var(--text); -webkit-text-stroke: .35px currentColor; }
.drawer .ws .tabicon { flex: 0 0 13px; width: 13px; height: 13px; display: flex; align-items: center; justify-content: center; opacity: .5; }
.drawer .ws .tabicon svg { width: 100%; height: 100%; display: block; }
.drawer .ws.on .tabicon { opacity: 1; }
.drawer .ws .tabcopy { display: flex; flex: 1 1 auto; flex-direction: column; justify-content: center; min-width: 0; max-width: none; text-align: left; gap: 0; }
.drawer .ws .label { display: block; max-width: none; overflow: hidden; font-size: 12px; line-height: 14px; text-overflow: ellipsis; white-space: nowrap; }
.drawer .ws .path { display: block; min-width: 0; font: inherit; color: inherit; }
.drawer .ws .folder {
  display: flex;
  align-items: center;
  gap: 3px;
  max-width: none;
  min-width: 0;
  color: var(--subtle);
  font: 400 9px var(--font-sans);
  line-height: 10px;
  text-overflow: ellipsis;
  white-space: nowrap;
  -webkit-text-stroke: 0;
}
/* Where a search found its words in the tab's messages: two lines at most. */
.drawer .ws .wssnippet {
  display: -webkit-box;
  margin-top: 2px;
  overflow: hidden;
  color: var(--subtle);
  font: 400 10px var(--font-sans);
  line-height: 12px;
  white-space: normal;
  overflow-wrap: anywhere;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  -webkit-text-stroke: 0;
}
/* The model line: the agent's mark, the model, then the effort a shade quieter. */
.drawer .ws .mline { display: flex; align-items: center; gap: 3px; min-width: 0; margin-top: 2px; color: var(--dim); font: 400 9px var(--font-sans); line-height: 10px; white-space: nowrap; -webkit-text-stroke: 0; }
/* The status on the model line, at the strip tab's smaller size. */
.drawer .ws .mline .dot { flex: 0 0 10px; width: 10px; height: 10px; }
.drawer .ws .mline .dot svg { width: 10px; height: 10px; }
.drawer .ws .mline .dot svg.ring { width: 12px; height: 12px; }
.drawer .ws .mline .agents-n { line-height: 10px; }
.drawer .ws .mline .mname { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.drawer .ws .mline .meffort { flex: none; color: var(--subtle); }
.drawer .ws.unavailable .tabcopy { opacity: .55; }
.drawer .ws .wsclose {
  width: 40px;
  min-height: 40px;
  align-self: stretch;
  background: transparent;
  border: 0;
  border-radius: 0;
}
.drawer .ws .wsclose svg { width: 11px; height: 11px; }
.drawer .ws .wsclose:active { background: rgba(248, 81, 73, .14); color: var(--red); }

.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(--font-sans);
}
.drawer-new svg { width: 14px; height: 14px; display: block; }
.drawer-new:active { background: var(--input); color: var(--text); }

/* A project in the tab bar: a heading laid out like the explorer's folder row
   (its twistie, the name, the count in the row's quiet grey) over the project's
   tabs. The name is the manager row's, dim until the project is the active one;
   the box beside it is its meta line. */
.drawer .wsgroup { display: flex; flex-direction: column; }
.drawer .wsgroup + .wsgroup { margin-top: 6px; }
.drawer .wsgroup.dragging { position: relative; z-index: 3; background: #05070b; box-shadow: 0 6px 18px rgba(0, 0, 0, .5); touch-action: none; }
.drawer .wshead { display: flex; flex-wrap: wrap; align-items: center; min-height: 36px; padding-left: 4px; color: var(--dim); }
.drawer .wshead .xchev { padding: 0; background: transparent; border: 0; color: var(--dim); }
.drawer .wshead .xchev svg { width: 20px; height: 20px; display: block; transition: transform .12s ease; }
.drawer .wshead.open .xchev svg { transform: rotate(90deg); }
.drawer .wshead .wsname {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 8px 0;
  background: transparent;
  border: 0;
  text-align: left;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
.drawer .wshead .wsname .label { min-width: 0; overflow: hidden; color: var(--dim); font: 600 12.5px var(--font-sans); text-overflow: ellipsis; white-space: nowrap; }
.drawer .wshead.on .wsname .label { color: var(--text); }
/* The computer's name gives way first: a long one beside a project with its cross
   squeezed the project's own name to nothing. */
.drawer .wshead .wsname .box { flex: 0 100 auto; min-width: 0; overflow: hidden; color: var(--faint); font: 400 10.5px var(--font-sans); text-overflow: ellipsis; white-space: nowrap; }
.drawer .wshead .dot { margin-left: 6px; }
.drawer .wshead .count { flex: none; min-width: 14px; margin-left: 6px; color: var(--faint); font: 400 10.5px var(--font-sans); text-align: center; font-variant-numeric: tabular-nums; }
.drawer .wshead .wsplus, .drawer .wshead .wsclose, .drawer .wshead .wsedit {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--dim);
}
.drawer .wshead .wsplus svg { width: 14px; height: 14px; display: block; }
.drawer .wshead .wsclose svg { width: 11px; height: 11px; display: block; }
.drawer .wshead .wsedit svg { width: 14px; height: 14px; display: block; }
.drawer .wshead .wsplus:active, .drawer .wshead .wsedit:active { background: var(--input); color: var(--text); }
/* The project's Agent Commander: VCode's icon, dim until its tab is open, then
   lit and ringed like the Commander's tab. */
/* An open project's folders, on a line of their own under the name. */
.drawer .wshead .wsfolders {
  flex: 0 0 100%;
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  padding: 0 12px 6px 24px;
  color: var(--subtle);
  font: 400 10.5px var(--font-sans);
  white-space: nowrap;
}
.drawer .wshead .wsfolders svg { flex: none; width: 11px; height: 11px; display: block; }
.drawer .wshead .wsfolders .ftext { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.drawer .wshead .wsclose:active { background: rgba(248, 81, 73, .14); color: var(--red); }
.drawer .wsrows { display: flex; flex-direction: column; }
/* An open project's tabs, New tab and folds, set in from both edges, with a rail
   under the heading's twistie that says they are the project's: grey, violet
   while the project is the active one. */
.drawer .wsbody { position: relative; display: flex; flex-direction: column; margin: 0 8px 0 22px; }
.drawer .wsbody::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 2px;
  left: -9px;
  width: 2px;
  border-radius: 1px;
  background: var(--line-strong);
  pointer-events: none;
}
.drawer .wshead.on + .wsbody::before { background: var(--violet); opacity: .7; }
/* The open project's last row, the next tab's slot: as tall as a tab, darker than
   one and outlined in dots, its name where the tabs' names start (8px + the edge
   + the 19px ring + 5px = the rows' 12px + 13px mark + 8px). */
.drawer .wsnew {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: 43px;
  margin-top: 1px;
  padding: 7px 10px 7px 8px;
  background: transparent;
  border: 1px dotted var(--line-strong);
  border-radius: 0;
  color: var(--dim);
  font: 400 12px var(--font-sans);
  line-height: 14px;
  text-align: left;
}
.drawer .wsnew .ring {
  flex: 0 0 19px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 19px;
  height: 19px;
  box-sizing: border-box;
  border: 1.5px solid var(--faint);
  border-radius: 50%;
}
.drawer .wsnew .ring svg { width: 10px; height: 10px; display: block; }
.drawer .wsnew:hover,
.drawer .wsnew:focus-visible {
  background: var(--sel-bg);
  border-color: var(--sel-line);
  color: var(--text);
}
.drawer .wsnew:hover .ring,
.drawer .wsnew:focus-visible .ring {
  border-color: var(--violet);
  color: var(--violet);
}
.drawer .wsnew:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.drawer .wsnew:active { background: var(--input); color: var(--text); }
/* A double tap on a row renames it; the browser must not take it as a zoom. */
.drawer .ws .wsmain { touch-action: manipulation; }
/* While a row's name is edited the field takes the row's width. */
.drawer .ws:has(input.wsname) :is(.wsmore, .wsclose) { display: none; }
/* In the drawer the ⋯ is a bare 40px column, as the cross beside it is. */
.drawer .ws .wsmore { width: 36px; min-height: 40px; border: 0; border-radius: 0; background: transparent; }
/* A project's Done fold, under its live rows, quieter than its heading. */
.drawer .wsfoldhead { display: flex; align-items: center; min-height: 32px; padding-left: 14px; color: var(--faint); }
.drawer .wsfoldname { flex: 1; min-width: 0; display: flex; align-items: center; gap: 4px; padding: 0; background: transparent; border: 0; color: inherit; font: 600 10.5px var(--font-sans); letter-spacing: .02em; text-align: left; }
.drawer .wsfoldname svg { width: 16px; height: 16px; display: block; transition: transform .12s ease; }
.drawer .wsfold.open .wsfoldname svg { transform: rotate(90deg); }
.drawer .wsfoldhead .headlink { min-height: 32px; padding: 0 10px; background: none; border: 0; color: var(--dim); font: 700 10px var(--font-sans); letter-spacing: .08em; cursor: pointer; }
.drawer .ws.parked .tabcopy { opacity: .7; }
.drawer .ws .shelfnote { margin-top: 2px; color: var(--subtle); font: 400 9px var(--font-sans); line-height: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -webkit-text-stroke: 0; }

/* ---------- 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; }

/* The folder picker sits over the new-thread card (.popup is 35) that opens it. */
.drawer.folderpick { z-index: 40; }
.folderpick-use { margin: 0 10px calc(env(safe-area-inset-bottom) + 12px); }

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

/* Files and Changes: two icon tabs where the title was. The shown one is lit the
   way a selected chip is; the other stays dim. */
.xtabs { display: flex; gap: 4px; margin-right: auto; }
.xtab[aria-selected="true"] { color: var(--text); }
.xtabs[hidden] { display: none; }
/* The docked column's name for its side, in place of the tabs, set like the CHANGES
   bar under the tree and lined up with it. */
.xtitle { margin-right: auto; padding-left: 4px; }
.xtitle[hidden] { display: none; }
.xtab[aria-selected="false"] { background: transparent; border-color: transparent; }

/* The panel's name. The root's full path is its title, for hover. */
.xpath {
  flex: 1;
  min-width: 0;
  font: 600 11.5px var(--font-sans);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Settings is the primary search field; drawers and Files use the same size. */
.search-field {
  flex: 0 0 auto;
  min-width: 0;
  height: 42px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 6px 0 12px;
  background: var(--input);
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  color: var(--subtle);
}
.search-field:focus-within { outline: 2px solid var(--blue); outline-offset: 1px; }
.search-field > svg { flex: 0 0 15px; width: 15px; height: 15px; }
.search-field > input {
  flex: 1;
  min-width: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text);
  font: 400 16px var(--font-sans);
}
.search-field > input::placeholder { color: var(--subtle); }
.xfilter { margin: 0 10px; }
.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; }

/* Both sides in the wide column: Changes under the tree, as tall as its rows up to
   half the column, each list scrolling on its own. */
.xboth { flex: 0 1 auto; max-height: 50%; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--line); }
.xboth[hidden] { display: none; }
.xbothhead { display: flex; align-items: center; gap: 6px; padding: 8px 10px 2px; }
.xbothhead .xdelta { margin-left: auto; }
.xboth .xlist { flex: 1 1 auto; }
/* Dragged to a height, it keeps that share of the column however many rows it has. */
.xboth { position: relative; }
.xboth.sized { flex: 0 0 calc(var(--changes-share) * 100%); max-height: none; }

/* The handles that size the docked column and Changes under the tree, drawn like
   a pane seam. Off everywhere but the column: a phone's drawer has neither. */
.xsash { display: none; position: absolute; z-index: 2; }
#frame.dock-files > #explorer .xsash, #frame.dock-tabs > #drawer .xsash { display: block; }
#tabs-sash { top: 0; bottom: 0; right: 0; width: 5px; cursor: col-resize; }
#explorer-sash { top: 0; bottom: 0; left: 0; width: 5px; cursor: col-resize; }
#explorer-both-sash { left: 0; right: 0; top: -4px; height: 7px; cursor: row-resize; }
.xsash:hover, .xsash.dragging { background: var(--sel-line); }
/* While one is dragged the whole frame wears its cursor and selects nothing. */
#frame.sash-x, #frame.sash-x * { cursor: col-resize; }
#frame.sash-y, #frame.sash-y * { cursor: row-resize; }
#frame.sash-x *, #frame.sash-y * { user-select: none; }

/* Files apart from Changes, so either can sit on any side of the other. In the
   drawer it is the whole panel, as it always was. */
.xfiles { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: inherit; position: relative; }

/* Where a drag of either head put Changes: over the tree, or a panel of its own
   left or right of it, the line between them on Changes' inner edge. */
#explorer-panel.place-above .xboth { order: -1; border-top: 0; border-bottom: 1px solid var(--line); }
#explorer-panel.place-above #explorer-both-sash { top: auto; bottom: -4px; }
#explorer-panel.place-left, #explorer-panel.place-right { flex-direction: row; gap: 0; }
#explorer-panel.place-left .xboth, #explorer-panel.place-right .xboth {
  flex: 0 0 calc(var(--changes-beside, .5) * 100%);
  max-height: none;
  border-top: 0;
}
#explorer-panel.place-left .xboth { order: -1; border-right: 1px solid var(--line); }
#explorer-panel.place-right .xboth { border-left: 1px solid var(--line); }
#explorer-panel.place-left .xfiles, #explorer-panel.place-right .xfiles { gap: 7px; }
#explorer-panel.place-above .xbothhead, #explorer-panel.place-left .xbothhead, #explorer-panel.place-right .xbothhead { flex: 0 0 auto; height: var(--head-h, 38px); padding: 0 10px; }
#explorer-panel.place-left #explorer-both-sash, #explorer-panel.place-right #explorer-both-sash { top: 0; bottom: 0; width: 7px; height: auto; cursor: col-resize; }
#explorer-panel.place-left #explorer-both-sash { left: auto; right: -4px; }
#explorer-panel.place-right #explorer-both-sash { left: -4px; right: auto; }

/* With both on, each head is a handle: dragged onto a half of the other panel,
   it moves its side there. The half under the pointer is lit, the dragged panel
   dims, and its name follows the pointer. */
#explorer-panel.split .xhead, #explorer-panel.split .xbothhead { cursor: grab; }
#frame.xdragging, #frame.xdragging * { cursor: grabbing; user-select: none; }
.xdragged { opacity: .45; }
.xfiles::after, .xboth::after {
  content: "";
  display: none;
  position: absolute;
  z-index: 3;
  pointer-events: none;
  background: rgba(121, 192, 255, .14);
  outline: 2px solid var(--blue);
  outline-offset: -2px;
  border-radius: 4px;
}
.drop-left::after, .drop-right::after, .drop-above::after, .drop-below::after { display: block; }
.drop-left::after { inset: 0 50% 0 0; }
.drop-right::after { inset: 0 0 0 50%; }
.drop-above::after { inset: 0 0 50% 0; }
.drop-below::after { inset: 50% 0 0 0; }
.xghost {
  position: fixed;
  z-index: 100;
  pointer-events: none;
  transform: translate(10px, 10px);
  padding: 5px 10px;
  font: 700 11px var(--font-sans);
  letter-spacing: .08em;
  color: var(--text);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .5);
}

.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;
  /* A long press is the row's menu, not a text selection or the phone's own
     callout. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.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(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xrow .xtags { flex: 0 1 auto; max-width: 76px; margin-left: 6px; overflow: hidden; color: var(--accent); font: 400 9px var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
.xrow .to { flex: 0 0 auto; margin-left: 4px; font: 400 11px var(--font-mono); color: var(--faint); }
.xrow .size { flex: 0 0 auto; margin-left: 6px; font: 400 10.5px var(--font-sans); 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; }
.xglyph svg { display: block; width: 14px; height: 14px; }

/* 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(--font-sans); 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); }

/* A Changes row, as VS Code's source control list draws one: the name, its folder
   dimmed after it, then the counts, the done mark once viewed, and the letter. */
.xchange { padding-left: 8px; }
.xchange .xglyph { margin-left: 0; }
.xchange .name { flex: 0 1 auto; }
.xchange .xdir { flex: 1 1 0; min-width: 0; margin-left: 6px; font: 400 11px var(--font-mono); color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A checkout's heading on the Changes side: repository › worktree, branch, counts. */
.xsect { display: flex; align-items: baseline; gap: 6px; padding: 14px 8px 4px; font: 600 12.5px var(--font-sans); color: var(--text); }
.xsect:first-child { padding-top: 6px; }
.xsect .xsname { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xsect .xsbranch { flex: 1 1 0; min-width: 0; font: 400 11px var(--font-mono); color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xsect .xdelta { flex: 0 0 auto; margin-left: auto; }
.xchange .xcounts { margin-left: auto; padding-left: 8px; }
.xchange .xviewed { flex: 0 0 auto; margin-left: 6px; }
.xchange .xviewed svg { width: 12px; height: 12px; }
.xchange .xbadge { margin-left: 0; }
.xlist .dl.meta.xerror { color: var(--red); }
.xnote { padding: 10px 8px 0; font: 400 11.5px var(--font-sans); color: var(--dim); }

/* The row menu: New file, New folder, Rename, Delete, or the name box in their
   place. The panel's width less its padding; app.js sets `top` from the row. */
.xmenu {
  display: none;
  position: absolute;
  left: 10px;
  right: 10px;
  z-index: 2;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .5);
}
.xmenu.show { display: flex; }
.xmitem {
  height: 36px;
  padding: 0 10px;
  background: transparent;
  border: 0;
  border-radius: 7px;
  color: var(--text);
  font: 400 12px var(--font-sans);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The tab menu: the row menu's shape, at the pointer or under a row's ⋯. app.js
   sets `left` and `top`. Above the panes, their seams and the tab drag's wash.
   It opens over the transcript, whose --bg is one shade under --card, so a
   raised button's fill and top light and a deep shadow set it off; the
   selection bar matches. */
.xmenu.tabmenu {
  position: fixed;
  left: 0;
  right: auto;
  z-index: 60;
  min-width: 140px;
  max-width: 280px;
  background: var(--btn-bg);
  border-color: var(--sel-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 12px 32px rgba(0, 0, 0, .85);
}
/* A desktop menu's rows: shorter than the row menu's 36px thumb rows, each with
   its action's mark in front, and lit under the pointer. */
.tabmenu .xmitem { display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 10px 0 8px; }
.tabmenu .xmitem:hover { background: #2d3540; }
.tabmenu .xmitem svg { flex: 0 0 12px; width: 12px; height: 12px; color: #a3acb6; }
.tabmenu .xmitem span { overflow: hidden; text-overflow: ellipsis; }
.xmitem[hidden] { display: none; }
.xmitem:active { background: var(--sel-bg); }
.xmitem.danger { color: var(--red); background: rgba(248, 81, 73, .12); }
.xmname { display: flex; flex-direction: column; gap: 6px; padding: 6px; }
.xmname[hidden] { display: none; }
/* Opened from a row's ⋯ on a phone: the row menu's 36px thumb rows. */
@media (pointer: coarse) {
  .tabmenu .xmitem { height: 36px; gap: 10px; }
  .tabmenu .xmitem svg { flex-basis: 15px; width: 15px; height: 15px; }
}
.xmlabel { font: 400 11px var(--font-sans); color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.xminput {
  height: 34px;
  padding: 0 9px;
  background: var(--input);
  border: 1px solid var(--sel-line);
  border-radius: 8px;
  color: var(--text);
  font: 400 12px var(--font-mono);
}
.xminput:disabled { opacity: .6; }

/* 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(--font-sans); }
.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(--font-sans);
}
.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); }
/* The first screen on a trial with no agent: the card alone over the whole
   page, under the install screen (.auth, z-index 60), so that one comes first. */
#newthread.first-agent, #frame.wide #newthread.first-agent { position: fixed; inset: 0; z-index: 59; padding: 24px; }
#newthread.first-agent .popup-scrim { background: var(--bg); }
.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; }
/* A wide frame gives the folder path room. */
#frame.wide #newthread .popup-card { max-width: 600px; }
.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(--font-sans);
  letter-spacing: .08em;
  color: var(--dim);
  cursor: pointer;
}
.popup-card .headlink:hover { color: var(--text); }
/* The confirm popup: above the new-thread card, since its form could ask. */
#confirm { z-index: 45; }
.confirm-text { margin: 0; font: 400 13px/1.45 var(--font-sans); color: var(--dim); overflow-wrap: anywhere; }
.confirm-text .confirm-name { font-weight: inherit; color: var(--text); }
.confirm-text .inline-icon { color: var(--text); }
.confirm-text .inline-icon svg { width: 1.25em; height: 1.25em; vertical-align: -0.3em; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
.confirm-actions.go-first #confirm-go { order: -1; }
/* "Do not ask again" under the question (askClose in app.js). */
.confirm-tick { display: flex; align-items: center; gap: 8px; font: 400 13px var(--font-sans); color: var(--dim); cursor: pointer; }
.confirm-tick[hidden] { display: none; }
.confirm-tick input { width: 16px; height: 16px; margin: 0; accent-color: var(--green); }
.confirm-actions .btn { height: 38px; }
/* Recently closed: the closed tabs hang under the search like an autocomplete,
   one bordered block, each row the strip's tab with its time in place of the cross. */
.closed-card { max-width: 460px; }
.closed-combo { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--input); }
.closed-combo input { width: 100%; box-sizing: border-box; border: 0; background: transparent; color: var(--text); padding: 11px 12px; font: 400 14px var(--font-sans); outline: none; }
.closed-list { max-height: min(56vh, 420px); overflow-y: auto; }
#frame #closed-list .tab { display: flex; align-items: center; width: 100%; max-width: none; min-width: 0; height: auto; min-height: 56px; padding: 8px 12px; box-sizing: border-box; border: 0; border-top: 1px solid var(--line); border-radius: 0; position: relative; text-align: left; }
#frame #closed-list .tab .tabcopy { flex: 1; min-width: 0; max-width: none; }
#frame #closed-list .tab :is(.label, .folder) { max-width: none; }
.closed-when { margin-left: auto; padding-left: 10px; color: var(--dim); font: 400 12px var(--font-sans); white-space: nowrap; }
.closed-empty { padding: 14px 12px; border-top: 1px solid var(--line); color: var(--dim); font: 400 13px var(--font-sans); }
/* The command palette (openPalette in app.js): a popup at the top, one list of
   commands, tabs and projects, with a group label over each section. */
.popup.palette { align-items: flex-start; padding-top: 52px; box-sizing: border-box; }
.palette-card { max-width: 600px; }
.palette-group { padding: 9px 12px 5px; border-top: 1px solid var(--line); font: 700 10px var(--font-sans); color: var(--faint); letter-spacing: .08em; }
#frame #palette-list .tab { display: flex; align-items: center; gap: 10px; width: 100%; max-width: none; min-width: 0; height: auto; min-height: 44px; padding: 8px 12px; box-sizing: border-box; border: 0; border-top: 1px solid var(--line); border-radius: 0; text-align: left; background: var(--bg-chrome); }
/* The pick is drawn as the drawer draws the tab in front: the selected fill
   inside a full outline, with no coloured top edge. */
#frame #palette-list .tab.on { background: var(--sel-bg); box-shadow: inset 0 0 0 1px var(--sel-line); }
#frame #palette-list .tab.on::after { display: none; }
#frame #palette-list .tab.on .palette-keys kbd { color: var(--text); border-color: var(--sel-line); }
#frame #palette-list .tab .tabcopy { flex: 1; min-width: 0; max-width: none; }
#frame #palette-list .tab :is(.label, .folder) { max-width: none; }
.palette-keys { margin-left: auto; display: flex; gap: 3px; flex: none; }
.palette-keys kbd { font: 400 11px var(--font-mono); color: var(--dim); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; background: var(--bg); }
/* What's new (openWhatsNew in app.js): the mark, the badge pill, the image, the lists. */
.whats-new-mark { width: 20px; height: 20px; flex-shrink: 0; margin-right: 8px; }
.whats-new-card .stitle { flex: 0 1 auto; font: 600 15px var(--font-sans); color: var(--text); letter-spacing: normal; }
.whats-new-card .shead .icon-btn { margin-left: auto; }
.whats-new-badge { margin-left: 8px; padding: 1px 8px; border: 1px solid currentColor; border-radius: 999px; color: var(--blue); font: 500 12px var(--font-sans); }
.whats-new-shot { display: block; width: 100%; max-width: 378px; height: auto; margin: 12px 0; }
.whats-new-shot[hidden] { display: none; }
.whats-new-how { font: 600 9px var(--font-sans); letter-spacing: .1em; text-transform: uppercase; color: var(--subtle); }
.whats-new-list { margin: 4px 0 12px; padding-left: 20px; font: 400 13px/1.5 var(--font-sans); color: var(--text); }
.whats-new-bullets { list-style: disc; }
#frame.wide .whats-new-card { max-width: 420px; }

/* The diff sheet's files, one row each, on top of its body; a tap scrolls to the
   file. */
.dfiles[hidden] { display: none; }
.dfiles { display: flex; flex-direction: column; padding: 2px 12px 6px; background: var(--card); border-bottom: 1px solid var(--line-strong); }
.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(--font-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; }
/* Viewed in the diff 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, .viewer.docked { background: var(--card); }
.sheet.review .rhead, .sheet.viewer .rhead, .viewer.docked .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, .viewer.docked .rpath { flex: 1; min-width: 0; font: 600 11.5px var(--font-mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet.review .rhead .stitle { flex: 0 0 auto; }
.sheet.review .plus { font: 600 10.5px var(--font-sans); color: var(--green); }
.sheet.review .minus { font: 600 10.5px var(--font-sans); color: var(--red); }
.sheet.review .rhead .minus { margin-right: auto; }
.sheet.review .rchunk { display: block; }
.sheet.review .dl.add { background: rgba(126, 231, 135, .12); }
.sheet.review .dl.del { background: rgba(248, 81, 73, .12); }
.sheet.review .rbody, .sheet.viewer .rbody, .viewer.docked .rbody { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; background: var(--bg); }
/* The diff sheet's body has no padding of its own, so a file's header sticks
   flush to the top and runs edge to edge. */
.sheet.review .rbody { padding: 0; }

/* One file of the diff sheet: a header that sticks while its hunks go by, then
   the hunks. Viewed folds them away, as on GitHub. */
.dfile { border-bottom: 1px solid var(--line); }
.dfhead {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 12px;
  background: var(--input);
  border-bottom: 1px solid var(--line-strong);
  font: 500 12px var(--font-mono);
  color: var(--text);
}
.dfletter { flex: 0 0 auto; min-width: 14px; font: 600 11px var(--font-sans); text-align: center; }
.dfile.t-modified .dfletter { color: var(--git-modified); }
.dfile.t-added .dfletter { color: var(--git-added); }
.dfile.t-deleted .dfletter { color: var(--git-deleted); }
.dfpath { flex: 1; min-width: 0; overflow: hidden; }
/* A file name wider than the header wraps rather than run off it. */
.dfpath .pbase { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
.dfpath .pdir { color: var(--dim); }
.dfhead .counts { flex: 0 0 auto; font: 600 10.5px var(--font-sans); }
.dfviewed {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 28px;
  padding: 0 9px;
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  font: 400 12px var(--font-sans);
  color: var(--dim);
  cursor: pointer;
}
.dfviewed input { width: 16px; height: 16px; margin: 0; accent-color: var(--green); color-scheme: dark; }
.dfile.viewed .dfviewed { color: var(--text); }
.dfile.viewed .dfbody { display: none; }
.dfbody { padding: 4px 0 8px; }
.dfbody .dl { padding-left: 12px; padding-right: 12px; }
/* A line in the file now is a thing to tap, as in the viewer; one that carries a
   review comment is tinted violet. */
.sheet.review .dl[data-line] { cursor: pointer; }
.sheet.review .dl[data-line]:active { background: var(--sel-bg); }
.sheet.review .dl.noted { background: rgba(210, 168, 255, .10); }
.dfbody .lcnote, .dfbody .lcbox { width: auto; margin-left: 12px; margin-right: 12px; }
/* In the editor each is a block under its line (vendor/editor/marks.mjs). */
.cm-vBlocks > .lcnote, .cm-vBlocks > .lcbox { width: auto; margin-left: 12px; margin-right: 12px; }

/* 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. app.js sets --gutter to the last line
   number's digits; up to four fit the 3.4em the column always had. */
.sheet.viewer .code, .viewer.docked .code { --gutter-w: max(3.4em, calc(var(--gutter, 0ch) + 10px)); white-space: pre-wrap; overflow-wrap: anywhere; padding: 0; }
.sheet.viewer .vline, .viewer.docked .vline { display: block; padding-left: calc(var(--gutter-w) + 10px); text-indent: calc(-1 * var(--gutter-w) - 10px); }
.sheet.viewer .gutter, .viewer.docked .gutter {
  display: inline-block;
  text-indent: 0;
  width: var(--gutter-w);
  padding-right: 10px;
  text-align: right;
  color: var(--faint);
  user-select: none;
}
/* Not CodeMirror's zero-width image beside each widget: a block one drops the
   widget (the grey completion after the caret) onto a row of its own. */
.sheet.viewer img:not(.cm-widgetBuffer), .viewer.docked img:not(.cm-widgetBuffer) { max-width: 100%; display: block; }
/* A video fits the body: the full width, and no taller than the body shows. */
.sheet.playview .rbody { display: flex; align-items: center; }
.viewer-video { display: block; max-width: 100%; max-height: 100%; margin: 0 auto; background: #000; }
/* The picture zooms by its width and this scroller pans it (image-zoom.js). Auto
   margins centre it while it fits and, unlike justify-content, leave every edge
   reachable once it is wider than the sheet. The browser keeps one-finger panning;
   the pinch is the script's. */
.sheet.shotview .rbody { display: flex; align-items: flex-start; }
.sheet.shotview .rbody > img { flex: none; margin: 0 auto; }
.sheet.shotview .rbody, .sheet.viewer .rbody:has(> img), .viewer.docked .rbody:has(> img) { touch-action: pan-x pan-y; }
/* A fitted picture leaves a sideways drag to the script: it is the swipe to the
   thread's next or previous picture. */
.sheet.shotview .rbody:not(.zoomed) { touch-action: pan-y; }
.sheet.shotview .shotcount { flex: none; font: 500 11.5px var(--font-mono); color: var(--faint); font-variant-numeric: tabular-nums; }
.sheet.viewer .rbody > img, .viewer.docked .rbody > img { cursor: zoom-in; user-select: none; }
.sheet.viewer .rbody.zoomed > img, .viewer.docked .rbody.zoomed > img { cursor: grab; }
.sheet.viewer .rbody.dragging > img, .viewer.docked .rbody.dragging > img { cursor: grabbing; }

/* ---------- 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.
   The gutter's darker shade is painted by the line, not the number, so it runs
   down every row a long line wraps onto. */
.sheet.viewer .vline.added, .viewer.docked .vline.added { background: linear-gradient(to right, rgba(126, 231, 135, .23) var(--gutter-w), rgba(126, 231, 135, .10) 0); }
.sheet.viewer .vline.removed, .viewer.docked .vline.removed { background: linear-gradient(to right, rgba(248, 81, 73, .23) var(--gutter-w), rgba(248, 81, 73, .10) 0); cursor: default; }
.sheet.viewer .vline.removed .gutter, .viewer.docked .vline.removed .gutter { color: var(--red); }
.rbody.nodiff .vline.removed { display: none; }
.rbody.nodiff .vline.added { background: 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 tinted violet,
   its gutter a shade darker, and the comment stands under it. */
.sheet.viewer .vline, .viewer.docked .vline { cursor: pointer; }
.sheet.viewer .vline:active, .viewer.docked .vline:active { background: var(--sel-bg); }
.vline.noted { background: linear-gradient(to right, rgba(210, 168, 255, .23) var(--gutter-w), rgba(210, 168, 255, .10) 0); }

/* A comment is a review thread card: who and when above a bordered thread of the
   file, the quoted line and the comment. It sits inside a <pre>, so it says for
   itself that its text wraps like prose; the body and the code keep their own
   line breaks. A button, so every part inside is a span laid out as a block. */
.lcnote {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  margin: 6px 0 8px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--text);
  font: 400 13px/1.45 var(--font-sans);
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
  cursor: pointer;
}
.lcavatar {
  flex: 0 0 24px;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--violet);
  color: var(--bg);
  font: 700 11px var(--font-sans);
}
.lchead { display: flex; align-items: center; gap: 8px; }
.lcwho { flex: 1; min-width: 0; font: 400 13px var(--font-sans); color: var(--dim); }
.lcwho b { font-weight: 600; color: var(--text); }
.lcthread { display: flex; flex-direction: column; border: 1px solid var(--line-strong); border-radius: 8px; overflow: hidden; background: var(--bg); }
.lcfile { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--input); border-bottom: 1px solid var(--line-strong); }
.lcpath { flex: 1; min-width: 0; font: 500 12px var(--font-mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lcdir { color: var(--dim); }
.lcpill, .lcbadge { flex: 0 0 auto; height: 18px; padding: 0 7px; display: inline-flex; align-items: center; border: 1px solid var(--btn-line); border-radius: 9px; color: var(--dim); font: 500 11px var(--font-sans); }
.lcpill.outdated { background: rgba(227, 179, 65, .13); border-color: rgba(227, 179, 65, .38); color: var(--amber); }
.lcline { display: grid; grid-template-columns: 3.2em minmax(0, 1fr); background: var(--card); font: 400 12px/1.6 var(--font-mono); color: var(--text); }
.lcnum { padding-right: 8px; text-align: right; color: var(--faint); background: var(--bg); }
.lccode { padding: 0 10px; white-space: pre-wrap; color: var(--lc-code, inherit); }
/* The quoted line is what the file said then, no longer what it says. */
.lcnote.stale { --lc-code: var(--dim); }
.lcrow { display: flex; gap: 10px; padding: 12px; border-top: 1px solid var(--line-strong); background: var(--card); }
.lcmain { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.lcmeta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.lcname { font: 600 13px var(--font-sans); color: var(--text); }
.lctime { font: 400 12px var(--font-sans); color: var(--dim); }
.lcbody { font: 400 13.5px/1.5 var(--font-sans); color: var(--text); white-space: pre-wrap; }

/* The box is the thread's reply row and its actions. Under a card it joins the
   card's thread; on a line with no comment yet it stands alone. */
.lcbox {
  display: flex;
  flex-direction: column;
  margin: 4px 0 8px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  overflow: hidden;
  white-space: normal;
}
.lcnote:has(+ .lcbox) { margin-bottom: 0; }
.lcnote:has(+ .lcbox) .lcthread { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.lcnote + .lcbox { margin-top: 0; border-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
.lcreply { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; background: var(--bg); border-top: 1px solid var(--line-strong); }
.lcbox > .lcreply:first-child { border-top: 0; }
.lcreply .lcavatar { margin-top: 4px; }
.lctext {
  flex: 1;
  min-width: 0;
  min-height: 64px;
  padding: 6px 10px;
  background: var(--input);
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  color: var(--text);
  font: 400 13px/1.45 var(--font-sans);
  resize: vertical;
}
.lcactions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; padding: 10px 12px; background: var(--card); border-top: 1px solid var(--line-strong); }
.lcactions .btn { height: 32px; white-space: nowrap; }
/* On a phone the three buttons fit one row: the box under a line of the editor
   is about 270px wide on a 390px screen, which Close, Comment and Start review
   fill at 8px a side. A narrower screen wraps them rather than widening the box. */
@media (max-width: 759px) {
  .lcactions { gap: 6px; padding: 8px 10px; }
  .lcactions .btn { padding: 0 8px; }
  .cm-vBlocks > .lcnote, .cm-vBlocks > .lcbox { margin-left: 4px; margin-right: 4px; }
}
.lcactions .btn:disabled, .rvbar .btn:disabled { opacity: .4; }

/* A cite's comment, at the foot of the card it quotes: the cited words on one
   clipped line, the comment under them. */
.citenotes { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
/* Under a paragraph of the card, with the next one a line below. */
.body > .citenotes { margin: 6px 0 12px; }
/* The words a comment box is open on, still marked once the field takes the
   selection (citeHold in app.js). */
::highlight(cite-held) { background-color: rgba(121, 192, 255, .3); }
.citenote, .citebox {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  padding: 5px 10px;
  background: rgba(121, 192, 255, .06);
  border: 0;
  border-left: 2px solid rgba(121, 192, 255, .55);
  border-radius: 0 6px 6px 0;
  color: var(--text);
  font: 400 13px/1.4 var(--font-sans);
  text-align: left;
  white-space: normal;
  overflow-wrap: anywhere;
}
.citenote { cursor: pointer; }
.citequote { display: flex; gap: 6px; min-width: 0; color: var(--dim); font-size: 12px; }
.citequote .mark { flex: none; color: var(--blue); }
.citequote .words { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.citenote .said { white-space: pre-wrap; }
/* The box: one line that grows as it is typed into, ✓ and ✕ beside it. */
.citerow { display: flex; align-items: flex-end; gap: 4px; }
.citefield {
  flex: 1;
  min-width: 0;
  min-height: 30px;
  padding: 5px 8px;
  background: var(--input);
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  color: var(--text);
  font: 400 13px/1.4 var(--font-sans);
  resize: none;
  overflow-y: auto;
}
.citeact {
  flex: none;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  color: var(--dim);
}
.citeact:first-of-type { color: var(--green); }
.citeact svg { width: 14px; height: 14px; display: block; }
/* Docked above the composer, for a cite whose card is not drawn. */
.citebox.docked { flex: 0 0 auto; width: auto; margin: 0 10px 6px; background: var(--card); }
/* The box has the screen while it has the focus. A wide frame has room for both. */
html[data-cite-editing] #frame:not(.wide) .composer { display: none; }
/* On a wide frame: the line comment's box, the cited words as its head. */
.citebox.roomy {
  gap: 0;
  max-width: var(--compose-w);
  padding: 0;
  background: none;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  overflow: hidden;
}
.citebox.roomy > .citequote { padding: 8px 12px; font-size: 13px; background: var(--card); }
.citebox.roomy > .lcreply { border-top: 1px solid var(--line-strong); }
.citebox.roomy.docked { margin: 0 auto 6px; width: calc(100% - 20px); }

/* The bar under a selection in the transcript. Under the sheets, which cover the
   transcript it belongs to. */
.citebar {
  position: fixed;
  z-index: 25;
  display: flex;
  gap: 2px;
  padding: 4px;
  background: var(--btn-bg);
  border: 1px solid var(--sel-line);
  border-radius: 12px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 12px 32px rgba(0, 0, 0, .85);
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
.citebar[hidden] { display: none; }
/* Three quiet actions, each an icon and its word; none is boxed. */
.citebar .selact {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: 500 13px var(--font-sans);
  white-space: nowrap;
}
.citebar .selact:hover, .citebar .selact:active { background: #2d3540; }
.citebar .selact svg { width: 16px; height: 16px; flex: 0 0 auto; color: #a3acb6; }
/* 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(--font-sans); 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; }

/* ---------- editing a file ---------- */

/* In edit mode CodeMirror is the body: it scrolls itself and draws its own gutter,
   so the body gives up its padding and its scroll, and the editor takes all of it
   — the full sheet on a phone, the column beside the thread on a wide frame. */
.sheet.viewer.editing .rbody, .viewer.docked.editing .rbody { padding: 0; overflow: hidden; }
.sheet.viewer .cm-editor, .viewer.docked .cm-editor { height: 100%; }
.sheet.viewer .cm-editor.cm-focused, .viewer.docked .cm-editor.cm-focused { outline: none; }

/* The editor's find widget (vendor/editor/find.mjs). On a phone a bar under the
   header that pushes the text down; on a wide screen it floats at the editor's top
   right, as VS Code's does. CodeMirror keeps the match it scrolls to clear of it. */
.viewer .cm-editor .cm-panels.cm-panels-top { border-bottom: 1px solid var(--line); background: var(--bg-chrome); }
.cm-find { display: flex; gap: 2px; padding: 6px 8px 6px 2px; font: 400 13px var(--font-sans); color: var(--text); }
.cm-find-rows { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.cm-find-row { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }
.cm-find-replace[hidden] { display: none; }
.cm-find-field { flex: 1 1 100%; min-width: 0; display: flex; align-items: center; gap: 1px; height: 38px; padding: 0 4px 0 10px;
  background: var(--input); border: 1px solid var(--line-strong); border-radius: 6px; }
.cm-find-field:focus-within { border-color: var(--blue); }
.cm-find-none .cm-find-row:first-child .cm-find-field, .cm-find-invalid .cm-find-row:first-child .cm-find-field { border-color: var(--red); }
.cm-find-input { flex: 1; min-width: 0; height: 100%; padding: 0; border: 0; outline: 0; background: none; color: var(--text); font: 400 15px var(--font-mono); }
.cm-find-input::placeholder { color: var(--subtle); }
.cm-find-toggle { flex: none; min-width: 30px; height: 28px; padding: 0 4px; border: 1px solid transparent; border-radius: 4px; background: none; color: var(--dim); font: 600 12px var(--font-mono); }
.cm-find-toggle[aria-pressed="true"] { color: var(--text); background: rgba(121, 192, 255, .22); border-color: var(--blue); }
.cm-find-btn { flex: none; width: 38px; height: 38px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 5px; background: none; color: var(--lit); }
.cm-find-btn:hover, .cm-find-toggle:hover { background: var(--btn-bg); color: var(--text); }
.cm-find-btn svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.cm-find-btn[hidden] { display: none; }
.cm-find-chevron { width: 26px; height: 38px; }
.cm-find-chevron svg { transition: transform .12s; }
.cm-find-chevron[aria-expanded="true"] svg { transform: rotate(90deg); }
.cm-find-count { flex: 1; padding: 0 8px; text-align: right; font: 400 12px var(--font-sans); color: var(--dim); white-space: nowrap; }
.cm-find-none .cm-find-count { color: var(--red); }
.cm-find-nav { display: flex; align-items: center; flex: 1 1 100%; order: 3; }
/* The phone's two rows: the field and close, then the count and the arrows. */
.cm-find-row:first-child .cm-find-field { flex: 1 1 0; }
.cm-find-close { order: 2; }
.cm-find-replace .cm-find-field { flex: 1 1 0; }
@media (min-width: 760px) {
  .viewer .cm-editor .cm-panels.cm-panels-top { position: absolute; top: 6px; right: 18px; left: auto; width: min(500px, calc(100% - 36px)); border: 0; background: none; }
  .cm-find { padding: 5px 6px 5px 2px; background: var(--card); border: 1px solid var(--line-strong); border-radius: 8px; box-shadow: 0 6px 18px rgba(0, 0, 0, .5); }
  .cm-find-row { flex-wrap: nowrap; gap: 4px; }
  .cm-find-field { height: 30px; padding: 0 3px 0 9px; }
  .cm-find-input { font-size: 13px; }
  .cm-find-toggle { min-width: 24px; height: 22px; font-size: 11px; }
  .cm-find-btn { width: 28px; height: 28px; }
  .cm-find-btn svg { width: 16px; height: 16px; }
  .cm-find-chevron { width: 20px; height: auto; align-self: stretch; }
  .cm-find-nav { flex: none; order: 0; }
  .cm-find-count { flex: none; min-width: 64px; text-align: center; padding: 0 4px; }
}

/* The diff sheet's find bar (public/find-bar.js), the editor's widget again: a bar
   under the header on a phone; on a wide screen a host of no height, so the widget
   floats over the diffs' top right. The matches are CSS highlights, in the
   editor's colours. */
.sheet.review .rfind { flex: none; border-bottom: 1px solid var(--line); background: var(--bg-chrome); }
.sheet.review .rfind[hidden] { display: none; }
::highlight(find-match) { background-color: rgba(227, 179, 65, .22); }
::highlight(find-current) { background-color: rgba(227, 179, 65, .6); }
@media (min-width: 760px) {
  .sheet.review .rfind { position: relative; z-index: 3; height: 0; border: 0; background: none; }
  .sheet.review .rfind .cm-find { position: absolute; top: 6px; right: 18px; width: min(500px, calc(100% - 36px)); box-sizing: border-box; }
}
/* The transcript's find bar (transcriptFindOf in public/app.js, which sets its top): the same widget,
   floating over the transcript's top right on every width. The transcript is
   absolute over its parent, so the bar sits over it there too. */
.tfind { position: absolute; right: 18px; z-index: 5; width: min(500px, calc(100% - 36px)); }
.tfind[hidden] { display: none; }
.tfind .cm-find { box-sizing: border-box; background: var(--bg-chrome); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,.35); }
/* A neutral .btn with a word in it, sized to a head's 30px icon buttons. */
#viewer-save:disabled, #viewer-undo:disabled, #viewer-redo:disabled { opacity: .35; }
#viewer-close.danger { color: var(--red); border-color: rgba(248, 81, 73, .5); }
/* Unsaved edits: a dot after the path, where a desktop editor puts it on the tab. */
.sheet.viewer .rpath.dirty::after, .viewer.docked .rpath.dirty::after { content: ' \25CF'; color: var(--amber); }

/* The minimap, VS Code's: a 72px column in the body's right padding (48px on a
   phone), placed by public/minimap.js. The padding is at least its width, so no
   text runs under it. The slider shows where the view is. */
.sheet.viewer.mapped .rbody, .viewer.docked.mapped .rbody { padding-right: 84px; }
.sheet.viewer.mapped.mapnarrow .rbody { padding-right: 56px; }
#frame.wide .sheet.viewer.mapped:not(.shotview) > .rbody, #frame.wide .viewer.docked.mapped > .rbody { padding-right: max(84px, (100% - var(--read-w)) / 2); }
.sheet.viewer .minimap, .viewer.docked .minimap { position: absolute; z-index: 2; cursor: default; touch-action: none; }
.sheet.viewer .minimap canvas, .viewer.docked .minimap canvas { display: block; width: 100%; height: 100%; }
.sheet.viewer .mmslider, .viewer.docked .mmslider { position: absolute; left: 0; right: 0; background: rgba(121, 121, 121, .14); }
.sheet.viewer .minimap:hover .mmslider, .viewer.docked .minimap:hover .mmslider { background: rgba(121, 121, 121, .26); }
.sheet.viewer .minimap.dragging .mmslider, .viewer.docked .minimap.dragging .mmslider { background: rgba(121, 121, 121, .38); }
/* A save the disk refused. The review bar's shape, amber: something to decide
   before the edits can land. One line where it fits; on a phone the buttons wrap
   under the words rather than squeezing them. */
.rvbar.vconflict { flex-wrap: wrap; background: rgba(227, 179, 65, .08); }
.rvbar.vconflict .count { color: var(--amber); min-width: max-content; }
.rvbar.vconflict .btn[hidden] { display: none; }
/* Edit mode's language server, the conflict strip's shape in the editor's blue:
   an offer, not a decision. The words wrap; the command is code text under them. */
.rvbar.vlsp { align-items: flex-start; background: rgba(121, 192, 255, .07); }
.rvbar.vlsp .vlsp-words { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }
.rvbar.vlsp .count { color: var(--blue); white-space: normal; }
.rvbar.vlsp code { font: 11px/1.4 var(--font-mono); color: var(--dim); overflow-wrap: anywhere; }
.rvbar.vlsp code[hidden], .rvbar.vlsp .btn[hidden], .rvbar.vlsp .icon-btn[hidden] { display: none; }

/* Edit mode's bar under the text: the problem under the cursor. Above a phone's
   keyboard, since the sheet ends there. */
.vproblem { flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; padding: 7px 12px; background: var(--card); border-top: 1px solid var(--line-strong); font: 13px/18px var(--font-sans); color: var(--body); }
.vproblem[hidden] { display: none; }
.vproblem-sev { flex: none; font-weight: 700; }
.vproblem.error .vproblem-sev { color: var(--red); }
.vproblem.warning .vproblem-sev { color: var(--amber); }
.vproblem-text { flex: 1; min-width: 0; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; }
.vproblem-source { color: var(--subtle); }
.vproblem-next { flex: none; padding: 0 2px; background: none; border: 0; font: 12px/18px var(--font-mono); color: var(--dim); cursor: pointer; }
.vproblem-next:hover { color: var(--text); }

.icon-btn { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; 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; }

/* The resume screen: a search box with the scope inside it, dense rows, and the
   selected row's last turns docked under the list with its two buttons; list and
   preview side by side at 760px and wider. */
.sheet.resume { background: var(--bg); }
.rshead { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 12px 12px 10px; border-bottom: 1px solid var(--line); }
.rsearch { flex: 1; min-width: 0; height: 42px; display: flex; align-items: center; gap: 8px; padding: 0 6px 0 12px; background: var(--input); border: 1px solid var(--line-strong); border-radius: 9px; color: var(--subtle); }
.rsearch:focus-within { border-color: var(--sel-line); }
.rsearch > svg { flex: 0 0 15px; width: 15px; height: 15px; }
.rsearch input { flex: 1; min-width: 0; height: 100%; background: transparent; border: 0; outline: 0; color: var(--text); font: 400 16px var(--font-sans); }
.rsearch input::-webkit-search-cancel-button { display: none; }
.rscope { flex: 0 0 auto; height: 28px; padding: 0 9px; border-radius: 6px; border: 1px solid var(--line-strong); background: var(--sel-bg); color: var(--dim); font: 500 11.5px var(--font-sans); white-space: nowrap; }
.rscope:active { color: var(--text); border-color: var(--sel-line); }
.rshead .icon-btn { width: 42px; height: 42px; border-color: var(--line-strong); color: var(--text); }
.rshead .icon-btn svg { width: 17px; height: 17px; }

.rsbody { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.rslist { flex: 1 1 0; min-height: 96px; overflow-y: auto; overscroll-behavior: contain; }
.rsrow { width: 100%; display: flex; align-items: center; gap: 10px; padding: 9px 14px; background: transparent; border: 0; border-bottom: 1px solid var(--line); text-align: left; color: var(--text); }
.rsrow.on { background: var(--sel-bg); box-shadow: inset 3px 0 0 var(--sel-line); }
.rsrow .info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.rsrow .name { font: 600 13.5px var(--font-sans); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rsrow .snip { font: 400 12px var(--font-sans); color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rsrow .when { flex: 0 0 auto; font: 400 11px var(--font-sans); color: var(--subtle); font-variant-numeric: tabular-nums; }
.rsrow .icon { flex: 0 0 16px; width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; }
.rsfoot { padding: 12px 14px; font: 400 11.5px var(--font-sans); color: var(--faint); }
/* A query word where it appears: the amber of the mockup on the code face, no box. */
.sheet.resume mark.hl { background: transparent; color: var(--amber); font-family: var(--font-mono); font-weight: 500; }
/* A worktree's folder, the one tag a row carries. */
.rstag { display: inline-flex; align-items: center; gap: 4px; margin-right: 6px; color: var(--violet); font: 400 10.5px var(--font-mono); vertical-align: -1px; }
.rstag svg { flex: 0 0 auto; width: 12px; height: 12px; }

.rsside { flex: 1.25 1 0; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--line-strong); background: var(--card); }
.rspreview { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 9px; padding: 12px 16px; }
.rspreview .rstitle { display: none; font: 600 15px var(--font-sans); color: var(--text); }
.rsmeta { font: 400 11.5px var(--font-sans); color: var(--subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Each item headed as in the transcript: the YOU pill, the agent's mark and name,
   a tool's pill and its title. */
.rsitem { display: flex; flex-direction: column; gap: 5px; }
.rsby { display: flex; align-items: center; gap: 7px; min-width: 0; }
.rstool { min-width: 0; font: 400 11.5px var(--font-mono); color: var(--subtle); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rsu { font: 400 13.5px/1.45 var(--font-sans); color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
.rsa { font: 400 13px/1.45 var(--font-sans); color: var(--body); white-space: pre-wrap; overflow-wrap: anywhere; }
.rsacts { flex: 0 0 auto; display: flex; gap: 8px; padding: 10px 16px calc(env(safe-area-inset-bottom) + 14px); }
.rsacts .btn { flex: 1; height: 42px; font-size: 14px; }
.rsacts .btn.primary { flex: 1.3; }

@media (min-width: 760px) {
  .rsbody { flex-direction: row; }
  .rslist { flex: 0 0 min(400px, 42%); border-right: 1px solid var(--line); }
  .rsside { flex: 1 1 0; border-top: 0; background: var(--bg); }
  .rspreview { padding: 16px 20px; }
  .rspreview .rstitle { display: block; }
  .rsacts { padding: 12px 20px 16px; }
  .rsacts .btn, .rsacts .btn.primary { flex: 0 0 auto; height: 36px; padding: 0 20px; }
}

.ws {
  display: flex;
  align-items: stretch;
  gap: 4px;
  padding: 4px 4px 4px 9px;
  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); }
/* The side tab list keeps neutral row borders; per-tab colour underlines live in
   the top strip only. */
.drawer .ws[data-colour]:not(.on) { border-color: var(--line-strong); }
.drawer .ws.on[data-colour] { border-color: var(--sel-line); }

/* 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 .info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; text-align: left; }
.ws .name { font: 600 12.5px var(--font-sans); 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(--font-sans); color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ws .state { flex: 0 0 auto; font: 600 12px var(--font-sans); color: var(--faint); }
.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; }

/* 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; }
/* A settings group: a title with its status on the right, a help line, then one
   panel of rows with hairlines between them. */
.sheet.settings .group { gap: 8px; margin-top: 6px; }
/* 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="email"], .sheet.settings input[type="text"], .sheet.settings textarea, .sheet.settings select, .sheet.settings .unit-field {
  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(--font-sans);
}
.sheet.settings input[type="email"], .sheet.settings input[type="text"] {
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  font: 500 12px var(--font-sans);
}
.newthread input, .newthread .newagentpick { flex: 1; }

/* The folder field, on its own line under a label and a hint, the path in mono.
   The grey suggestion (.cwdghost) lies over the input in the same font and
   padding: the typed text in it is transparent, so the rest starts at the caret. */
.newthread .cwdline { display: flex; flex-direction: column; gap: 6px; }
.newthread .cwdline[hidden] { display: none; }
.newthread .cwdhead { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 30px; padding: 0 0 0 2px; }
/* On the label's line the browse button is a small one; in a project card's
   Add folder slot it stays beside the field at the field's height. */
.newthread .cwdhead .cwd-browse { width: 34px; height: 30px; border-radius: 8px; }
.newthread .cwdlabel { font: 600 12px var(--font-sans); color: var(--body); }
.newthread .cwdbox { position: relative; flex: 1; display: flex; min-width: 0; }
.newthread input.new-cwd { height: 44px; padding: 0 62px 0 12px; font: 500 13px var(--font-mono); border-color: var(--line-strong); }
.newthread input.new-cwd:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(121, 192, 255, .14); }
.newthread .cwdghost { position: absolute; inset: 0 62px 0 13px; display: flex; align-items: center; pointer-events: none; overflow: hidden; white-space: pre; font: 500 13px var(--font-mono); }
.newthread .cwdghost[hidden] { display: none; }
.newthread .cwdghost-typed { color: transparent; }
.newthread .cwdghost-rest { color: var(--faint); }
/* A terminal's caret: a blinking blue block where the typing goes, drawn over the
   input while the browser's own (made transparent) would sit at the same spot. */
.newthread .cwdcaret { display: none; width: 1ch; height: 20px; margin-right: -1ch; border-radius: 2px; background: var(--blue); box-shadow: 0 0 6px rgba(121, 192, 255, .55); animation: cwdblink 1.06s steps(1) infinite; }
.newthread .cwdbox.termcaret .cwdcaret { display: inline-block; }
.newthread .cwdghost-under { color: var(--faint); }
.newthread .cwdbox.termcaret .cwdghost-under { animation: cwdblinkunder 1.06s steps(1) infinite; }
@keyframes cwdblinkunder { 0% { color: var(--bg); } 50% { color: var(--faint); } }
.newthread .cwdbox.termcaret input.new-cwd { caret-color: transparent; }
@keyframes cwdblink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .newthread .cwdcaret { animation: none; }
  .newthread .cwdbox.termcaret .cwdghost-under { animation: none; color: var(--bg); }
}
.newthread .cwdfill { position: absolute; right: 7px; top: 50%; transform: translateY(-50%); display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 8px; border-radius: 7px; border: 1px solid var(--line-strong); background: var(--bg-chrome); color: var(--dim); font: 500 11px var(--font-sans); white-space: pre; }
.newthread .cwdfill[hidden] { display: none; }
.newthread .cwdfield { flex: 1; display: flex; gap: 6px; min-width: 0; }
.newthread .cwd-browse { flex: none; width: 44px; height: 44px; border-radius: 9px; }
.newthread .cwd-browse svg { width: 16px; height: 16px; }
.newthread .new-default[hidden] { display: none; }
.newthread .newagentpick { display: inline-flex; align-items: center; gap: 7px; text-align: left; }
.newthread .newagentpick:disabled { color: var(--faint); }
/* The app's box pick, first in the row and dressed as the agent pick. With three
   in the row the two picks take what their names need, the box at most a third,
   and the folder keeps the rest. */
.newthread .new-box { flex: 0 1 auto; max-width: 33%; }
.newthread .row:has(.new-box) .new-agent { flex: 0 0 auto; }
.newthread .new-box span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Workstation <name> ⌄: the box pick says what it picks, so it takes what that needs. */
.newthread .new-box { max-width: 100%; }
.newthread .new-box .pick-label { color: var(--dim); flex: none; }
.newthread .new-box svg { width: 12px; height: 12px; flex: none; color: var(--dim); }

/* The card as New project and Edit project (projectSection in app.js): the agent
   pick goes, NAME and FOLDERS sit under the row, and the folder field drops into
   the Add folder slot while a folder is being added. Each row is the same well
   as the inputs, the path in mono like the folder menu's rows. */
.newthread .new-agent[hidden], .newthread .row[hidden], .newthread .project-form[hidden],
.newthread .project-add[hidden], .newthread .project-add-field[hidden] { display: none; }
.newthread .project-form { display: flex; flex-direction: column; gap: 8px; }
.newthread .plabel { padding: 4px 2px 0; font: 700 10px var(--font-sans); color: var(--faint); letter-spacing: .08em; }
.newthread .project-name { flex: none; }
.newthread .project-folders { display: flex; flex-direction: column; gap: 6px; }
.newthread .project-folders:empty { display: none; }
.newthread .project-folder {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  padding: 0 4px 0 10px;
  background: var(--input);
  border: 1px solid var(--line);
  border-radius: 9px;
}
.newthread .project-folder .sname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 600 12px var(--font-mono); color: var(--text); }
/* Main is a filled star and the word in blue; Make main the empty star, alone. */
.newthread .project-main { flex: none; display: inline-flex; align-items: center; gap: 5px; padding: 0 4px; color: var(--blue); font: 600 12px var(--font-sans); }
.newthread .project-main svg { width: 13px; height: 13px; display: block; }
.newthread .project-make-main { flex: none; width: 30px; height: 30px; color: var(--dim); background: none; border: 0; box-shadow: none; }
.newthread .project-make-main svg { width: 14px; height: 14px; }
.newthread .project-drop { flex: none; width: 30px; height: 30px; color: var(--dim); background: none; border: 0; }
.newthread .project-drop svg { width: 12px; height: 12px; }
.newthread .project-add {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 38px;
  background: transparent;
  border: 1px dashed var(--line);
  border-radius: 9px;
  color: var(--dim);
  font: 600 12px var(--font-sans);
}
.newthread .project-add svg { width: 14px; height: 14px; display: block; }
.newthread .project-add:active { background: var(--input); color: var(--text); }
.newthread .project-add-field { display: flex; flex-direction: column; gap: 6px; }
.newthread .project-hint { padding: 0 2px; font: 400 11px var(--font-sans); color: var(--dim); }

/* Settings keep the same dark 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(--font-sans); letter-spacing: -.025em; }
.sheet.settings .shead .icon-btn { width: 42px; height: 42px; border-radius: 6px; background: var(--input); border-color: var(--line-strong); color: var(--text); }
.sheet.settings .shead .icon-btn svg { width: 17px; height: 17px; }
.sheet.settings .gtitle { display: flex; align-items: center; gap: 8px; min-height: 18px; padding: 0 2px; }
.sheet.settings .gname { font: 600 12px var(--font-sans); color: var(--text); }
/* The group's status: plain text, a green check before a settled state, no pill. */
.sheet.settings .gstatus { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; min-width: 0; font: 400 12px var(--font-sans); color: var(--dim); text-align: right; }
.sheet.settings .gstatus svg { width: 13px; height: 13px; flex: 0 0 13px; color: var(--green); }
.sheet.settings .gstatus.err { color: var(--red); }
.sheet.settings .rows { background: var(--bg); border: 1px solid var(--line-strong); border-radius: 10px; overflow: hidden; }
.sheet.settings .rows > .setting { padding: 12px 14px; }
/* After a shown row, so a search that hides the first leaves no line on top. */
.sheet.settings .rows > .setting:not(.unmatched) ~ .setting { border-top: 1px solid var(--line); }
/* The search under the title, sticky with it; the rows and groups it leaves out. */
.sheet.settings .shead { flex-wrap: wrap; }
/* Settings uses the shared magnifier field and spans below its heading. */
.sheet.settings .settings-search {
  flex: 1 0 100%;
  min-width: 0;
}
/* !important: .setting.row and the agent rows set their own display. */
.sheet.settings .unmatched { display: none !important; }
.sheet.settings .settings-none[hidden] { display: none; }
/* A group of buttons, not rows: they stack inside the panel's padding. */
.sheet.settings .rows.stack { display: flex; flex-direction: column; gap: 11px; padding: 14px; }
.sheet.settings .group .btn { height: 44px; }
.sheet.settings .help { margin: 0; padding: 0 2px; font: 400 12px/1.45 var(--font-sans); color: var(--dim); }
.sheet.settings .help.err { color: var(--red); }
.sheet.settings .help code, .sheet.settings .note code { font: 400 11.5px var(--font-mono); color: var(--body); }
.sheet.settings .help .account-email { font-weight: 600; color: var(--text); }
.sheet.settings .setting { display: flex; flex-direction: column; gap: 8px; }
/* A row: label and one line of help on the left, the control on the right. */
.sheet.settings .setting.row { flex-direction: row; align-items: center; justify-content: space-between; gap: 16px; }
.sheet.settings .setting .name { flex: 1; min-width: 0; font: 500 13.5px/1.3 var(--font-sans); color: var(--text); display: flex; flex-direction: column; gap: 2px; }
.sheet.settings .setting .note { font: 400 12px/1.45 var(--font-sans); color: var(--dim); }
.sheet.settings .setting .note:empty { display: none; }
.sheet.settings .agent-title { display: flex; align-items: center; gap: 6px; }
.sheet.settings .setting.agent-row { gap: 12px; }
.sheet.settings .agent-mark { flex-basis: 20px; width: 20px; height: 20px; }
/* The Agents card: a row per agent that opens a box of its settings under it. */
.sheet.settings .rows > .agent-item:not(.unmatched) ~ .agent-item { border-top: 1px solid var(--line); }
.sheet.settings .agent-item > .agent-row { padding: 12px 14px; cursor: pointer; }
.sheet.settings .agent-item > .agent-row:focus-visible { outline: 2px solid var(--sel-line); outline-offset: -2px; }
.sheet.settings .agent-item .xchev { margin-right: 0; color: var(--dim); }
.sheet.settings .agent-item .xchev svg { width: 18px; height: 18px; transition: transform .12s ease; }
.sheet.settings .agent-item.open .xchev svg { transform: rotate(90deg); }
.sheet.settings .agent-more { padding: 0 14px 4px 46px; }
.sheet.settings .agent-more > .setting { padding: 10px 0; border-top: 1px solid var(--line); }
/* Update every hour, on: a green mark beside the version, turning while it runs. */
.sheet.settings .agent-row .note { display: flex; align-items: center; gap: 6px; }
.sheet.settings .agent-cycle { flex: 0 0 auto; display: inline-flex; width: 14px; height: 14px; color: var(--green); }
.sheet.settings .agent-cycle svg { width: 100%; height: 100%; display: block; }
.sheet.settings .agent-cycle.running svg { animation: rot 1.2s linear infinite; }
.sheet.settings .agent-cycle.failed { color: var(--red); }
@media (prefers-reduced-motion: reduce) { .sheet.settings .agent-cycle.running svg { animation: none; } }
/* The custom prompt: full width under its label, in the face file contents use. */
.sheet.settings textarea { box-sizing: border-box; width: 100%; min-height: 76px; padding: 9px 10px; border: 1px solid var(--btn-line); border-radius: 6px; font: 400 13px/1.5 var(--font-sans); resize: vertical; }
.sheet.settings textarea:focus-visible, .sheet.settings input[type="email"]:focus-visible, .sheet.settings input[type="text"]:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }

/* A number with its unit: the field and a segment naming what it counts. */
.sheet.settings .unit-field { flex: 0 0 auto; display: flex; align-items: center; height: 30px; border: 1px solid var(--btn-line); border-radius: 6px; overflow: hidden; }
.sheet.settings .unit-field:focus-within { outline: 2px solid var(--blue); outline-offset: 1px; }
.sheet.settings .unit-field input { width: 72px; height: 100%; padding: 0 8px; background: transparent; border: 0; color: var(--text); font: 500 12.5px var(--font-sans); text-align: right; outline: none; -moz-appearance: textfield; }
.sheet.settings .unit-field input::-webkit-inner-spin-button, .sheet.settings .unit-field input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.sheet.settings .unit { align-self: stretch; display: grid; place-items: center; padding: 0 8px; border-left: 1px solid var(--line-strong); color: var(--dim); font: 500 11px var(--font-sans); }

/* The stream selects are drawn as combo boxes by public/combo.js; the select
   itself stays hidden and holds the value. */
.sheet.settings .select-control { flex: 0 0 auto; min-width: 0; }
.sheet.settings .select-control select { display: none; }

/* 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. */
.sheet.settings input[type="checkbox"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sheet.settings .switch, .slashmenu .sfast .switch {
  flex: 0 0 auto;
  box-sizing: border-box;
  width: 36px;
  height: 20px;
  border-radius: 10px;
  background: var(--btn-bg);
  border: 1px solid var(--btn-line);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .35);
  position: relative;
  transition: background .12s ease-out;
}
.sheet.settings .switch::after, .slashmenu .sfast .switch::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #8b949e;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .45);
  transition: transform .12s ease-out, background .12s ease-out;
}
.sheet.settings input[type="checkbox"]:checked + .switch, .slashmenu .sfast[aria-checked="true"] .switch { background: var(--btn-primary); border-color: rgba(240, 246, 252, .1); }
.sheet.settings input[type="checkbox"]:checked + .switch::after, .slashmenu .sfast[aria-checked="true"] .switch::after { transform: translateX(16px); background: #fff; }
.sheet.settings input[type="checkbox"]:focus-visible + .switch { outline: 2px solid var(--sel-line); outline-offset: 2px; }
.sheet.settings input[type="checkbox"]:disabled + .switch { opacity: .45; }

@media (max-height: 850px) {
  .sheet.settings { padding: 12px 16px 16px; gap: 8px; }
  .sheet.settings .rows > .setting { padding: 10px 12px; }
  .sheet.settings .shead { top: -12px; padding: 12px 0; }
}
@media (min-width: 720px) {
  .sheet.settings, .sheet.sessions { padding-left: max(24px, calc((100% - 1200px) / 2)); padding-right: max(24px, calc((100% - 1200px) / 2)); }
}
@media (prefers-reduced-motion: reduce) {
  .sheet.settings .switch, .sheet.settings .switch::after, .slashmenu .sfast .switch, .slashmenu .sfast .switch::after { 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(--font-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: center; gap: 9px; min-height: 40px; padding: 8px 12px; border-color: var(--line); background: #10141a; }
.cwdmenu .srow svg { width: 14px; height: 14px; flex: none; color: var(--dim); }
.cwdmenu .srow.on svg { color: var(--blue); }
.cwdmenu .sname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cwdmenu .branch { flex: none; color: var(--dim); margin-left: auto; font: 400 11px var(--font-sans); }
/* The list hangs off the field's bottom edge as one panel, the way a browser's
   address bar list does: the field loses its bottom corners, the rows lose their
   own borders and sit on lines. Beside a browse button it stops short of it. */
.newthread .cwdfield:has(+ .cwdmenu.show) input.new-cwd { border-bottom-left-radius: 0; border-bottom-right-radius: 0; box-shadow: none; }
.newthread .cwdmenu.show { gap: 0; margin: -7px 0 0 0; border: 1px solid var(--blue); border-top-color: var(--line); border-radius: 0 0 9px 9px; background: #10141a; }
.newthread .cwdfield:has(.cwd-browse) + .cwdmenu.show { margin-right: 50px; }
.newthread .cwdmenu .srow { border: 0; border-radius: 0; background: transparent; min-height: 40px; }
.newthread .cwdmenu .srow + .srow { border-top: 1px solid var(--line); }
.newthread .cwdmenu .srow:active { background: var(--sel-bg); }
.newthread .cwdmenu .srow.on { background: rgba(121, 192, 255, .14); box-shadow: inset 3px 0 0 var(--blue); color: var(--text); }
.newthread .cwdmenu .srow.on .sname { color: var(--text); font-weight: 600; }
/* Logged-out agents wear the amber dot and open the login sheet. */
.newagentmenu .agent-option.needs-login .sname { color: var(--amber); }
/* A row that installs an agent reads as a plain action: the name in body type,
   and Claude's "recommended" on the same line. */
.newagentmenu .srow.install-option { flex-direction: row; align-items: baseline; gap: 8px; }
.newagentmenu .install-option .sname { font: 600 13px var(--font-sans); }
/* No folder to pick while the box has no agent (renderNewThread). */
.newthread .cwdfield[hidden] { display: none; }
/* Box rows carry no mark, and a box that cannot take a tab says why under its name. */
.newagentmenu .srow.box-option { padding: 8px 9px; }
.newagentmenu .box-option .snote { font: 400 11px var(--font-sans); color: var(--dim); }
.newagentmenu .box-option.on { border-color: var(--sel-line); background: var(--sel-bg); }
.newagentmenu .box-option:disabled { color: var(--faint); border-color: var(--line); }
.newagentmenu .box-option.add-box .sname { color: var(--blue); }
/* 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(--font-sans); color: var(--text); }
.agent-login-row { display: flex; gap: 8px; }
.agent-login-link { overflow-wrap: anywhere; }
/* A vendor's login URL runs to hundreds of characters: two lines of it, then an
   ellipsis. Open link and Copy link take the whole URL. */
.agent-login-link { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
/* The code a claude login pastes back, in the face the vendor's page shows it in. */
.agent-login-code {
  flex: 1;
  min-width: 0;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--btn-line);
  border-radius: 6px;
  background: var(--input);
  color: var(--text);
  font: 400 13px var(--font-mono);
}
.agent-login-code:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
/* The code a codex login shows, to type on the vendor's page. */
.agent-login-usercode { font: 600 22px var(--font-mono); letter-spacing: 2px; color: var(--text); padding: 2px 0; user-select: all; }
/* An install's last output lines and the command to run by hand: a code block's
   face and frame, wrapped, never scrolled sideways. */
.agent-install-lines, .agent-install-command {
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--bg);
  color: var(--dim);
  font: 400 11px/1.5 var(--font-mono);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.agent-install-command { color: var(--text); user-select: all; }
/* The command to run with its green Copy at the end of the line, as the site's
   install command: one frame, the code borderless inside it. */
.agent-install-line { display: flex; align-items: center; gap: 8px; padding: 4px 4px 4px 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--bg); }
.agent-install-line > .agent-install-command { flex: 1; min-width: 0; padding: 4px 0; border: 0; border-radius: 0; background: none; }
.agent-install-line > .btn { flex: 0 0 auto; height: 30px; padding: 0 12px; }
/* An npm install's fix under its greyed update switch: the command, then Copy. */
.sheet.settings .agent-more > .agent-update-fix { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; border-top: 0; padding-top: 0; }
.sheet.settings .agent-update-fix .agent-install-line { align-self: stretch; }
/* The New thread card on a box with no agent installed: what it says, then an
   Install button per agent, wrapping when three do not fit. */
/* In a fault card the buttons take what their words need, not the row. */
.fault .agentlogin { align-items: flex-start; padding-top: 10px; }
.fault .agentlogin > .agent-login-link, .fault .agentlogin > .agent-login-row:has(.agent-login-code) { align-self: stretch; }
/* The settings sheet's Agents group: the computer pick sits at the right of the
   title, dressed as the New thread form's box pick, and its menu opens under the
   help line. The login runs on its own screen (.agent-screen). */
.sheet.settings .agents-box {
  margin-left: auto;
  max-width: 50%;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--input);
  color: var(--text);
  font: 500 12px var(--font-sans);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sheet.settings .agents-box::after { content: " \25BE"; color: var(--dim); }
.sheet.settings .agents-box-menu { margin: 0; }
.sheet.settings .agents-box-menu .srow.box-option { padding: 10px 12px; }
.sheet.settings .rows > .setting.row .btn { flex: 0 0 auto; height: 38px; padding: 0 14px; }
/* An agent's install or login on its own screen over the settings: the agent and
   the workstation, then the steps in a larger face, the buttons at full height. */
.sheet.settings.agent-screen { gap: 18px; }
.sheet.settings .agent-screen-back svg { transform: rotate(180deg); }
.sheet.settings .agent-screen-who { display: flex; align-items: center; gap: 14px; padding: 4px 2px 0; }
.sheet.settings .agent-screen-who .smark { flex: 0 0 36px; width: 36px; height: 36px; }
.sheet.settings .agent-screen-who .name { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sheet.settings .agent-screen-name { font: 600 18px/1.2 var(--font-sans); color: var(--text); }
.sheet.settings .agent-screen-who .note { font: 400 13px var(--font-sans); color: var(--dim); }
.sheet.settings .agent-screen-host .agentlogin { gap: 14px; padding: 16px 14px; background: var(--bg); border: 1px solid var(--line-strong); border-radius: 10px; }
.sheet.settings .agent-screen-host .agent-login-text { font: 400 14px/1.45 var(--font-sans); }
.sheet.settings .agent-screen-host .agent-login-link { font: 400 13px/1.45 var(--font-sans); }
.sheet.settings .agent-screen-host .agent-login-row .btn { flex: 1; height: 44px; }
.sheet.settings .agent-screen-host .agent-install-line .btn { flex: 0 0 auto; height: 36px; }
.sheet.settings .agent-screen-host .agent-login-code { height: 44px; font: 400 14px var(--font-mono); }
.sheet.settings .agent-screen-host .agent-login-row:has(.agent-login-code) .btn { flex: 0 0 auto; }
.sheet.settings .agent-screen-host .agent-login-usercode { font-size: 28px; }
/* With a mouse the settings buttons are the height of the sheet's selects, and
   the agent screen's buttons take what their words need, not the row. */
@media (pointer: fine) {
  .sheet.settings .group .btn,
  .sheet.settings .rows > .setting.row .btn,
  .sheet.settings .agent-screen-host .agent-login-row .btn { height: 32px; padding: 0 14px; }
  .sheet.settings .agent-screen-host .agent-login-row:not(.agent-install-line) .btn { flex: 0 0 auto; min-width: 88px; }
  .sheet.settings .agent-screen-host .agent-install-line .btn { height: 30px; }
}

/* The /mcp sheet: the Agents rows, one per MCP server. Its groups sit in one
   holder, which lays out as if they were the sheet's own children. */
#mcp-groups { display: contents; }
.sheet.settings .mcp-mark { display: flex; align-items: center; justify-content: center; }
.sheet.settings .mcp-name { font-family: var(--font-mono); overflow-wrap: anywhere; }
.sheet.settings .mcp-more { padding: 2px 16px 14px 52px; }
.sheet.settings .mcp-detail { font-family: var(--font-mono); opacity: .75; word-break: break-all; }
.sheet.settings .mcp-more .agent-login-code { font-family: var(--font-mono); }
/* 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 { color: var(--dim); }
.newagentmenu .srow.agent-option .sname { color: inherit; }
.newagentmenu .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 { position: absolute; left: 10px; top: 10px; }

/* 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(--font-sans);
  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); }
.toast > .spinner { margin-top: 0; }
.toast > .toast-action { flex: none; white-space: nowrap; margin-left: 6px; padding: 0; border: 0; background: none; color: var(--accent, #7aa2ff); font: inherit; font-weight: 600; cursor: pointer; }

/* 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); }


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

/* The boot cover: the page's background over the shell until app.js lifts it. */
#boot-cover { position: fixed; inset: 0; z-index: 55; background: var(--bg); }
html.booted #boot-cover { display: none; }

.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(--font-sans); color: var(--green); letter-spacing: .1em; }
.auth-hint { font: 400 11.5px/1.5 var(--font-sans); 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(--font-mono);
  outline: none;
}

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

/* The router sheet (#remote-sheet) and the Install VCode screen in the same
   frame: header with the logo and a state dot, then the body. */
#remote-sheet, #install-screen { background: #000; }
#remote-sheet .auth-card, #install-screen .auth-card {
  gap: 0;
  padding: 0;
  overflow: hidden;
  background: #11151b;
  border: 1px solid #313a46;
  border-radius: 12px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .07), 0 0 0 1px rgba(0, 0, 0, .65),
    0 12px 24px rgba(0, 0, 0, .6), 0 32px 64px rgba(0, 0, 0, .65);
}
.signin-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line); }
.signin-logo { display: block; height: 18px; width: auto; }
.signin-state { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; min-width: 0; font: 500 10px var(--font-sans); color: var(--dim); }
.signin-dot { flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: var(--amber); }
.signin-dot.on { background: var(--green); }
.signin-state > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.signin-body { display: flex; flex-direction: column; gap: 12px; padding: 16px; }
#remote-sheet .auth-hint b { font-weight: 600; color: var(--text); }
#remote-sheet .auth-hint, #install-screen .auth-hint { font: 400 13px/1.5 var(--font-sans); color: var(--dim); }
#rs-err { display: flex; align-items: center; gap: 6px; min-height: 0; font: 500 12.5px/1.4 var(--font-sans); }
#rs-err::before { content: ''; flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: var(--red); }
#rs-err:empty { display: none; }

/* The remote screens (public/remote.mjs) are the auth sheet's card, one step at a time. */
/* The Install VCode screen: the sign-in card's frame, one big button. */
.is-icon { align-self: center; width: 88px; height: 88px; margin-top: 4px; filter: drop-shadow(0 6px 22px rgba(140, 90, 255, .45)); }
.is-title { font: 600 19px var(--font-sans); color: var(--text); text-align: center; }
.is-center { text-align: center; }
.is-perks { display: flex; flex-direction: column; gap: 8px; margin: 2px 0; padding: 0; list-style: none; font: 400 13px/1.4 var(--font-sans); color: var(--text); }
.is-perks li { display: flex; align-items: center; gap: 10px; }
.is-perks svg { flex: 0 0 auto; width: 16px; height: 16px; color: var(--green); }
.is-perks[hidden] { display: none; }
/* The desktop app's install command on a computer: selectable whole, wrapped, never scrolled. */
.is-command {
  margin: 0;
  padding: 10px 12px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  font: 400 12px/1.6 var(--font-mono);
  color: var(--text);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  user-select: all;
}
.is-command[hidden] { display: none; }
#install-screen .btn.is-go { height: 56px; display: flex; align-items: center; justify-content: center; gap: 10px; font-size: 16px; }
#install-screen .btn.is-go[hidden] { display: none; }
.is-go svg { width: 22px; height: 22px; }
#install-screen .is-hint { color: var(--text); }
.is-never { display: flex; align-items: center; justify-content: center; gap: 8px; font: 400 13px var(--font-sans); color: var(--dim); }
#install-screen .is-never input { width: 16px; height: 16px; margin: 0; padding: 0; accent-color: var(--green); }
.is-skip { align-self: center; padding: 6px; background: none; border: 0; color: var(--dim); font: 500 13px var(--font-sans); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }

.remote-step { display: flex; flex-direction: column; gap: 10px; }
.remote-step[hidden] { display: none; }
.remote-message { font: 600 15px/1.4 var(--font-sans); color: var(--text); }
.remote-message + .auth-hint { margin-top: -6px; }
/* The sheet's inputs wear the sign-in field: the input well, 8px corners, mono. */
#remote-sheet input {
  height: 44px;
  padding: 0 12px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  color: var(--text);
  font: 400 14px var(--font-mono);
}
#remote-sheet input:focus { border-color: var(--green); outline: none; }
#remote-sheet input[aria-invalid="true"] { border-color: var(--red); }
#rs-claim-intro[hidden] + #rs-claim-sent { margin-top: -6px; }
/* The claim's code, as v-code.dev/login draws its own: #rs-claim-code lies over
   the eight boxes, invisible, and takes every tap, keystroke and paste;
   public/remote.mjs draws its digits into the boxes. */
.code-cells { position: relative; }
.code-cells[hidden] { display: none; }
#remote-sheet .code-cells input {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  color: transparent;
  caret-color: transparent;
  font-size: 16px;
}
.code-cells input::selection { background: transparent; }
.code-row { display: flex; gap: 6px; }
.code-cell {
  flex: 1;
  min-width: 0;
  height: 48px;
  display: grid;
  place-items: center;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  color: var(--text);
  font: 500 20px var(--font-mono);
}
/* A wider gap in the middle, so the code reads as two fours. */
.code-cell:nth-child(5) { margin-left: 6px; }
.code-cells:focus-within .code-cell.active { border-color: var(--green); box-shadow: 0 0 0 1px var(--green) inset; }
/* Signed out, the login card sits over the empty app (the shell before a box
   opens), dimmed and lightly blurred rather than on black. */
#remote-sheet:has(#rs-login:not([hidden])) {
  background: rgba(8, 9, 11, .3);
  -webkit-backdrop-filter: blur(2.5px);
  backdrop-filter: blur(2.5px);
}

/* A combo box: the trigger, then a menu 4px under it at its width. The keyboard's
   row is raised (--btn-bg and a hairline); the picked row keeps a green check;
   a row that cannot be picked stays, dimmed, with the reason under its name. */
.combo { position: relative; }
.combo-trigger {
  width: 100%;
  height: 44px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  color: var(--text);
  font: 600 13px var(--font-sans);
  text-align: left;
}
.combo-trigger svg { flex: 0 0 auto; width: 14px; height: 14px; color: var(--dim); transition: transform .15s; }
.combo-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-trigger:focus-visible, .combo-trigger[aria-expanded="true"] {
  outline: none;
  border-color: #58a6ff;
  box-shadow: 0 0 0 3px rgba(88, 166, 255, .15);
}
.combo-trigger[aria-expanded="true"] svg { transform: rotate(180deg); }
.combo-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px;
  background: var(--input);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, .55);
}
.combo-menu[hidden] { display: none; }
.combo-option {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 6px 10px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--text);
  text-align: left;
}
.combo-option.highlight, .combo-option:hover:not(:disabled) { background: var(--btn-bg); border-color: var(--line-strong); }
.combo-option:disabled { opacity: .45; }
.combo-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.combo-name { font: 600 13px var(--font-sans); }
.combo-note { font: 400 11px var(--font-sans); color: var(--dim); }
.combo-check { flex: 0 0 auto; width: 14px; height: 14px; color: var(--green); visibility: hidden; }
.combo-option[aria-selected="true"] .combo-check { visibility: visible; }
/* The compact combo at the right of a settings row, per the pack's "in a settings
   row" trigger. Its menu is fixed to the page (combo.js places it), so a row
   panel's overflow does not clip it. */
.combo.compact .combo-trigger { width: auto; min-width: 104px; height: 30px; gap: 8px; padding: 0 8px 0 10px; border-color: var(--btn-line); border-radius: 6px; font: 500 12.5px var(--font-sans); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04); cursor: pointer; }
.combo.compact .combo-trigger svg { width: 13px; height: 13px; }
.combo.compact .combo-trigger:hover:not([aria-expanded="true"]) { border-color: #4d5663; }
.combo.compact .combo-trigger:focus-visible, .combo.compact .combo-trigger[aria-expanded="true"] { border-color: #79c0ff; box-shadow: 0 0 0 3px rgba(121, 192, 255, .18); }
.combo-menu.floating { position: fixed; top: auto; left: auto; right: auto; z-index: 60; gap: 1px; border-radius: 10px; box-shadow: 0 10px 28px rgba(0, 0, 0, .5); }
.combo-menu.floating .combo-option { min-height: 36px; padding: 0 10px; gap: 10px; cursor: pointer; }
.combo-menu.floating .combo-option.highlight, .combo-menu.floating .combo-option:hover:not(:disabled) { background: #212830; border-color: transparent; box-shadow: inset 0 0 0 1px var(--btn-line); }
.combo-menu.floating .combo-name { flex: 1; font: 500 13px var(--font-sans); white-space: nowrap; }
.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); white-space: nowrap; }
.remote-fingerprint {
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  font: 700 15px var(--font-sans);
  letter-spacing: .12em;
  color: var(--green);
  text-align: center;
  word-break: break-all;
}
/* The phone QR (remote.mjs drawQr): white with a quiet zone, so a camera reads it on the dark card. */
.remote-qr { align-self: center; width: 220px; height: 220px; border-radius: 8px; background: #fff; image-rendering: pixelated; }
.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: 8px;
  color: var(--text);
  font: 600 13px var(--font-sans);
  text-align: left;
}
.remote-box-note { font: 400 11px var(--font-sans); color: var(--dim); }

/* reload probe */

/* 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; }

/* ---------- 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: 9;
  grid-row: var(--row-keys);
  flex: 0 0 auto;
  display: none;
  place-items: center;
  width: 38px;
  height: 34px;
  border-radius: 6px;
  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(--font-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(--font-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: 32px;
  min-width: 32px;
  padding: 0 9px;
  border-radius: 16px;
  /* See-through, so the text it floats over still reads as there under it. */
  background: rgba(33, 40, 48, .55);
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
  backdrop-filter: blur(10px) saturate(1.3);
  border: 1px solid rgba(255, 255, 255, .12);
  color: var(--text);
  font: 600 10px var(--font-sans);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .5);
  /* 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; }
/* An arrow landing on a line: the end of the transcript, not just "down". */
.tobottom .arrow { width: 15px; height: 15px; }

/* 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; }
.pathlabel, .item.tool .head .meta, .item.diff .head .meta { font-family: var(--font-mono); }

/* 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. The bottom
   pixel row stays out of it: that is the underline, which runs on under the +. */
.tabstrip.overflowing {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent), linear-gradient(#000, #000);
  mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent), linear-gradient(#000, #000);
  -webkit-mask-size: 100% 100%, 100% 1px;
  mask-size: 100% 100%, 100% 1px;
  -webkit-mask-position: 0 0, 0 100%;
  mask-position: 0 0, 0 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* 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(--font-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(--font-sans);
  outline: none;
}

/* 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(--font-sans);
  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 ⋯ that opens the tab menu, worn like rename beside it. */
.ws .wsmore {
  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);
  font: 600 14px var(--font-sans);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.ws .wsmore:active { background: rgba(230, 237, 243, .16); border-color: var(--sel-line); color: var(--text); }

/* 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(--font-sans);
  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. */
.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(--font-sans);
  white-space: nowrap;
}

/* A message another tab sent: that tab's mark and name, a button that opens it.
   Drawn like an agent's head, so it reads as who wrote it, not as a control. */
.item .head .sender {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.item .head .sender .agentname { text-decoration: underline dotted; text-underline-offset: 3px; }
/* An Agent Commander's mark: VCode's icon, as its tab name carries it. */
.item .head .sender .sendermark {
  flex: none;
  width: 14px;
  height: 14px;
  margin: 3px 4px 3px 3px;
  border-radius: 50%;
}

/* ---------- 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 widths and writes `wide`, `dock-tabs` 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 two columns. --dock-l is how
   wide the tab column is and --dock-r the explorer's, 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. --zoom is set by
   app.js: 1.1 at 100% on the zoom control beside the bell, which steps it per
   device (wide-layout.js). app.js reads the zoom back (frameZoom) for the places
   it mixes screen px with the frame's. */
#frame.wide {
  max-width: none;
  --dock-l: 0px;
  --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: 584px;
  /* The reading column: the transcript, the diff sheet and the file viewer stop at
     twice the composer and centre in what is left. Past it a reply's buttons, a
     strip's count and a file's +N -M sat a screen away from the text they belong to. */
  --read-w: calc(var(--compose-w) * 2);
  /* #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(var(--room, 100dvh) / var(--zoom));
}

/* After #frame.wide, which zeroes them: equal specificity, so source order is
   what makes these win. */
#frame.dock-tabs { --dock-l: var(--tabs-w, 264px); }
#frame.dock-files { --dock-r: var(--files-w, 280px); }

/* Every layer that floats over the transcript, moved into the room between the
   columns. Leaving one out puts it under a docked panel. A .popup is left out on
   purpose: it is modal, so its scrim covers the docked columns too (35 is over
   their 25) and a click beside the card reaches nothing behind it. */
#frame.wide .thread,
#frame.wide .panel,
#frame.wide .endpage,
#frame.wide .underfoot,
#frame.wide .sheet {
  left: var(--dock-l);
  right: var(--dock-r);
}
/* All threads is modal like a .popup, which the new-thread card's All threads
   opens: it covers the docked columns too (30 is over their 25), so nothing
   beside it takes a click. Its padding keeps the list in the middle. */
#frame.wide .sheet.sessions { left: 0; right: 0; }

/* 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(var(--dock-l) + (100% - var(--dock-l) - 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. The right side clears the
   transcript's 10px scrollbar too, plus 4px, so a narrow column does not put the
   box against the bar. */
/* The diff sheet and the file viewer read in the transcript's column: header,
   review bar and body alike, so a file's +N -M and Viewed stay beside its path.
   The image viewer keeps the whole sheet. */
#frame.wide .sheet.review > *,
#frame.wide .sheet.viewer:not(.shotview) > *,
#frame.wide .viewer.docked > * {
  padding-left: max(12px, (100% - var(--read-w)) / 2);
  padding-right: max(12px, (100% - var(--read-w)) / 2);
}
/* The diff body has no padding of its own at any width; only the column's. */
#frame.wide .sheet.review > .rbody {
  padding-left: max(0px, (100% - var(--read-w)) / 2);
  padding-right: max(0px, (100% - var(--read-w)) / 2);
}
/* A line comment is a message box, so it takes the composer's width, under the
   code it is about, and the thread above it lines up with it. */
#frame.wide .lcbox,
#frame.wide .lcnote { max-width: var(--compose-w); }

/* A button as wide as its sheet is a bar, not a button: on a wide frame these keep
   the width of their label. */
#frame.wide .sheet.settings .rows.stack { align-items: flex-start; }
/* Except Edit system prompt, the group's only control: full width and tall, as
   on a phone, so the wide sheet does not shrink it to a small label. */
#frame.wide .sheet.settings #settings-system-prompt .rows.stack { align-items: stretch; }
.sheet.settings #system-prompt-open { height: 44px; }
#frame.wide .newthread .new-thread { align-self: flex-end; padding: 0 22px; }

#frame.wide .underfoot > * {
  margin-left: max(10px, (100% - var(--compose-w)) / 2);
  margin-right: max(14px, (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;
}

/* On a wide frame the banner sits in the bottom bar, at its left end, over the bar's
   empty part. As a row of its own it pushed the composer and the panes up the
   moment the connection dropped, under the words being typed. Out of the flow, so
   showing it moves nothing. The version and the dot keep the right end. */
#frame.wide .underfoot > .offline-banner {
  position: absolute;
  left: 0;
  bottom: 0;
  max-width: calc(100% - 160px);
  height: 22px;
  margin: 0;
  padding: 0 12px;
  gap: 6px;
  background: none;
  border-radius: 0;
  white-space: nowrap;
}

#frame.wide .underfoot > .offline-banner .msg {
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  font: 500 10.5px var(--font-sans);
}

#frame.wide .underfoot > .offline-banner .q { font: 500 10.5px var(--font-sans); }

#frame.wide .underfoot > .offline-banner .dot svg { width: 10px; height: 10px; }

/* The three stacked rows the phone has become one row as tall as its tallest child:
   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: auto minmax(0, 1fr) auto auto;
  grid-template-rows: auto;
  align-items: center;
  /* The one row runs the frame's full width, over the explorer's column too, so the
     bell, the folder and the cog end at the window's right edge. Its top edge is the
     explorer's line; its bottom one is the pane area's top border. */
  border-top: 1px solid var(--line);
}

/* 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
   cards sit at the left, where the phone has them, all on the one row: a third
   agent (Muse) wrapped onto a second row and made the header taller. */
/* The Commander's icon, moved here from the rail (applyWideLayout): the row's
   first cell, at the frame's left edge. */
#frame.wide .overhead > .conductor-open {
  grid-column: 1;
  grid-row: 1;
  align-self: end;
  margin-left: 6px;
}

#frame.wide .agents {
  grid-column: 2;
  grid-row: 1;
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 260px);
  justify-content: start;
  /* At the foot of the row with the icons: centred, the meters sat 7px above them. */
  align-self: end;
  padding-top: 0;
  padding-bottom: 0;
}

/* The card's top padding was the row's tallest part; without it the icons' 34px
   slot sets the height. */
#frame.wide .agent { padding-top: 0; }

/* 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: 4;
  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 are one bar across the foot of the window, the tab strip's grey, right
   aligned. The band's last row, so the pane area stops above it. Full width: the
   band's other rows keep the composer's column. Room for more system facts later. */
#frame.wide .underfoot > .app-version {
  height: 22px;
  margin: 0;
  padding: 0 12px calc(env(safe-area-inset-bottom));
  gap: 8px;
  background: var(--bg-chrome);
  color: var(--dim);
  font: 500 10.5px var(--font-sans);
}

#frame.wide .underfoot > .app-version .link-dot:not(.off) { width: 7px; height: 7px; }

/* 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 update button, just before the bell and the cog. It is
   the mode strip's 34px slot at the foot of the row, so the five icons share one
   baseline; a 48px bar centred its two 7px higher.

   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: 3;
  grid-row: 1;
  align-self: end;
  flex-basis: 34px;
  height: 34px;
  gap: 2px;
  padding: 0;
  text-shadow: none;
}
#frame.wide .topbar svg { filter: none; }

/* The update button lives in the band's middle cell; the bell, the folder, install 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 .report-open,
#frame.wide .chip.notify {
  width: 30px;
  height: 30px;
  border-radius: 5px;
}

#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; }
#frame.wide .report-open svg { width: 20px; height: 20px; }

/* The frame's zoom, beside the bell: two 30px steps either side of the percent, in
   the row's own grey. Desktop only; app.js shows it where the zoom holds. */
.zoom { display: none; }
#frame.wide .zoom.show { display: inline-flex; align-items: center; gap: 0; }

#frame.wide .zoom button {
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--dim);
}

#frame.wide .zoom .zoom-step {
  width: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

#frame.wide .zoom .zoom-step svg { width: 16px; height: 16px; display: block; }
#frame.wide .zoom .zoom-level { min-width: 40px; font: 500 11px var(--font-sans); font-variant-numeric: tabular-nums; }
#frame.wide .zoom button:active { background: rgba(230, 237, 243, .07); color: var(--text); }
#frame.wide .zoom button:disabled { opacity: .4; }
/* The same slot, but the install offer keeps its 24px mark here too. */
#frame.wide .install svg { width: 24px; height: 24px; }

/* ---------- 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, and a hover.
   The selected fill has a thin colour edge at the top and coloured sides. The
   rules below it are the shape; the pane's lane wears them. */
#frame.wide .tabstrip {
  height: 38px;
  gap: 3px;
}

/* Square tabs at the lane's height meet directly, with one divider between them.
   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. The last tab keeps its divider, which
   sets it apart from the + after the lane. */
#frame.wide .tab:has(+ .tab.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 is --sel-bg inside --sel-line on the two sides. In a
   pane header, the sides take the tab's colour instead (below). */
#frame.wide .tab.on {
  background: var(--sel-bg);
  border-left-color: var(--sel-line);
  border-right-color: var(--sel-line);
}

/* The selected tab keeps its coloured top edge and sides at rest; the moving
   selection draws the same highlight while the strip glides. */
#frame:not(.wide) .tabstrip:not(.gliding) > .tab.on,
#frame.wide .pane-head .pane-tabs:not(.gliding) > .tab.on {
  z-index: 2;
  border-left-color: var(--tab-colour, var(--violet));
  border-right-color: var(--tab-colour, var(--violet));
}

#frame:not(.wide) .tabstrip > .tab.dragging,
#frame.wide .pane-head .pane-tabs > .tab.dragging { z-index: 3; }

#frame.wide .pane-head .pane-tabs > .tab.on:first-child { border-left-color: var(--tab-colour, var(--violet)); }

/* The close cross has a stable 18px slot, so its visibility never moves the
   neighbouring tabs. Phones and desktops show it on every tab. On a phone, the
   selected and armed tabs expand its hit area past the glyph to the right edge
   and 9px above and below; on the others, only the glyph closes a tab. */
.tab .tabclose {
  display: inline-flex;
  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.on .tabclose::before,
#frame:not(.wide) .tab.armed .tabclose::before { content: ''; position: absolute; inset: -9px -8px -9px -4px; }

.tab.armed .tabclose { color: var(--red); background: rgba(248, 81, 73, .14); }
.tab .tabclose svg { width: 11px; height: 11px; display: block; }
#frame:not(.wide) .tab .tabclose svg { width: 18px; height: 18px; flex-shrink: 0; }

/* The cross stays visible on desktop too. The pointer brightens it on hover; an
   armed cross stays red until the second press or the three seconds are out. */
#frame.wide .tab .tabclose { visibility: visible; }

/* Lit the way the tab bar lights its close: no box, the cross turns --text and
   bolder. */
#frame.wide .tab .tabclose { transition: color .14s ease; }
#frame.wide .tab .tabclose:hover { color: var(--text); }
#frame.wide .tab .tabclose:hover svg { filter: drop-shadow(0 0 .25px currentColor); }
#frame.wide .tab.armed .tabclose:hover { color: var(--red); background: rgba(248, 81, 73, .14); }

/* A file tab with unsaved edits: the header's amber dot in the cross's place, up
   on every tab. The pointer over the tab brings the cross back; an armed tab
   keeps its red cross. */
.tab.dirty:not(.armed) .tabclose::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--amber); }
.tab.dirty:not(.armed) .tabclose svg { display: none; }
#frame.wide .tab.dirty .tabclose { visibility: visible; }
@media (hover: hover) {
  .tab.dirty:not(.armed):hover .tabclose::after { content: none; }
  .tab.dirty:not(.armed):hover .tabclose svg { display: block; }
}

/* ---------- 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;
  /* Below the one row on top, on the same line the pane area starts from. */
  top: var(--overhead-h, 48px);
  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 it is a column with the window's edge on three sides, so it is closed
     on all four like the pane area beside it. */
  border: 1px solid var(--line);
}

/* Docked, the header is the row beside the pane headers, the same height, so the
   explorer's title sits on the tabs' line. */
#frame.dock-files > #explorer .xhead { flex: 0 0 auto; height: var(--head-h, 38px); }

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

/* The same for the tab drawer on the left, under the one row on top like the
   explorer. Its panel keeps the phone's list and rows; only where it sits
   changes, and the 78% a phone gives it becomes the whole column. */
#frame.dock-tabs > #drawer {
  display: block;
  right: auto;
  top: var(--overhead-h, 48px);
  width: var(--dock-l);
}
#frame.dock-tabs > #drawer .drawer-scrim { display: none; }

#frame.dock-tabs > #drawer .drawer-panel {
  transform: none;
  transition: none;
  width: 100%;
  max-width: none;
  padding-top: 8px;
  border: 1px solid var(--line);
}

/* Below the explorer's width it still arrives as an overlay from the folder
   button or the right edge, and below the tab bar's the tab drawer from the left
   edge and the Tabs button. */

/* 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"],
#changes-open[aria-pressed="true"],
#tabs-open[aria-pressed="true"] { color: var(--lit); }

/* 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: 6px; }
#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 .xhead .xtab[aria-selected="true"] { background: rgba(230, 237, 243, .08); }
#frame.wide .xchange { padding-left: 6px; }
#frame.wide .xnote { padding: 6px 6px 0; font-size: 11px; }
#frame.wide .xfilter { margin: 0 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: var(--dock-l);
  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);
  /* The window's own left edge, so the pane area is closed on every side: the
     explorer's column closes the right. */
  border-left: 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, in the explorer's
   --line like every other edge of the frame. */
.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);
  border-bottom: 1px solid var(--line);
  /* 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(14px, (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(--font-sans);
  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; }
/* A pane's tabs narrow to fit before its strip scrolls, so a narrow pane shows
   "billing serv…" rather than the middle of a name cut at both ends. The name
   keeps a few letters; only past that does the strip scroll. The floor is what
   the tab's marks, its cross and those letters take; a pin adds its mark and a
   gap, or the cross is pushed onto the tab's edge. */
#frame.wide .pane-tabs .tab { flex-shrink: 1; min-width: 132px; }
#frame.wide .pane-tabs .tab:has(.pinmark) { min-width: 142px; }
#frame.wide .pane-tabs .tab:has(.count, .agents-n) { min-width: 170px; }
#frame.wide .pane-tabs .tab:has(.pinmark):has(.count, .agents-n) { min-width: 188px; }
#frame.wide .pane-tabs .tab .tabcopy { min-width: 40px; }
/* The tab whose name is being edited keeps its width: the field would run over
   the next tab. */
#frame.wide .pane-tabs .tab:has(.wsname) { flex-shrink: 0; }
/* A pane head too narrow for that keeps the mark, the status and the name: the
   counts and the cross go (Ctrl+W and the tab's menu still close it). */
@container (max-width: 300px) {
  #frame.wide .pane-tabs .tab { gap: 5px; padding: 0 6px; }
  #frame.wide .pane-tabs .tab,
  #frame.wide .pane-tabs .tab:has(.count, .agents-n) { min-width: 84px; }
  #frame.wide .pane-tabs .tab:has(.pinmark) { min-width: 102px; }
  #frame.wide .pane-tabs .tab .tabcopy { min-width: 32px; }
  #frame.wide .pane-tabs .tab .count,
  #frame.wide .pane-tabs .tab .agents-n,
  #frame.wide .pane-tabs .tab .tabclose { display: none; }
}
.pane-tabs .tab.dragging { transition: none; }
/* Under reduced motion they step aside at once. The rest of the strip's motion is
   skipped in public/strip-motion.js. */
@media (prefers-reduced-motion: reduce) { .pane-tabs .tab { 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; padding-right: 2px; }
/* The 2px after the last tab: a strip that scrolls stops a fraction of a pixel
   short of its content (CSS zoom rounds the two apart), so with nothing after
   the last tab its right edge, the selected tab's coloured one included, was
   always past the clip. The pad is scrolled into, and the edge lands inside it. */

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

/* Every pane shows its buttons, focused or not: hiding them on the others made
   them appear and vanish under the mouse. A pane too narrow for the buttons and a tab keeps Agent, Shell and Close. The
   splits and the zoom are still Ctrl+D, Ctrl+Shift+D and Ctrl+B z, and a tab
   dragged to the pane's edge splits it. */
#frame.wide .pane-head { container-type: inline-size; }
@container (max-width: 360px) {
  #frame.wide .pane-btn[data-act="split-right"],
  #frame.wide .pane-btn[data-act="split-down"],
  #frame.wide .pane-btn[data-act="zoom"] { display: none; }
}

/* 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: 6px;
  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; }
.pane-btn[hidden] { display: none; }

/* A preview tab: a globe where a file tab has its file's glyph. */
.tab.previewtab .xglyph { display: inline-flex; align-items: center; justify-content: center; color: var(--dim); }
/* A preview in a pane: the docked viewer's head over a frame that takes the rest.
   The page in it paints its own background; white until it does, as a browser. */
.viewer.docked.preview .rbody { display: flex; padding: 0; overflow: hidden; }
/* A page takes the pane's whole width, not the reading column a file gets. */
#frame.wide .viewer.docked.preview > .rbody { padding: 0; }
.previewframe { flex: 1 1 auto; width: 100%; height: 100%; border: 0; background: #fff; }
/* The pane head's port list: the explorer's row menu, hung under the button. */

/* 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 max(10px, (100% - var(--read-w)) / 2) calc(var(--pane-foot-h, 0px) + 8px);
}

/* The pane's plan and subagent rows read in the transcript's column. The rule under
   each still crosses the pane: it marks the pane's header, not the column. */
#frame.wide .pane .plan-summary,
#frame.wide .pane .subs-summary,
#frame.wide .pane .plan-details,
#frame.wide .pane .subs-details {
  padding-left: max(10px, (100% - var(--read-w)) / 2);
  padding-right: max(10px, (100% - var(--read-w)) / 2);
}

/* 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: var(--box-facts) minmax(0, 1fr) auto;
}
.pane-foot .composer textarea { min-height: 0; }

/* A pane too narrow for the row of keys keeps attach, the agent's mark, the level,
   the mic and send, with no gaps between them: the model and effort keys go, as
   they had shrunk to a letter, and send stays inside the box. Both the box and
   the stub, which share the row's tracks. */
#frame.wide.split .pane-foot { container-type: inline-size; }
/* Past that, a pane narrower than the keys cuts them at the box's edge: the words,
   the mic and send never draw over the next pane or off the screen. */
#frame.wide.split .pane-foot .composer,
#frame.wide.split .pane-foot .pane-entry { overflow-x: clip; overflow-clip-margin: 2px; }
@container (max-width: 280px) {
  .pane-foot .composer, .pane-foot .pane-entry { column-gap: 0; }
  #frame.wide .pane-foot .agentpick .pick-name,
  #frame.wide .pane-foot .modelpick,
  #frame.wide .pane-foot .effortpick { display: none; }
}

/* The Agent Commander's popup (renderCommanderPop in app.js): a card that drops
   from the rail's icon over the page, which keeps drawing the tab under it. Its
   top sits under the rail (--cpop-top) and its tail points at the icon
   (--cpop-tail), both measured by app.js. Over the bands (20), under the drawers
   (25) and the sheets (30). */
.commander-pop {
  position: absolute;
  top: var(--cpop-top, 96px);
  right: 8px;
  bottom: calc(8px + env(safe-area-inset-bottom));
  width: min(560px, calc(100% - 16px));
  z-index: 24;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--conductor);
  border-radius: 14px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, .6), 0 0 24px rgba(179, 107, 255, .14);
  animation: cpop-in .24s cubic-bezier(.2, .8, .2, 1);
  transform-origin: var(--cpop-tail, calc(100% - 24px)) 0;
}
.commander-pop[hidden] { display: none; }
@keyframes cpop-in { from { opacity: 0; transform: translateY(-6px) scale(.94); } }
/* On a wide frame it drops from the icon at the row's left end. */
#frame.wide .commander-pop { left: 8px; right: auto; bottom: auto; height: min(820px, calc(100% - var(--cpop-top, 96px) - 16px)); }
/* A docked tab column is a drawer (25) that stays open beside the panes, and the
   popup drops over it. An open drawer that is not docked still covers the popup. */
#frame.wide.dock-tabs .commander-pop { z-index: 26; }
/* The tail: a corner of the card poking up at the icon. On a phone the strip
   stands between the card and the icon, so it has none. */
#frame:not(.wide) .commander-pop::before { display: none; }
.commander-pop::before {
  content: "";
  position: absolute;
  top: -7px;
  left: calc(var(--cpop-tail, calc(100% - 24px)) - 6px);
  width: 12px;
  height: 12px;
  background: var(--bg);
  border: solid var(--conductor);
  border-width: 1px 0 0 1px;
  border-top-left-radius: 3px;
  transform: rotate(45deg);
}
.cpop-head { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px 10px 8px 12px; border-bottom: 1px solid var(--line); }
.cpop-title { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 7px; color: var(--text); font: 600 13px var(--font-sans); }
.cpop-title img { width: 18px; height: 18px; display: block; }
/* The pane inside it: in the card's flow, not placed by the pane area. */
.commander-pop > .pane {
  position: relative;
  flex: 1 1 auto;
  border: 0;
  border-radius: 0 0 14px 14px;
  background: transparent;
}
/* The phone's own rules for it; a wide frame's pane rules already fit. The
   transcript scrolls under the foot, which floats over it as in a pane. */
#frame .commander-pop .pane .thread {
  position: relative;
  inset: auto;
  flex: 1 1 0;
  min-height: 0;
  will-change: auto;
  padding: 12px 10px calc(var(--pane-foot-h, 0px) + 8px);
}
#frame:not(.wide) .commander-pop .pane-foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  max-height: 100%;
  pointer-events: none;
}
#frame:not(.wide) .commander-pop .pane-foot > * { pointer-events: auto; }
#frame:not(.wide) .commander-pop .pane-foot .composer { grid-template-rows: none; margin: 0 8px 8px; }
#frame:not(.wide) .commander-pop .pane-foot > .statusbar { padding: 0 14px; }
.commander-pop .app-version { display: none; }
/* The rail's agent/shell switch says nothing while the popup is up: the
   Commander has no shell there (syncSendMode). */
#frame.commander-up #send-modes,
#frame.commander-up #send-modes + .divider { visibility: hidden; }
@media (prefers-reduced-motion: reduce) {
  .commander-pop { animation: none; }
}

/* 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: var(--box-facts) 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; }
/* A pane with a file tab in front: the file where the transcript was, under the
   pane's own header, and nothing else: no strips, no jump pill, no footer. The
   composer waits hidden in the foot; a file is not a thread to type to. */
.pane-file { display: none; }
.pane.shows-file .pane-file { display: flex; flex: 1 1 0; min-height: 0; flex-direction: column; }
#frame.wide.split .pane.shows-file .thread,
.pane.shows-file .planstrip,
.pane.shows-file .subs,
.pane.shows-file .tobottom,
.pane.shows-file .pane-newpage,
#frame.wide .pane.shows-file > .panel,
.pane.shows-file .pane-foot { display: none; }
/* A file another pane's focus is not on yet: its path, until a click makes it live. */
/* The viewer docked in a pane's file host: in the pane's flow rather than over the
   page. Docked but not in a pane yet (the frame between a click and the render
   that moves it), it draws nothing. */
.viewer.docked { position: relative; display: flex; flex: 1 1 0; min-height: 0; flex-direction: column; }
#frame > .viewer.docked { display: none; }
/* A file tab: no dot and no agent mark, its file's glyph in their place. */
.tab.filetab .xglyph { flex: 0 0 auto; }
.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 fill, 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, .96); }

/* 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:not([hidden]),
.pane-entry .permpick:not([hidden]) {
  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 13px/1.45 var(--font-sans);
  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); }

/* A pane in Shell mode: its tab's terminal fills the pane under the header, and
   the plan, the subagents, the conversation and the footer with the composer step
   aside. A phone never shows it: Shell mode there is #panel, the whole screen. */
.pane-term { display: none; }
#frame.wide .pane.term-on > :is(.planstrip, .subs, .thread, .pane-foot, .tobottom) { display: none; }
#frame.wide .pane.term-on > .pane-term {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-height: 0;
  background: var(--card);
}
.pane-term .pbody { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }

/* 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: compare them after the zoom. */
#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 matters is the floor, not these two
     numbers: the room left between the chip's corner and the inside of the
     box's border. */
  --box-pad: 3px 11px 9px;
  --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 ten tracks at the smaller keys. */
  --box-cols: 30px auto 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: seven and a half of those lines,
     fixed — six, and a quarter more once the box was a quarter wider. 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(7.5 * 1.45 * 13.75px + 8px);
  /* The facts row goes on top here, above the words. */
  --row-facts: 1;
  --row-text: 2;
  --row-keys: 3;
}

/* 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 ten 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 .composer .dghost,
#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: 6px;
  font-size: 16px;
}

#frame.wide .mic svg,
#frame.wide .steer svg,
#frame.wide .send svg { width: 15px; height: 15px; }
#frame.wide .send.stop { width: 28px; height: 28px; border-radius: 50%; }
#frame.wide .send.stop 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: 6px; }
#frame.wide .miccancel svg { width: 12.5px; height: 12.5px; }
#frame.wide .micpct { width: 25px; font: 600 10px/25px var(--font-sans); }

#frame.wide .agentpick,
#frame.wide .modelpick,
#frame.wide .effortpick,
#frame.wide .permpick {
  height: 25px;
  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, the line under it
   follows, and the rail and search bar tighten to match. */
#frame.wide .slashmenu { max-height: 46dvh; padding: 4px; gap: 2px; }
#frame.wide .slashmenu.show { box-shadow: none; }
#frame.wide .slashmenu.railed.show { padding: 0; gap: 0; grid-template-columns: 40px minmax(0, 1fr); }
#frame.wide .slashmenu .sbar { height: 28px; margin: 6px 6px 4px; font-size: 11px; border-radius: 7px; }
#frame.wide .slashmenu .sbar > svg { width: 13px; height: 13px; flex-basis: 13px; }
#frame.wide .slashmenu .sslot { flex-basis: 30px; width: 30px; height: 30px; border-radius: 7px; }
#frame.wide .slashmenu .sslot > svg, #frame.wide .slashmenu .sslot .sicon, #frame.wide .slashmenu .sslot .sicon svg { width: 15px; height: 15px; }
#frame.wide .slashmenu .sslot.on::before { left: -5px; top: 7px; bottom: 7px; }
#frame.wide .slashmenu .sgrp, #frame.wide .slashmenu .slabel { font-size: 8px; padding: 6px 4px 2px; }
#frame.wide .slashmenu .srow { padding: 4px 6px; border-radius: 6px; gap: 8px; }
#frame.wide .slashmenu .slead { flex-basis: 16px; }
#frame.wide .slashmenu .slead > svg, #frame.wide .slashmenu .slead .sicon, #frame.wide .slashmenu .slead .sicon svg { width: 12px; height: 12px; }
#frame.wide .slashmenu .sname { font-size: 10.5px; }
#frame.wide .slashmenu .sname.sans { font-size: 11px; }
#frame.wide .slashmenu .sdesc { font-size: 9px; }
#frame.wide .slashmenu .sfast { margin: 6px 6px 0; padding: 5px 8px; border-radius: 7px; }
#frame.wide .slashmenu .sftitle { font-size: 11px; }
#frame.wide .slashmenu .sfnote, #frame.wide .slashmenu .sfwhy { font-size: 9.5px; }
#frame.wide .slashmenu .sstar, #frame.wide .slashmenu .sdefault { width: 22px; height: 22px; }
#frame.wide .slashmenu .sstar svg, #frame.wide .slashmenu .sdefault svg { 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; }
#frame.wide .send[data-countdown] { width: 28px; height: 28px; border-radius: 50%; }

/* 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 6px; }
#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; }

/* ---------- steps on a line ----------
   Everything the agent does in a turn hangs off one thin line in a left gutter,
   each row's icon a stop on it: the icon carries the kind, grey (red on a
   failed step) so colour stays with the diff and the shell; the word beside it is
   sentence case with no chip. A user row breaks the line, so each turn is its
   own run. --step-y is the icon's centre from the row's top, the head's middle.
   The icons centre with `translate`, not `transform`, so an animation that writes
   `transform` cannot move them. */
.thread { --step-gutter: 24px; --step-x: 8px; --step-y: 9px; --next-y: 9px; --step-gap: 18px; --step-ink-gap: 4px; }
.item.step, .streaming.step { position: relative; padding-left: var(--step-gutter); }
/* An open group's steps sit one step in: their icons on a line of their own,
   the turn's line running on past them at --step-x. */
.item.step[data-member] {
  --member-x: calc(var(--step-x) + 22px);
  margin-left: 0;
  border-left: 0;
  padding-left: calc(var(--step-gutter) + 22px);
}
.item.step[data-member] > .head > .glyph { left: calc(var(--member-x) + 1px); }
.item.step[data-member]:has(+ .step:not(.streaming, [data-final]))::after { top: 0; }
.item.step[data-member]:has(+ .step[data-member])::before {
  content: '';
  position: absolute;
  left: var(--member-x);
  width: 1px;
  top: var(--line-top);
  bottom: var(--line-bottom);
  background: #3a4350;
}
.step > .head > .glyph,
.step > .head > .tag.agentmark,
.streaming.step > .head > .spinner {
  position: absolute;
  left: calc(var(--step-x) + 1px);
  top: var(--step-y);
  translate: -50% -50%;
}
/* The spinner's middle on the middle of the label's lowercase letters, 2px below
   the icons' line. */
.streaming.step > .head > .spinner { top: calc(var(--step-y) + 2px); }
/* A head holding the copy button is the button's 24px tall, not 18px, so its
   icon and line centre 3px lower, and the line above ends 3px lower to meet it. */
.step:has(> .head > .copy) { --step-y: 12px; }
.step:has(+ .step > .head > .copy) { --next-y: 12px; }
/* A segment stops --step-ink-gap short of the drawing on either side, not of the
   14px box: the Claude mark fills its box, the plan and stop icons leave 2.3px.
   --ink-top and --ink-bottom are the row's own icon's empty edge, --next-ink the
   next row's top one. Measured off the symbols at 14px. */
.step {
  --ink-top: .55px;
  --ink-bottom: .55px;
  --next-ink: .55px;
  --line-top: calc(var(--step-y) + 7px + var(--step-ink-gap) - var(--ink-bottom));
  --line-bottom: calc(7px + var(--step-ink-gap) - var(--next-ink) - var(--next-y));
}
.step:has(> .head > .tag.agentmark) { --ink-top: 0px; --ink-bottom: 0px; }
.step:has(> .head > :is(.glyph, .tag.agentmark) use:is([href="#i-step-bash"], [href="#i-step-search"], [href="#i-step-wakeup"], [href="#brand-copilot"])) { --ink-top: 1.15px; --ink-bottom: 1.15px; }
.step:has(> .head > :is(.glyph, .tag.agentmark) use:is([href="#i-step-plan"], [href="#i-step-stop"], [href="#brand-muse"])) { --ink-top: 2.3px; --ink-bottom: 2.3px; }
.step:has(> .head > .glyph use:is([href="#i-step-edit"], [href="#i-step-tool"])) { --ink-bottom: 1.15px; }
.step:has(> .head > .glyph use:is([href="#i-step-message"], [href="#i-step-think"])) { --ink-top: 1.15px; }
.step:has(+ .step > .head > .tag.agentmark) { --next-ink: 0px; }
.step:has(+ .step > .head > :is(.glyph, .tag.agentmark) use:is([href="#i-step-bash"], [href="#i-step-search"], [href="#i-step-wakeup"], [href="#i-step-message"], [href="#i-step-think"], [href="#brand-copilot"])) { --next-ink: 1.15px; }
.step:has(+ .step > .head > :is(.glyph, .tag.agentmark) use:is([href="#i-step-plan"], [href="#i-step-stop"], [href="#brand-muse"])) { --next-ink: 2.3px; }
.step > .head > .glyph { display: grid; place-items: center; width: 14px; height: 14px; color: var(--dim); }
.step > .head > .glyph svg { width: 14px; height: 14px; display: block; }
.step > .head > .glyph.error { color: var(--red); }
.item.diff.step > .head > .tag.diff { color: var(--text); }
/* A step with a step after it takes the gap below it into its own box, and the
   line stops inside that box: a row off screen is paint-contained
   (content-visibility), which would clip a line drawn out into the gap.
   The line skips the live rows (Writing, Thinking, a running tool's status):
   they go when the turn ends, and a line drawn to one would pull back. It skips
   a turn's last reply too (data-final), the live turn's newest included, so the
   answer stands off the line and the line reaches a reply only once a step
   follows it. */
.step:has(+ .step) {
  margin-bottom: calc(-1 * var(--step-gap));
  padding-bottom: var(--step-gap);
}
.step:has(+ .step:not(.streaming, [data-final]))::after {
  content: '';
  position: absolute;
  left: var(--step-x);
  width: 1px;
  top: var(--line-top);
  bottom: var(--line-bottom);
  background: #3a4350;
}

/* Hover, for a mouse. Two treatments by what the control looks like at rest.
   One drawn as a button, with a box of its own (send, Retry, a message's copy
   and edit, New project), lifts on a lit wash with a ring and a shadow, its icon
   a size up, and a press sets it back down. Everything else that takes a click
   is bare text or a bare glyph: the top strip, the Commander, the sidebar's +
   and close, the composer's pickers and facts, the mic, and in the transcript
   the steps that open, the changes line, links and the plan and subagent strips.
   The drawer's New tab slot keeps the selected-tab tint.
   It turns --text and bolder, with the same text stroke the open tab uses and a
   hairline shadow on a glyph, so nothing beside it moves. Coloured text and
   glyphs keep their colour and brighten. #frame lifts these over the wide
   frame's own rules; the states that mean something (recording, a pressed
   toggle) are left out. */
@media (hover: hover) {
  #frame :is(.icon-btn:not(.xhead .icon-btn), .rowbtn, .copy, .act, .send, .drawer-new, .wsnew),
  #frame :is(.conductor-open, .notify, .zoom-step, .zoom-level, .files, .cog, .report-open, .install, .pane-btn, .wsplus, .wsclose, .wsname, .xchev, .xhead .icon-btn, .agentpick, .modelpick, .effortpick, .permpick, .attach, .mic, .seg, .subs-summary, .plan-summary, .item button.changes, .item a.link),
  #frame :is(.item.diff, .item.oneline, .item.stepgroup) > .head,
  #frame .wsmain {
    transition: color .14s ease, background-color .14s ease, box-shadow .14s ease, filter .14s ease, transform .14s cubic-bezier(.2, .8, .3, 1.4);
  }

  #frame :is(.icon-btn:not(.xhead .icon-btn), .rowbtn, .copy, .act, .send, .drawer-new, .wsnew) svg {
    transition: transform .14s cubic-bezier(.2, .8, .3, 1.4);
  }
  #frame :is(.icon-btn:not(.xhead .icon-btn), .rowbtn, .copy, .act, .send, .drawer-new):hover:not(:disabled) {
    color: var(--text);
    background-color: rgba(230, 237, 243, .17);
    border-radius: var(--hover-radius, 6px);
    box-shadow: inset 0 0 0 1px rgba(230, 237, 243, .22), inset 0 1px 0 rgba(255, 255, 255, .1), 0 4px 12px rgba(0, 0, 0, .65), 0 1px 2px rgba(0, 0, 0, .5);
    transform: translateY(-2px);
  }
  #frame :is(.act, .send):hover:not(:disabled) { filter: brightness(1.3); }
  #frame :is(.icon-btn:not(.xhead .icon-btn), .rowbtn, .copy, .act, .send, .drawer-new):hover:not(:disabled) svg { transform: scale(1.14); }
  #frame :is(.icon-btn:not(.xhead .icon-btn), .rowbtn, .copy, .act, .send, .drawer-new):active:not(:disabled) {
    transform: translateY(0) scale(.95);
    box-shadow: inset 0 0 0 1px rgba(230, 237, 243, .22);
  }

  #frame :is(.conductor-open, .notify, .zoom-step, .zoom-level, .files, .cog, .report-open, .install, .pane-btn, .wsplus, .wsclose, .wsname, .xchev, .xhead .icon-btn, .agentpick, .modelpick, .effortpick, .permpick, .attach, .mic, .seg, .subs-summary, .plan-summary, .item button.changes, .item a.link):not(.rec, .loading, .busy):hover:not(:disabled),
  #frame :is(.item.diff, .item.oneline, .item.stepgroup):hover > .head {
    -webkit-text-stroke: .45px currentColor;
  }
  #frame :is(.conductor-open, .notify, .zoom-step, .zoom-level, .files, .cog, .report-open, .install, .pane-btn, .wsplus, .wsclose, .wsname, .xchev, .xhead .icon-btn, .agentpick, .modelpick, .effortpick, .permpick, .attach, .mic, .seg, .subs-summary, .plan-summary, .item button.changes, .item a.link):not(.rec, .loading, .busy):hover:not(:disabled) svg,
  #frame :is(.item.diff, .item.oneline, .item.stepgroup):hover > .head svg {
    filter: drop-shadow(0 0 .25px currentColor);
  }
  #frame :is(.notify, .zoom-step, .zoom-level, .files, .cog, .report-open, .pane-btn, .wsplus, .wsclose, .wsname, .xchev, .xhead .icon-btn, .agentpick, .modelpick, .effortpick, .permpick, .attach, .mic, .seg, .item button.changes):not(.amber, .green, .changes, .rec, .loading, .busy, [aria-pressed="true"]):hover:not(:disabled),
  #frame :is(.item.diff, .item.oneline, .item.stepgroup):hover > .head {
    color: var(--text);
  }
  #frame :is(.conductor-open, .install, .item a.link, .seg.amber, .seg.green, .seg.changes):hover:not(:disabled) { filter: brightness(1.3); }
  /* A lit toggle rests at --lit, a shade under --text, so the hover still has
     somewhere to go. */
  #frame :is(.notify, .files)[aria-pressed="true"]:hover:not(:disabled) { color: var(--text); }

  #frame .wsmain:hover { color: var(--text); background-color: rgba(230, 237, 243, .1); box-shadow: inset 0 0 0 1px rgba(230, 237, 243, .18); }
  /* The tab bar lights the whole row, close button included, the way the strip
     lights a whole tab; the selected row keeps its fill, as the selected tab does. */
  #frame .drawer .ws .wsmain:hover { background-color: transparent; box-shadow: none; }
  #frame .drawer .ws:hover .wsmain { color: var(--text); }
  #frame .drawer .ws:not(.on, .dragging):hover { background-image: linear-gradient(rgba(230, 237, 243, .05), rgba(230, 237, 243, .05)); }
}

/* A lasting preview link in the transcript, outside the grouped system steps. */
.item.preview-card {
  padding: 14px 12px;
  border-left: 3px solid var(--blue);
  background: rgba(121, 192, 255, .06);
  border-radius: 0 9px 9px 0;
  gap: 9px;
}
.preview-card .head { gap: 8px; }
.preview-card .tag { font-size: 13px; font-weight: 600; text-transform: none; }
.preview-icon { width: 17px; height: 17px; color: var(--blue); flex: 0 0 auto; }
.preview-icon svg { width: 100%; height: 100%; }
.preview-ready { margin-left: auto; color: var(--green); font-size: 11px; }
.preview-card .preview-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 44px;
  color: var(--blue);
  font-size: 15px;
  line-height: 1.5;
  text-decoration: underline;
  text-underline-offset: 4px;
}
.preview-link svg { width: 14px; height: 14px; }
.preview-link:hover { color: var(--text); }
.preview-link:active { opacity: .75; }
.preview-link:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 3px; }
.preview-address { color: var(--dim); font: 11px var(--font-mono); overflow-wrap: anywhere; }

/* The desktop app on Windows and Linux: no system title bar (windowFrame in
   desktop/platform.js; .wco from fitTitleBar in app.js). The band's top row is
   the window's handle, and the three window buttons sit over the row's right
   end in the row's colour: Windows draws them, on Linux the page does
   (.win-controls below). Only controls are cut out of the handle: the
   meters are text, so the row drags through them. On a wide frame the row stops short of them; the
   env() lengths are the window's, so they are divided by the frame's zoom. They
   are never less than Windows' three 46px buttons: a window that reports no
   area, or the wrong one, would otherwise put the cog under them. On a narrow
   one the band starts below them. */
.wco .overhead { -webkit-app-region: drag; }
.wco .overhead button,
.wco .overhead input,
.wco .overhead [role="group"],
.wco .overhead .planstrip { -webkit-app-region: no-drag; }
.wco #frame.wide .overhead {
  padding-right: calc(max(138px, 100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw)) / var(--zoom, 1));
}
.wco #frame:not(.wide) .overhead {
  padding-top: max(35px, env(titlebar-area-height, 0px));
  background: linear-gradient(var(--bg), var(--bg)) top / 100% max(35px, env(titlebar-area-height, 0px)) no-repeat;
}

/* The desktop app's window buttons on Linux (wireWindowControls in app.js): the
   size of Windows' own, as tall as the top row (--row-h from fitTitleBar), above
   the sign-in screen. The sign-in screen's top strip drags the window. */
.win-controls {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 70;
  display: flex;
  -webkit-app-region: no-drag;
}
.win-controls[hidden] { display: none; }
.win-controls button {
  width: 46px;
  height: var(--row-h, 35px);
  display: grid;
  place-items: center;
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--dim);
  cursor: default;
}
.win-controls svg { width: 10px; height: 10px; }
.win-controls button:hover { background: rgba(255, 255, 255, .08); color: var(--text); }
.win-controls .win-close:hover { background: #c42b1c; color: #fff; }
.wco .auth.show::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 35px;
  -webkit-app-region: drag;
}
