/* Claude Web Studio — dark, dense, terminal-adjacent. Zero external assets. */
:root{
  --bg:#0e1014; --bg-2:#12141a; --bg-3:#171a21; --bg-4:#1d212a;
  --line:#252a35; --line-2:#323847;
  --tx:#e6e9ef; --tx-2:#a6adbb; --tx-3:#6b7383;
  --acc:#d97757;          /* Claude clay */
  --acc-2:#e89b7d;
  --blue:#4d8bf0; --blue-2:#6fa4ff;   /* «send» — an action, not a warning */
  --ok:#4ec9a0; --warn:#e0b352; --err:#f0796a; --think:#8b7fd4;
  /* Derived: text on a fill, state backgrounds, code highlighting, shadows, veils. Not one
     interface colour stays hard-wired into a rule — otherwise the theme paints half the
     screen. */
  --on-acc:#1a1207; --on-blue:#07162c; --on-err:#2a0f0c; --on-ok:#06231a;
  --err-bg:#2a1614; --err-bd:#5a2b28; --warn-bg:#2a1a12;
  --think-2:#a99fe0; --strong:#ffffff; --cold:#5f93b8;
  --veil:#000000aa; --shadow:#00000099; --hair:#ffffff66;
  --sx-s:#9ece8a; --sx-c:#5f6673; --sx-k:#c88fd6; --sx-n:#e0b352; --sx-key:#7fb8e6;
  --add-bg:#12291f66; --add-tx:#8fd6b4; --del-bg:#2a161533; --del-tx:#e39b93;
  --mono:'JetBrains Mono','Cascadia Code',Consolas,'SF Mono',monospace;
  /* FIGURES READ WITH THE EYES ARE NOT CODE. A zero with no slash, distinguishable 6/8/9,
     tabular width. On Win11 that is Segoe UI Variable Text (drawn for a small type size),
     on the phone — the system grotesque. Monospace is the enemy here: in all three of ours
     the zero is struck through. */
  --figures:'Segoe UI Variable Text','Segoe UI',-apple-system,'SF Pro Text',Roboto,'Inter',system-ui,sans-serif;
  --sans:'Inter',-apple-system,'Segoe UI',Roboto,sans-serif;
  --r:10px;
  /* One multiplier for the whole interface: +/− in the settings move it, not the sizes
     separately. Everything measured off the viewport is divided back by it — otherwise the
     screen «grows» together with the content and an extra scroll appears. */
  --ui-scale:1;
}
*{box-sizing:border-box;margin:0;padding:0}
html{zoom:var(--ui-scale)}
html,body{height:100%}
/* THE SCROLL DOES NOT GO PAST THE EDGES OF THE FEED.
   You scrolled to the end and flicked again — the phone's browser picks the movement up as
   the PAGE: it pulls the document, twitches its own address bar, and the bottom panel goes
   with it. Hence «the bottom window judders while scrolling», even though nothing at all is
   happening inside the panel itself. overscroll-behavior cuts that chain (and removes
   pull-to-refresh along with it, which on the panel would mean losing your place in the
   conversation). */
html,body{overscroll-behavior:none}
body{background:var(--bg);color:var(--tx);font:14px/1.55 var(--sans);overflow:hidden;-webkit-font-smoothing:antialiased}
.hidden{display:none!important}
.spacer{flex:1}
.muted{color:var(--tx-3);font-weight:400}
kbd{font:11px var(--mono);background:var(--bg-4);border:1px solid var(--line-2);border-radius:4px;padding:1px 5px;color:var(--tx-2)}

/* The rail's width is a variable, not a number: it is dragged by the right edge with the
   mouse and remembered (app.js → studioRail). 264px is only the default value. */
:root{--rail-w:264px}
/* ═══ HOW WIDE THE CONVERSATION IS ═══
   The feed, the composer and every strip that lines up with them share ONE number, so they can
   never drift out of line — eleven separate `max-width:900px` were the same measure copied
   eleven times, and a change to one of them would have left the input field and the text it
   answers on different axes.
   900px was a reading measure (~90 characters a line). The owner asked for the opposite on
   09.09 — «maximize content width on desktop» — with a screenshot of ~460px of dead gutter on
   the left of a 1920px window and ~330px on the right. So it fills what there is: `100%`, held
   off the edges by the 22px padding `.wrap` already carries.
   ⚠ THE VALUE LIVES HERE AND NOWHERE ELSE. To put a ceiling back — on an ultrawide screen a
   line of prose really does become hard to follow — set this one line to `min(1500px, 100%)`
   and every strip follows. Do not write a number into the rules below. */
:root{--read-w:100%}
/* THE SCREEN HEIGHT IS THE SMALL ONE (svh), NOT THE DYNAMIC ONE (dvh).
   `dvh` changes while the phone's browser hides and shows its own address bar — that is,
   exactly while you are scrolling. The composer is glued to the bottom edge of the grid and
   travelled with it: «the bottom panel either swims away or gets dragged along». `svh` is
   the height with the browser's panels SHOWN: it never changes, the layout stands rock
   solid, and the extra strip under the application is painted with the same background
   (body). The first line is the spare for engines that do not know about svh yet. */
.app{display:grid;grid-template-columns:var(--rail-w) 1fr;height:calc(100vh / var(--ui-scale));
  height:calc(100svh / var(--ui-scale))}

/* ═══ sidebar ═══ */
.sidebar{background:var(--bg-2);border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;position:relative}
/* The width handle: a narrow strip along the rail's right edge. It is caught with slack
   (the cursor is not obliged to hit a pixel) and is visible only under the cursor and while
   dragging. */
.rail-grip{position:absolute;top:0;right:-3px;width:7px;height:100%;z-index:30;cursor:col-resize;
  background:transparent;border:0;padding:0}
.rail-grip::after{content:'';position:absolute;top:0;right:3px;width:1px;height:100%;background:var(--acc);opacity:0;transition:opacity .12s}
.rail-grip:hover::after,.rail-grip.on::after{opacity:.85;width:2px}
body.rail-dragging{cursor:col-resize;user-select:none}
.side-head{display:flex;align-items:center;justify-content:space-between;padding:14px 14px 10px}
.brand{font-weight:600;letter-spacing:.2px;color:var(--acc);display:flex;align-items:center;gap:0;flex:1;min-width:0}
.brand span{color:var(--tx)}
/* The assistant stands BY THE PANEL'S NAME (see placeTalk in app.js): pressed against the
   right edge of the line, a little smaller than the header one — next to the connection
   dot it is a pair of state signs, not an action button. The pulse and the green ring are
   the same ones, they live on the node itself. */
.brand .talk-btn{margin-left:auto;width:28px;height:28px;flex:none}
.brand .talk-btn .talk-ico{width:16px;height:16px}
/* The server restart stands next to the connection dot — they are about one and the same thing:
   is the one at the other end alive. Usually dimmed; it lights up by itself when the server's
   code has been updated and is waiting for a restart. */
.restart-btn{margin-left:auto;width:23px;height:23px;flex:none;display:flex;align-items:center;
  justify-content:center;border:1px solid transparent;border-radius:7px;background:none;
  color:var(--tx-3);font-size:13.5px;line-height:1;cursor:pointer;opacity:.45;transition:.15s}
.restart-btn:hover{opacity:1;color:var(--acc);border-color:var(--line-2);background:var(--bg-3)}
.restart-btn.need{opacity:1;color:var(--warn);border-color:var(--warn);
  background:color-mix(in srgb, var(--warn) 12%, transparent)}
.restart-btn.going{opacity:1;color:var(--acc);animation:restart-spin .9s linear infinite}
@keyframes restart-spin{to{transform:rotate(360deg)}}
.new-btn{margin:0 12px 12px;padding:9px;background:var(--bg-4);color:var(--tx);border:1px solid var(--line-2);border-radius:var(--r);cursor:pointer;font:500 13px var(--sans);transition:.15s}
.new-btn:hover{background:var(--acc);border-color:var(--acc);color:var(--on-acc)}
.side-block{padding:0 12px 12px;display:flex;flex-direction:column;min-height:0}
.side-block.grow{flex:1}
.side-label{display:flex;align-items:center;gap:6px;font:700 11px var(--sans);letter-spacing:1.1px;text-transform:uppercase;color:var(--tx-2);margin-bottom:7px}
/* The counter next to the heading is a caption, not a heading: no caps, one line. */
#tree-total{font:10.5px var(--mono);letter-spacing:0;text-transform:none;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.select,.chip-select{background:var(--bg-3);color:var(--tx);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font:13px var(--sans);width:100%;cursor:pointer;outline:none}
.select:focus,.chip-select:focus{border-color:var(--acc)}
.mini-btn{margin-left:auto;background:none;border:none;color:var(--tx-3);cursor:pointer;font-size:13px}
.mini-btn:hover{color:var(--acc)}
.sessions{overflow-y:auto;display:flex;flex-direction:column;gap:3px;min-height:0}
.sess{padding:7px 9px;border-radius:8px;cursor:pointer;border:1px solid transparent}
.sess:hover{background:var(--bg-3)}
.sess.active{background:var(--bg-4);border-color:var(--line-2)}
.sess-t{font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sess-m{font-size:10.5px;color:var(--tx-3);margin-top:2px;display:flex;gap:6px}
/* The connection is the dot by the studio's name. Red = the socket fell, green = alive. The
   caption «connected» and the «log out» button held a whole strip at the bottom for the sake
   of something looked at once a day; logging out moved into the account (the round button at
   the top right). */
.conn-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-left:15px;
  vertical-align:middle;background:var(--err);box-shadow:0 0 7px var(--err);transition:.2s;
  border:0;padding:0;flex:none}
.conn-dot.on{background:var(--ok);box-shadow:0 0 7px var(--ok)}
/* ── THE SAME DOT — THE RESTART ALARM ──
   The ⟳ button held room in the header permanently for the sake of an event that happens once
   an hour. Now the connection dot shouts about it itself: yellow, bigger and blinking — the
   question is one and the same, «is the one at the other end alive, and is he on the right
   code». A press asks and restarts. */
/* ⚠ BLINKING IS A REQUEST FOR AN ACTION, AND MOST OF THE TIME THERE IS NO ACTION (30.08).
   The dot shouted the whole time the code on disk was newer than the running one — and that is
   hours: the server honestly waits for a gap between turns and will leave by itself. «The
   yellow button is still lit, it is strictly unclear what I am supposed to do in this case» —
   exactly about this. So a steady yellow = «new code is waiting, I will take it myself», and
   only `.soon` (the countdown has started) or being an orphan (the button is the only cure)
   bring the blinking back. */
/* ⛔ AND IT IS THE TURNING ARROWS, NOT A DISC (the owner, 23.09: «мы же хотели, чтобы там
   вращались стрелки, намекая на то, что нужно перезагрузить… а она не вращается»). A still amber
   disc had to be HOVERED to learn that it meant «restart owed» — and a sign nobody hovers is a
   sign that says nothing; that is exactly the hole the separate ⬇ «updates available» icon was
   built to plug on 19.09, and two signs about one fact then carried two different tooltips. So
   the dot wears the same pair of chasing arrows as the red one — the shape everybody reads as
   «refresh» — in the warning ink, and the ⬇ is gone. THE SPEED IS THE SENTENCE: 3.4 s against the
   red ring's 1.15 s. Amber turns slowly = «new code is waiting, I take it myself at the first
   gap»; red spins fast = «this tab is stale, press me». Identical speeds would make one indicator
   say two things. */
.conn-dot.need{width:16px;height:16px;border:0;border-radius:0;cursor:pointer;box-shadow:none;
  background:var(--warn);
  -webkit-mask:var(--spin-arrows) center/100% 100% no-repeat;
          mask:var(--spin-arrows) center/100% 100% no-repeat;
  animation:conn-spin 3.4s linear infinite}
/* The countdown HAS started — now there is something to press (postpone), so the arrows race and
   breathe. ⚠ `conn-cry` cannot be reused here: it animates `transform`, and so does the rotation. */
.conn-dot.need.soon{animation:conn-spin 1.1s linear infinite,conn-blink 1.2s ease-in-out infinite}
.conn-dot.need:hover{background:color-mix(in srgb, var(--warn) 78%, var(--tx))}
@keyframes conn-blink{0%,100%{opacity:1}50%{opacity:.35}}
@media (prefers-reduced-motion: reduce){.conn-dot.need{animation:none}}
/* ── THE LENGTH OF THE WAIT, BESIDE THE SIGN THAT IS WAITING ──
   «52 минуты – это хорошая штука… можно и оставить» (the owner, 23.09). It was the one thing the
   ⬇ icon added that the dot did not have, so it moved here rather than dying with it: an hour of
   waiting read as «nothing is happening» until it was given a number. It is a caption on the
   arrows — same ink, no box, and the same tooltip is glued to both. */
/* ⚠ `.brand .conn-age`, NOT `.conn-age`: the header already paints its own text with
   `.brand span{color:var(--tx)}` (0,1,1), which outranks a bare class (0,1,0) — so the caption
   came out in ordinary text ink beside amber arrows and read as part of the panel's NAME. The
   narrow header also clips its spans (`.brand > span{overflow:hidden;text-overflow:ellipsis}`),
   which would turn «1 h 06 min» into «1 h 0…» on his phone; `flex:none` + `overflow:visible`
   keep the number whole. Both were caught by `npm run composer`, not by eye. */
.brand .conn-age{font:600 10px/1 var(--sans);color:var(--warn);white-space:nowrap;letter-spacing:0;
  margin-left:6px;flex:none;overflow:visible;cursor:pointer}
/* ⚠ Hidden by the ATTRIBUTE, and never painted by id: a flat `display` outranks [hidden], and
   that mine has gone off three times in this header. */
.conn-age[hidden]{display:none}
.conn-dot.going{animation:restart-spin .9s linear infinite}
/* ── THE SAME DOT SAYS «THE PAGE ITSELF IS STALE» ──
   Two different staleness questions share one dot, and they must not look alike: the SERVER on
   old code blinks amber (above), the BROWSER holding old front-end code turns into a softly red
   spinning ring — a ring, because a spinning disc looks like a still disc. Click = hard reload. */
/* THE SIGN IS TWO ARROWS CHASING EACH OTHER, not a cut-off ring. A semicircle read as
   «something broke and is hanging»; a recognisable pair of arrows reads as «refresh» at a
   single glance. It is drawn with a mask: the shape lives in `--spin-arrows`, the colour is
   an ordinary `background`, so the same glyph suits the dot on the left, the button on the
   right, and any of the ten themes. */
:root{--spin-arrows:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='%23000' stroke='%23000' stroke-width='3.2' stroke-linecap='round'%3E%3Cpath fill='none' d='M4.2 12A7.8 7.8 0 0 1 15.66 5.11'/%3E%3Cpath stroke='none' d='M19.55 7.18 14.30 7.67 17.02 2.55Z'/%3E%3Cpath fill='none' d='M19.8 12A7.8 7.8 0 0 1 8.34 18.89'/%3E%3Cpath stroke='none' d='M4.45 16.82 9.70 16.33 6.98 21.45Z'/%3E%3C/g%3E%3C/svg%3E")}
.conn-dot.stale{width:18px;height:18px;border:0;border-radius:0;cursor:pointer;box-shadow:none;
  background:var(--err);
  -webkit-mask:var(--spin-arrows) center/100% 100% no-repeat;
          mask:var(--spin-arrows) center/100% 100% no-repeat;
  animation:conn-spin 1.15s linear infinite}
.conn-dot.stale:hover{background:color-mix(in srgb, var(--err) 75%, var(--tx))}
/* ── THE LOGO SPINS WITH THE ARROWS BESIDE IT ──
   «Roll favicon in that top panel the same as you do with red half circles. So both» (the owner,
   03.09). The red spinning arrows say «this tab is on old code», and they are 18px in a header
   that is scanned from the LEFT — the eye starts at the logo and often never travels the extra
   two centimetres. Two things turning at once in one line cannot be missed, and it costs nothing:
   the same keyframes, the same speed, the same phase. No JS at all — the state is already on the
   dot, and `:has()` reads it from its neighbour, so there is no second place to keep in step. */
.brand:has(.conn-dot.stale) img{animation:conn-spin 1.15s linear infinite}
.brand img{display:inline-block;transform-origin:50% 50%}
@media (prefers-reduced-motion: reduce){.brand:has(.conn-dot.stale) img{animation:none}}
/* ── …AND IT TURNS WHILE THE HOUSE IS WORKING, WHICH IS A DIFFERENT SENTENCE ──
   «Roll favicon and Icon on page near Claude Studio — all the time if any session is doing
   something! Stop only if no active processes at all even background! If ONLY loops that are
   waiting there — in that case 3 times slower» (the owner, 04.09). The verdict is computed in
   web/app.js (`rollState`) and lands as ONE class on the document, so the header's logo turns
   with no JS per frame and keeps its phase across every repaint; the tab's own icon is drawn
   frame by frame there, because a browser renders an SVG favicon as a still picture.
   2.4 s, not the 1.15 s of the red arrows: this sign lives for HOURS at a time, and at the
   arrows' speed an hour of honest work reads as an alarm. The waiting loop is 3× that, literally
   — 7.2 s — which is slow enough to be read as «not now, but it will».
   ⛔ STALENESS OUTRANKS WORK, and it does so by specificity, not by luck: `.brand:has(…)` above
   scores (0,3,1) against this rule's (0,2,1). The red arrows ask for an ACTION; the turning logo
   only reports. Two speeds at once would be one indicator saying two things. */
html.rolling .brand img{animation:conn-spin 2.4s linear infinite}
html.rolling.rolling-slow .brand img{animation-duration:7.2s}
@media (prefers-reduced-motion: reduce){html.rolling .brand img{animation:none}}
@keyframes conn-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){.conn-dot.stale{animation:none}}
@keyframes conn-cry{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.4);opacity:.4}}
@media (prefers-reduced-motion: reduce){.conn-dot.need.soon{animation:none}}

/* ── HARD REFRESH — WITH A BIG BUTTON IN THE RAIL'S HEAD ──
   The panel lives in an open tab for days on end, edits reach the disk by the dozen an hour —
   this is THE most frequent button here, and on the phone it has to be hit with a finger, not
   hunted for in the curtain. */
.hard-btn{width:30px;height:30px;flex:none;display:flex;align-items:center;justify-content:center;border:none!important;background:none!important;
  border:1px solid var(--line-2);border-radius:9px;background:var(--bg-3);color:var(--tx);
  font-size:16px;line-height:1;cursor:pointer;transition:.15s;margin-left:8px}
.hard-btn:hover{color:var(--acc)}
.hard-btn svg{width:20px;height:20px;display:block}
@media (max-width:860px){.hard-btn svg{width:25px;height:25px}}
/* BOTH ALARMS SAY ONE AND THE SAME THING — «this tab is on old code» — and that is why they
   look like a pair: on the left a red one spins, on the right the same button SLOWER (2.6 s
   against 1.15 s). The difference in speed is not decoration: identical speeds would read
   as one indicator duplicated by oversight. At rest the button is white — it is the most
   frequent button here and it must not look switched off. */
.hard-btn.need{color:var(--warn);border-color:var(--warn);
  background:color-mix(in srgb, var(--warn) 14%, transparent)}
.hard-btn.need svg{animation:conn-spin 2.6s linear infinite}
@media (prefers-reduced-motion: reduce){.hard-btn.need svg{animation:none}}
.hard-btn.going{animation:restart-spin .9s linear infinite}
@media (max-width:860px){.hard-btn{width:40px;height:40px;font-size:20px;border-radius:11px}}

/* ═══ main ═══ */
.main{display:flex;flex-direction:column;min-width:0;min-height:0}
.topbar{display:flex;align-items:center;gap:14px;padding:10px 16px;border-bottom:1px solid var(--line);background:var(--bg-2)}
/* ═══ ← → : THE TRAIL OF ROWS THIS WINDOW HAS STOOD IN (web/visits.js) ═══
   A PAIR, drawn as one control: two arrows 2px apart read as «back and forward», two arrows
   14px apart read as two unrelated buttons. The topbar's own gap is 14, so the pair carries its
   own. */
.nav-pair{display:flex;align-items:center;gap:2px;flex:none;margin-right:-8px}
.nav-btn{flex:none;width:26px;height:26px;padding:0;display:flex;align-items:center;justify-content:center;
  border:1px solid transparent;border-radius:8px;background:none;color:var(--tx-2);cursor:pointer;transition:.15s}
.nav-btn svg{width:17px;height:17px;display:block}
.nav-btn:hover:not(.off){background:var(--bg-4);border-color:var(--line-2);color:var(--tx)}
.nav-btn:active:not(.off){background:var(--bg-3)}
/* ⚠ DIM, NOT GONE, AND DIMMED BY A CLASS RATHER THAN BY `disabled`. A button that disappears
   when there is nowhere to go takes the whole control off the screen on a fresh panel — a door
   nobody has seen does not exist for a person — and it moves the task's name sideways every time
   the trail runs out, which is the one thing the topbar may not do. The `disabled` ATTRIBUTE was
   worse still: the browser swallows every mouse event on a disabled control, right button
   included, so the list of everything this window has opened would be shut exactly where it is
   most wanted — at the start of a trail. The press is refused in JS; the events still arrive. */
.nav-btn.off{opacity:.28;cursor:default}
@media(max-width:860px){
  /* The topbar is the most expensive real estate on the phone, and ☰ already stands to the left
     of these. The pair keeps a finger's height and gives up its padding, never its size. */
  .nav-pair{gap:0;margin-right:-10px}
  .nav-btn{width:30px;height:30px}
  .nav-btn svg{width:18px;height:18px}
}
.title-wrap{min-width:0;flex:1}
.chat-title{font-weight:600;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chat-sub{font:11px var(--mono);color:var(--tx-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ⛔ WHEN SOMETHING IN THE CAPTION IS RED, THE CAPTION MAY TAKE A SECOND LINE. `nowrap` + `hidden`
   above is right for a footnote and wrong for a warning: at 390px the red build chip was drawn,
   had a real bounding box, and sat past the clipped right edge where no eye would ever find it.
   The class is set by `paintSub` only while a chip is red, so a healthy header never grows. */
.chat-sub.has-red{white-space:normal;text-overflow:clip;line-height:1.35}
/* The project is not a footnote but the answer to «where am I»: it is set in the accent, the
   technical part beside it stays grey. */
.sub-proj{color:var(--acc-2);font-weight:600;letter-spacing:.1px}
.sub-rest{color:var(--tx-3)}
.sub-proj+.sub-rest::before{content:' · '}
/* ═══ THE TECHNICAL CHIPS: A NUMBER THAT ANSWERS A CLICK (27.08) ═══
   A button, not text — and it is obliged to look like text: the line under the task's name is
   a footnote, not a toolbar. Hence the transparent background and the border that appears
   only under the cursor: «this can be pressed» is said at the moment it is already being
   looked at. */
.sub-chip{font:inherit;color:var(--tx-3);background:none;border:0;padding:0 3px;margin:0;
  border-radius:5px;cursor:default;font-variant-numeric:tabular-nums;line-height:1.2}
button.sub-chip{cursor:pointer}
button.sub-chip:hover{color:var(--tx-1);background:var(--bg-3)}
.sub-chip+.sub-chip::before{content:'· ';color:var(--tx-4, var(--tx-3));opacity:.6}
.sub-calls{color:var(--tx-2)}
/* A turn is running — the figure is alive: in the accent, and every new call makes it
   wink. Without the animation «work is going on» is indistinguishable from «it is stuck»:
   there may be no text on the screen at that moment at all (the model is thinking over its
   tenth tool). */
.sub-calls.live{color:var(--acc)}
.sub-calls.bump{animation:sub-bump .6s ease-out}
/* THE TWO TOOL COUNTERS: 🔧 what worked, 🐞 what did not. Both are our own SVG (see svgBug /
   svgWrench in app.js), so the colour here reaches the glyph through `currentColor` and the size
   through `em` — an emoji would have taken neither. */
.sub-tool-ok,.sub-tool-bad{display:inline-flex;align-items:center;gap:3px}
.sub-tool-ok svg,.sub-tool-bad svg{width:1.15em;height:1.15em;flex:0 0 auto}
.sub-tool-ok{color:var(--ok)}
.sub-tool-bad{color:var(--err)}
button.sub-tool-ok:hover,button.sub-tool-bad:hover{color:var(--tx-1)}
.sub-tool-ok.bump,.sub-tool-bad.bump{animation:sub-bump .6s ease-out}
@keyframes sub-bump{
  0%{transform:scale(1);text-shadow:none}
  22%{transform:scale(1.35);color:var(--acc);text-shadow:0 0 10px color-mix(in srgb,var(--acc) 65%,transparent)}
  100%{transform:scale(1);text-shadow:none}
}
/* Some of the servers did not come up — that is not decoration but the reason the tool
   «does not work»: let it be visible before anyone reaches for it. */
.sub-mcp.part{color:var(--warn, var(--err))}
@media (prefers-reduced-motion:reduce){.sub-calls.bump,.sub-tool-ok.bump,.sub-tool-bad.bump{animation:none}}
/* ON THE PHONE THE LINE IS SHORT, AND IT IS CUT FROM THE END — so the order IS the priority.
   The model and the version go first: they do not change for hours and are asked about once
   a week, while the calls, the turns, MCP and the tools are what is happening right now.
   Both doors are intact: the version and the model are named in the settings and in the
   cards of those same chips at the desk. */
/* ⛔ THE WRENCH GOES, THE LADYBUG STAYS. Of the two tool counters only one asks for anything: a
   success count is reassurance and can wait for the desk, a failure count is the reason to look.
   Thirty pixels on a phone buys exactly one of them. */
/* ⛔ THE BUILD CHIP'S BASE `display` IS DECLARED BEFORE THE MOBILE RULE, NOT AFTER IT. Written
   after, it has the same specificity and later wins — so a HEALTHY build id refused to stand down
   at 390px and ate a line of the phone's most expensive row. The red override below is deliberate
   and is the opposite bargain: reassurance may be cut from the line, a warning may not. */
.sub-build{display:inline-flex;align-items:center;gap:4px}
@media (max-width:860px){.sub-model,.sub-ver,.sub-build,.sub-tool-ok{display:none}}
/* ═══ «AM I ON THE NEWEST BUILD» — AND RED IS THE ONLY THING IT EVER SAYS LOUDLY ═══
   Quiet by default, in the same grey as the rest of the footnote: being current is the normal
   state and a permanently coloured sign teaches the eye to skip this corner — the very reason the
   🐞 chip is hidden at zero. The dot and the colour appear only on a PROVEN gap (server/build.js
   answers `ok:null`, not `false`, when it could not ask), and then they outrank the phone's own
   width rule below: reassurance may be cut from a short line, a warning may not. Same bargain as
   the ladybug above, and it was paid for the same way — on 16.10 a machine served code 18 days
   old and nothing on the screen was allowed to say so. */
.sub-ver.stale,.sub-build.bad{color:var(--err)}
button.sub-ver.stale:hover,button.sub-build.bad:hover{color:var(--tx-1)}
.build-dot{width:.52em;height:.52em;border-radius:50%;background:var(--err);flex:0 0 auto;
  display:inline-block;box-shadow:0 0 5px color-mix(in srgb,var(--err) 55%,transparent)}
.sub-ver.stale .build-dot{margin-right:3px}
/* ⛔ AND ON 19.10 THE PHONE'S HALF OF THAT BARGAIN WAS REVOKED BY THE OWNER HIMSELF — the red
   override that stood here no longer reaches ≤860px: «version, the number of turns, MCP, tools and
   the versions must not be printed; it does not fit anyway — up there only the three stripes, the
   project's name and the task's name are left». He said it over a photograph of his own header, in
   which this footnote had wrapped onto five lines. The warning keeps the DESK, where it has room to
   be read, and a version is not actionable from a phone anyway — the gap is closed at the machine.
   The whole footnote now stands down on a narrow screen: see § THE PHONE'S HEADER HOLDS FIVE THINGS
   at the end of this file. Gate: `npm run buildchip` §8, `npm run voice`. */
/* THE GAP BETWEEN THE COUNTERS IS ROOM FOR A BUTTON, NOT AIR.
   14px between four chips on the phone ate more than forty pixels of width — exactly
   what the talk button costs, and it had nowhere to stand. The chips themselves were not
   squeezed: the figures stayed the same type size, only the interval was squeezed. */
.meters{display:flex;align-items:center;gap:9px}
@media (max-width:860px){.meters{gap:5px}}
.meter{width:96px}
.meter-top{display:flex;justify-content:space-between;font:11px var(--mono);color:var(--tx-2)}
.meter-unit{color:var(--tx-3)}
.bar{height:3px;background:var(--bg-4);border-radius:2px;margin-top:3px;overflow:hidden}
.bar i{display:block;height:100%;background:var(--ok);transition:width .4s}
.bar i.warm{background:var(--acc)}
.bar i.hot{background:var(--err)}
.cache{font:11.5px var(--mono);color:var(--tx-2);display:flex;gap:5px;align-items:center}

/* ── THE TALK BUTTON WITH THE ASSISTANT ─────────────────────────────────────
   Three states, and the difference between them is visible at arm's length, because it is
   read with peripheral vision: silent — an ordinary outline in the header's colour; a
   conversation is going on — a green fill and a slow pulse of the ring around it; he himself
   is speaking right now — the ring beats fast. The ring is a separate element, not a shadow
   on the button: box-shadow in an animation repaints the whole header every frame, while a
   transform on its own layer does not touch its neighbours. */
.talk-btn{flex:none;width:34px;height:34px;display:flex;align-items:center;justify-content:center;
  position:relative;border:1px solid var(--line-2);border-radius:50%;background:var(--bg-3);
  color:var(--tx-2);cursor:pointer;transition:background .15s,color .15s,border-color .15s}
.talk-btn:hover{background:var(--bg-4);color:var(--tx)}
.talk-ico{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}
.talk-ring{position:absolute;inset:-3px;border-radius:50%;border:2px solid var(--ok);
  opacity:0;pointer-events:none}
/* A CONVERSATION IS GOING ON. Green — the same as the live connection and the running turn: «it works by itself». */
.talk-btn.on{background:var(--ok-bg,var(--bg-4));border-color:var(--ok);color:var(--ok)}
.talk-btn.on .talk-ring{animation:talk-ring 2s ease-out infinite}
/* HE IS SPEAKING. The same pulse, but twice as fast and with a fill: audible with no sound. */
.talk-btn.say{animation:talk-say 1s ease-in-out infinite}
.talk-btn.say .talk-ring{animation-duration:1s}
@keyframes talk-ring{0%{transform:scale(.86);opacity:.9}70%{transform:scale(1.35);opacity:0}100%{opacity:0}}
@keyframes talk-say{0%,100%{background:var(--ok-bg,var(--bg-4))}50%{background:var(--ok);color:var(--bg-2)}}
@media (prefers-reduced-motion:reduce){
  .talk-btn.on .talk-ring{animation:none;opacity:.75}
  .talk-btn.say{animation:none}
}
@media (max-width:860px){.talk-btn{width:36px;height:36px}.talk-ico{width:19px;height:19px}}

/* ═══ stream ═══ */
/* THE FEED HAS NO SMOOTH SCROLLING ANY MORE — AND THAT IS THE MAIN CURE FOR «IT FLOATS».
   `scroll-behavior:smooth` turns EVERY assignment of scrollTop into an animation, and
   there are dozens of them a second: autoscroll on every token that arrives, the
   frame-by-frame compensation of the growing input field, the anchor correction during a
   pinch. Every new target cut off the unfinished previous one — the feed travelled on
   its own and past the mark. Smoothness, where it is really wanted, asks for itself
   explicitly: scrollTo({behavior:'smooth'}).
   overflow-anchor:none — for the same reason: the browser holds ITS OWN anchor when the
   layout changes and argues with ours (text scale, reading mode, the composer growing).
   There may be only one anchor, and it is ours. */
.stream{flex:1;overflow-y:auto;padding:20px 0 8px;overflow-anchor:none;overscroll-behavior:contain}
/* ═══ ONE SCROLLBAR FOR THE WHOLE PANEL, AND IT IS THIN ═══
   The rule knew the feed and `.sessions`, but not the RAIL: `.tree` is a separate scrollable
   box, and it got the 17px Windows system bar, grey-and-white, with buttons. «It has become
   very fat and ugly» (the owner, 26.08) — next to a thin feed that really does look like a
   breakage. `scrollbar-width/color` is for Firefox, `::-webkit-*` for Chromium; we keep both,
   the panel lives in both. The track is transparent: the bar must be noticeable only when the
   eyes are following it. */
.stream,.sessions,.tree,.sheet-body,.insp-body,.picker-list,.flow-body{
  scrollbar-width:thin;scrollbar-color:var(--line-2) transparent}
.stream::-webkit-scrollbar,.sessions::-webkit-scrollbar,.tree::-webkit-scrollbar,
.sheet-body::-webkit-scrollbar,.insp-body::-webkit-scrollbar,.picker-list::-webkit-scrollbar,
.flow-body::-webkit-scrollbar{width:9px;height:9px}
.stream::-webkit-scrollbar-thumb,.sessions::-webkit-scrollbar-thumb,.tree::-webkit-scrollbar-thumb,
.sheet-body::-webkit-scrollbar-thumb,.insp-body::-webkit-scrollbar-thumb,
.picker-list::-webkit-scrollbar-thumb,.flow-body::-webkit-scrollbar-thumb{
  background:var(--line-2);border-radius:5px;border:2px solid transparent;background-clip:content-box}
.stream::-webkit-scrollbar-thumb:hover,.tree::-webkit-scrollbar-thumb:hover{background:var(--tx-3);background-clip:content-box}
.stream::-webkit-scrollbar-track,.tree::-webkit-scrollbar-track,.sheet-body::-webkit-scrollbar-track{background:transparent}
.wrap{max-width:var(--read-w);margin:0 auto;padding:0 22px}
/* THE FLOATING BUTTONS NEED A LANE OF THEIR OWN, AND THE WIDE COLUMN TOOK IT (09.09).
   🔊 and ▼ are `position:fixed; right:14px` and 46px across — they used to hang in the empty
   gutter beside a 900px column, so nobody had to think about them. A column that fills the
   window runs straight under them, and text under a button is text that cannot be read or
   selected. 66px = 14 + 46 + a little air. The composer keeps its own 44px button column on
   the same side, so the two texts still end at very nearly the same place. */
@media(min-width:861px){ .wrap{padding-right:66px} }
/* The live line is a direct child of the feed, not of .wrap: only that way is it GUARANTEED to
   be the last one, whatever gets appended to the conversation. It is obliged to hold its width
   itself. */
.stream>.live-line{width:calc(100% - 44px);max-width:var(--read-w);margin:0 auto}
/* The percentage next to the volume, the price with the agents, and the fold of small change. */
.cx-pct{margin-left:7px;font-size:.85em;opacity:.55;font-style:normal}
.cx-money{margin-left:7px;font-size:.85em;opacity:.8;font-style:normal}
.cx-fold{display:flex;align-items:center;gap:10px;width:100%;margin-top:3px;padding:7px 10px;
  background:none;border:1px dashed var(--line-2);border-radius:10px;color:inherit;
  font:inherit;font-size:12.5px;opacity:.72;cursor:pointer;text-align:left}
.cx-fold:hover{opacity:1}
.cx-fold-t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cx-fold-v{font-variant-numeric:tabular-nums;opacity:.85}
.cx-fold-a{opacity:.6}
.cx-group.fixed{border-left:2px solid var(--line-2);padding-left:10px}
.cx-group.fixed .cx-row{opacity:.88}
.cx-group.off .cx-track{opacity:.45}

.empty{max-width:560px;margin:12vh auto;text-align:center;color:var(--tx-3)}
.empty-mark{font-size:38px;color:var(--acc);margin-bottom:10px}
.empty h1{font-size:20px;color:var(--tx);font-weight:600;margin-bottom:8px}
.empty p{font-size:13.5px;line-height:1.7}
.empty-tips{display:flex;gap:16px;justify-content:center;margin-top:22px;font-size:11.5px;flex-wrap:wrap}

/* `position:relative` is here for the pair of context buttons that hover brings up at the bottom
   right of a bubble (see `.ctx-btns` below). Nothing else inside a message is absolutely
   positioned against anything above it: the copy button, the attachment tiles and the picture
   card each already have a positioned parent of their own. */
.msg{margin-bottom:14px;position:relative}
/* WHO IS SPEAKING IS SEEN WHILE SCROLLING, NOT WHILE READING.
   Your own lines stand on the RIGHT, in their own fill and a little smaller; Claude's reply
   goes on the left across the full width. Before, both were identical blocks the full width
   of the column, and on a fast scroll the «mine / his» boundary simply did not read — the eye
   hunted for it by the meaning of the text. */
.msg-user{margin-left:auto;width:fit-content;max-width:82%;
  background:color-mix(in srgb, var(--acc) 13%, var(--bg-2));
  border:1px solid color-mix(in srgb, var(--acc) 26%, var(--line));
  border-right:2px solid var(--acc);
  border-radius:var(--r) var(--r) 5px var(--r);
  padding:10px 14px;white-space:pre-wrap;word-break:break-word;
  font-size:13.3px;line-height:1.55;color:var(--tx);box-shadow:0 1px 5px var(--shadow)}
/* A caption at the bubble's edge: on a long feed you can see where your own line starts. */
.msg-user::before{content:attr(data-who);display:block;font:600 9.7px/1 var(--sans);letter-spacing:.35px;
  text-transform:none;color:var(--acc);opacity:.82;margin-bottom:5px;text-align:right}
/* ═══ A SECOND PERSON WRITES IN THIS PANEL TOO ═══
   Two people share one feed: the owner and a guest account with her own folders. The caption alone
   is not enough — it is 9.7px at the top edge of a bubble, and «what did SHE ask for» is a question
   asked while scrolling past. So a guest's line keeps the shape of a sent message and changes its
   ink: the thinking violet instead of the clay accent, which is the one hue in every palette that
   is already «not the owner's voice». The Telegram guest is the same colour with a dashed edge —
   he writes from outside the panel entirely. */
.msg-user.guest{background:color-mix(in srgb, var(--think, #8b7fd4) 14%, var(--bg-2));
  border:1px solid color-mix(in srgb, var(--think, #8b7fd4) 32%, var(--line));
  border-right:2px solid var(--think, #8b7fd4)}
.msg-user.guest::before{color:var(--think, #8b7fd4);opacity:.95}
.msg-user.guest.via-tg{border-style:dashed;border-right-style:solid}
/* ═══ THE THIRD STATUS: THE LINE THE STUDIO SENT ITSELF ═══
   A continuation prompt is neither his and nor the model's: when a turn lands at the window band
   and hands over, the panel opens the next session and writes the errand itself. Drawn in his
   accent under the caption «you», that was a plain untruth — so it keeps the shape of a sent
   line (right-hand side, same corner) and loses everything that says «a person»: no accent, a
   dashed edge, muted ink, and its own caption naming which link of the chain it is. Long ones
   arrive folded behind a soft fade — a machine's note to itself is glanced at, not read. */
.msg-user.self{background:color-mix(in srgb, var(--tx-3) 9%, var(--bg-2));
  border:1px dashed color-mix(in srgb, var(--tx-3) 36%, var(--line));
  border-right:2px solid color-mix(in srgb, var(--tx-3) 58%, var(--line));
  box-shadow:none;color:color-mix(in srgb, var(--tx) 82%, var(--bg-2))}
.msg-user.self::before{content:attr(data-who);color:var(--tx-3);opacity:.95;text-transform:none;
  letter-spacing:.3px;font-size:9.5px}
.msg-user.self.fold{max-height:5.4em;overflow:hidden;cursor:pointer;
  -webkit-mask-image:linear-gradient(#000 58%,transparent);mask-image:linear-gradient(#000 58%,transparent)}
.msg-user.self.fold.open{max-height:none;-webkit-mask-image:none;mask-image:none}
/* ═══ TWO MARKS ABOVE A LINE: WHEN IT WAS SENT AND WHEN IT WAS TAKEN UP ═══
   They stand ABOVE the bubble and along its right edge — that is, on the boundary where your
   own line begins: while scrolling the feed, the time is read together with «who is speaking»,
   without stopping. Monospaced and small: this is a support for the eye, not content. The
   order is not accidental — first «I sent it», then «they took it up»: the wait lies between
   them. */
.msg-when{display:flex;gap:9px;justify-content:flex-end;align-items:baseline;
  font:10.5px var(--mono);color:var(--tx-3);margin:0 2px 4px 0;letter-spacing:-.1px}
.when-go{color:var(--tx-3);opacity:.85}
/* «How long ago that was» — the last thing on the line, and the faintest: it is the only mark here
   that keeps moving, so it must not pull the eye every half-minute. Its own gap is tighter than the
   9px between the two clocks, because it belongs to the clock in front of it rather than standing
   beside it as a third mark. */
.when-ago{color:var(--tx-3);opacity:.6;margin-left:-4px}
/* ═══ AND WHEN THE MACHINE POSTED IT, NOT HIM ═══
   «put bot (gray metal please) icon or loop icon there to see that this is auto-posted - not
   mine!» (the owner, 04.09). A run of the task loop travels the USER's road, so it arrived
   wearing all three of his marks at once — the 🧑, the «you» in the corner, his accent fill —
   and the only thing that gave it away was reading the first line of the text. Two of the three
   marks now tell the truth, and the fill is deliberately left alone: moving a familiar bubble to
   a new colour makes the whole feed unfamiliar to fix one line of it.

   THE BOT IS DRAWN, NOT TYPED. 🤖 is taken: it is the agent's card and another session's card,
   both of which are VOICES that said something. This is not a voice, it is machinery, so it gets
   grey metal instead of a colour emoji — a light-to-dark ramp down the face is what reads as
   metal, and the mid-greys are chosen so it stands out on a dark theme and on a light one alike
   (a theme token would follow the palette, which is the opposite of what «metal» means here).
   The shape is a MASK, so the eyes and the mouth are holes and the bubble shows through them —
   and no markup is written for it, which is why `npm run dom` counts no new point. */
.msg-user[data-tag]::before{content:attr(data-tag);color:var(--tx-3);opacity:.9}
/* ═══ WHY THIS MESSAGE SENT ITSELF ═══
   A voice-triggered turn names the phrase that fired it, under its own text (20.09). It is a
   FOOTNOTE, not a second message: smaller, dimmer, cut off from the task by a rule — the eye must
   read it as «about the message» and never as part of what was asked. The phrase itself is the
   only bright thing in the line, because it is the answer to the whole question.
   `white-space:normal` undoes the bubble's `pre-wrap`: this line is prose we wrote, not dictation. */
.msg-hot{margin-top:9px;padding-top:7px;white-space:normal;
  border-top:1px dashed color-mix(in srgb, var(--acc) 34%, transparent);
  font:400 11px/1.45 var(--sans);color:var(--tx-3);text-align:left}
.msg-hot b{font-weight:700;color:var(--acc);white-space:pre-wrap}
.msg-hot-i{margin-right:5px}
/* ONE BOT, TWO PLACES. The agent's card asks for exactly the same mark (the owner, 11.09: «change
   icon to that gray one»), and the shape is shared by SELECTOR rather than by a second copy of the
   data URL: two drawings of one thing drift apart on the first correction. */
.when-sent[data-bot],.ac-ico[data-bot]{--bot-ico:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><defs><mask id='b'><rect width='16' height='16' fill='black'/><g fill='white'><rect x='7.25' y='1.7' width='1.5' height='2.9' rx='.75'/><circle cx='8' cy='1.45' r='1.35'/><rect x='2.3' y='4' width='11.4' height='9.6' rx='2.7'/><rect x='.4' y='6.5' width='1.5' height='3.7' rx='.75'/><rect x='14.1' y='6.5' width='1.5' height='3.7' rx='.75'/></g><circle cx='5.85' cy='8.05' r='1.5' fill='black'/><circle cx='10.15' cy='8.05' r='1.5' fill='black'/><rect x='5.9' y='10.75' width='4.2' height='1.3' rx='.65' fill='black'/></mask></defs><rect width='16' height='16' fill='white' mask='url(%23b)'/></svg>")}
.when-sent[data-bot]::before{content:'';display:inline-block;width:12px;height:12px;margin-right:5px;
  vertical-align:-2px;
  background:linear-gradient(180deg,#e4eaef 0%,#aab4bd 30%,#69737c 56%,#8d97a0 76%,#ccd4da 100%);
  -webkit-mask:var(--bot-ico) center/contain no-repeat;mask:var(--bot-ico) center/contain no-repeat;
  filter:drop-shadow(0 0 .6px rgba(0,0,0,.45))}
/* A loop is the one machine that will do this again in fifteen minutes, so its corner tag says
   `loop` where the other three say `auto`: «not mine» is the answer to the question he asked,
   «and it is coming back» is the one he asks next. */
.msg-assistant{padding:2px 0 4px;word-break:break-word;max-width:94%}

/* TAKE THIS MESSAGE INTO THE NEXT PROMPT (owner, 16-17.09: two buttons on each bubble, on hover,
   for the desk only). The same shape as the copy button on a code block, and for the same reason:
   a door that is needed once in twenty messages may not stand open in all twenty.

   THE GROUP STAYS INSIDE THE BOX, and that is not a matter of taste. Hung in the 14px gap below,
   half of it would lie outside `.msg` — `.msg:hover` would go false the moment the pointer left
   the box to reach the button, and adding `.ctx-btns:hover` cannot save it: `:hover` needs a hit
   test, a hit test needs `pointer-events`, and `pointer-events` is what the hidden state takes
   away. The two conditions chase each other and the button flickers instead of being pressed. So
   it sits at the bottom right corner INSIDE the message, over a solid fill — the last line of an
   answer almost never reaches that corner, and while nobody is hovering there is nothing there.

   THE CAPTION IS `attr(data-lbl)`, not a text node, so it stays out of `innerText` — three places
   in `web/app.js` read a message as plain text, and a real caption would ride out inside every
   answer he copies. Same trick, same reason, as the corner tag above. */
.ctx-btns{position:absolute;right:0;bottom:1px;z-index:3;display:flex;gap:4px;
  opacity:0;pointer-events:none;transition:opacity .12s}
.msg:hover>.ctx-btns,.msg:focus-within>.ctx-btns{opacity:1;pointer-events:auto}
.ctx-btn{font:600 10.5px/1 var(--sans);letter-spacing:.2px;color:var(--tx-2);
  background:var(--bg-2);border:1px solid var(--line-2);border-radius:7px;
  padding:5px 8px;cursor:pointer;white-space:nowrap;box-shadow:0 1px 6px var(--shadow)}
.ctx-btn::after{content:attr(data-lbl)}
.ctx-btn:hover{color:var(--tx);border-color:var(--acc)}
.ctx-btn:active{background:var(--bg-4)}
.ctx-btn:focus-visible{outline:2px solid var(--acc);outline-offset:1px}
/* HIS WORD: THE DESK ONLY — AND «THE DESK» IS MEASURED BY WIDTH, THE SAME WAY THE JS MEASURES IT.
   At 430px two captioned buttons do not fit beside a bubble at all; the phone reaches the same two
   doors through the selection strip's «...» menu instead. `phoneWide()` in `web/app.js` builds no
   nodes below 860px and this rule draws none — one number, said twice, so the two halves cannot
   drift apart.
   ⛔ `(hover:none)` WAS IN THIS QUERY AND HAD TO COME OUT (17.09). It is true of more machines
   than it sounds: a touch-screen laptop reports it at 1920px, and so does every Playwright page
   opened with `hasTouch:true` — which is how `npm run composer` opens ALL of its pages, at every
   width. The buttons were `display:none` for the whole run, so the gate measured a box of zero by
   zero and then waited six seconds to click something that was not drawn. A rule that hides a
   feature on a wide screen because the screen can also be touched is a rule that hides it from
   people who have a mouse in their hand. */
@media (max-width:860px){.ctx-btns{display:none}}

/* ═══ THE THIRD VOICE: A BACKGROUND AGENT'S REPORT ═══
   «These are other agents' results — then it is not my colour but some other one, blue»
   (21.08). There are three voices in the feed and there were two colours: an agent's report
   arrived in the human's bubble, captioned «you», and read as a foreign insert under his
   name. The geometry answers «who is speaking» before the reading: your own — on the RIGHT
   in the accent, Claude — the full width on the left, an agent — MIRRORING the human's
   bubble, on the left and in its own blue (`--bot` = `--cold`, which is declared in all ten
   themes, so the card does not fall out of a single one). */
/* ⚠ ONE LINE TALL WHEN THERE IS ONE LINE TO SAY (the owner, 11.09: «make it compact»). Eight of
   these stand in a row on an ordinary turn — a crew of background agents reports one after another —
   so every pixel of padding here is paid eight times, and the tallest thing on the screen was the
   part that carried the least. `min-width` is what decides it in practice: at 320px an eleven-word
   errand wrapped, and the card that says the most was the one forced to grow. */
.agentcard{--bot:var(--cold);margin:0 auto 8px 0;width:fit-content;max-width:88%;min-width:min(260px,100%);
  border:1px solid color-mix(in srgb, var(--bot) 34%, var(--line));
  border-left:2px solid var(--bot);
  border-radius:var(--r) var(--r) var(--r) 5px;
  background:color-mix(in srgb, var(--bot) 9%, var(--bg-2));
  box-shadow:0 1px 5px var(--shadow);overflow:hidden}
.ac-head{display:flex;align-items:center;gap:7px;padding:5px 11px;user-select:none;cursor:default}
/* The heading is a button only where there is something to unfold: a short report is always open. */
.agentcard.foldable .ac-head{cursor:pointer}
.agentcard.foldable .ac-head:hover{background:color-mix(in srgb, var(--bot) 7%, transparent)}
.ac-ico{font-size:14px;line-height:1;flex:none}
/* THE GREY METAL BOT OF 04.09, IN THE CARD TOO. Not the emoji: 🤖 is a colour picture that outweighs
   every word next to it, and a column of eight of them reads as eight alarms. The shape comes from
   the shared mask above; only the size differs, because here it stands beside 12.5px text. */
.ac-ico[data-bot]{width:13px;height:13px;
  background:linear-gradient(180deg,#e4eaef 0%,#aab4bd 30%,#69737c 56%,#8d97a0 76%,#ccd4da 100%);
  -webkit-mask:var(--bot-ico) center/contain no-repeat;mask:var(--bot-ico) center/contain no-repeat;
  filter:drop-shadow(0 0 .6px rgba(0,0,0,.45))}
/* WHAT THE AGENT WAS ON — it takes whatever room is left, and gives it back first. `min-width:0` is
   load-bearing: a flex item refuses to shrink below its content without it, so the ellipsis would
   never appear and a long errand would push the status and the size out of the card. */
.ac-who{font:600 12.5px var(--sans);color:var(--tx);flex:0 1 auto;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The twin caption «you»: it is what answers the question of who sent this text. */
.ac-tag{font:600 9px/1 var(--sans);letter-spacing:.7px;text-transform:uppercase;color:var(--bot);
  border:1px solid color-mix(in srgb, var(--bot) 45%, transparent);border-radius:99px;
  padding:2px 6px;white-space:nowrap;flex:none}
/* The KIND of agent, when the errand above it has not already said so. Monospaced, because it is an
   identifier out of the launch (`general-purpose`, `code-reviewer`) and not a phrase. */
.ac-sub{font:10.5px var(--mono);color:var(--tx-3);white-space:nowrap;flex:none;
  overflow:hidden;text-overflow:ellipsis;max-width:26ch}
/* How long it ran. Dim on purpose — it is read when the answer surprises you, not on every card. */
.ac-dur{font:11px var(--mono);color:var(--tx-3);white-space:nowrap;flex:none}
/* A PEER IS A FOURTH VOICE, NOT A SECOND KIND OF AGENT. Same geometry — the reader's question is
   the same and a second layout for it is noise — but its own hue, because the RELATION differs: a
   background agent was launched by this row and reports to it, another session was launched by the
   owner, answers to nobody here, and can write back. ⚠ The hue MUST be an existing token: `--warn`
   is declared in `:root` and overridden by every theme, whereas the `--warm` this rule was first
   written with EXISTS NOWHERE — `var()` on an undeclared token falls back to nothing and the card
   would have lost its border and its tag in all ten themes at once. That is the same argument that
   made `--bot` an alias rather than a hex on 21.08, and it has to be re-checked, not remembered. */
.agentcard[data-via="peer"]{--bot:var(--warn)}
.ac-st{font:11px var(--mono);color:var(--ok)}
.ac-st.bad{color:var(--err)}
.ac-tw{margin-left:auto;font:11px var(--mono);color:var(--tx-3);white-space:nowrap;flex:none;
  padding-left:4px}
.ac-body{padding:2px 11px 9px;font-size:13px;line-height:1.6;color:var(--tx-2);
  border-top:1px solid color-mix(in srgb, var(--bot) 18%, transparent)}
.ac-body>:first-child{margin-top:6px}
.agentcard.folded .ac-body{display:none}
/* On the phone the caption is NOT hidden: «who sent this» is the main question asked of such a
   block, and it is from the phone that it is read most often. Tight — we wrap the line, we do
   not throw the answer away. */
@media (max-width:720px){.agentcard{max-width:100%}
  .ac-head{flex-wrap:wrap;gap:4px 8px}
  .ac-who{max-width:100%}
  /* …but «no report» IS thrown away here, and only it: it is the absence of an answer, and on 430px
     it took a whole third line of the card to say that nothing was written. The size of a real
     report keeps its place — that one is content. */
  .ac-tw{margin-left:0}
  .ac-tw[data-none]{display:none}}
/* Sent attachments as pictures, not as a list of paths: «what exactly did I send» is a
   question about an image, and you cannot answer it with the line D:\Temp\… */
.att-grid{display:flex;flex-wrap:wrap;align-items:flex-start;gap:8px;margin-top:9px}
.att-cell{width:152px;padding:0;overflow:hidden;display:flex;flex-direction:column;
  border:1px solid var(--line);border-radius:var(--r);background:var(--bg-2);
  color:var(--tx-3);font:inherit;cursor:zoom-in;transition:.15s}
.att-cell:hover{border-color:var(--acc);color:var(--tx-2)}
.att-img{width:100%;height:98px;object-fit:cover;display:block;background:var(--bg-3)}
.att-name{font-size:10.5px;padding:5px 8px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A NON-PICTURE IS THE SAME TILE, JUST WITH A GLYPH INSTEAD OF A SNAPSHOT. What stood here
   was `width:auto;cursor:default` — that is, an .xlsx turned into a narrow caption with no
   icon and no door, even though in the composer it was a tile with 📗 («among the sent ones,
   instead of a picture you made some sort of path caption», 21.08). The width, the height and
   the caption are the same as a snapshot's. */
.att-cell.file{cursor:pointer;width:152px;max-width:100%}
.att-cell.file .att-ico{display:grid;place-items:center;height:98px;font-size:34px;line-height:1;
  background:var(--bg-3);border-bottom:1px solid var(--line)}
.notice{font:11.5px var(--mono);color:var(--tx-3);padding:4px 0}

/* ═══ THE FEED IS BEING READ — AND IT SAYS SO WHERE READING MODE CANNOT HIDE IT ═══
   «while you "load" content - I see for 20 seconds wrong picture... add loader maybe» (owner,
   21.09). The old loader was a `.notice`, and `.stream.qa-only .notice{display:none}` — his own
   reading mode — hid it: twenty seconds of a blank window with no sign of life. This has its OWN
   class and is NEVER put back into `.notice`; `npm run composer` refuses that.
   The bars are the SHAPE of a conversation (one short bubble on the right, two answers on the
   left), so the window reads as «it is coming», not as an empty error box. */
.feed-load{opacity:0;animation:fl-in .25s .18s ease-out forwards}
.fl-head{display:flex;align-items:center;gap:8px;font:11.5px var(--mono);color:var(--tx-3);padding:2px 0 12px}
/* A turning ring, not three dots: the dots belong to `.live-line`, which means «a turn is
   running», and the two must not be mistaken for one another. */
.fl-ring{width:13px;height:13px;flex:0 0 auto;border-radius:50%;
  border:2px solid color-mix(in srgb,var(--acc) 28%,transparent);border-top-color:var(--acc);
  animation:fl-spin .8s linear infinite}
.fl-secs{font-variant-numeric:tabular-nums;color:var(--tx-2);margin-left:2px}
.fl-bar{height:52px;border-radius:var(--r);margin-bottom:14px;
  background:linear-gradient(100deg,var(--bg-2) 30%,var(--bg-3) 48%,var(--bg-2) 66%);
  background-size:280% 100%;animation:fl-sweep 1.5s linear infinite}
/* 42%, so the bar's LEFT edge lands well past the middle of the column: «whose line is this» is
   read from the side it stands on, and a skeleton at 52% starts just left of centre and says the
   opposite of what it means. */
.fl-bar.fl-me{margin-left:auto;width:42%;max-width:82%;height:38px;
  border-radius:var(--r) var(--r) 5px var(--r);
  background:linear-gradient(100deg,
    color-mix(in srgb,var(--acc) 9%,var(--bg-2)) 30%,
    color-mix(in srgb,var(--acc) 18%,var(--bg-2)) 48%,
    color-mix(in srgb,var(--acc) 9%,var(--bg-2)) 66%);
  background-size:280% 100%}
.fl-bar.fl-short{width:64%;height:34px}
@keyframes fl-in{to{opacity:1}}
@keyframes fl-spin{to{transform:rotate(360deg)}}
@keyframes fl-sweep{from{background-position:140% 0}to{background-position:-140% 0}}
/* Still visible, merely still: the point is «something is happening», and for a person who has
   asked for less motion the ring and the counter already say it. */
@media (prefers-reduced-motion:reduce){
  .feed-load{opacity:1;animation:none}
  .fl-ring{animation-duration:2.4s}
  .fl-bar{animation:none;background:var(--bg-2)}
  .fl-bar.fl-me{background:color-mix(in srgb,var(--acc) 9%,var(--bg-2))}
}
.err-box{border:1px solid var(--err-bd);background:var(--err-bg);color:var(--err);border-radius:8px;padding:9px 12px;font:12.5px var(--mono);white-space:pre-wrap}

/* «The turn did not finish» is the only plate that speaks about what did NOT happen. The flat
   caption «write the next message» under interrupted work meant «everything is fine», even
   though there was no answer (incident 17.08 12:47: the server restarted in the middle of a
   turn). */
.broke{display:grid;grid-template-columns:auto 1fr;gap:2px 11px;align-items:start;
  border:1px solid var(--warn);border-radius:var(--r);padding:11px 14px;margin:12px 0 4px;
  background:color-mix(in srgb, var(--warn) 11%, var(--bg-2))}
.broke-ico{grid-row:1/4;font-size:15px;line-height:1.2;color:var(--warn)}
.broke-t{font:600 13px var(--sans);color:var(--tx)}
.broke-d{font:11.5px/1.6 var(--mono);color:var(--tx-2)}
.broke-go{grid-column:2;justify-self:start;margin-top:9px;cursor:pointer;
  border:1px solid var(--warn);background:transparent;color:var(--warn);
  border-radius:99px;padding:5px 13px;font:500 12px var(--sans);transition:.15s}
.broke-go:hover{background:var(--warn);color:var(--on-warn,var(--bg))}

/* thinking */
.think{border-left:2px solid var(--think);padding:4px 0 4px 12px;margin:6px 0 10px;color:var(--think-2);font-size:12.8px;font-style:italic;white-space:pre-wrap}
.think-head{font:600 10.5px var(--sans);letter-spacing:.8px;text-transform:uppercase;color:var(--think);font-style:normal;cursor:pointer;user-select:none;display:flex;gap:6px;align-items:center;margin-bottom:3px}
.think.collapsed .think-body{display:none}
.think-body{max-height:340px;overflow-y:auto}

/* tool cards */
.tool{border:1px solid var(--line);background:var(--bg-2);border-radius:9px;margin:6px 0;overflow:hidden}
.tool-head{display:flex;align-items:center;gap:9px;padding:7px 11px;cursor:pointer;font-size:12.5px}
.tool-head:hover{background:var(--bg-3)}
.tool-ico{font-size:12px;width:15px;text-align:center}
.tool-name{font:600 12px var(--mono);color:var(--acc-2)}
.tool-sum{font:12px var(--mono);color:var(--tx-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.tool-state{font-size:11px;color:var(--tx-3)}
.tool-state.ok{color:var(--ok)}
.tool-state.bad{color:var(--err)}
.tool-body{display:none;border-top:1px solid var(--line);padding:9px 11px;background:var(--bg)}
.tool.open .tool-body{display:block}
.tool-body pre{font:11.8px/1.5 var(--mono);color:var(--tx-2);white-space:pre-wrap;word-break:break-word;max-height:420px;overflow:auto}
.tool-body .lbl{font:600 10px var(--sans);letter-spacing:.8px;text-transform:uppercase;color:var(--tx-3);margin:6px 0 3px}
.tool.running .tool-ico{animation:pulse 1.1s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}

/* markdown */
.md p{margin:.5em 0}
.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:.9em 0 .4em;font-weight:600;line-height:1.3}
.md h1{font-size:17px}.md h2{font-size:15.5px}.md h3{font-size:14px;color:var(--tx-2)}
/* A document goes deeper than an answer does: below the third level the size stops falling and
   the difference is carried by colour and weight, or a heading ends up smaller than its text. */
.md h4,.md h5,.md h6{font-size:13px;color:var(--tx-2)}
.md h5,.md h6{font-weight:500;color:var(--tx-3)}
.md ul ul,.md ol ol,.md ul ol,.md ol ul{margin-top:.18em;margin-bottom:.18em}
.md ul,.md ol{margin:.45em 0 .45em 1.3em}
.md li{margin:.18em 0}
.md code{font:12.4px var(--mono);background:var(--bg-4);padding:1px 5px;border-radius:4px;color:var(--acc-2)}
.md pre{background:var(--bg-2);border:1px solid var(--line);border-radius:8px;padding:11px 13px;overflow-x:auto;margin:.6em 0}
.md pre code{background:none;padding:0;color:var(--tx-2);font-size:12.2px;line-height:1.55}
.md a{color:var(--acc-2)}
.md strong{color:var(--strong);font-weight:600}
.md blockquote{border-left:2px solid var(--line-2);padding-left:11px;color:var(--tx-2);margin:.5em 0}
/* ── A TABLE IS AN ANSWER, NOT A PRINTOUT ───────────────────────────────────────
   A grey grid «like in a console» is read line by line, while columns exist precisely so
   that the eye can travel down them without counting the bars. Hence: a tinted, sticky head
   (a long table does not lose its headings), every other row shaded, no vertical lines — air
   does their work — and the whole block in a rounded frame, so that it reads as ONE object
   inside the answer. Colours only through tokens: the theme will override them.
   THE FRAME BELONGS TO A WRAPPER, THE WIDTH TO THE TABLE (03.09). `display:block` on the
   <table> was what bought the horizontal scroll on a phone, and it cost the thing it was
   drawing: a block is not a table box, so the frame took the answer's whole width while the
   rows inside shrank to their own content — a four-column table drew a box with 40% of it
   empty, and that emptiness is what the owner's screenshot marked with three red arrows. The
   scroll moved onto `.tbl-wrap` (built in `md()` right next to the <table>), and the table is
   a table again: `width:100%`, so the free width goes to the widest text column instead of to
   nothing, and the block lines up with the paragraphs and code above and below it. */
.md .tbl-wrap{display:block;overflow-x:auto;max-width:100%;margin:.75em 0;
  border:1px solid var(--line);border-radius:10px;background:var(--bg-2)}
.md table{border-collapse:separate;border-spacing:0;width:100%;font-size:12.6px;
  background:none;border:0}
.md th,.md td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--hair);vertical-align:top}
/* The first column is the row's NAME — that is the column the eye runs down looking for its
   line, so it is inked like a heading rather than like the data beside it. */
.md tbody td:first-child{color:var(--strong)}
/* ON A PHONE A TABLE SCROLLS INSTEAD OF SQUEEZING. Pressed into 430px, the first column of a
   four-column table became six lines of one word each — a shape that has stopped being a
   table, and it was doing that long before the frame was fixed. A floor under the width hands
   the row its line back and finally gives the wrapper something to scroll, which is what the
   horizontal pan on it was built for.
   ⛔ THE FLOOR IS PUT UNDER THE CELL, NOT UNDER THE TABLE, and that is not a matter of taste:
   `min-width:min(560px,max-content)` is INVALID — a CSS math function does not take an
   intrinsic keyword — so the whole declaration is dropped in silence and the phone stays
   squeezed while the rule reads as if it worked. A floor per cell also keeps itself honest
   with no arithmetic: four columns pass 430px and the table scrolls, two columns do not reach
   it and the table stays whole, no scroll and no silly gaps. */
@media(max-width:860px){ .md th,.md td{min-width:8.5em} }
.md thead th{position:sticky;top:0;z-index:1;background:var(--bg-3);color:var(--strong);
  font-weight:600;white-space:nowrap;border-bottom:1px solid var(--line-2);
  box-shadow:inset 0 -2px 0 var(--acc)}
.md tbody tr:nth-child(even){background:var(--bg-3)}
.md tbody tr:last-child td{border-bottom:0}
.md tbody tr:hover{background:var(--bg-4)}
/* A number in a cell is monospaced: a column of figures is obliged to line up by digits. */
.md td{font-variant-numeric:tabular-nums}

/* ── HTML straight in the feed: a card with a sandboxed frame ── */
.htmlcard{margin:.7em 0;border:1px solid var(--line);border-radius:12px;overflow:hidden;
  background:var(--bg-2);box-shadow:0 6px 22px var(--shadow)}
.hc-bar{display:flex;align-items:center;gap:6px;padding:5px 7px 5px 10px;
  background:var(--bg-3);border-bottom:1px solid var(--line)}
.hc-tag{flex:1;font:10.5px/1 var(--mono);letter-spacing:.09em;color:var(--tx-3)}
.hc-btn{font-size:11px;padding:3px 9px;border-radius:8px;border:1px solid var(--line-2);
  background:var(--bg-4);color:var(--tx-2);cursor:pointer}
.hc-btn:hover{color:var(--tx);border-color:var(--acc)}
.hc-frame{display:block;width:100%;height:180px;border:0;background:Canvas;color-scheme:light dark;transition:height .18s}
.hc-src{display:none;margin:0;border-top:1px solid var(--line);max-height:340px;overflow:auto}
.htmlcard.showsrc .hc-frame{display:none}
.htmlcard.showsrc .hc-src{display:block}

/* ── A path in an answer is a DOOR. A report opens as a frame over the feed, a picture with
   the magnifier, a folder and a file with this machine's file explorer. All four look the
   same: a chip with an icon that shows what it is; a report also has «↗» on the right —
   open in a tab. ── */
/* INLINE-BLOCK, NOT INLINE-FLEX — the copy is why. A flex container BLOCKIFIES its children,
   and the browser puts a line break around every block-level box when it serialises a selection:
   the pill was copied as «your ⏎MEMORY.md⏎ is over its size limit», i.e. one sentence torn into
   three lines. Inline-block draws the same pill (measured: same box, same ellipsis) and comes
   out of the clipboard as one line. `gap` goes with flex, so the spacing is margins now. */
.htmlink,.pathlink{display:inline-block;max-width:100%;vertical-align:baseline;white-space:nowrap;
  line-height:15px;   /* the flex box was 15px of content tall; without this the pill takes the prose's */
  padding:1px 3px 1px 7px;border:1px solid var(--line-2);border-radius:999px;background:var(--bg-3)}
.pathlink:not(.htmlink){padding-right:7px}
.pathlink[data-kind="dir"]{border-color:var(--line-2);border-style:dashed}
/* WHAT IS COPIED OUT OF A CHIP IS THE FILE NAME, AND ONLY THE FILE NAME.
   The name used to be a `<button>`, and every browser's own stylesheet marks a form control
   `-webkit-user-select:none`: selecting a paragraph and copying it pasted «your  is over its
   size limit» — the file the sentence was about fell out of BOTH clipboard flavours, and out of
   `getSelection()` with them (the owner, 05.09). It is a `<span role="button">` now, selectable
   like the prose around it. The icon and the «↗» stay OUT of the copy on purpose: they are the
   chip's furniture, and «📄report.html↗» pasted into a letter is not what was on the screen. */
.hl-ico{display:inline-block;vertical-align:middle;margin-right:5px;font-size:11px;line-height:1;
  user-select:none;-webkit-user-select:none}
.hl-name{display:inline-block;vertical-align:middle;font-size:12.4px;color:var(--acc);
  background:none;border:0;padding:0;cursor:pointer;user-select:text;-webkit-user-select:text;
  max-width:min(46ch,calc(60vw / var(--ui-scale,1)));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hl-name:hover{text-decoration:underline}
.hl-name:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:5px}
.hl-tab{display:inline-grid;vertical-align:middle;margin-left:5px;place-items:center;
  width:20px;height:20px;border-radius:50%;
  color:var(--tx-2);background:var(--bg-4);text-decoration:none;font-size:11px;
  user-select:none;-webkit-user-select:none}
.hl-tab:hover{color:var(--tx);background:var(--acc);color:var(--on-acc)}

/* ── A CHIP THAT IS NOT A FILE ON THIS DISK ──────────────────────────────────────────────
   Two of them, and both have to be legible as «this is git talking» BEFORE they are pressed,
   because everything the ordinary chip promises — the explorer, the app, the download — is
   exactly what these two cannot do. A DASHED border says it without a word and without a
   colour: the panel already uses dashes for the chip whose target is not an ordinary file
   (`[data-kind="dir"]`) and for the guessed option chips, so the reading is learned once.
   ⛔ NOT A COLOUR OF ITS OWN. Eleven palettes, and a hex here would be invisible in some of
   them; the border is `--line-2` like every other chip, the ink stays `--acc`. */
.pathlink.gitlink,.pathlink.branchlink{border-style:dashed}
/* The branch name a file is read out of, riding ON the chip rather than in a tooltip: there is
   no hover on a phone, and «which branch» is the fact that explains why the file is not where
   the reader went to look. It is a second door (the branch card), so it answers the keyboard
   and says so on hover. `user-select:none` for the same reason the icon has it — it is the
   chip's furniture, and «unfinished.js unfinished-mark» is not what was on the screen. */
.gl-ref{display:inline-block;vertical-align:middle;margin-left:6px;padding:0 6px;
  border-radius:999px;background:var(--bg-4);color:var(--tx-2);
  font-size:10.5px;line-height:15px;max-width:18ch;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;cursor:pointer;user-select:none;-webkit-user-select:none}
.gl-ref:hover{background:var(--acc);color:var(--on-acc)}
.gl-ref:focus-visible{outline:2px solid var(--acc);outline-offset:2px}

/* ── The preview window over the feed ── */
#htmlview{display:none;z-index:245}
#htmlview.open{display:grid}
/* ── THE WINDOW IS MEASURED FROM THE VEIL'S BOX, NOT FROM THE SCREEN ──
   `96vw` is the width of the SCREEN, while the window lives INSIDE .modal-back with its
   padding: on a 390px phone that is 374px of window against 370px of box, and it stuck out
   to the right by exactly those 4px («it sticks out to the right a little» — the owner,
   19.08). The height was worse: 96vh is counted from the LARGE viewport while the box is
   counted from 100svh, and the top of the window with the cross went off the edge.
   Percentages are taken from the parent, which is already divided by --ui-scale (see
   .modal-back), so there is no division here at all any more: one measure instead of two that
   disagree. A size picked by hand arrives inline from app.js and overrides these values. */
.hv-win{position:relative;width:min(1100px,100%);
  height:min(900px,100%);min-width:0;min-height:min(200px,100%);
  max-width:100%;max-height:100%;
  display:flex;flex-direction:column;
  background:var(--bg-2);border:1px solid var(--line-2);border-radius:14px;overflow:hidden;
  box-shadow:0 30px 80px var(--shadow)}
/* The size corner. It is taken with a finger too (touch-action:none), hence 20px and not 10. */
.hv-grip{position:absolute;right:0;bottom:0;width:20px;height:20px;cursor:nwse-resize;
  touch-action:none;z-index:3;opacity:.5;
  background:linear-gradient(135deg,transparent 0 46%,var(--tx-3) 46% 54%,transparent 54% 70%,
    var(--tx-3) 70% 78%,transparent 78%)}
.hv-grip:hover{opacity:1}
.hv-win.sizing{user-select:none}
/* While the corner is dragged, the report frame must not catch the mouse: a foreign origin eats the gesture. */
.hv-win.sizing .hv-frame{pointer-events:none}
.hv-top{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 12px;
  background:var(--bg-3);border-bottom:1px solid var(--line)}
/* The file name large, the path under it small: from a phone there is nothing to read a path
   with, and «is this the right report» has to be understood at a single glance. */
.hv-title{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;overflow:hidden}
.hv-name{font:600 13px var(--sans);color:var(--tx);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
/* WHEN THIS ONE WAS SAVED — the second half of «show its name and the date and time» (17.09). It
   rides on the title line, not among the buttons: it is a fact about the file, and the button row
   may not grow a member that appears only for some files. */
.hv-line1{display:flex;align-items:baseline;gap:8px;min-width:0}
.hv-when{font:10.5px var(--mono);color:var(--tx-2);font-style:normal;white-space:nowrap;flex:0 0 auto}
.hv-nav{display:inline-flex;gap:2px;margin-right:6px;flex:0 0 auto}
.hv-nav .hv-btn{min-width:30px;text-align:center}
.hv-path{font:10.5px var(--mono);color:var(--tx-3);font-style:normal;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
.hv-btn{font-size:11.5px;padding:4px 10px;border-radius:8px;border:1px solid var(--line-2);
  background:var(--bg-4);color:var(--tx-2);text-decoration:none}
.hv-btn:hover{color:var(--tx);border-color:var(--acc)}
/* GREY IS A SENTENCE, NOT A DECORATION: «this door exists, and this file is not the file for
   it». It is a real `disabled`, so the press does nothing at all — a button that answers
   nothing is read as a breakage and pressed again. */
/* An <a> takes no `disabled`, so a dead LINK is one with no `href` — inert, unfocusable, and
   invisible to the middle button and to Ctrl+click. `aria-disabled` is what makes it look the
   part and what a screen reader reads; the two are set together by `hvDoor`. */
.hv-btn[disabled],.hv-btn[aria-disabled="true"]{opacity:.4;cursor:default}
.hv-btn[disabled]:hover,.hv-btn[aria-disabled="true"]:hover{color:var(--tx-2);border-color:var(--line-2)}
.hv-btn.hidden{display:none}
/* THE TWO PHONE CONTROLS DO NOT EXIST AT THE DESK (the owner, 24.09: «в стационарной десктопной
   версии пусть пока все так останется»). They are drawn only inside the phone media query below,
   and an #id there beats an #id here by source order — so this pair must stay ABOVE it. */
/* ⋯ IS THE ONE CONTROL THAT DOES NOT EXIST AT THE DESK (the owner, 24.09: «в стационарной
   десктопной версии пусть пока все так останется») — everything it holds is a visible button here.
   «📄 Copy file» used to hide with it, and since 25.09 it does not: it is a door of its own («их
   две»), and its promise — the FILE, not the text — has no other button anywhere. */
#hv-more{display:none}
/* THE WORDLESS COPY. «Вот это просто Copy сделай просто обычной иконкой, как это все делают.
   Стандартную иконку Copy, можно же без текста.» — the two-sheets glyph, drawn as an SVG so it
   takes the button's own ink (`currentColor`) and greys with it, and squarer padding because there
   is no label to balance. */
.hv-ico{display:inline-flex;align-items:center;justify-content:center;padding:4px 7px;vertical-align:middle}
.hv-ico svg{display:block}
#hv-grab{display:inline-flex;align-items:center;gap:4px}
#hv-grab .hv-grab-l{font-style:normal}
/* «✓ copied» — the clipboard is invisible, so the button answers with itself, exactly like the
   copy button on a code block. */
.hv-grab.done{border-color:var(--ok,var(--acc));color:var(--tx)}
.hv-btn.done{border-color:var(--ok,var(--acc));color:var(--tx)}
/* THE ⋯ SHEET. It is not a second set of doors: every row clicks the button that is hidden above
   it, and mirrors that button's own `disabled` and its own `title` — a greyed row says why, in
   the same words the greyed button would have said. */
.hv-sheet{position:absolute;z-index:6;top:calc(100% + 4px);right:6px;min-width:216px;
  max-width:min(320px,92vw);background:var(--bg-3);border:1px solid var(--line);border-radius:12px;
  padding:6px;box-shadow:0 14px 38px rgba(0,0,0,.45);display:flex;flex-direction:column;gap:2px}
.hv-sheet.hidden{display:none}
.hv-mi{display:flex;align-items:center;gap:9px;width:100%;padding:10px;border:0;border-radius:8px;
  background:transparent;color:var(--tx);font:13px var(--sans);text-align:left}
.hv-mi:hover{background:var(--bg-4)}
.hv-mi[disabled]{opacity:.38}
.hv-mi-i{flex:0 0 18px;text-align:center;font-size:14px}
.hv-mi-n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A row that holds controls rather than being one: the pager, the reading scale. */
.hv-mi-row{display:flex;align-items:center;gap:6px;padding:6px 10px 2px}
.hv-mi-row .hv-mi-n{flex:1}
.hv-mi-row .hv-btn{display:inline-block;flex:0 0 auto;min-width:34px;text-align:center;padding:6px 8px}
.hv-sep{height:1px;background:var(--line);margin:4px 6px}
/* THE FILE NAME MATTERS MORE THAN THE LABELS ON THE BUTTONS. On the phone four labelled
   buttons ate the whole line, and of «which file is this» only «gate-n…» was left. Narrow —
   icons; wider — words. */
.hv-lbl{font-style:normal;margin-left:5px}
/* ON THE PHONE THE FILE NAME LIVES ON A LINE OF ITS OWN. There are nine buttons in the head
   (plus three for the zoom), and in 390px they left the name not one pixel — and along with
   it they pushed the ✕ off the edge too, that is, there was nothing to close the window with.
   The heading takes the whole first line, the buttons wrap under it; the zoom buttons
   themselves are squeezed — the percent figure is legible as it is. */
@media (max-width:760px){.hv-lbl{display:none}
  .hv-btn{padding:4px 8px;font-size:13px}
  .hv-top{flex-wrap:wrap;gap:4px;row-gap:6px;padding:6px 8px}
  .hv-title{flex:1 0 100%}
  /* The buttons are squeezed so that the ✕ stays on ONE line with them: a cross wrapped onto
     a third line both looks lost and eats height away from the text itself.
     ⛔ MEASURED 11.09 AT 390px, AND IT HAD QUIETLY STOPPED BEING TRUE. Nine buttons and the zoom
     trio filled the line to the pixel, so the ✕ dropped onto a third row and sat alone at its
     LEFT edge — exactly what this rule exists to prevent, and it had been like that long enough
     for nobody to notice, because the gate check that says so never ran (it stood below a crash).
     Every number here is arithmetic against 390px: a new button in this header spends ~34px and
     must be measured again — `npm run composer`, «and ✕ stayed in the button row». The `auto`
     margin is the belt: whatever line the ✕ ends up on, it ends up at the right of it, where a
     thumb looks for a close button, instead of at the left where it reads as a stray glyph. */
  .hv-btn{padding:4px 4px}
  .hv-zb{min-width:20px;padding:4px 3px;font-size:12px}
  #hv-zp{min-width:34px;font-size:10px}
  /* ⛔ THE ARITHMETIC ABOVE RAN OUT ON 12.09, AND THE ANSWER IS NOT A NARROWER BUTTON.
     Nothing in this head hides any more (a door that moves is a door pressed by mistake), so
     the row is ALWAYS the full twelve — two more than the line it had already filled to the
     pixel. Squeezing further would make eleven identical grey stamps nobody can tell apart.
     So the ✕ STOPS COMPETING FOR A LINE AT ALL: it is lifted out of the flex flow and pinned to
     the top-right corner, with the padding that used to hold it left reserved for it. It cannot
     wrap, cannot be pushed and cannot be reached by any future button added to this head; the
     buttons wrap below the title over as many rows as they need.
     ⛔ TWO REORDERINGS WERE TRIED FIRST AND BOTH MADE IT WORSE, because `order` decides the
     sequence of a line but not WHICH line, and whether this head wraps at all depends on
     `--ui-scale`: the panel lives under CSS `zoom`, so 390 screen pixels can be a thousand
     layout pixels and the whole row then fits on ONE line. With the title at `flex:1 0 100%`
     the ✕ dropped to the second row at its LEFT edge (x=14 of 390); with `flex:1 1 0` the row
     stopped wrapping and the ✕ landed in the MIDDLE, between the title and the buttons (x=118).
     A corner does not have opinions about either. */
  .hv-top{position:relative;padding-right:34px}
  .hv-x{position:absolute;top:6px;right:8px}
  /* ═══ ONE ROW ON THE PHONE (the owner, 24.09) ═══
     «Вверху ничего из надписей не надо. Это бессмысленно с такой мелкотой, там все равно никто
     ничего не прочитает. Оставь только какие кнопки.» — and of the buttons, one big copy, the
     three dots, and the ✕. Everything else is a row in the sheet, which clicks these same
     hidden buttons. The head stops wrapping, so the ✕ comes back INTO the row (it was pinned to
     the corner precisely because the row used to wrap onto three lines) and closes it at the
     right, where a thumb looks for it. */
  .hv-top{flex-wrap:nowrap;padding:6px 8px;gap:6px}
  .hv-x{position:static;flex:0 0 auto}
  .hv-top .hv-title,.hv-top .hv-nav,.hv-top #hv-tab,.hv-top #hv-md,.hv-top #hv-read,
  .hv-top #hv-diff,.hv-top #hv-app,.hv-top #hv-save,.hv-top #hv-copy,
  .hv-top #hv-path-copy,.hv-top #hv-dir,.hv-top .hv-zoom{display:none}
  #hv-grab{display:inline-flex;align-items:center;justify-content:center;gap:7px;
    flex:1 1 auto;min-width:0;padding:10px;font-size:14px;font-weight:600;
    color:var(--tx);border-color:var(--acc)}
  #hv-grab .hv-grab-l{font-style:normal;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  #hv-more{display:inline-block;flex:0 0 auto;padding:10px 13px;font-size:16px;line-height:1}}
/* The frame's box clips a magnified report; the frame inside is narrowed by the layout and
   stretched back with a transform — that way the «zoom» comes out real (the report's lines
   re-wrap), even though we cannot reach inside a foreign origin. */
.hv-fwrap{position:relative;flex:1;min-height:0;overflow:hidden;background:Canvas;color-scheme:light dark}
.hv-fwrap.hidden{display:none}
.hv-frame{position:absolute;top:0;left:0;width:100%;height:100%;border:0;background:Canvas;color-scheme:light dark;
  transform-origin:0 0}
.hv-frame.hidden{display:none}
/* The reading zoom buttons: the same meaning as the picture's magnifier, the same order «− % +». */
.hv-zoom{display:flex;align-items:center;gap:2px;flex:0 0 auto}
.hv-zb{min-width:26px;padding:4px 7px;text-align:center;font-variant-numeric:tabular-nums}
#hv-zp{min-width:44px}
/* A file's text: it is read with the eyes, hence monospaced, with long lines wrapped and
   a scroll of its own — the preview window must not drag the feed along with it. */
.hv-text{flex:1;min-height:0;overflow:auto;padding:12px 14px;background:var(--bg-1);
  font:12.5px/1.55 var(--mono);color:var(--tx);white-space:pre-wrap;word-break:break-word;tab-size:2;
  /* The reading zoom grows the TYPE SIZE, not a picture of the text: the line wrapping stays,
     and a magnified log does not run a kilometre off to the right. The multiplier is its own,
     the feed's --read-scale will not do here — this is a different place with a different
     accustomed value. */
  font-size:calc(12.5px * var(--hv-zoom,1))}
/* These two say «the source view», and the source view is always a DIRECT child of the box:
   a `>` here is what keeps them off the drawn markdown, whose type sizes are its own. */
.hv-text>pre,.hv-text>pre code{font-size:inherit}
.hv-text.hidden{display:none}
.hv-text .hv-none{display:flex;flex-direction:column;align-items:center;gap:10px;
  padding:44px 16px;font:14px var(--sans);color:var(--tx-2);text-align:center;white-space:normal}
.hv-text .hv-none b{font-size:15px;color:var(--tx)}
/* The press that asks again after a dead end — see hvNone(). It is the only thing in this box
   that can be aimed at, so it is drawn as a button and not as another grey line. */
.hv-text .hv-none .hv-again{margin-top:4px;padding:7px 16px;font:14px var(--sans);cursor:pointer;
  color:var(--tx);background:var(--bg-2);border:1px solid var(--bd);border-radius:8px}
.hv-text .hv-none .hv-again:hover{border-color:var(--acc);color:var(--acc)}
/* A CSV is appended to the box as a table of its own; a table inside a drawn .md is the
   feed's table and must keep the feed's look, hence `>` on every one of these. */
.hv-text>table{border-collapse:collapse;font:12px var(--mono);white-space:pre}
.hv-text>table th,.hv-text>table td{border:1px solid var(--line-2);padding:3px 8px;text-align:left}
.hv-text>table th{position:sticky;top:-12px;background:var(--bg-2);font-weight:600}
.hv-text>table tr:nth-child(even) td{background:var(--bg-2)}
/* ═══ THE SPEND LINE AT THE END OF AN ANSWER ═══
   Two quantities are read in it: how many CALLS the turn cost and how much of the key it ate.
   A background, not another shade of grey: a shade in monospaced small print is not found by
   the eye, while a background is found at once. The colours are theme tokens, so the line
   stays legible in any palette; the figures are monospaced and tabular so that neighbouring
   turns compare in a column. */
/* The typesetting: --figures instead of monospace (the owner 26.08: «there is some sort of slash
   inside the zero… everything is bold, nothing is legible»), contrast one step higher
   (--tx-2), while the labels (till, R, W, O) and the separators are, on the contrary, muted —
   in an instrument one hunts for FIGURES, the letters only say which is which. Weight 800 on
   the plates ate the shape of a digit: 600 is enough, the background does the work. */
.md .statline{font:500 12.5px/1.85 var(--figures);color:var(--tx-2);letter-spacing:.012em;
  font-variant-numeric:tabular-nums lining-nums;font-feature-settings:'tnum' 1,'lnum' 1,'zero' 0}
.md .statline .sv{font-weight:600;padding:1px 7px;border-radius:6px;white-space:nowrap}
.md .statline .sv-turns{color:var(--blue);background:color-mix(in srgb,var(--blue) 18%,transparent)}
.md .statline .sv-key{color:var(--acc);background:color-mix(in srgb,var(--acc) 18%,transparent)}
.md .statline .sv-cost{color:var(--ok);background:color-mix(in srgb,var(--ok) 16%,transparent)}
.md .statline .lb{font-style:normal;font-weight:400;color:var(--tx-3)}
.md .statline .sp{font-style:normal;color:var(--tx-3);opacity:.45;padding:0 3px}
/* ═══ A LINK IN AN ANSWER IS THE SAME DOOR AS A PATH, AND IT LOOKS LIKE A DOOR ═══
   It opens HERE (a window over the feed), which is why it has an underline and an arrow:
   «this will take you off the page» is a promise the panel precisely does NOT make, and the
   eye must see the difference between a link outward and a file chip. A long address has no
   right to tear the layout apart: it wraps at any character. */
.md a.weblink{color:var(--acc-2);text-decoration:underline;text-decoration-color:var(--line-2);
  text-underline-offset:2px;word-break:break-word}
.md a.weblink::after{content:'↗';font-size:.82em;margin-left:2px;color:var(--tx-3);vertical-align:super}
.md a.weblink:hover{text-decoration-color:var(--acc)}
/* ═══ A MARKDOWN FILE DRAWN AS A DOCUMENT ═══
   The look is the feed's (.md — one set of rules for both places, or a table would come out
   one way in an answer and another way in the file it quotes). Only two things are this
   window's own: the box inherits `white-space:pre-wrap` from the source view and has to give
   it back, or every blank line of the source would stay a blank line of the document; and the
   sizes are `em`, so the reading zoom of THIS window (`--hv-zoom`) still moves all of them. */
.hv-text .hv-md{font:calc(13.5px * var(--hv-zoom,1))/1.62 var(--sans);color:var(--tx);
  white-space:normal;word-break:normal;padding-bottom:12px}
.hv-text .hv-md h1{font-size:1.5em}
.hv-text .hv-md h2{font-size:1.28em}
.hv-text .hv-md h3{font-size:1.1em}
.hv-text .hv-md h4,.hv-text .hv-md h5,.hv-text .hv-md h6{font-size:1em}
.hv-text .hv-md code{font-size:.87em}
.hv-text .hv-md pre{white-space:pre}
.hv-text .hv-md pre code{font-size:.85em}
.hv-text .hv-md table{font-size:.92em}
/* The first heading of a file starts at the very top: a document does not begin with a gap. */
.hv-text .hv-md>:first-child{margin-top:0}

/* A foreign page shown as text: it is read with the eyes, so not monospaced. */
.hv-text .hv-plain{font:14px/1.62 var(--sans);white-space:pre-wrap;color:var(--tx)}
/* ═══ A FILE EDIT: THE COLOUR CARRIES THE MEANING, THE SIGN STAYS WHERE IT IS ═══
   `+`/`−` are deliberately not removed: an edit is copied whole (into a letter, into another
   chat), and without the signs it stops being a patch. The background is a tinted theme token,
   not «green/red»: in a light palette a pure colour burns the eyes, while this one stays
   legible in any. */
.hv-diffview{margin:0;font:12.5px/1.5 var(--mono);white-space:pre;overflow-x:auto}
.hv-diffview>div{padding:0 6px}
.hv-diffview .d-add{background:color-mix(in srgb,var(--ok) 16%,transparent);color:var(--tx)}
.hv-diffview .d-del{background:color-mix(in srgb,var(--err) 16%,transparent);color:var(--tx-2)}
.hv-diffview .d-hunk{color:var(--think);font-weight:600;margin-top:6px}
.hv-diffview .d-file{color:var(--tx-3)}
#hv-diff.on{color:var(--tx);border-color:var(--acc);background:color-mix(in srgb,var(--acc) 14%,transparent)}
.hv-cut{margin-top:10px;padding:6px 10px;border-radius:8px;background:var(--bg-2);
  font:11.5px var(--sans);color:var(--tx-3);white-space:normal}
/* A READY COMMAND IS NOT SELECTED WITH THE MOUSE — IT IS TAKEN WITH ONE BUTTON.
   The button lives in the corner of the block and appears on hover; on the phone there is no
   hover, so there it is always visible (see the media query below) — otherwise there would be
   nothing to copy with. */
.codeblk{position:relative}
/* The cost meter at the top of an answer — hidden by ⚙ → General → «hide the cost and cache meter».
   A class on <html>, so it applies to the whole history at once (web/app.js → METER_SHAPE). */
html.no-meter .codeblk.meter-blk{display:none}
.codeblk>.cp-btn{position:absolute;top:6px;right:6px;z-index:2;opacity:0;transition:opacity .12s;
  padding:3px 8px;border-radius:7px;border:1px solid var(--line-2);background:var(--bg-2);
  color:var(--tx-2);font:11px var(--sans);cursor:pointer}
.codeblk:hover>.cp-btn,.codeblk>.cp-btn:focus-visible{opacity:1}
.codeblk>.cp-btn:hover{color:var(--tx);border-color:var(--acc)}
.codeblk>.cp-btn.done{opacity:1;color:var(--ok,var(--acc));border-color:var(--ok,var(--acc))}
@media (hover:none){.codeblk>.cp-btn{opacity:.75}}

/* ▶ RUN — AND IT IS DRAWN LIKE A DOOR, NOT LIKE THE COPY BUTTON.
   Copy costs nothing and hides until you hover; this one spends the machine, so it stands in the
   open under the block, at full opacity, on a phone as well. It is the one place in this panel
   that runs what a model wrote, and a button that has to be hunted for is a button whose meaning
   gets guessed. */
/* THE BAR IS PART OF THE BLOCK, NOT A THING UNDER IT. `.md pre` carries its own border, radius
   and bottom margin, so a strip simply appended would float half a line below a fully rounded
   box and read as belonging to the next paragraph. The class is put on by `showRuns` rather than
   taken from `[data-run]`: the mark is on every runnable block, the BAR only on the ones the
   owner's window actually built one on. */
.codeblk.run-on{margin-bottom:.6em}
.codeblk.run-on>pre{margin-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}
.codeblk>.run-bar{display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  padding:7px 10px;border:1px solid var(--line);border-top:0;background:var(--bg-2)}
.codeblk>.run-bar:last-child{border-radius:0 0 8px 8px}
.codeblk>.run-bar .run-btn{padding:4px 11px;border-radius:8px;border:1px solid var(--line-2);
  background:var(--bg);color:var(--tx-2);font:11.5px var(--sans);cursor:pointer;
  transition:color .12s,border-color .12s,background .12s}
.codeblk>.run-bar .run-btn:hover:not(:disabled){color:var(--tx);border-color:var(--acc);
  background:color-mix(in srgb,var(--acc) 12%,transparent)}
.codeblk>.run-bar .run-btn:disabled{opacity:.45;cursor:default}
/* The three seconds before anything is spawned: the button says what it is about to do, and the
   ✕ beside it is the whole undo. It is tinted, not animated — a moving button invites a second
   press, and a second press on this one is exactly what the pause exists to prevent. */
.codeblk>.run-bar .run-btn.arming{color:var(--tx);border-color:var(--acc);
  background:color-mix(in srgb,var(--acc) 20%,transparent)}
.codeblk>.run-bar .run-cancel{padding:4px 10px;border-radius:8px;border:1px solid var(--line-2);
  background:var(--bg);color:var(--tx-3);font:11.5px var(--sans);cursor:pointer}
.codeblk>.run-bar .run-cancel:hover{color:var(--tx);border-color:var(--tx-3)}
.codeblk>.run-bar .run-where{font:11px var(--sans);color:var(--tx-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}

/* THE OUTPUT BOX — the terminal, such as it is, and it never grows past a screenful.
   `max-height` plus its own scroll: a build that prints two thousand lines must not push the
   rest of the answer off the screen, and the box follows its own tail while you are at the
   bottom of it (see `write()` — it stops following the moment you scroll up to read). */
.codeblk>.run-out{border:1px solid var(--line);border-top:0;background:var(--bg-2);
  border-radius:0 0 8px 8px}
.codeblk>.run-out .run-head{display:flex;align-items:center;gap:10px;padding:6px 10px;
  font:11px var(--sans);color:var(--tx-3)}
.codeblk>.run-out .run-state{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.codeblk>.run-out.live .run-state{color:var(--tx-2)}
.codeblk>.run-out.done .run-state{color:var(--ok,var(--acc))}
.codeblk>.run-out.bad .run-state{color:var(--err)}
.codeblk>.run-out .run-stop{padding:3px 9px;border-radius:7px;border:1px solid var(--line-2);
  background:var(--bg);color:var(--tx-2);font:11px var(--sans);cursor:pointer}
.codeblk>.run-out .run-stop:hover{color:var(--tx);border-color:var(--err)}
.codeblk>.run-out .run-log{margin:0;padding:0 10px 9px;max-height:42vh;overflow:auto;
  font:11.5px/1.5 var(--mono);color:var(--tx-2);white-space:pre-wrap;word-break:break-word}
.codeblk>.run-out .run-log:empty{padding-bottom:0}

/* result strip */
.result{display:flex;gap:14px;font:11px var(--mono);color:var(--tx-3);padding:7px 0 2px;border-top:1px dashed var(--line);margin-top:10px;flex-wrap:wrap}

/* ═══ permissions ═══ */
.perm-dock{padding:0 22px;max-width:var(--read-w);margin:0 auto;width:100%}

/* The «go back to the turn» strip is gone: with the session pool (server/pool.js) a turn
   belongs to a rail row, runs by itself and is visible right there — there is nowhere and no
   reason to go back. */
.perm{background:var(--bg-3);border:1px solid var(--warn);border-radius:var(--r);padding:12px 14px;margin-bottom:10px}
.perm-t{font-weight:600;font-size:13px;margin-bottom:3px}
.perm-d{font-size:12px;color:var(--tx-2);margin-bottom:8px}
.perm pre{font:11.5px var(--mono);background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:8px;max-height:180px;overflow:auto;margin-bottom:9px;color:var(--tx-2)}
.perm-btns{display:flex;gap:8px}
.perm-btns button{padding:6px 13px;border-radius:7px;border:1px solid var(--line-2);background:var(--bg-4);color:var(--tx);cursor:pointer;font:500 12.5px var(--sans)}
.perm-btns .yes{background:var(--ok);border-color:var(--ok);color:var(--on-ok)}
.perm-btns .always{background:var(--bg-4);border-color:var(--ok);color:var(--ok)}
.perm-btns .no{border-color:var(--err);color:var(--err)}

/* ═══ WAITING FOR THE KEY ═══
   The card lives next to the permissions and for the same reason: this is the only thing a
   person is obliged to see immediately and without hunting. The tone is amber, not red —
   nothing broke; the work is intact and will leave by itself (the owner, 01.09). */
.wait-dock{padding:0 22px;max-width:var(--read-w);margin:0 auto;width:100%}
/* NAME IT `.wait-card`, NEVER A BARE `.wait`. The rail has a waiting beacon of its own -
   `.dot.wait` (web/tree.js -> svgWait) - and a single-word class matched that 8x8 dot as
   well: this card's border (1px, 3px on the left) plus `padding:11px 14px 12px` blew the dot
   up to exactly 32x25, so the hourglass hung outside its row. That is what «какая-то круглая
   штука, больше строки» (owner, 03.09) actually was, and two rounds of flexbox fixes went
   past it because the numbers looked like a min-width story. The card shares nothing with
   the beacon but the word; every other part of it already carries the `wait-` prefix. */
.wait-card{background:var(--bg-3);border:1px solid var(--warn);border-left-width:3px;
  border-radius:var(--r);padding:11px 14px 12px;margin-bottom:10px}
.wait-card.down{border-color:var(--err);border-left-color:var(--err)}
.wait-h{display:flex;align-items:center;gap:8px;margin-bottom:5px}
.wait-i{font-size:14px;line-height:1;animation:wait-breathe 2.6s ease-in-out infinite}
.wait-t{font:600 13px var(--sans);color:var(--tx)}
.wait-w{font-size:12px;color:var(--tx-2);margin-bottom:6px}
/* The main sentence is the most legible one in the card: it is what answers «what am I waiting for». */
.wait-p{font-size:12.5px;line-height:1.45;color:var(--tx);margin-bottom:10px}
.wait-btns{display:flex;gap:8px;flex-wrap:wrap}
.wait-btns button{padding:6px 13px;border-radius:7px;border:1px solid var(--line-2);
  background:var(--bg-4);color:var(--tx);cursor:pointer;font:500 12.5px var(--sans)}
.wait-btns button:disabled{opacity:.5;cursor:default}
.wait-btns .go{border-color:var(--acc);color:var(--acc)}
.wait-btns .no{border-color:var(--line-2);color:var(--tx-3)}
.wait-f{font:11px var(--mono);color:var(--tx-3);margin-top:9px;word-break:break-word}
@media (prefers-reduced-motion: reduce){.wait-i{animation:none}}

/* ═══ composer ═══ */
.composer{border-top:1px solid var(--line);background:var(--bg-2);padding:11px 22px 12px}
.composer-row{position:relative;max-width:var(--read-w);margin:0 auto;display:flex;gap:9px;align-items:flex-end}
/* The height travels smoothly: dictation adds lines in batches, and an instant jump of the
   field reads as a jerk of the whole page (autoGrow moves the feed by the same delta). */
/* LINE HEIGHT 1.3, NOT 1.5 (the owner, 01.09): «the distance between the lines a bit tighter,
   so that more text fits in». The field has a CEILING (220px) — which means the line spacing
   here is not taste but the number of visible lines: on the phone 16px/1.5 gave 9 lines before
   the internal scroll, 16px/1.3 gives 10. The multiplier is unitless — both type-size
   overrides below inherit it (14px at the desk, 16px on the phone: otherwise iOS zooms the
   page). */
.input{flex:1;background:var(--bg-3);border:1px solid var(--line);border-radius:var(--r);padding:11px 13px;color:var(--tx);font:14px/1.3 var(--sans);resize:none;max-height:220px;outline:none}
/* The field's height is driven by autoGrow() frame by frame together with the feed's scroll —
   a CSS transition here was a second animation on the same quantity, and the feed travelled
   ahead of the field (a jerk). */
.input:focus{border-color:var(--acc)}
.input::placeholder{color:var(--tx-3)}
/* ── RECORDING IS VISIBLE INSIDE THE FIELD ──
   The status line lives at the far right of the strip below; the eye is on the field. So the
   same sentence is mirrored IN the input — placeholder-sized while it is empty, a chip in the
   top right corner once there is text under it — and the field itself breathes red while the
   mic is open. Absolute + pointer-events:none: the composer's height may not move by a pixel. */
.input-badge{position:absolute;z-index:3;pointer-events:none;opacity:0;transition:opacity .15s;
  font:11.5px/15px var(--mono);color:var(--warn);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  max-width:min(70%,420px);padding:2px 7px;border-radius:8px;
  background:color-mix(in srgb, var(--bg-3) 86%, transparent);
  border:1px solid color-mix(in srgb, var(--warn) 40%, transparent)}
.input-badge.on{opacity:1}
.input-badge.big{font-size:13px;line-height:17px;padding:3px 9px;max-width:min(88%,560px)}
/* The real width ceiling is set by paintInputBadge inline — by the width of the FIELD itself;
   what stays here is only the type size. On the phone it is a point and a half smaller: 38
   characters of thirteen-point monospace fit into 312 pixels of field, and the recording
   phrase is longer. */
@media (max-width:560px){
  .input-badge{font:11px/14px var(--mono)}
  .input-badge.big{font-size:12px;line-height:16px}
}
.input.badged::placeholder{color:transparent}
/* THE TEXT STARTS ONE LINE BELOW THE BADGE, NOT UNDERNEATH IT. The field gives the badge the
   first line with a padding of its own: the badge is absolute and does not move the composer's
   height, but it IS obliged to move the caret — otherwise what is dictated lands exactly on
   top of «● recording 00:07». */
.input.badged{padding-top:34px}
/* THE BADGE'S SECOND LINE IS THE DICTATION'S ADDRESS («task · project»), and it takes one more
   line away from the field. A line of its own, not the tail of the first: the gate measures the
   width of the first one (it has no right to be truncated), and a task name together with a
   project name does not fit into it on the phone. Inside the badge the line is cut with an
   ellipsis — the badge's overall `scrollWidth` does not grow from it. */
.ib-row{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  color:var(--tx-2);opacity:.9;font-size:.88em;letter-spacing:0}
.input.badged2{padding-top:52px}
/* THE RECORDING FIGURES ARE THE ONLY THING READ FROM A DISTANCE (the owner, 02.09). «Make the
   recording time — those very digits — a bit bigger and bolder, maybe red, so that from a
   distance you can see how many seconds the recording has been running and that everything is
   working.» The type size is up by a quarter, but line-height:1 leaves the height of the
   badge's LINE as it was (the line box at 14/16px is taller than the digits themselves) —
   otherwise the badge would have grown and climbed onto the text under it, while the field
   hands it exactly 34/52 pixels of padding. The red is the same one the field's border
   breathes with: one state — one colour. */
.ib-clock{font-size:1.25em;line-height:1;font-weight:700;color:var(--err);
  font-variant-numeric:tabular-nums;letter-spacing:.3px}
.ib-dot{color:var(--err);animation:ib-blink 1.5s ease-in-out infinite}
@keyframes ib-blink{0%,100%{opacity:1}50%{opacity:.25}}
@media (prefers-reduced-motion: reduce){.ib-dot{animation:none}}
/* THE SECOND SET OF DIGITS: HOW LOUD IT IS RIGHT NOW (the owner, 21.09). The clock proves the
   recording is running; it proved exactly that for twelve minutes over a microphone that captured
   nothing. The level proves the other half — that sound is arriving — so it is drawn in the same
   weight as the clock and in the accent colour, quietly, because in ordinary use it is simply a
   number that dances while he talks. */
.ib-lvl{font-size:1.15em;line-height:1;font-weight:700;color:var(--acc);
  font-variant-numeric:tabular-nums;letter-spacing:.3px}
/* ═══ …AND THE DIGITS GAVE THE PLACE UP TO A STRIP THAT DANCES (the owner, 21.09) ═══
   «You made that speaker, and now it jumps all the time… instead of those signs it would be far
   better to show, under the recording, that strip — the way they visualise music, with the coloured
   bars dancing. That is far prettier, clearer and more logical than these digits and the speaker.»
   The complaint and the cure are one thing: a number stepping between one and three characters
   moves the whole line it stands in, four times a second, in the badge over the field he is reading.
   The strip is a box of a FIXED width, so the line cannot move again whatever the level does; the
   digits are still in the DOM for every painter and gate that compares the line (`.ib-num`, drawn at
   font-size 0 — kept as text, not as pixels), and the number itself is in the `title`. */
.ib-lvl .ib-num{font-size:0;letter-spacing:0}
.ib-bars{display:inline-flex;align-items:flex-end;gap:2px;height:1.05em;width:43px;
  vertical-align:-.12em;margin-left:1px}
/* The heights are written by `barsTick` from the spectrum of the live microphone — no CSS animation
   here on purpose: the movement IS the data, and a transition on top of a 60 fps write only smears
   it. The colours run green → amber → red across the strip, the way an equaliser is read. */
.ib-b{width:3px;min-height:1px;border-radius:1.5px;height:8%;background:var(--acc)}
.ib-b:nth-child(1),.ib-b:nth-child(2){background:#3fb883}
.ib-b:nth-child(3),.ib-b:nth-child(4){background:#7cc45a}
.ib-b:nth-child(5),.ib-b:nth-child(6){background:#e0be4e}
.ib-b:nth-child(7),.ib-b:nth-child(8){background:#e59a4e}
.ib-b:nth-child(9){background:#e5714e}
/* Under the alarm the whole strip is one colour — the error one — and it blinks with the word
   «SILENT» beside it (the `.deaf` rule below owns the blink). A flat red strip is the picture of
   exactly what happened: the recording is running and nothing is arriving. */
.ib-lvl.deaf .ib-b{background:var(--err)}
/* …AND WHEN NOTHING IS ARRIVING, THE DIGITS BLINK — his own instruction, word for word: «let the
   digits blink, for instance». Not a fade like the dot above (that reads as «working»): a hard
   on/off twice a second, which the eye cannot hold still on and cannot mistake for the pulse of a
   healthy recording. Both the clock and the level carry it, in the field and in the status line. */
.ib-clock.deaf,.ib-lvl.deaf{color:var(--err);animation:ib-alarm .5s steps(1,end) infinite}
@keyframes ib-alarm{0%,49%{opacity:1}50%,100%{opacity:.1}}
/* Nobody is left without the signal when animation is switched off at the OS level — the same
   digits are underlined in the error colour instead of blinking. */
@media (prefers-reduced-motion: reduce){
  .ib-clock.deaf,.ib-lvl.deaf{animation:none;text-decoration:underline wavy var(--err)}
}
.input.rec,.input.rec:focus{border-color:color-mix(in srgb, var(--err) 60%, var(--line));
  animation:input-rec 1.7s ease-in-out infinite}
@keyframes input-rec{0%,100%{box-shadow:0 0 0 0 transparent}
  50%{box-shadow:0 0 0 2px color-mix(in srgb, var(--err) 20%, transparent)}}
@media (prefers-reduced-motion: reduce){.input.rec{animation:none}}
.mic-btn,.send-btn,.stop-btn{width:40px;height:40px;flex:none;border-radius:var(--r);border:1px solid var(--line-2);background:var(--bg-3);color:var(--tx-2);cursor:pointer;display:flex;align-items:center;justify-content:center;position:relative;transition:.15s}
.mic-btn:hover,.send-btn:hover{border-color:var(--acc);color:var(--acc)}

.send-btn{background:var(--blue);border-color:var(--blue);color:var(--on-blue);padding:0;box-shadow:0 2px 7px var(--shadow)}
.send-ico{width:18px;height:18px;display:block;fill:currentColor}
.send-btn:hover{background:var(--blue-2);border-color:var(--blue-2);color:var(--on-blue);
  transform:translateY(-1px);box-shadow:0 4px 12px var(--shadow)}
.send-btn:active{transform:translateY(1px);box-shadow:0 1px 3px var(--shadow)}
/* While the transcription is running, the send button stays a SEND BUTTON: a spinner running
   over the paper plane read as «busy, do not touch», whereas pressing it is exactly what one
   can and should do. Waiting is shown only AFTER the press — when the promise has been made. */
.send-btn.waiting{cursor:pointer}
/* ARMED: we are waiting for the tail of the dictation. The button does not blink as a whole
   (blinking reads as «it broke») — the paper plane becomes a little smaller and waves spread
   out from the button. The movement is visible with peripheral vision, and the size of the
   button under the finger does not change. */
.send-btn.armed{border-style:dashed}
.send-btn.armed .send-ico{transform:scale(.8)}
.send-ico{transition:transform .18s ease}
.send-btn.armed::before,.send-btn.armed::after{content:'';position:absolute;inset:0;
  border-radius:inherit;border:2px solid var(--blue);pointer-events:none;
  animation:send-wave 1.5s cubic-bezier(.22,.6,.3,1) infinite}
.send-btn.armed::after{animation-delay:.75s}
@keyframes send-wave{0%{transform:scale(.94);opacity:.8}100%{transform:scale(1.7);opacity:0}}
@media (prefers-reduced-motion: reduce){.send-btn.armed::before,.send-btn.armed::after{animation:none;opacity:.5}}
/* «Stop» is not a tiny black square but a full-blooded red button: a rounded white square in
   the centre and a soft halo that spreads while the turn is running. */
.stop-btn{background:var(--err);border-color:var(--err);color:var(--on-err);
  box-shadow:0 2px 8px color-mix(in srgb, var(--err) 45%, transparent)}
.stop-ico{width:13px;height:13px;border-radius:4px;background:currentColor;display:block}
.stop-btn:hover{transform:translateY(-1px);box-shadow:0 4px 14px color-mix(in srgb, var(--err) 55%, transparent)}
.stop-btn:active{transform:translateY(1px)}
.stop-btn::before{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  border:2px solid var(--err);animation:stop-halo 1.9s ease-out infinite}
@keyframes stop-halo{0%{transform:scale(1);opacity:.5}100%{transform:scale(1.55);opacity:0}}
@media (prefers-reduced-motion: reduce){.stop-btn::before{animation:none;opacity:0}}
.mic-ico{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round}
/* Recording is on — that must be visible from the other end of the room: instead of the
   microphone a large red circle pulses, rather than a 6×6 dot blinking in the corner. */
.mic-btn.rec{border-color:var(--err);color:var(--err);background:var(--err-bg)}
.mic-btn.rec .mic-ico{display:none}
.mic-btn.rec .rec-dot{width:17px;height:17px;border-radius:50%;background:var(--err);
  animation:rec-pulse .85s ease-in-out infinite}
@keyframes rec-pulse{0%,100%{transform:scale(.7);opacity:.65}50%{transform:scale(1.1);opacity:1}}
/* The button is the one part of the recording that stays visible when the field has been scrolled
   away or the phone is at arm's length — so the alarm reaches it too: the soft pulse of a healthy
   recording becomes the same hard blink the digits carry, and the circle turns amber-on-red. */
.mic-btn.rec.deaf .rec-dot{animation:ib-alarm .5s steps(1,end) infinite}
@media (prefers-reduced-motion: reduce){.mic-btn.rec.deaf .rec-dot{animation:none;opacity:.5}}
.mic-btn.busy{opacity:.55;cursor:wait}
@keyframes blink{50%{opacity:.15}}
.composer-bar{max-width:var(--read-w);margin:7px auto 0;display:flex;gap:8px;align-items:center;font-size:12px}
/* The mirrors of the toggles under the field are for a wide screen only; on the phone .only-wide hides them. */
/* THIS STRIP HAS NO RIGHT TO BECOME TWO LINES.
   It stands UNDER the input field, that is, it carries the height of the whole bottom: one flag
   wrapping onto a second line and the composer grew, and the buttons slid up UNDER the finger.
   The price of the mistake here is not cosmetic: «I missed the button, I pressed send instead
   of finish the transcription». So there are no wraps at all; what does not fit goes into a
   horizontal scroll (we hide the scrollbar: it would add those same pixels of height). */
.composer-flags{display:flex;gap:2px;align-items:center;flex-wrap:nowrap;min-width:0;
  overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}
.composer-flags::-webkit-scrollbar{display:none}
.composer-flags .flag{flex:0 0 auto;white-space:nowrap}
.composer-flags .flag span{display:inline-flex;align-items:center;gap:5px}
/* Half a pixel down: `align-items:center` centres the 13px BOX on the line box, and the line box
   is taller than the ink — so a box-centred icon reads high beside 11.5px lowercase text. Measured:
   box centre 36.9px, middle of the letters 36.5px, and the eye still looked lifted. */
.ic-lash{width:13px;height:13px;flex:0 0 auto;transform:translateY(.5px)}
/* Two eyes, one slot: an OPEN eye while thoughts and actions are shown, the closed lash once
   they are hidden. Same viewBox, same stroke width, same 13px box — only the shape changes,
   so the row does not move by a pixel when the box is ticked. */
.flag .ic-eye-shut{display:none}
.flag:has(input:checked) .ic-eye-open{display:none}
.flag:has(input:checked) .ic-eye-shut{display:inline-block}
.flag-select{flex:0 1 auto;min-width:0;text-overflow:ellipsis}
.composer-flags .flag{display:flex;align-items:center;gap:5px;color:var(--tx-3);font-size:11.5px;
  cursor:pointer;padding:3px 7px;border-radius:7px;border:1px solid transparent;user-select:none;transition:.15s}
.composer-flags .flag:hover{color:var(--tx-2);background:var(--bg-3);border-color:var(--line)}
.composer-flags .flag:has(input:checked){color:var(--acc);background:var(--bg-3);border-color:var(--line-2)}
.composer-flags input{width:13px;height:13px;margin:0;accent-color:var(--acc);cursor:pointer}
/* ═══ 🔁 IN THE HEADER: THE SIGN THAT MONEY IS BEING SPENT WITH NOBODY WATCHING ═══
   The composer's «🔁 Loop / ⏹ Stop loop» key is gone (the owner, 03.09 — it moved into the
   «Send» long-press menu). What is left on screen is this: one small red badge beside the
   figures, drawn ONLY while the open row is looping, and the popup behind it is where the
   loop is stopped. Red, because it says the same thing the rail's badge says — this row is
   still sending itself work — and that is the one state in the panel worth a colour of its
   own. It does not blink: a light no press can switch off is what makes a working panel feel
   broken (canon 30.08), and this one is switched off by pressing it.
   The runs counter is a SECOND storey inside the same button, not a widening of it: the
   header's width is measured by the gate, and a growing number here would push the figures
   the header exists for over the edge. */
/* ⚠ TWO CLASSES IN EVERY SELECTOR, AND THAT IS THE WHOLE POINT OF THEM.
   `.icon-btn{color:var(--tx-2)}` stands ~140 lines BELOW this block at the same weight, so a
   lone `.loop-top{color:var(--acc)}` lost silently and the sign came out grey — measured by the
   gate as rgb(174,181,193) where the accent was asked for. Fourth instance of one mine in this
   file (☰ on the desk, the cog on the phone, the flame inside the ice cube): a property set on
   a component may not be written at one class's weight while another one-class rule for that
   same component lives lower down. */
.icon-btn.loop-top{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;
  color:var(--acc);background:color-mix(in srgb, var(--acc) 15%, transparent);
  border:1px solid color-mix(in srgb, var(--acc) 40%, transparent);
  border-radius:9px;padding:3px 7px;cursor:pointer;line-height:1;transition:.15s}
.icon-btn.loop-top.hidden{display:none}
.icon-btn.loop-top:hover{background:color-mix(in srgb, var(--acc) 27%, transparent);border-color:var(--acc)}
.icon-btn.loop-top .ico-loop{width:17px;height:17px;display:block;flex:0 0 auto}
.icon-btn.loop-top .lt-n{font:600 11px/1 var(--mono);letter-spacing:0}
.icon-btn.loop-top .lt-n:empty{display:none}
@media(max-width:860px){.icon-btn.loop-top{padding:2px 5px;gap:3px}
  .icon-btn.loop-top .ico-loop{width:15px;height:15px}}
/* ═══ ONE LOOP GLYPH FOR THE WHOLE PANEL — the header badge, the rail pill, the «Send» menu ═══
   «make loop icon better - thinner line so arrow will be distinguishable / and set this icon in
   left panel as well and to send button menu» (the owner, 03.09).
   Two things were wrong and they are one thing. ONE STATE WAS DRAWN THREE WAYS: an inline SVG in
   the header and the 🔁 EMOJI in the rail and in the menu — and an emoji is the OS's own glyph,
   so it takes neither the theme's colour nor the row's size and is a different picture on the
   phone than on the desk (the canon that made the microphone and the machine meters our own SVG).
   AND THE LINE WAS 2.9 OF 24: at the 12-17px this sign is ever drawn at, a stroke that fat
   swallowed its own arrowhead and the whole thing read as a bare ∞ — «this row is repeating
   itself» became «infinity», which is a different sentence.
   So: **0.9 of 24 — half of the 1.8 this was first built at** («even twice thinner… line - too
   wide», the owner, 03.09) — and the head is a triangle of its OWN, four times the stroke wide
   («arrows - slightly bigger»). That ratio is the whole trick: the arrow is told from the line by
   the DIFFERENCE between them, so thinning the line and fattening the head are one move, not two.
   A head drawn by the stroke itself would only be a thickening at this size.
   ⚠ THE PRICE OF A HALF-WIDTH LINE IS PAID IN THE SMALLEST HOME. At 12px a 0.9 stroke is under
   half a device pixel and the loop goes pale, so the rail's badge draws it at 13 and the menu at
   16 — a bigger CANVAS, never a second shape: two strokes for one sign is two signs to keep in
   step. Measured at 12/15/17/24/72 before it was written: data/shots/loop-icon-sizes.png.
   It is a MASK, the idiom `--spin-arrows` above already uses: the colour is then an ordinary
   `background: currentColor`, so all ten themes paint it themselves, the size is set by whoever
   draws it, and not one byte leaves the machine. */
:root{--loop-arrow:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='.9' stroke-linecap='round' stroke-linejoin='round' d='M12 12C10.1 9.3 8.4 8.2 6.4 8.2a3.8 3.8 0 1 0 0 7.6c2 0 3.7-1.1 5.6-3.8 1.9-2.7 3.6-3.8 5.6-3.8a3.8 3.8 0 1 1 0 7.6c-.24 0-.47-.02-.7-.07'/%3E%3Cpath fill='%23000' d='M13.58 13.11 18.65 13.84 15.18 18Z'/%3E%3C/svg%3E")}
.ico-loop{display:inline-block;width:1em;height:1em;flex:none;background:currentColor;
  -webkit-mask:var(--loop-arrow) center/100% 100% no-repeat;
          mask:var(--loop-arrow) center/100% 100% no-repeat}
/* The rail badge and the «Send» menu draw it from a `::before`, not from a node: both are built
   out of a plain text label (`el()` in web/tree.js and the menu's own builder take a string), and
   markup in this panel is written as NODES and never as innerHTML — a `::before` is how a
   string-only builder gets a glyph without a new markup-writing point (`npm run dom`). */
.pill.loop::before,.ctx-menu button.i-loop::before{content:'';display:inline-block;flex:none;
  background:currentColor;
  -webkit-mask:var(--loop-arrow) center/100% 100% no-repeat;
          mask:var(--loop-arrow) center/100% 100% no-repeat}
.pill.loop{display:inline-flex;align-items:center;gap:3px}
.pill.loop::before{width:13px;height:13px}
/* ⚠ THREE CLASSES IN THE `justify-content` SELECTOR ON PURPOSE. `.ctx-menu button.has-sub` sets
   `space-between` at the same (0,2,1) weight and stands ~770 lines BELOW this block, so a
   two-class rule here would lose silently and the icon, the text and the ▸ would be spread across
   the item — the same mine that already cost ☰ on the desk and the cog on the phone. */
.ctx-menu button.i-loop{display:flex;align-items:center;gap:7px}
.ctx-menu button.has-sub.i-loop{justify-content:flex-start}
.ctx-menu button.has-sub.i-loop::after{margin-left:auto}
/* Green, like the rail's badge — one state, one colour across the panel, and it stays that colour
   when the item is hovered: the sign answers «is this row repeating itself», not «am I pointing
   at this line». */
.ctx-menu button.i-loop::before{width:16px;height:16px;color:var(--ok)}
/* On the phone the sign is bigger and a touch heavier — at 16 px it read as a speck (the owner,
   31.10). A mask cannot be made bolder, so a hairline drop-shadow of its own colour thickens it. */
@media (max-width:860px){
  html:not(.pane) .ctx-menu button.i-loop{gap:9px}
  html:not(.pane) .ctx-menu button.i-loop::before{width:22px;height:22px;
    filter:drop-shadow(0 0 .6px currentColor) drop-shadow(0 0 .6px currentColor)}
}
/* «Stop the loop» — a red square of the panel's own, the stop sign every player uses. */
.ctx-menu button.i-stopsq{display:flex;align-items:center;gap:7px}
.ctx-menu button.i-stopsq::before{content:'';display:inline-block;flex:none;width:11px;height:11px;
  margin:0 2px 0 3px;border-radius:2px;background:var(--err,#e5484d)}
/* The model is the only drop-down among the tick boxes, so it looks like them and not like a
   form: the same height, the same border, the same muted colour. */
.flag-select{background:var(--bg-3);border:1px solid var(--line);color:var(--tx-2);
  border-radius:7px;padding:2px 6px;font:11.5px var(--sans);cursor:pointer;outline:none;max-width:190px}
.flag-select:hover,.flag-select:focus{border-color:var(--acc);color:var(--acc)}

/* ═══ THE LIVE LINE «WORK IS GOING ON RIGHT NOW» ═══
   The last line of the feed, sticky to its bottom: you went up to read — it stayed in view.
   It answers the question the little light in the rail does not answer: «is it thinking or has
   it hung?» */
.live-line{position:sticky;bottom:2px;z-index:4;display:flex;align-items:center;gap:9px;
  margin:6px 0 2px;padding:7px 12px;border-radius:11px;font:12.5px var(--sans);color:var(--tx-2);
  background:var(--bg-3);border:1px solid var(--line-2);box-shadow:0 6px 18px var(--shadow);
  width:fit-content;max-width:100%}
/* THE APPEARANCE IS SMOOTH, THE GEOMETRY IS MOTIONLESS.
   The line is born and dies several times per turn, and every one of those times the feed
   twitched: the height of the content changed in a jump. Only the opacity and the shift are
   animated (the height may not be animated — for all 200 ms the feed would be growing under
   the text), and `contain:layout` stops a change of the verb and the file name from
   recomputing the layout of the whole feed. */
.live-line{animation:live-in .22s ease-out;contain:layout}
@keyframes live-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: reduce){.live-line{animation:none}}
.live-what{font-weight:600;color:var(--acc);white-space:nowrap}
.live-detail{color:var(--tx-3);font:11.5px var(--mono);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;min-width:0}
.live-clock{color:var(--tx-3);font:11px var(--mono);flex:none;margin-left:auto;padding-left:6px}
/* Three dots running as a wave — the same sign of life the CLI has: the movement is visible
   with peripheral vision, while a frozen spinner would honestly mean «it has hung». */
.live-dots{display:flex;gap:3px;flex:none}
.live-dots b{width:5px;height:5px;border-radius:50%;background:var(--acc);display:block;
  animation:live-wave 1.05s ease-in-out infinite}
.live-dots b:nth-child(2){animation-delay:.14s}
.live-dots b:nth-child(3){animation-delay:.28s}
@keyframes live-wave{0%,60%,100%{opacity:.25;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
@media (prefers-reduced-motion: reduce){.live-dots b{animation:none;opacity:.8}}
/* «Only the question and the answers» hides the thoughts and the actions — but not the sign of
   life: without it the window in that mode looks dead exactly when work is going on. */
.qa-only .live-line{display:flex}
.chip-select{width:auto;padding:4px 8px;font-size:11.5px;border-radius:7px}
/* ═══ CHOOSING THE MODEL AND THE EFFORT ═══
   A button instead of a `<select>`: the native menu cannot do submenus, while «which model»
   and «at what effort» are one question with two levels. It looks like a list (the same
   background, the same border) so that the hand hunts for it where it used to hunt. */
.flag-pick{background:var(--bg-3);color:var(--tx);border:1px solid var(--line);border-radius:7px;
  padding:4px 8px;font:11.5px var(--sans);cursor:pointer;outline:none;max-width:280px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.flag-pick:hover{border-color:var(--acc)}
.chip-toggle{display:flex;align-items:center;gap:5px;color:var(--tx-3);cursor:pointer;font-size:11.5px;user-select:none}
.chip-toggle input{accent-color:var(--acc)}
.voice-status{font:11.5px var(--mono);color:var(--warn)}

/* ═══ THE TIME BAR: MINUTES OF THIS SESSION, COLOURED BY HOW FULL ITS WINDOW IS ═══
   Everything here is measured against ONE ceiling: nothing may stand taller than the letters of
   the checkbox beside it («no taller than the height of the letters of the checkbox next to it»,
   the owner). The track is 4px, the dashed mark and the head both 11px — the cap height of the
   11.5px label next to them. The row's own height therefore does not change by a pixel, which is
   the whole point: .composer-bar carries the height of the composer, and a taller bottom slides
   the send button under the thumb. */
.ctx-gauge{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px;
  background:none;border:0;padding:0;margin:0;font:inherit;color:var(--tx-3);cursor:pointer;
  -webkit-tap-highlight-color:transparent}
.ctx-gauge.hidden{display:none}
/* A session the next turn will not continue: the picture stays, and it stops shouting. Nothing
   moves on it any more, so it reads as the record of a life rather than as a live measurement. */
.ctx-gauge.over{opacity:.55}
.ctx-gauge:hover .cg-track{background-color:var(--line)}
.cg-zero,.cg-scale{flex:0 0 auto;font:9px var(--sans);color:var(--tx-3);line-height:1}
/* The ticks are drawn INTO the track's background at every fifth of it, so they hold their meaning
   at any scale: five equal steps, whatever number stands at the right-hand end. */
.cg-track{position:relative;flex:1 1 auto;min-width:36px;height:4px;border-radius:2px;
  background-color:var(--bg-4);transition:background-color .15s;
  background-image:repeating-linear-gradient(90deg,
    transparent 0 calc(20% - 1px), var(--bg) calc(20% - 1px) 20%)}
.cg-fill{position:absolute;left:0;top:0;bottom:0;width:0;border-radius:2px;
  background:linear-gradient(90deg,var(--cg-cool,var(--ok)),var(--cg-hot,var(--ok)))}
/* «About here the window will be full» — a guess, so it is dashed and it moves. */
.cg-mark{position:absolute;top:-3.5px;bottom:-3.5px;width:0;border-left:1.5px dashed var(--err);
  margin-left:-.75px;opacity:.9}
.cg-mark.soft{opacity:.45}
.cg-head{position:absolute;top:50%;width:5px;height:5px;margin:-2.5px 0 0 -2.5px;border-radius:50%;
  background:var(--strong);box-shadow:0 0 0 1px var(--bg-2)}
/* THE MINUTES RIDE ABOVE THE BAR — always, not only at the end.
   They used to stand BESIDE the head, and flipped to its left side past three quarters of the
   width. At the left of a long session's head there is only the fill, so the number ended up
   printed on the red it was measuring and could not be read (owner, 03.09, 372m on a 360m
   scale). Above the track there is no case where it lands on anything, so there is no case to
   get wrong. It is absolutely positioned, so it costs the row NOT ONE PIXEL of height: the
   9px of ink live in the 7px gap .composer-bar already keeps above itself. */
.cg-now{position:absolute;bottom:calc(100% + 3px);transform:translateX(-50%);
  font:9px var(--sans);font-style:normal;
  color:var(--tx-2);white-space:nowrap;pointer-events:none;line-height:1}
.cg-val{flex:0 0 auto;min-width:32px;text-align:right;font:600 11px var(--sans);line-height:1;
  font-variant-numeric:tabular-nums;color:var(--cg-hot,var(--tx-2))}
/* ═══ THE TICKS: WHO SPOKE, AND WHEN ═══
   A message is 2px wide and 8px tall; a session start is the same 11px as the dashed guess beside
   it, which is the cap height of the label in this row and therefore the ceiling nothing here may
   pass. The colours are the ones the bubbles already use — clay for his own line, the thinking
   violet for anyone else's, grey metal for a line the panel put there itself — so the strip and
   the feed under it are read in one vocabulary and never have to be learned twice. They are read
   from the LIVE theme by CSS variables: ten palettes ship here and half of them are not dark.
   The hairline outline is what keeps a tick legible where it crosses the coloured stretch. */
.cg-ticks{position:absolute;left:0;right:0;top:0;bottom:0;pointer-events:none}
.cg-tick{position:absolute;top:-2px;bottom:-2px;width:2px;margin-left:-1px;border-radius:1px;
  background:var(--acc);box-shadow:0 0 0 .5px color-mix(in srgb,var(--bg-2) 65%,transparent);
  pointer-events:auto;cursor:pointer}
.cg-tick.guest{background:var(--think,#8b7fd4)}
.cg-tick.bot{background:var(--tx-3);opacity:.8}
/* THE BOUNDARY BETWEEN TWO SESSIONS IS NOT A FOURTH VOICE, so it is not given a fourth colour:
   it is the strongest ink the theme has, full height, and that is the only thing on this bar drawn
   that way. Grey was tried first and was wrong — the panel's own lines are grey, so the structure
   of the row and one of the voices in it read as the same mark at a glance. */
.cg-tick.cut{top:-3.5px;bottom:-3.5px;width:1px;margin-left:-.5px;border-radius:0;
  background:var(--strong);opacity:.75;box-shadow:0 0 0 .5px color-mix(in srgb,var(--bg-2) 80%,transparent)}
/* Its own line on a phone — and nowhere at all on a wide screen, where the gauge lives in the
   flags row. An empty row must not cost the composer a pixel, hence :empty. */
.gauge-row{display:none}
/* ⏱ THE COUNTDOWN BESIDE THE TIME BAR. Two classes for the same reason the header sign has
   them — a one-class `display` here would be overruled by whatever is written lower down.
   Tabular digits, or «10 m» → «9 m» shifts the whole strip sideways once a minute; a fixed
   floor under the width keeps «33 s» and «10 m» in the same place. It may not grow the bottom
   bar: that row carries the height of the whole composer, and one wrap there slides «Send»
   under the thumb. */
/* It is a door as well as a readout since 19.09 (right button → the gap submenu and «stop»), so
   it is owed the hand a door gets. Nothing about its SIZE changes on hover — the bottom bar
   carries the height of the whole composer, and a chip that grows under the cursor slides «Send»
   sideways while the hand is on its way to it. */
.loop-left.loop-left{display:inline-flex;align-items:center;gap:3px;flex:0 0 auto;
  color:var(--acc);font:600 11px/1 var(--mono);letter-spacing:0;min-width:44px;
  justify-content:flex-end;white-space:nowrap;cursor:pointer;
  border-radius:5px;transition:background .12s,outline-color .12s}
/* ⛔ THE TINT IS GROWN WITH AN `outline`, NEVER WITH PADDING. Everything here is `border-box`, so
   padding would eat into the 44px floor and a negative margin to pay it back would move the whole
   strip by six pixels on every hover. An outline is drawn outside the box and costs no layout. */
.loop-left.loop-left:hover{background:color-mix(in srgb,var(--acc) 14%,transparent);
  outline:3px solid color-mix(in srgb,var(--acc) 14%,transparent)}
.loop-left.loop-left.hidden{display:none}
/* ⛔ OFF: THE SAME CHIP, NOT A SECOND SIGN (28.09). The 44px floor exists to stop «10 m» → «9 m»
   shifting the strip once a minute; with no number to draw there is nothing to hold still, so the
   floor is dropped and the chip costs the bar one icon. Muted, because it is an invitation and
   not a state — the accent colour on this row means «something is running». */
.loop-left.loop-left.off{min-width:0;color:var(--tx-2)}
.loop-left.loop-left.off:hover{color:var(--tx);
  background:color-mix(in srgb,var(--tx) 10%,transparent);
  outline:3px solid color-mix(in srgb,var(--tx) 10%,transparent)}
.loop-left .ll-ico{width:12px;height:12px;flex:0 0 auto;opacity:.85}
.gauge-row:empty{display:none!important}

/* ⚡ THE LOCK CHIP. Same bargain as .loop-left: it must not be taller than the letters of the
   checkbox beside it, and the hover tint is grown with an `outline` and never with padding —
   everything here is border-box, so padding would change the height of the whole bottom bar and
   one wrap there slides «Send» under the thumb. */
.sub-pin{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;
  background:none;border:0;padding:0;margin:0 2px;border-radius:5px;
  color:var(--acc);font:600 11px/1 var(--mono);white-space:nowrap;cursor:pointer;
  max-width:170px;min-width:0;overflow:hidden;transition:background .12s,outline-color .12s}
.sub-pin:hover{background:color-mix(in srgb,var(--acc) 14%,transparent);
  outline:3px solid color-mix(in srgb,var(--acc) 14%,transparent)}
/* NOT IN FORCE — the row is locked to a subscription that is switched off, so the next turn
   goes somewhere else. The one state where silence would be a lie. */
.sub-pin.off{color:var(--warn)}
.sub-pin.off:hover{background:color-mix(in srgb,var(--warn) 16%,transparent);
  outline:3px solid color-mix(in srgb,var(--warn) 16%,transparent)}
.sub-pin .sp-ico{width:11px;height:11px;flex:0 0 auto;opacity:.9}
/* NOT LOCKED YET, ON A NEW CHAT — the chip is the choice, not a fact (owner, 30.10). Muted text and
   an OPEN padlock (the shackle lifted), so it cannot be mistaken for a lock already set. */
.sub-pin.free{color:var(--tx-2);font-weight:500}
.sub-pin.free:hover{color:var(--acc)}
.sub-pin .sp-shackle{transition:transform .12s}
.sub-pin.free .sp-shackle{transform:translate(3px,-2.5px)}
.sub-pin b{min-width:0;overflow:hidden;text-overflow:ellipsis}
/* ON A PHONE THE STRIP IS CUT FROM THE END, SO THIS CHIP GIVES UP LETTERS BEFORE IT GIVES UP ITS
   PLACE: the padlock is the fact, the name is the detail, and the detail is one tap away. It may
   never push «Send» — that is the one thing the bottom bar is not allowed to do. */
@media (max-width:640px){.sub-pin{max-width:92px}.sub-pin .sp-pre{display:none}}
/* `[hidden]`-by-class must beat the flex above — the same trap the attention triangle fell into
   twice: a class that sets `display` outranks the zero-specificity rule that hides it. */
.sub-pin.sub-pin.hidden{display:none}


/* ═══ THE QUESTION THAT WAS ASKED ═══
   The red «?» in the rail says «you have been asked something», and that is not enough: the
   question stands as the second-to-last paragraph of a long report, and a person honestly does
   not find it. The strip shows the TEXT itself, leads to it in the feed, and lets the light be
   taken down by hand when there is nothing left to answer. It lives inside .composer — which
   means the same ResizeObserver measures its height, and the feed does not jump. */
.ask-bar{max-width:var(--read-w);margin:0 auto 6px;display:flex;align-items:center;gap:8px;
  padding:7px 10px;border:1px solid var(--err);border-radius:10px;background:var(--err-bg)}
.ab-ico{font-size:13px;line-height:1}
.ab-q{flex:1;min-width:0;font:12.5px var(--sans);color:var(--tx);
  overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;white-space:normal}
.ab-btn{flex:none;font:11.5px var(--sans);padding:4px 9px;border-radius:8px;cursor:pointer;
  border:1px solid var(--line-2);background:var(--bg-3);color:var(--tx-2)}
.ab-btn:hover{color:var(--tx);border-color:var(--acc)}
.ab-btn.ab-off{color:var(--ok)}
/* Yes and No are the ANSWER, and the three buttons after them are ways of not answering yet. One
   tap sends, so they are the only ones drawn at full strength — the rest stay the quiet grey they
   have always been. */
.ab-btn.ab-yes,.ab-btn.ab-no{font-weight:600;color:var(--tx)}
/* FIVE BUTTONS DO NOT FIT BESIDE A QUESTION ON A PHONE. The strip is a single flex line and every
   button in it is `flex:none`, so a new one does not push itself down — it squeezes the question,
   and the question is the only thing on the strip that has to be read. On a narrow screen the
   question takes the whole first line and the buttons drop under it. */
@media (max-width:860px){
  .ask-bar{flex-wrap:wrap}
  .ab-q{flex:1 1 calc(100% - 30px);-webkit-line-clamp:3}
  .ab-x{margin-left:auto}
}
.ab-x{flex:none;background:none;border:0;color:var(--tx-3);font-size:13px;cursor:pointer;padding:2px 4px}
/* The same strip explains the «!»: the trouble has already happened, so it is calmer than the
   question — the border is amber rather than alarm-red, and it does not pull the eye to itself
   a second time. */
.ask-bar.bad{border-color:var(--warn,#c08a2e);background:var(--warn-bg,rgba(192,138,46,.10))}
.ab-x:hover{color:var(--tx)}
.ab-hit{outline:2px solid var(--acc);outline-offset:3px;border-radius:6px;transition:outline-color .4s}

/* ═══ A CHOICE IN AN ANSWER IS A BUTTON ═══
   The chip is `display:inline`, not `inline-block`: it lives INSIDE the sentence and a caption
   that runs past the edge of a phone must wrap like the words around it. `box-decoration-break`
   is what then draws the border on BOTH halves — without it the second line comes out bare and
   the button looks broken in half. `font:inherit` keeps a `**bold**` caption bold: the whole
   `<strong>` is moved into the button, so the button must not overrule it. */
/* SOLID, NOT A GHOST (owner, 10.09: «like bootstrap 5? So filled, not transparent»). A labelled
   option is the panel's PRIMARY action, so it is drawn as one — the accent fill, the way
   Bootstrap draws `.btn-primary`. The fill is `--acc` and the ink is `--on-acc`, which every
   theme defines and `themes.js → ink()` computes when it does not, so the text stays legible on
   all eleven palettes instead of only on the blue one this was asked for. Never hardcode a blue
   here: «for my sky theme» is the theme picking the hue, not the stylesheet. */
/* ═══ THE MEASUREMENTS ARE BOOTSTRAP 5's, THE COLOURS ARE THE THEME'S (owner, 11.09) ═══
   «make the style of the buttons like they are on Bootstrap 5 and for my particular Sky theme.»
   The fill arrived on 10.09 and the SHAPE did not: 2px of vertical padding on a 15px line drew a
   thin strip, which is a chip, not a button. `.btn` is `.375rem .75rem` on a 1.5 line — 6px / 12px
   here — with a `.375rem` (6px) radius and a 400 caption, and that is what is copied. What is NOT
   copied is one hex of Bootstrap's blue: `--acc` is the hue his theme picked (Sky: #4d9bff) and
   `--on-acc` is the ink `themes.js → ink()` computes to stay legible on it, so the same rule draws
   all eleven palettes instead of the one this was asked for.
   ⚠ `display:inline` is what is ASKED FOR here and `inline-block` is what Chromium answers — a
   `<button>` is blockified whatever the sheet says, so the declaration is a preference and not a
   fact. It changes nothing that matters: the caption still wraps inside the box, so a 140-character
   option breaks over two lines on a phone instead of leaving the screen, which is the only promise
   this line was ever making. The gate measures THAT, at 430px, and not the computed word — an
   earlier version of the check asserted `inline`, went red, and was wrong about a panel that was
   drawing exactly what was wanted. The real price of an inline-level box is that vertical padding
   does not push the neighbouring lines apart — paid by `.has-opt` below, on the host. */
/* ⛔ `line-height` IS PART OF THE `font` SHORTHAND, so `font:inherit` drags the host's leading in
   with the family and the size — and the host's leading is the 2.8 that `.has-opt` sets to make
   ROOM for these buttons. The button then grew to fill the very gap that was opened for it and the
   three fills went back to touching, which is the defect `.has-opt` exists to prevent, arriving by
   the back door. Bootstrap's `.btn` is 1.5, and saying it here is what makes the 6px of padding
   mean 6px. Measured after: a 36px button on a 42px line. */
/* ═══ IT HAS TO LOOK PRESSABLE BEFORE IT IS READ (owner, 11.09) ═══
   «Let's add a bit more embossing - inside text - it is not obvious that this is a button.»
   A flat fill of the accent is also how a highlight, a tag and a code span are drawn, so the one
   thing separating a control from decoration was its colour. The cure is to give the box a face
   that catches light: a lit top edge, a shaded inside bottom lip, and a gloss down the fill.
   ⛔ EVERY ONE OF THOSE LAYERS IS `inset` — NOTHING IS DRAWN OUTSIDE THE RECT. The first version
   of this (11.09, morning) also carried a solid side wall `0 3px 0 var(--opt-edge)` and the
   shadow it cast `0 5px 9px var(--shadow)`, which is the textbook 3D-button trick. He looked at
   it and answered «just remove that strange shadow», with an arrow drawn at the dark block under
   the fill — a hard offset in the fill's own darker tone reads as a SECOND, misaligned button
   rather than as thickness, and on a dark page the blur under it reads as dirt. So the body is
   made of light on the face now, not of an object under it. Keep it that way: a new layer here
   with a non-zero Y offset and no `inset` puts that block straight back, and the gate below
   («nothing is drawn under the button») is what says so out loud.
   ⛔ THE FILL IS A TOKEN, NOT A HEX. `--opt-bg`/`--opt-tx` default to `--acc`/`--on-acc` in
   `themes.js → expand()`, so ten palettes are unchanged and a theme may name its own button
   colour — «Sky» does, because he asked for it by eye. Never hardcode a blue here. */
.opt-btn{display:inline;font:inherit;line-height:1.5;
  --opt-fill:var(--opt-bg,var(--acc));
  --opt-ink:var(--opt-tx,var(--on-acc));
  --opt-edge:color-mix(in srgb,var(--opt-fill) 52%,#000);
  /* The gloss: lighter along the top of the face, darker along the bottom. It is a BACKGROUND
     LAYER and `.opt-default` keeps it as its second layer — see there for why that matters. */
  --opt-gloss:linear-gradient(180deg,rgba(255,255,255,.20),rgba(255,255,255,.03) 46%,rgba(0,0,0,.16));
  /* The mark of the recommended option, as a SHAPE rather than as a character — see
     `.opt-default` below for why it may not be the emoji. It is declared here, on the button,
     because a custom property is inherited and `::before` is where it is spent; declaring it
     there would put the drawing inside the rule that uses it, and the next correction would
     fork it.
     ⛔ THE OUTLINE IS TRACED FROM THE PICTURE HE SENT, NOT DRAWN BY EYE (owner, 11.09 night:
     «but take this design. Existing one - too long»). The first drawing was a squat hand — a
     palm 9.6 wide against 7.1 tall, ratio 1.35 — and «too long» is exactly what a hand that
     wide reads as at 13px. His picture measures 1.12, so the proportions here are MEASURED
     off it rather than guessed: the file was decoded, the blue mask walked row by row, and
     the two components came back as cuff 0…3.97 × 5.85…15.92 and hand 4.05…16 × 0…15.92 in
     this viewBox's own units. The thumb therefore reaches the top edge and the cuff runs the
     full depth of the hand — that is the whole difference, and it is why the shape can be
     checked against a number instead of a taste. Three knuckles on the right edge are his
     picture's too; at 13px they are a soft wave rather than three lumps, which is the point
     of tracing instead of stylising. */
  --opt-thumb:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='black' d='M.5 5.85h2.97a.5.5 0 0 1 .5.5v9.07a.5.5 0 0 1-.5.5H.5a.5.5 0 0 1-.5-.5V6.35a.5.5 0 0 1 .5-.5zM8.6.05C7.79.05 7.28.85 7.3 1.82 7.33 3.27 7.16 4.16 6.66 4.95 6.24 5.62 5.63 6.15 5.15 6.5 4.92 6.67 4.79 6.94 4.79 7.23L4.79 14.9A1.02 1.02 0 0 0 5.81 15.92L12.72 15.92C13.46 15.92 13.84 15.32 13.6 14.86 13.44 14.55 13.28 14.44 13.45 14.26 13.66 14.02 14.3 13.92 14.56 13.42 14.82 12.92 14.7 12.36 14.42 12.08 14.2 11.86 13.98 11.8 14.12 11.6 14.3 11.36 15.06 11.32 15.38 10.84 15.7 10.36 15.62 9.7 15.3 9.36 15.08 9.13 14.84 9.06 14.86 8.9 14.89 8.7 15.2 8.64 15.44 8.4 15.86 7.98 16.02 7.44 15.95 6.94 15.86 6.32 15.34 5.88 14.64 5.88L10.28 5.88 10.32 3.8C10.34 2.85 10.24 2.12 9.98 1.42 9.72.72 9.28.05 8.6.05Z'/></svg>");
  --opt-lift:inset 0 1px 0 rgba(255,255,255,.34),inset 0 -2px 0 rgba(0,0,0,.22);
  color:var(--opt-ink);background-color:var(--opt-fill);
  background-image:var(--opt-gloss);background-repeat:no-repeat;
  background-size:100% 100%;background-position:0 0;
  /* «embossing - inside text»: the caption is cut into the face rather than laid on it. The
     direction is the theme's (`--opt-emb`) — a dark line under white ink, a light one under dark
     ink — because a black shadow beneath black letters is not an emboss, it is a smudge. */
  text-shadow:0 1px 0 var(--opt-emb);
  box-shadow:var(--opt-lift);
  cursor:pointer;border:1px solid var(--opt-edge);border-radius:6px;padding:6px 12px;text-align:left;
  box-decoration-break:clone;-webkit-box-decoration-break:clone;
  transition:background .15s ease-in-out,border-color .15s ease-in-out,color .15s ease-in-out,
    box-shadow .15s ease-in-out,transform .08s ease-in-out}
/* ═══ «"Option A" - can be also separated with small emboss - vertical line» (owner, 11.09) ═══
   The label and the sentence are two different things inside one caption — one names the choice,
   the other IS the choice — and running them together as «Option A — merge it now.» makes the eye
   parse a sentence to find where the answer starts. A two-tone rule (a dark groove, a light edge
   beside it) separates them the way the button's own face is separated from the page.
   ⛔ THE DASH IS STILL IN THE TEXT — `.opt-sep` only shrinks it to nothing. `dataset.send` is read
   off `textContent`, so deleting the separator node would quietly change the words sent under his
   name, and «Option A — merge it now.» is what an answer a week later has to read back. */
.opt-tag{font-weight:600;padding-right:9px;margin-right:9px;
  border-right:1px solid rgba(0,0,0,.30);box-shadow:1px 0 0 rgba(255,255,255,.22)}
.opt-sep{font-size:0}
/* ═══ A FILE NAME INSIDE A CAPTION IS STILL THE CAPTION (owner, 11.09 night) ═══
   «check that this text appears same style - same back, same text color - outline pill border -
   white color», with an arrow pointing at `trigger-tests` sitting inside an Option B button. The
   feed's own chip (`.md code`, line 608) is built for a chip lying on the PAGE: `--bg-4` behind it
   and `--acc-2` in front. On a filled button that is a dark hole with blue letters punched through
   the face — the one word of the sentence that the eye reads as belonging to something else, when
   it is usually the very word the choice is ABOUT.
   ⛔ EVERY VALUE HERE IS INHERITED OR `currentColor`, NOT A HEX. Same argument as the thumb: the
   ink of this button is `--opt-ink`, which `themes.js → ink()` computes for the fill the button
   actually wears — white on Sky, near-black on a light palette. `color:inherit` takes it, and a
   border of `currentColor` at 45% follows it, so «white» holds on his theme and the rule still
   draws sensibly on the other ten. A literal `#fff` here would be invisible on the light ones.
   ⚠ `.md code` sets `font:12.4px var(--mono)`, an ABSOLUTE size — pinned to 12.4px it stops
   growing with his reading scale exactly when the caption around it grows, so the chip shrinks
   away as the text is made bigger. `.9em` instead, which is relative to the caption it sits in.
   ⚠ `padding` and `border` here are what `box-decoration-break:clone` on the button already
   handles, so a pill on a caption that wraps to a second line is redrawn whole on both.
   ⚠ `text-shadow` is INHERITED, so the chip arrives carrying the button's emboss; a mono glyph
   at .9em with a 1px shadow under it reads as slightly out of focus, and the pill around it is
   already doing the separating the emboss was for. A spent option needs no rule of its own — its
   ink has drained to `--tx-2` and the pill follows it down by the same `currentColor`.
   The selector carries `.md` only to outrank `.md code` beyond doubt — both are (0,1,1) and this
   rule stands later in the file, but the ORDER is not something the next edit should have to know. */
.md .opt-btn code{background:none;color:inherit;text-shadow:none;
  font-family:var(--mono);font-size:.9em;
  border:1px solid color-mix(in srgb,currentColor 45%,transparent);
  border-radius:999px;padding:0 .55em}
/* The room those 6px need. An inline box overflows its line instead of growing it, so without this
   the fill of one option laps over the fill of the next and two buttons read as one block. 2.45
   was exactly enough to stop them overlapping and drew them touching, edge to edge, which reads as
   one striped slab; the extra fifth is the gap Bootstrap gets for free from `inline-block` plus a
   margin. Measured on the gate's own fixture: a 48px button on a 55px line.
   ⚠ 2.8 → 3.05 on 11.09 for the wall and the cast shadow, then BACK to 2.8 the same evening,
   when he had both removed. The number is a consequence, not a taste: room is owed to whatever is
   DRAWN, and a shadow is drawn OUTSIDE the rect, so while one existed the gate’s gap check could
   not see it — 7px of measured gap was 4px of visible gap. With every layer `inset` again the
   measured gap IS the visible gap and 2.8 is honest. Anything re-added outside the rect owes its
   own room back. */
.md p.has-opt,.md li.has-opt{line-height:2.8}
/* Hover and focus move the fill AWAY from the page — mixed toward `--tx`, so it lightens on a
   dark theme and darkens on a light one. Bootstrap can simply darken (`shade-color 15%`) because
   it owns one background; we do not, and a fixed direction goes the wrong way on half the
   palettes. */
.opt-btn:hover{background-color:color-mix(in srgb,var(--opt-fill) 88%,var(--tx));
  border-color:color-mix(in srgb,var(--opt-fill) 66%,#000);
  /* The lit top edge gets brighter under the pointer instead of the box getting taller. It used
     to grow its wall by a pixel and lift; with no wall left, a lift is a shift of the text over
     nothing, so the light does the work. Still INSET — see the ⛔ above. */
  box-shadow:inset 0 1px 0 rgba(255,255,255,.46),inset 0 -2px 0 rgba(0,0,0,.22)}
/* Bootstrap's own pressed state. The travel was 3px while the button stood on a 3px wall — it had
   to fall the distance it claimed to stand. There is no wall now, so 1px: enough that a press is
   felt in the same frame, small enough that nothing jumps. */
.opt-btn:active{background-color:color-mix(in srgb,var(--opt-fill) 80%,var(--tx));
  border-color:color-mix(in srgb,var(--opt-fill) 52%,#000);
  transform:translateY(1px);
  box-shadow:inset 0 3px 5px rgba(0,0,0,.125),inset 0 1px 0 rgba(0,0,0,.18)}
/* `.25rem` of half-transparent accent — `.btn:focus-visible` to the millimetre. The body stays
   under the ring: a focused button is still a raised button. */
.opt-btn:focus-visible{outline:none;
  box-shadow:var(--opt-lift),0 0 0 4px color-mix(in srgb,var(--opt-fill) 50%,transparent)}
/* ═══ THE ONE I WOULD TAKE ═══
   «or just make it bold» — so, both: a 600 caption and a mark the eye lands on before it reads.
   ⛔ THE MARK IS A THUMB IN FRONT, NOT A BAR UNDERNEATH, AND NO LONGER A ► (owner, 11.09 evening:
   «let's remove that rude line below and replace with white "play" triangle in the front», then
   the same night: «that symbol looks weird. change it to be thumbs up 👍 … do it white stylish as
   color of text», with a picture of the Character Map open over the button). The bar was a white
   3px background layer along the bottom edge, and underlining is how a browser draws a LINK and
   how this feed draws a correction — on a filled button it read as damage rather than as a
   recommendation. The ► that replaced it is what every media player on earth draws for PLAY, so
   on a button it said «press me» to all three options and «the one I would take» to none. A mark
   in front says «start here» in the reading direction, at the place the eye already begins, and
   costs the button no height at all — that half has held through all three drawings.
   ⛔ IT IS A MASKED SHAPE, NOT THE EMOJI CHARACTER, and that is what makes «as color of text»
   buildable at all: a colour 👍 cannot be given a colour, it outweighs every word beside it, and
   the machine these pictures are taken on has no emoji font — so a character here would show him a
   tofu rectangle in every screenshot this project ever sends. A mask is painted with
   `background`, so the mark IS the caption's own ink: `--opt-ink` is the colour the letters beside
   it are drawn in, which `themes.js → ink()` computes for the fill the button actually wears —
   white on Sky, near-black on a light palette, one rule for all eleven.
   ⚠ NOT `--strong`, which the triangle used. That token is the whitest ink for the PAGE; this mark
   lies on the FILL, and the two part company the moment a theme names an `--opt-bg` of its own —
   which is exactly what Sky does.
   ⛔ IT IS `::before` CONTENT, WHICH IS NOT `textContent` — so it never reaches `dataset.send`
   and the words that travel under his name stay «Option A — merge it now.» That is the same
   reason the ✓ of `.picked` is drawn this way, and it is why the two can share one pseudo-
   element: `.picked` and `.sent` are written AFTER this rule at equal specificity, so a chosen
   recommendation shows the tick and not the thumb. Order is load-bearing here — moving this rule
   below them would leave a picked option still recommending itself.
   ⚠ AND THE TICK GETS ITS BOX BACK BY ITSELF. Everything that makes this a picture — the size, the
   mask, the `background` that colours it — is set in THIS rule, and this rule stops MATCHING the
   moment `.picked` arrives, so a `content:'✓ '` is drawn as an ordinary character with nothing
   left over. That only holds while the two live in separate rules; merged into one selector, the
   tick would be painted into a 1em masked box and vanish.
   The gloss stays where it belongs, on `.opt-btn`: this rule sets `background` on the pseudo-
   element only, so there is still not one place here that can wipe the emboss off the one button
   the eye is meant to land on. */
.opt-btn.opt-default{font-weight:600}
/* Scoped away from the two states that own this pseudo-element already, so the ✓ of a chosen or a
   spent option is never drawn at the triangle's size and colour. It cannot fight `.picked::before`
   for specificity, because it stops MATCHING the moment that class arrives. */
.opt-btn.opt-default:not(.picked):not(.sent)::before{content:'';display:inline-block;
  /* Sized in `em`, so the mark grows with his reading scale and with the phone's own type size —
     a mark pinned to 13px is a mark that shrinks away exactly when the text is made bigger. */
  width:1em;height:1em;
  /* ⛔ THE GAP IS A MARGIN, NOT A SPACE IN THE CONTENT. A trailing space inside generated content
     is collapsible white space like any other, so its width is the font's and it is the first
     thing a narrow line gives back — the ► draft drew «►Option A» touching. 6px whatever the face
     and whatever the width. */
  margin-right:6px;
  /* An `inline-block` sits ON the baseline, and a thumb standing on it floats above the letters'
     x-height; dropped a seventh of an em it centres against the caption beside it. */
  vertical-align:-.15em;
  /* THE COLOUR IS THE CAPTION'S. `background` paints through the mask, so this one declaration is
     the whole of «white, as the colour of the text» — and it inverts with the theme because
     `--opt-ink` does. */
  background:var(--opt-ink);
  -webkit-mask:var(--opt-thumb) center/contain no-repeat;
  mask:var(--opt-thumb) center/contain no-repeat}
/* Chosen and not yet gone. Now that EVERY option is filled, the fill can no longer be what says
   «this is the one» — the ring does, plus the tick. It is still the one thing on screen saying
   the countdown is about him. */
.opt-btn.picked{background-color:color-mix(in srgb,var(--opt-fill) 76%,var(--tx));
  color:var(--opt-ink);border-color:color-mix(in srgb,var(--opt-fill) 52%,#000);
  box-shadow:var(--opt-lift),0 0 0 3px color-mix(in srgb,var(--opt-fill) 38%,transparent)}
.opt-btn.picked::before{content:'✓ '}
/* Already sent. The fill drains back toward the page so a spent choice cannot be mistaken for one
   still on offer — but it stays pressable, because answering the same question twice is his to
   do, and a chosen option that looks untouched is the feed forgetting his own decision. */
/* A spent choice lies FLAT — no gloss, no lit edge, no emboss. Those layers are what say «this can
   still be pressed for the first time», so a sent option keeps none of them. */
.opt-btn.sent{background-color:color-mix(in srgb,var(--acc) 16%,var(--bg-2));
  background-image:none;
  border-color:color-mix(in srgb,var(--acc) 34%,transparent);
  border-style:dashed;color:var(--tx-2);box-shadow:none;text-shadow:none;transform:none}
.opt-btn.sent .opt-tag{border-right-color:var(--line-2);box-shadow:none}
.opt-btn.sent::before{content:'✓ ';color:var(--ok)}
@media (prefers-reduced-motion:reduce){.opt-btn{transition:none}
  .opt-btn:hover,.opt-btn:active{transform:none}}

/* ═══ A GUESS IS DRAWN AS A GUESS ═══
   A row of chips UNDER the answer, not inside its sentences — the panel worked these out from the
   text and cannot honestly point at the words they came from. The caption says so in words, and
   the chips are dashed, so «the panel was told this» and «the panel thinks this» never look alike.
   Nothing here may be quieter than a labelled button by so much that it reads as decoration: it is
   still a control that sends a message under his name.
   THIS IS THE PAIR BOOTSTRAP DRAWS: a labelled option is `.btn-primary` (filled), a guess is
   `.btn-outline-primary` — same hue, same weight of border, no fill until the pointer is on it.
   Filling BOTH when the fill arrived on 10.09 would have quietly deleted the one distinction this
   whole block exists for, so the outline is load-bearing, not a leftover. */
.opt-guess{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:10px 0 2px;
  padding-top:8px;border-top:1px dashed var(--line-2)}
.og-cap{flex:0 0 100%;font:11.5px var(--sans);color:var(--tx-3)}
/* ⛔ AND THE 3D BODY OF 11.09 STOPS AT THIS LINE. A guess is a `.btn-outline-primary`: no fill, so
   no face to light, no wall to stand on and nothing to press IN. Letting the emboss reach these
   chips would have made the panel's own guesses look exactly as solid as the words a session
   actually wrote — which is the one distinction this whole block exists for. Each state says it
   again, because `:hover` and `:active` on `.opt-btn` own `transform` and `box-shadow` and would
   otherwise reach a chip that declares neither. */
.opt-btn.og-btn{display:inline-block;font:12.5px var(--sans);padding:3px 10px;
  background-color:transparent;background-image:none;color:var(--acc);border:1px dashed var(--acc);
  box-shadow:none;text-shadow:none;transform:none}
.opt-btn.og-btn:hover{background-color:color-mix(in srgb,var(--acc) 18%,transparent);color:var(--acc);
  border-color:var(--acc);box-shadow:none;transform:none}
.opt-btn.og-btn:active{box-shadow:inset 0 2px 4px rgba(0,0,0,.14);transform:none}
/* Picked, a guess stops hedging and fills — it is about to speak under his name. */
.opt-btn.og-btn.picked{border-style:solid;background-color:var(--acc);color:var(--on-acc);
  border-color:var(--acc);box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 38%,transparent)}
.opt-btn.og-btn.sent{border-style:dashed;background-color:color-mix(in srgb,var(--acc) 14%,var(--bg-2));
  border-color:color-mix(in srgb,var(--acc) 34%,transparent);color:var(--tx-2);box-shadow:none}

/* The pen on an open question: quiet, because it sits at the end of a line that is being READ. */
.ask-pen{display:inline-block;margin-left:6px;padding:2px 6px;line-height:1;vertical-align:baseline;
  font:11.5px var(--sans);border:1px solid var(--line-2);border-radius:7px;
  background:var(--bg-3);color:var(--tx-3);cursor:pointer}
.ask-pen:hover{color:var(--acc);border-color:var(--acc)}

/* ═══ THE FIVE QUIET SECONDS, DRAWN ═══
   Same place and same shape as the Yes/No strip above the typing box — one strip in one place is
   one habit. The border is the ACCENT and not the alarm red: nothing has gone wrong here, an
   answer is simply on its way, and a countdown drawn in the colour of a fault reads as one. */
/* Fixed to the VIEWPORT, not parked inside the composer — the composer folds to nothing in
   reading mode and would take the countdown and its Cancel button with it. Same geometry as
   `.undo-bar`, which exists for exactly this job: be seen above the field, whatever the field is
   doing. `--composer-h` is written by web/mobile.js and goes to ~0 when the field folds, so the
   strip simply settles lower instead of floating over nothing. */
/* ⛔ THE `left` AND THE `width` HERE ARE ONLY THE FALLBACK — `fitPickBar()` in web/app.js overwrites
   both on every tick, with the box of THIS WINDOW rather than of the screen. In the workspace the
   screen holds up to eight conversations, and a countdown centred on it stood over all of them
   (owner, 11.09). These two values are what a strip drawn before the layout can be measured gets,
   and they are what it has always had. */
.pick-bar{position:fixed;left:50%;bottom:calc(var(--composer-h, 84px) + 16px);transform:translateX(-50%);
  z-index:220;width:calc(92vw / var(--ui-scale,1));max-width:var(--read-w);
  display:flex;align-items:center;gap:8px;box-shadow:0 10px 30px var(--shadow);
  padding:7px 10px;border:1px solid var(--acc);border-radius:10px;background:var(--bg-4)}
.pb-ico{flex:none;font-size:13px;line-height:1;color:var(--acc)}
.pb-list{flex:1;min-width:0;font:12.5px var(--sans);color:var(--tx);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The seconds are the one number on this strip, and they change four times a second: tabular
   figures keep the strip from twitching sideways under a counting digit. */
.pb-left{flex:none;font:11.5px var(--mono);color:var(--tx-2);font-variant-numeric:tabular-nums}
.pb-btn{flex:none;font:11.5px var(--sans);padding:4px 9px;border-radius:8px;cursor:pointer;
  border:1px solid var(--line-2);background:var(--bg-3);color:var(--tx-2)}
.pb-btn:hover{color:var(--tx);border-color:var(--acc)}
.pb-btn.pb-go{font-weight:600;color:var(--tx);border-color:var(--acc)}
/* On a phone the list of chosen answers takes the whole first line and the buttons drop under it
   — exactly as the Yes/No strip does, and for the same reason: the only thing on this strip that
   MUST be read is what is about to be sent in his name. */
@media (max-width:860px){
  .pick-bar{flex-wrap:wrap}
  .pb-list{flex:1 1 calc(100% - 30px);white-space:normal;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
  .pb-go{margin-left:auto}
}
.turn-cost{font:11.5px var(--mono);color:var(--tx-3)}

/* ═══ mobile ═══ */
.only-mobile{display:none}
.icon-btn{background:none;border:none;color:var(--tx-2);font-size:17px;cursor:pointer;padding:2px 6px}
@media(max-width:860px){
  .app{grid-template-columns:1fr}
  /* ═══ ON A PHONE THE LIST OPENS THE WHOLE WIDTH (owner, 09.09) ═══
     It was a 264px drawer, chosen when a phone meant 430 local pixels and the strip of
     conversation left showing was a way out of the list. On his real 374 it is 71% of the
     screen — wide enough to hide the talk, narrow enough to cut every task name — and the
     leftover strip is not a door anybody aims at. Full width, so a name has the room a name
     needs.
     ⛔ `inset:0`, NEVER `width:100vw`. `vw` is SCREEN pixels used as LOCAL ones, so under
     `zoom:--ui-scale` a 100vw panel is `scale` times wider than the screen — the mine that
     drove the translate window off the right edge. A fixed element's containing block IS the
     viewport, so stretching it by its edges is exactly full screen at every zoom. */
  .sidebar{position:fixed;inset:0;width:auto;z-index:40;transform:translateX(-100%);transition:transform .2s}
  .sidebar.open{transform:none;box-shadow:0 0 40px var(--shadow)}
  /* Full width means the conversation is no longer visible behind it, so the tap that used to
     dismiss the list has nothing to land on: the ✕ becomes the way out and is owed the size of a
     finger rather than of a glyph. The left swipe and the back gesture are the other two doors. */
  #btn-close-side{width:38px;height:38px;display:grid;place-items:center;font-size:19px;
    padding:0;border-radius:9px;color:var(--tx-2)}
  /* A 7px col-resize strip at the screen's own edge is a width drag nobody meant to start:
     the rail has no width to choose here, it is all of it. */
  .rail-grip{display:none}
  /* While the panel is in the hand there are no transitions: it is obliged to stand exactly
     where the finger is. The shadow, though, is already the one of an open panel: it has come
     out, even if not all the way. */
  .sidebar.dragging{transition:none;box-shadow:0 0 40px var(--shadow)}
  .only-mobile{display:block}
  .meters .meter{width:64px}
  .wrap,.composer,.perm-dock,.wait-dock{padding-left:13px;padding-right:13px}
  .composer{padding-bottom:max(12px,env(safe-area-inset-bottom))}
}

/* ═══════════════════ session heat ═══════════════════ */
.sess{position:relative;padding-left:11px}
.sess::before{content:'';position:absolute;left:3px;top:9px;bottom:9px;width:2px;border-radius:2px;background:var(--ok)}
.sess.heat-fresh::before{background:var(--ok)}
.sess.heat-warm::before{background:var(--warn)}
.sess.heat-hot::before{background:var(--acc)}
.sess.heat-dead::before{background:var(--err);box-shadow:0 0 8px var(--err)}
.sess-top{display:flex;align-items:center;gap:6px}
.sess-gauge{height:2px;background:var(--bg-4);border-radius:2px;margin:5px 0 4px;overflow:hidden}
.sess-gauge i{display:block;height:100%;background:var(--tx-3);transition:width .3s}
.sess.heat-warm .sess-gauge i{background:var(--warn)}
.sess.heat-hot  .sess-gauge i{background:var(--acc)}
.sess.heat-dead .sess-gauge i{background:var(--err)}
.sess-m{justify-content:space-between}
.sess-ctx{font-family:var(--mono)}
.warm-yes{color:var(--acc)}
.warm-no{opacity:.5}
.sess-warn{font-size:10px;color:var(--err);margin-top:3px}
.sess.heat-dead{opacity:.85}

/* ═══════════════════ agent strip ═══════════════════ */
.agent-strip{display:flex;gap:7px;padding:0 22px;flex-wrap:wrap}
.agent-strip:not(:empty){padding-top:9px}
.agent-chip{display:flex;align-items:center;gap:6px;background:var(--bg-3);border:1px solid var(--line-2);border-radius:99px;padding:3px 11px;font-size:11.5px;cursor:pointer}
.agent-chip:hover{border-color:var(--acc)}
.chip-tool{font:10.5px var(--mono);color:var(--tx-3)}
.agent-dot{width:6px;height:6px;border-radius:50%;background:var(--acc);animation:pulse 1.2s infinite}
/* An agent's plate is a button, not a caption: it opens his long sheet. The type and the MODEL
   side by side, because «general-purpose» five times in a row does not tell one agent from
   another. */
.agent-chip{appearance:none;color:var(--tx-2);font-family:inherit;text-align:left}
.ac-type{font:600 11.5px var(--mono);color:var(--acc-2)}
.ac-model{font:10.5px var(--mono);color:var(--tx-3);background:var(--bg-2);border:1px solid var(--line);
  border-radius:99px;padding:0 6px;line-height:15px}
.agent-card{cursor:pointer}
.agent-card:hover{border-color:var(--acc)}

/* ── one agent whole: the prompt, the thinking, the tools ────────────────── */
.agent-view{width:min(860px,calc(96vw / var(--ui-scale,1)))}
.ag-meta{display:flex;flex-wrap:wrap;gap:5px 16px;padding:9px 16px;border-bottom:1px solid var(--line);background:var(--bg-3)}
.ag-cell{display:flex;align-items:baseline;gap:6px}
.ag-k{font-size:10.5px;color:var(--tx-3);text-transform:uppercase;letter-spacing:.4px}
.ag-v{font:600 12px var(--mono);color:var(--tx)}
.ag-body{display:flex;flex-direction:column;gap:8px}
.ag-task{border:1px solid var(--line-2);border-radius:10px;background:var(--bg-3);overflow:hidden}
.ag-task-h{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 11px;cursor:pointer}
.ag-task-t{font:600 12px var(--sans);color:var(--acc-2)}
.ag-task-n{font:10.5px var(--mono);color:var(--tx-3)}
.ag-prompt{margin:0;padding:0 11px 11px;white-space:pre-wrap;word-break:break-word;
  font:12px/1.55 var(--mono);color:var(--tx-2);max-height:46vh;overflow:auto}
.ag-task.folded .ag-prompt{max-height:64px;overflow:hidden;
  -webkit-mask-image:linear-gradient(#000 40%,transparent);mask-image:linear-gradient(#000 40%,transparent)}
.ag-step{white-space:pre-wrap;word-break:break-word;font:12.4px/1.6 var(--sans)}
/* Thinking is not an answer: muted and with a rule on the left, so the long sheet reads as a stream. */
.ag-thinking{color:var(--tx-3);font-style:italic;border-left:2px solid var(--line-2);padding-left:10px}
.ag-text{color:var(--tx)}
.ag-tool{display:flex;gap:8px;align-items:baseline;font:11.5px var(--mono);color:var(--tx-3)}
.ag-tool-n{color:var(--acc-2);flex:none}
.ag-tool-t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ag-tool.bad .ag-tool-n{color:var(--err)}
.ag-more{font:11px var(--mono);color:var(--tx-3);text-align:center;padding:4px}
.ag-done{border-top:1px solid var(--line);padding-top:8px;font:12px/1.55 var(--sans);color:var(--ok)}

/* ── all the agents at once: one after another, nothing to click ─────────── */
.ag-bar{display:flex;align-items:center;gap:10px;padding:7px 16px;border-bottom:1px solid var(--line);
  font-size:11.5px;color:var(--tx-3);flex-wrap:wrap}
.ag-note{flex:1;min-width:0}
.act-btn.thin{padding:2px 9px;font-size:11px}
.act-btn.ag-all{width:100%;margin-bottom:8px}
.agent-view{width:min(1000px,calc(96vw / var(--ui-scale,1)))}
.ag-stack{display:flex;flex-direction:column;gap:14px}
/* ⚠ A FLEX COLUMN SHRINKS ITS CHILDREN, AND `overflow:hidden` MAKES THAT INVISIBLE (07.09).
   The cards are flex items in a scrolling box; with the default `flex-shrink:1` the browser
   squeezed every one of them to fit the modal's height and clipped the rest — so an agent with
   55 steps in the DOM drew a 174-pixel card that ended at the fold, and eleven agents turned the
   focused one into an empty orange sliver. Nothing was missing from the data: it was cut off by
   the layout. `flex:0 0 auto` is the whole cure. */
.ag-stack>.ag-one{flex:0 0 auto}
.ag-one{border:1px solid var(--line);border-radius:10px;background:var(--bg-2);overflow:hidden}
.ag-one.focus{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc)}
.ag-one.done{opacity:.82}
.ag-one.done .agent-dot{animation:none;background:var(--ok)}
.ag-one-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 11px;
  background:var(--bg-3);border-bottom:1px solid var(--line)}
.ag-one-n{font:600 12.5px var(--mono);color:var(--acc-2)}
.ag-eff{font:10.5px var(--mono);color:var(--tx-3)}
.ag-state{font:11px var(--mono);color:var(--tx-3)}
.ag-state.on{color:var(--acc)}
.ag-one-t{margin-left:auto;font:10.5px var(--mono);color:var(--tx-3)}
.ag-one-d{font-size:12px;color:var(--tx-2);padding:8px 11px 0;line-height:1.45}
.ag-one .ag-meta{background:transparent;border-bottom:0;padding:8px 11px 2px}
.ag-one .ag-task{margin:8px 11px 0}
.ag-steps{display:flex;flex-direction:column;gap:8px;padding:9px 11px 11px}
.ag-steps .pane-empty{padding:4px 0}
.ag-one .ag-done{margin:0 11px 11px}
.ag-more{appearance:none;background:transparent;border:0;color:var(--tx-3);font:11px var(--mono);
  cursor:pointer;text-align:left;padding:2px 0}
.ag-more:hover{color:var(--acc)}

/* ── where it came from, what it cost, and the four folds ────────────────── */
.ag-from{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;padding:6px 11px 0;font-size:11.5px;color:var(--tx-3)}
.ag-proj{font:600 11.5px var(--mono);color:var(--tx-2)}
.ag-row{font:600 11.5px var(--sans);color:var(--acc-2)}
.ag-row.guess{font-weight:400;font-style:italic;color:var(--tx-3)}
.ag-sep{color:var(--line-2)}
.ag-bg{border:1px solid var(--line-2);border-radius:99px;padding:0 6px;font:10px var(--mono)}
.ag-nums{display:flex;flex-wrap:wrap;gap:4px 8px;padding:8px 11px 2px}
.ag-num{display:flex;flex-direction:column;gap:1px;min-width:74px;padding:4px 8px;
  border:1px solid var(--line);border-radius:8px;background:var(--bg-3)}
.ag-num.cash{border-color:var(--acc-2)}
.ag-num-v{font:600 12.5px var(--mono);color:var(--tx)}
.ag-num-k{font-size:9.5px;color:var(--tx-3);text-transform:uppercase;letter-spacing:.4px}
.ag-folds{display:flex;flex-direction:column;gap:6px;padding:9px 11px 11px}
.ag-fold{border:1px solid var(--line-2);border-radius:10px;background:var(--bg-3);overflow:hidden}
.ag-fold-h{display:flex;align-items:baseline;gap:8px;padding:7px 10px;cursor:pointer}
.ag-fold-h:hover{background:var(--bg-2)}
.ag-arrow{color:var(--acc-2);font-size:10px;flex:none;transform:rotate(90deg);transition:transform .12s}
.ag-fold.folded .ag-arrow{transform:none}
.ag-fold-t{font:600 12px var(--sans);color:var(--acc-2);flex:none}
.ag-fold-n{font:10.5px var(--mono);color:var(--tx-3);flex:none}
.ag-peek{font-size:11px;color:var(--tx-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.ag-fold-b{padding:0 10px 10px}
.ag-fold.folded .ag-fold-b{display:none}
.ag-fold .ag-prompt{padding:0;max-height:52vh}
.ag-fold .ag-steps{padding:2px 0 0}
.ag-sub{font:11px var(--mono);color:var(--tx-3);padding:2px 0 6px}
.ag-result{color:var(--tx);max-height:60vh;overflow:auto}

/* ═══════════════════ inspector ═══════════════════ */
.app{grid-template-columns:var(--rail-w) 1fr 0}
.app:has(.inspector.open){grid-template-columns:var(--rail-w) 1fr 372px}
.inspector{background:var(--bg-2);border-left:1px solid var(--line);display:flex;flex-direction:column;min-height:0;overflow:hidden;width:0;transition:width .18s ease}
.inspector.open{width:372px}
.insp-head{display:flex;align-items:center;gap:8px;padding:9px 10px 9px 12px;border-bottom:1px solid var(--line)}
.insp-tabs{display:flex;gap:3px;flex:1}
.insp-tab{background:none;border:none;color:var(--tx-3);font:500 12px var(--sans);padding:5px 9px;border-radius:7px;cursor:pointer;white-space:nowrap}
.insp-tab:hover{color:var(--tx-2);background:var(--bg-3)}
.insp-tab.active{background:var(--bg-4);color:var(--tx)}
.insp-btn{font-size:15px}
.insp-body{flex:1;overflow-y:auto;min-height:0}
.insp-pane{display:none;padding:12px}
.insp-pane.active{display:block}
.pane-actions{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap}
.act-btn{background:var(--bg-4);border:1px solid var(--line-2);color:var(--tx);border-radius:7px;padding:5px 11px;font:500 12px var(--sans);cursor:pointer}
.act-btn:hover{border-color:var(--acc);color:var(--acc)}
.act-btn.ghost{background:none;color:var(--tx-3);font-size:11px;padding:3px 8px}
/* In the settings curtain the button lives under the thumb: full width and with a big target. */
.act-btn.wide{display:block;width:100%;text-align:left;padding:10px 12px;margin-top:9px;font-size:13px}
.act-btn.need{color:var(--warn);border-color:var(--warn);background:color-mix(in srgb, var(--warn) 12%, transparent)}
.act-btn.going{opacity:.55;pointer-events:none}
.pane-hint{font-size:11px;color:var(--tx-3)}
.pane-empty{color:var(--tx-3);font-size:12.5px;line-height:1.6;padding:14px 2px}
.pane-note{font-size:11.5px;line-height:1.55;color:var(--tx-3);background:var(--bg-3);border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin-bottom:10px}
.pane-note b{color:var(--tx-2)}
.pane-sub{font:600 10.5px var(--sans);letter-spacing:.9px;text-transform:uppercase;color:var(--tx-3);margin:16px 0 7px}

.ctx-head{margin-bottom:12px}
.ctx-big{font:600 27px var(--mono);color:var(--tx);line-height:1.1}
.ctx-sub{font-size:11.5px;color:var(--tx-3);margin-top:3px}
.bars{display:flex;flex-direction:column;gap:6px}
.bar-row{display:grid;grid-template-columns:104px 1fr 48px;align-items:center;gap:8px}
.bar-name{font-size:11.5px;color:var(--tx-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar-track{height:6px;background:var(--bg-4);border-radius:3px;overflow:hidden}
.bar-track i{display:block;height:100%;background:linear-gradient(90deg,var(--acc),var(--acc-2));border-radius:3px}
.bar-val{font:11.5px var(--mono);color:var(--tx-3);text-align:right}

.mini-table{border:1px solid var(--line);border-radius:8px;overflow:hidden}
.mini-row{display:flex;justify-content:space-between;gap:8px;padding:4px 9px;font-size:11.5px;border-bottom:1px solid var(--line)}
.mini-row:last-child{border-bottom:none}
.mini-row:nth-child(odd){background:var(--bg-3)}
.mini-n{color:var(--tx-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.mini-v{font-family:var(--mono);color:var(--acc-2)}
.mini-note{color:var(--tx-3);font-size:10.5px}
.mini-table.wide .mini-row{padding:6px 10px;font-size:12px}

.mcp-sum{display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-size:11.5px;color:var(--tx-2);margin-bottom:9px}
.mcp-sum-tok{color:var(--acc-2);font-family:var(--mono)}
.mcp-list{display:flex;flex-direction:column;gap:5px}
.mcp-row{display:flex;gap:9px;align-items:flex-start;padding:8px 10px;border:1px solid var(--line);border-radius:9px;background:var(--bg-3);opacity:.55;transition:.15s}
.mcp-row.on{opacity:1;border-color:var(--line-2)}
.mcp-row input{margin-top:3px;accent-color:var(--acc);cursor:pointer}
.mcp-main{flex:1;min-width:0}
.mcp-line{display:flex;justify-content:space-between;gap:8px;align-items:baseline}
.mcp-name{font:600 12.5px var(--sans)}
.mcp-tok{font:11.5px var(--mono);color:var(--acc-2)}
.mcp-track{height:3px;background:var(--bg-4);border-radius:2px;margin:5px 0 4px;overflow:hidden}
.mcp-track i{display:block;height:100%;background:var(--acc)}
.mcp-meta{display:flex;gap:8px;font-size:10.5px;color:var(--tx-3);flex-wrap:wrap}
.mcp-off{color:var(--err)}
.mcp-det{margin-top:6px}
.mcp-det summary{font-size:11px;color:var(--tx-3);cursor:pointer;margin-bottom:5px}
.mcp-det summary:hover{color:var(--acc)}

.agents-list{display:flex;flex-direction:column;gap:7px}
.agent-card{border:1px solid var(--line);border-radius:9px;padding:9px 11px;background:var(--bg-3)}
.agent-card.done{opacity:.6}
.agent-card.done .agent-dot{animation:none;background:var(--ok)}
.agent-top{display:flex;align-items:center;gap:7px}
.agent-name{font:600 12px var(--mono);color:var(--acc-2)}
.agent-tok{margin-left:auto;font:11px var(--mono);color:var(--tx-3)}
.agent-desc{font-size:12px;color:var(--tx-2);margin-top:4px;line-height:1.45}
.agent-tool{font:11px var(--mono);color:var(--tx-3);margin-top:4px}
.agent-sum{font-size:11.5px;color:var(--tx-3);margin-top:5px;border-top:1px dashed var(--line);padding-top:5px;line-height:1.5}

.week-bar-big{height:8px;background:var(--bg-4);border-radius:4px;overflow:hidden;margin-bottom:14px}
.week-bar-big i{display:block;height:100%;background:var(--ok);transition:width .4s}
.week-bar-big i.warm{background:var(--acc)}
.week-bar-big i.hot{background:var(--err)}

@media(max-width:1200px){
  .app:has(.inspector.open){grid-template-columns:var(--rail-w) 1fr 330px}
  .inspector.open{width:330px}
}
@media(max-width:860px){
  .app,.app:has(.inspector.open){grid-template-columns:1fr}
  .inspector{position:fixed;inset:0 0 0 auto;width:min(400px,calc(92vw / var(--ui-scale,1)));z-index:45;transform:translateX(100%);transition:transform .2s}
  .inspector.open{transform:none;width:min(400px,calc(92vw / var(--ui-scale,1)));box-shadow:0 0 40px var(--shadow)}
  .agent-strip{padding-left:13px;padding-right:13px}
}

/* ═══════════════════ code, diffs, syntax ═══════════════════ */
html{overflow-x:clip}
.code-pre{background:var(--bg);border:1px solid var(--line);border-radius:7px;padding:9px 11px;overflow-x:auto}
.code-pre code{font:11.8px/1.55 var(--mono);color:var(--tx-2);white-space:pre}
.path-line{font:11.5px var(--mono);color:var(--acc-2)!important;background:none;padding:0}
.s{color:var(--sx-s)}          /* strings  */
.c{color:var(--sx-c);font-style:italic}   /* comments */
.k{color:var(--sx-k)}          /* keywords */
.n{color:var(--sx-n)}          /* numbers  */
.key{color:var(--sx-key)}        /* json keys */

.diff{border:1px solid var(--line);border-radius:7px;overflow:hidden;font:11.8px/1.5 var(--mono)}
.diff-line{display:flex;gap:8px;padding:1px 9px;white-space:pre-wrap;word-break:break-word}
.diff-sign{opacity:.7;user-select:none;flex:none;width:9px}
.diff-txt{flex:1}
.diff-line.del{background:var(--del-bg);color:var(--del-tx)}
.diff-line.add{background:var(--add-bg);color:var(--add-tx)}

.notice.n-warn{color:var(--warn)}
.notice.n-warning{color:var(--warn)}
.notice.n-error{color:var(--err)}
/* The offer under a deaf microphone: one chip per input he could switch to. It is drawn while he
   is still talking and is often pressed on a phone with the field open, so the chip carries a real
   touch target (the 11.5px line of a notice is not one) and wraps instead of running off the edge. */
.notice.mic-swap{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:6px 0}
.notice.mic-swap button{min-height:32px;padding:5px 11px;border-radius:999px;cursor:pointer;
  background:var(--bg-3);border:1px solid var(--warn);color:var(--tx-1);font:12px var(--mono)}
.notice.mic-swap button:hover{background:var(--warn);color:var(--bg-1)}

/* ═══════════════════ project tree ═══════════════════ */
.tree-search{width:100%;background:var(--bg-3);border:1px solid var(--line);border-radius:8px;
  padding:7px 10px;color:var(--tx);font:12.5px var(--sans);outline:none}
.tree-search:focus{border-color:var(--acc)}
/* `overflow-y:auto` makes the other axis `auto` BY ITSELF (the specification: visible next to
   auto does not live that way) — that is, the rail also grew a HORIZONTAL bar the moment one
   row did not fit in width. It ate height from below and blinked together with the footer. The
   same mine had already gone off on `.sheet-body`. `scrollbar-gutter:stable` holds room for
   the bar always: otherwise the list, growing to the edge of the screen, twitches the whole
   width of the rows by 9px. */
.tree{overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;min-height:0;flex:1;
  display:flex;flex-direction:column;gap:1px;margin:0 -6px}
/* ── A RAIL ROW HAS NO RIGHT TO SHRINK ──
   `.tree` is a flex column, and flex squeezes its items when the content is longer than the
   screen. min-height saves the row itself from that, but on the phone the row lies inside the
   swipe WRAPPER (`.swipe`), which had no height: it was squeezed from 34px down to 22 and
   lower, and `overflow:hidden` cut the letters off. The longer the list, the stronger it was —
   and the rail on the phone turned into emptiness with the fold strips (they have no wrapper,
   which is why they survived) and stumps of rows along the edge. */
.tree>*{flex:0 0 auto}
.tree-loading{color:var(--tx-3);font-size:12px;padding:10px 8px}
/* ═══ THE RAIL IS STILL SCANNING — a line that MOVES, and says how long it will wait ═══
   A still line cannot be told from a frozen page, and «failed» after six seconds was a true
   sentence about a panel that was perfectly alive (the owner, 30.09). The ring is OUR OWN
   element, never an emoji: this box has no emoji font, and an emoji takes neither the theme's
   colour nor the line's size. */
.tree-wait{display:flex;align-items:center;gap:7px}
.tree-spin{width:11px;height:11px;flex:0 0 auto;border-radius:50%;
  border:2px solid color-mix(in srgb,var(--tx-3) 40%,transparent);
  border-top-color:var(--tx-2);animation:tree-spin 1.15s linear infinite}
@keyframes tree-spin{to{transform:rotate(360deg)}}
/* The press that means «I am waiting» — it hands the sockets back and asks again. */
.tree-again{margin-top:8px;padding:5px 11px;border-radius:7px;font-size:12px;
  border:1px solid var(--line-2);background:var(--bg-3);color:var(--tx);cursor:pointer}
.tree-again:hover{background:var(--bg-4)}
.row{display:flex;align-items:center;gap:5px;padding:4px 8px 4px 0;border-radius:7px;cursor:pointer;
  font-size:12.5px;user-select:none;min-height:25px}
/* On the phone the rail is not a side strip but a full-screen list for the thumb: 12.5px there
   is read at the limit, and a 25px row is not hit with a finger the first time. */
@media (max-width:860px){
  /* Bigger letters, tighter rows. That is not a contradiction: before, 13.8px was stretched over
     34px of height — that is, half the row was taken up by air, and it was still small to read. */
  .row{font-size:15px;min-height:27px;gap:7px;padding:1px 8px 1px 0}
  .tree{gap:0}
  .row-tok,.pill,.row-heat{font-size:11.5px}
  /* The list's heading and the magnifier are what the hands use in the rail. */
  .side-label{font-size:12.5px;letter-spacing:.6px}
  .side-label .mini-btn{font-size:18px}
  .side-block{padding:0 10px 6px}
}
.row:hover{background:var(--bg-3)}
/* ── «I AM STANDING HERE» ──
   The row open in this window is obliged to be visible from the other end of the room: a thin
   2px strip read as an accidental shadow, and the owner could not say which task he was on. So
   three signs at once — a thick accent bar on the left, a filled background and an accented
   name. */
/* ⚠ THE FILL IS A VARIABLE, AND THAT IS THE WHOLE CURE OF 06.09. `.swipe .row` (the phone's
   swipe wrapper, ~1400 lines further down) weighs the same two classes and stands LATER in the
   file, so its `background-color:var(--bg-2)` silently replaced this fill on every phone: all
   that survived of «I am standing here» was the 3px bar, because `box-shadow` is a different
   property — «когда я просто стою на задаче, она еле-еле слева скобочкой выделяется» (the owner,
   06.09). Exactly the class of bug the status colours hit on 28.08, one row lower down.
   Naming the colour `--row-bg` means the wrapper, the sticky heading and this rule all read ONE
   decision instead of three copies that drift apart — and a custom property is not reset by the
   `background:` shorthand, so the status tint in `background-image` survives untouched. */
.row.current{--row-bg:var(--bg-4);background:var(--row-bg);box-shadow:inset 3px 0 0 var(--acc);position:relative}
/* ── A THEME MAY GIVE THE OPEN ROW A FILL OF ITS OWN («Sky», the owner, 03.09) ──
   «active line /chat in left panel - light blue with white text. Accent arc - saturated blue or
   white.» A bright fill cannot be written into `.row.current` above, because the row's children
   carry the RAIL's colours — the quiet `--tx-3` of a project name, of the tail and of a cold
   heat digit — and every one of them would vanish on it. So the fill comes with its own ink,
   and the whole block is behind `[data-rowfill]`, which `themes.js` writes on <html> ONLY for a
   theme that declared `--cur-bg`. The ten older themes match this byte for byte through the
   defaults in `expand()` (`--cur-bg` = `--bg-4`, `--cur-tx` = `--tx`, `--cur-arc` = `--acc`), so
   nothing about them may move. The attribute is what buys that: an unguarded rule would have
   repainted a project row's name in every palette at once.

   ⚠ THE FILL GOES THROUGH `--row-bg` HERE TOO, AND THE ARC IS THE ROW'S OWN `box-shadow`.
   The arc used to be a `::before` child; the 06.09 cure above moved both the fill and the bar
   onto the row itself, so a theme's fill written as a bare `background:` would be wiped again by
   `.swipe .row` on the phone, and an arc written on a `::before` that no longer exists would
   simply not be drawn. Same two decisions as `.row.current`, in the theme's own colours. */
[data-rowfill] .row.current{--row-bg:var(--cur-bg);background:var(--row-bg);color:var(--cur-tx);
  box-shadow:inset 3px 0 0 var(--cur-arc)}
[data-rowfill] .row.current .row-name{color:var(--cur-tx)}
[data-rowfill] .row.current .row-tail,
[data-rowfill] .row.current .row-tok,
[data-rowfill] .row.current .row-here,
[data-rowfill] .row.current .row-heat.cold{color:var(--cur-tx-2);opacity:1}
/* ── THE OPEN TASK IS RINGED WITH ONE HAIRLINE, AND THE RING IS AN INSET SHADOW (30.09) ──
   «add 1 px thin outer white line» (the owner). A `border` or an `outline` was never available
   here: a border grows the row by 2px, and the rail's own header law says text may be shortened
   but a control may never be pushed off the edge — an open row one line wider would shove its
   beacon, its name and its numbers, i.e. the whole line would jump the moment the caret arrived.
   `inset 0 0 0 1px` is clipped by the row's own 7px radius, so the ring follows the corners for
   free and costs not one pixel of layout.
   ⚠ THE ARC IS LISTED FIRST ON PURPOSE. A `box-shadow` list paints first-on-top, so the 3px arc
   stays whole over the ring's left edge instead of being cut into by it — the two are one sign,
   and a ring drawn across the arc would read as a broken bracket.
   ⛔ The ring is the TASK's, never the folder's: see `--cur-bg-2` in themes.js — the whole point
   of this pair is that the open folder and the open task stopped looking like one slab. */
[data-rowfill] .row-task.current{box-shadow:inset 3px 0 0 var(--cur-arc),inset 0 0 0 1px var(--cur-line)}
/* The folder holding the open task keeps its HALF bar (see `.row-project.current` below): the
   theme changes the colour of that decision, never the decision — and since 30.09 it also keeps
   the QUIETER of the two fills (`--cur-bg-2`), or a theme that names a fill of its own repaints
   the branch as loudly as the work and the 06.09 law «a folder and a task may not read the same»
   is lost to specificity. */
[data-rowfill] .row-project.current{--row-bg:var(--cur-bg-2);
  box-shadow:inset 2px 0 0 color-mix(in srgb,var(--cur-arc) 45%,transparent)}
/* ── THE OPEN ROW WAVES ONCE WHEN THE PANEL IS OPENED (30.09) ──
   «maybe also on panel load - somehow blink it (active task) or background 2 changes as soft
   transition from lighter to darker back colors» (the owner). A panel is opened onto a rail of
   dozens of rows and the eye has to FIND the one it left off in; a fill, however well chosen, is
   a still thing among still things, and motion is the one attribute peripheral vision reads for
   free. Two beats, exactly as asked — lighter, back, lighter, back — and then it is over: this is
   a greeting, not a beacon. A pulse that repeated would be a fourth meaning for motion in a rail
   that already spends it on a running turn, on a question and on an open microphone.
   ⚠ THE BACKGROUND IS ANIMATED AS `background-color`, NEVER THROUGH `--row-bg`. A custom property
   cannot be interpolated without `@property`, and an animation ON it would jump between two
   values instead of gliding. `background-color` also leaves the status tint alone — that one
   lives in `background-image` (28.08) — so a red «urgent» row still waves in its own colour.
   ⚠ An animated declaration outranks every normal one, so this also survives `.swipe .row` on the
   phone — the shorthand that has now twice eaten the open row's fill (28.08, 06.09).
   The class is written by the BUILDER in `web/tree.js` (`helloOn`), never onto the live node: the
   keyed repaint of 01.09 keeps a node only while its markup matches the freshly built one, so a
   class added behind the builder's back would make this the one row rebuilt on every frame — and
   a rebuilt node restarts its animation from frame 0, i.e. the greeting would never end. */
@keyframes cur-hello{
  0%,45%,90%,100%{background-color:var(--row-bg)}
  22%,67%{background-color:color-mix(in srgb,var(--row-bg) 76%,var(--cur-arc))}
}
.row.current.hello{animation:cur-hello 1.9s ease-in-out 1}
@media (prefers-reduced-motion: reduce){.row.current.hello{animation:none}}
.row-task.current .row-name{color:var(--acc);font-weight:600}
/* The caret marker: it says «the window is looking HERE». There is exactly one row, nothing can jump. */
.row-here{font:10px/1 var(--mono);color:var(--acc);flex:none;opacity:.9}
/* ── A TASK'S STATUS: THE COLOUR OF THE TAB, NOT AN ICON INSIDE IT ──
   «I do not want a strongly green underline at the bottom, it distracts. A light background on
   the tab is exactly what I want» (the owner, 28.08). This is an edit of his own canon of
   26.08, where it was exactly the other way round — a weak gradient plus a bright 2px strip
   along the bottom; the strip is gone from EVERY status now. The reason: a thin bright stroke
   is a LINE, the eye catches it as a border and comes back to it, that is, it CALLS; whereas
   an even fill is read with peripheral vision as a property of the row and does not distract.
   One device is left — a flat background across the whole tab, following its radius.
   ⚠ The fill is `background-image`, not `background`: `.row:hover` and `.row.current` write
   the background COLOUR with that same shorthand, and the status is obliged to live on top of
   both without cancelling them. Hence the `linear-gradient` of two IDENTICAL stops — that is
   the only way to paint the image layer with an even colour (a flat `background-image:<color>`
   does not exist in CSS).
   ⚠ `position:relative` stays, even though the status no longer has an `::after` of its own:
   it is held by `.row-hold::after` (the «the row is held» strip), which needs a positioned
   ancestor.
   There is deliberately no icon: it would cost a column in the narrowest place of the panel,
   while a colour takes up nothing. `work` has no rules at all — the rail without statuses has
   not changed by a single pixel. */
/* ── «WORKED IN THE LAST 24 HOURS» — A STEP OF BRIGHTNESS, NEVER A COLOUR ──
   «add highlight to "fresh" session within last 24h … or even better - task line background
   emphasis. And this again - via additional checkbox» (the owner, 15.09). Off by default; the
   tick is `fresh-24h` in Settings → General and `web/tree.js → freshRow` reads it at paint time.
   ⛔ THE WASH IS `--tx`, AND THAT IS THE WHOLE REASON IT IS ALLOWED TO EXIST. On his own rail
   about half the rows are under a day old — and «an accent spent eleven times is not an accent»
   (05.09, the revoked project band). `--acc` in this rail has exactly one job, «I am standing
   here», and `--err` / `--warn` / `--ok` are the three statuses; a fourth colour drawn on eight
   rows out of seventeen would take the answer away from the one row that needs it. A wash of the
   TEXT colour is not a signal competing with those — it is the row standing one step nearer. It
   also runs the right way on every palette by itself: it lifts a dark theme and deepens a light
   one, because `--tx` is the colour that contrasts with the background in either.
   ⚠ `background-image`, like the status, and never the `background` shorthand: the shorthand
   would wipe the status tint, and on a phone `.swipe .row` would wipe this one (28.08, 06.09).
   ⚠ IT STANDS ABOVE THE THREE STATUS RULES ON PURPOSE. Both are two classes, so the order in the
   file decides, and a status is set BY HAND and outranks every clock in this rail.
   Gate: `npm run fresh` — it asks THIS stylesheet, on a hand-built row, both bare and inside
   `.swipe`, and it reads the computed paint rather than the class, because a class check is green
   right through the two outages named above. */
.row-task.fresh{background-image:linear-gradient(color-mix(in srgb,var(--tx) 7%,transparent),color-mix(in srgb,var(--tx) 7%,transparent))}
.row-task.st-urgent,.row-task.st-almost,.row-task.st-done{position:relative}
/* The red one of the three is the only one obliged to CALL rather than merely answer a
   question: it is denser than the others. The geometry of all three is one and the same: the
   fill takes up not one pixel, so the list does not breathe when a status changes. */
.row-task.st-urgent{background-image:linear-gradient(color-mix(in srgb,var(--err) 25%,transparent),color-mix(in srgb,var(--err) 25%,transparent))}
.row-task.st-almost{background-image:linear-gradient(color-mix(in srgb,var(--warn) 21%,transparent),color-mix(in srgb,var(--warn) 21%,transparent))}
.row-task.st-done{background-image:linear-gradient(color-mix(in srgb,var(--ok) 19%,transparent),color-mix(in srgb,var(--ok) 19%,transparent))}
/* The colour of the TEXT of a row with a status lives not here but in the cluster of name
   colours (`.row-task .row-name` lower down the file) — the same place where cooling paints
   the name a dull `--tx-3`: the rules are of equal weight, and the order decides. */
/* Work that is done still does not shout: the name is a hair paler than the other statuses —
   that is enough for the eye to run past it, and too little to make the row hard to read. */
.row-task.st-done .row-name{opacity:.9}
.row-task.st-done.current .row-name{opacity:1}
/* A task that has not been born yet: there is no session on disk, but the row already stands
   and is highlighted — otherwise «new session» looks like a button that did nothing. */
.row-task.row-draft .row-name{color:var(--acc);font-style:italic}
.row-draft .row-heat{color:var(--tx-3);opacity:.8}
/* `overflow:hidden` cuts not only sideways but DOWNWARDS as well: with a fractional zoom the
   row's height is rounded down, and only the top halves of the letters are left. An explicit
   row height with slack (and `padding-block`) makes the clipping impossible at any scale. */
.row-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  line-height:1.5;padding:1px 0;min-height:1.5em}
/* The tail of a name («— what is happening to this thing») is paler than the head: the head is
   caught by the eye while running down the rail, the tail is read to the end once you have
   stopped. A colour rule of its own on a descendant beats any inherited one — both on the
   current row and on a cooled one. */
.row-tail{color:var(--tx-3);font-weight:400;opacity:.92}
.row-meta{display:flex;align-items:center;gap:5px;flex:none}
.row-tok{font:10.5px var(--mono);color:var(--tx-3)}
.pill{font:10px var(--mono);background:var(--bg-4);border-radius:9px;padding:0 5px;color:var(--tx-3)}
.pill.warm{background:var(--warn-bg);color:var(--acc)}

/* The only figure on the right is time. While the cache is alive it is the minutes of «how
   much longer it can be done cheaply», from green to red; once it has cooled — how much has
   passed, in cold blue. */
.row-heat{font:10.5px/1 var(--mono);min-width:27px;text-align:right;letter-spacing:-.2px}
.row-heat.w4{color:var(--ok)}
.row-heat.w3{color:var(--warn)}
.row-heat.w2{color:var(--acc)}
.row-heat.w1{color:var(--err)}
.row-heat.cold{color:var(--cold);opacity:.8}
/* ⟳ THE SYNC MARK ON A PROJECT LINE (web/tree.js -> gitState). Quiet on purpose: colour only
   when there is something to say, nothing at all for a folder that is not a repo with a remote. */
.gitmark{font:10px/1 var(--mono);letter-spacing:-.2px;opacity:.85}
/* A project held at the top by a hand: our own glyph, so it takes the theme's colour and the row's
   size. No state colour of its own — a pin is a fact, not an alarm. */
.pinmark{width:11px;height:11px;flex:0 0 auto;color:var(--tx-2);opacity:.9}
.gitmark.ok{color:var(--ok)}
.gitmark.wait{color:var(--warn)}
.gitmark.off{color:var(--cold);opacity:.42}

/* ── the fold: long since cooled ───────────────────────────────────────────
   One row instead of twenty. Deliberately muted: this is not the work but the door to it,
   and it must fall into the field of view last. */
.row-fold{color:var(--tx-3);font-size:12px;opacity:.72}
.row-fold:hover{opacity:1;color:var(--tx-2)}
.row-fold .row-name{font-style:normal;letter-spacing:.1px}
.row-fold.open{color:var(--tx-3)}
/* Emerging from the fold: the rows swim out from where they came, stepped by 22 ms. Without
   that, the unfolding reads as «the tree suddenly became a different one». */
@keyframes rail-unfold{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}
.row.unfold,.swipe-wrap.unfold{animation:rail-unfold .19s ease both}
@media (prefers-reduced-motion: reduce){.row.unfold,.swipe-wrap.unfold{animation:none}}

/* ── THE LIGHT ON THE LEFT: attention. (On the right — the price: the minutes of a warm cache.) ──
   Pulsing — a turn is running right now, there is no point going there.
   Steady — the turn has FINISHED and it has not been read yet: this is where to go.
   Out — it has been read. The room for it is always taken, otherwise the rows would jump
   sideways every time a turn starts and finishes. */
/* The same three rules serve the TILES IN THE RAIL'S HEAD (`.tot-pill .dot`): the sign «work
   is going on here» and the sign «finished and unread» are obliged to be exactly the same in
   the caption as they are on the rows below it — otherwise the head answers the same question
   in a second, slightly different voice. */
.row .dot,.tot-pill .dot,.asks-btn .dot{width:8px;height:8px;border-radius:50%;flex:none;background:transparent}
/* «Finished and unread» — a tick: it differs from the pulsing dot in shape and not only in
   movement, so it reads both with peripheral vision and on a still screen. */
/* The width is exactly that of the extinguished slot (8px): a wider tick made the row two
   pixels longer, and the rail twitched sideways every time a turn ended. The glyph is bigger
   than the slot — it is drawn on top, taking up no room. */
/* A DARKER GREEN THAN `--ok`, AND ONLY HERE. The owner asked for it twice on 04.09 («green a
   bit darker», then «green — even darker»), so the disc keeps 50% of the theme's green: the
   first ask took it to 65%, the second to 50. It is MIXED DOWN from the theme's own green and
   never written as a hex, so all 11 palettes keep their own hue and only this disc goes deeper;
   and the white tick inside gains contrast, which is the reason the request is right.
   Mixing with pure black in sRGB scales all three channels by the same factor, which is exactly
   what leaves the hue alone — a darker green, not a greyer one. That is why the second operand
   is #000 and not some hand-picked dark green: a hand-picked one would drag every palette's hue
   towards itself. `--ok` itself is untouched — it paints the lane dots, the toast and the diff,
   and none of those carry a white glyph inside them. */
.row .dot.done,.tot-pill .dot.done,.asks-btn .dot.done{background:none;box-shadow:none;
  color:color-mix(in srgb, var(--ok) 50%, #000);overflow:visible;
  font:700 11px/1 var(--sans);display:flex;align-items:center;justify-content:center}
/* THE CLASSIC ✓ — THE DEFAULT AGAIN SINCE 10.09, and it takes `--ok` WHOLE. The darkened mix
   above was computed for the filled square, where the green is a BODY carrying a white tick on
   top of it; the bare ✓ is the glyph itself, and at 50% of the theme's green it reads as a
   half-erased mark on every one of the eleven palettes. `.tick` is put on by `web/tree.js`
   exactly when it draws the character, so the two looks can never take each other's colour. */
.row .dot.done.tick,.tot-pill .dot.done.tick{color:var(--ok)}
/* THE CHECKBOX, AND ITS SIZE IS HIS OWN MEASUREMENT. «Round checkbox instead of green V:
   small — as circle near remove» (10px), then «15% bigger» (11.5), then «15% smaller» — 9.8
   (04.09). The SHAPE stopped being round the same day — «let's change to rounded square to not
   mix with green circle of ongoing process» — and it lives in `web/tree.js` (`svgDone`), not
   here: the shape is the file's business, the size is the row's. The number lives HERE and not in the SVG for the same reason every other glyph in
   this panel is sized in CSS: the shape is the file's business, the size is the row's.
   The SLOT stays 8px and the icon is drawn ON TOP of it — the same trick «!» and «?» already
   use. A beacon that took its own width would make the row two pixels longer the moment a turn
   ended, and the rail would twitch sideways every time work finished.

   ⚠ ITS HEIGHT IS SET BY HAND, AND IT IS NOT THE GEOMETRIC CENTRE. Three asks on 04.09: «a
   little more centered to the capital letter — now a bit higher — need even», then «position —
   15% lower (compare to its size)». So the first ask lifted it 0.7px, and the second put it
   0.8px BELOW the flex centre instead — 15% of 9.8px is 1.47px, and -0.7 + 1.47 = 0.77.
   ⚠ DO NOT «CORRECT» THIS BACK TO THE MEASURED CENTRE. The measurement says the opposite, and
   the measurement is not the authority here: the flex box centres the icon on the LINE BOX (the
   em box plus the leading), and the cap centre sits ABOVE that, so a level icon wants a lift,
   not a drop — the icon centre stood 0.97px below the cap centre on a project row and 0.41px
   below it on a task row. His eye asked for it lower anyway, twice in a row and the second time
   with a number. The eye wins; the numbers are recorded so nobody spends the afternoon
   rediscovering that this looks wrong on paper.
   ⚠ PIXELS AND NOT em, ON PURPOSE. `em` here resolves against the DOT'S OWN font-size, which
   this file pins at 11px for the «!» and «?» glyphs — so an em rule would look like it follows
   the row's letters and would in fact be a constant in disguise. The 15% he named is 15% of the
   ICON, and the icon is a fixed 9.8px, so a pixel IS the honest unit for it.
   A relative `top` and not a margin: the icon is drawn on top of a slot it does not own, so it
   has to move without moving anything else. */
.row .dot.done .beacon-ok,.tot-pill .dot.done .beacon-ok{width:9.8px;height:9.8px;
  display:block;flex:none;position:relative;top:0.8px}
/* WAITING FOR A KEY. Not trouble and not done: the work is accepted, it's queued and will
   move on by itself. The mark is calm — it informs, it doesn't call; there's nowhere to call
   to, nothing to do.
   The colour and the glyph size are two separate decisions and both are kept: `--warn` is what
   the SVG below inherits through `currentColor`, and the 11px/1 is the fallback scale for a row
   that draws the sign as a character rather than as the SVG. */
.row .dot.wait{background:none;box-shadow:none;overflow:visible;color:var(--warn);
  font-size:11px;line-height:1;
  display:flex;align-items:center;justify-content:center;opacity:.85;
  animation:wait-breathe 2.6s ease-in-out infinite}
/* A custom SVG (see svgWait in tree.js), not the hourglass emoji: the size is set in pixels
   rather than font-size - for a colour-emoji glyph font-size did nothing at all, and the glyph
   drew over the row as its own disproportionate blob. 11px is the same scale as the
   neighbouring checkmark, and it overflows the 8px slot in exactly the way that checkmark does.
   `min-width:0` and `flex:none` are the belt, not the cure: they keep the glyph from being
   squeezed by whatever the slot happens to be, which is how it once computed `width: 0px` at
   `height: 11px`. The cure was renaming the feed's waiting card - see `.wait-card` above. */
.row .dot.wait svg{width:11px;height:11px;min-width:0;min-height:0;flex:none;display:block}
@keyframes wait-breathe{0%,100%{opacity:.85}50%{opacity:.4}}
@media (prefers-reduced-motion: reduce){.row .dot.wait{animation:none}}
/* Broke off — the same «you have to go in here» sign, but of another shape and another colour:
   the work ended not with an answer but with a break, and a green tick would lie about that. */
/* IT BROKE OFF — THIS IS NOT «unread», THIS IS TROUBLE, AND IT SHOUTS.
   A turn that fell on an error (the key, a limit, the death of the process) may not be shown with
   the same quiet sign as finished work: you have to GO there, and better while the session is
   warm — on a cooled one the same conversation will run again and at the cold price. So the sign
   is large, bold, red and pulsing. Cooled down — the pulse leaves (there is nowhere left to
   shout), but it stays red forever: there was a problem in this row, and it is visible even when
   everything has been read. */
.row .dot.done.bad{color:var(--err);font-size:15px;font-weight:900;text-shadow:0 0 8px var(--err);
  animation:bad-cry 1.05s ease-in-out infinite}
.row .dot.done.bad.cool{animation:none;text-shadow:none}
@keyframes bad-cry{0%,100%{transform:scale(1);opacity:1}45%{transform:scale(1.5);opacity:.45}}
@media (prefers-reduced-motion: reduce){.row .dot.done.bad{animation:none}}
/* ASKED AND WAITING — the only beacon with a shelf life. Finished work can be read tomorrow too;
   a question has to be closed while the session is warm, otherwise the same conversation will run
   again and at the cold price. So it is red and it BLINKS — like the pulse of a running turn, but
   with a «?» sign, so that «work is going on there» is not confused with «I am being waited for
   there». */
/* The size is the same as that of «!»: an 11px «?» on a 27px row read as a speck — «it is very
   small, it could be a little bigger». The glyph is drawn ON TOP of its own 8px slot, so the row
   does not become one pixel wider because of it. */
.row .dot.done.ask,.asks-btn .dot.done.ask{color:var(--err);font-size:15px;font-weight:900;
  text-shadow:0 0 8px var(--err);animation:ask-blink 1.6s ease-in-out infinite}
/* Cooled down — there is nowhere left to shout (the same conversation will run at the cold price
   anyway), but the «?» stays red: in this row an answer is being waited for. */
.row .dot.done.ask.cool{animation:none;text-shadow:none}
@keyframes ask-blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(1.18)}}
@media (prefers-reduced-motion: reduce){.row .dot.done.ask,.asks-btn .dot.done.ask{animation:none}}
.row .dot.live,.tot-pill .dot.live{background:var(--ok);animation:beacon 1.15s ease-in-out infinite}
/* THE ROW'S BACKGROUND AGENTS: A BLUE NUMBER OVER THE BEACON, NOT A SECOND SIGN BESIDE IT.
   «When you show the running agents, it is as if it shifts the tab further to the right. Although
   by rights there is still room on the left, so that the line where the tabs' text begins would
   stay a single one» (the owner, 28.08). A separate element in the flow is its width PLUS the
   gap, that is, the names of all rows with agents moved 19px to the right and came back when the
   agents finished: the rail is searched by eye along the left edge of the names, and a moving
   edge costs more than any sign.
   The technique is the same one «!» and «?» are already drawn with: the glyph is drawn ON TOP of
   its own 8px slot (`.row .dot` — `position:relative`), so the row does not become one pixel
   wider while the sign is large. The sign is opaque and covers the beacon under it — that is no
   loss: while the agents are working the row is WORKING anyway (see `beaconOf` → 'live agents'),
   and a green «done» tick under it would be a plain untruth. */
.row .dot{position:relative}
.row .dot .crew{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  min-width:14px;height:14px;border-radius:99px;background:var(--bot);white-space:nowrap;
  color:var(--bg);font:700 9.5px/14px var(--mono);text-align:center;padding:0 3px;
  animation:beacon-crew 1.6s ease-in-out infinite;z-index:2}
/* The turn ITSELF is running too, not only the agents — a green ring around the blue number: two
   facts («work is going on here» and «and the agents too») are obliged to be visible at the same
   time, and there is no room for a second sign and never will be. */
.row .dot.live:not(.agents) .crew{box-shadow:0 0 0 2px var(--ok)}
@keyframes beacon-crew{0%,100%{opacity:1;box-shadow:0 0 8px var(--bot)}50%{opacity:.45;box-shadow:0 0 0 var(--bot)}}
@media (prefers-reduced-motion: reduce){.row .dot .crew{animation:none}}
/* THE MICROPHONE TO THE LEFT OF THE BEACON: «what is being said now will travel into THIS row».
   It pulses because it answers the question «is it still recording?» — a still sign does not
   answer that one.
   It takes up room only while the dictation is running: a permanent indent for the sake of a rare
   sign would move the names of all rows to the right forever. */
.row .row-mic{flex:none;width:17px;height:17px;margin-right:3px;color:var(--err);
  display:flex;align-items:center;justify-content:center;
  animation:mic-pulse 1.1s ease-in-out infinite}
.row .row-mic svg{width:17px;height:17px;display:block;
  filter:drop-shadow(0 0 4px color-mix(in srgb,var(--err) 65%,transparent))}
@keyframes mic-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.86)}}
@media (prefers-reduced-motion: reduce){.row .row-mic{animation:none}}
/* «WHERE THE RECORDING IS GOING ON» IS READ WITH PERIPHERAL VISION, AND A SIGN — ONLY WITH
   CENTRAL VISION.
   So the recording row is coloured whole and breathes together with the microphone: it is visible
   from a neighbouring tab without peering at the rail. The red scar on the left — so the
   background is not confused with the highlight of the open row, which has a background of its
   own. */
.row.rec{border-radius:6px;box-shadow:inset 2px 0 0 var(--err);
  animation:rec-row 1.6s ease-in-out infinite}
@keyframes rec-row{0%,100%{background:color-mix(in srgb,var(--err) 10%,transparent)}
  50%{background:color-mix(in srgb,var(--err) 26%,transparent)}}
@media (prefers-reduced-motion: reduce){.row.rec{animation:none;
  background:color-mix(in srgb,var(--err) 18%,transparent)}}
/* SELECTED MEANS ALREADY COPIED. The pill lives for a second and catches nothing: it is a
   message, not a button, and it may not hang over the feed being clickable (this mine has already
   gone off here). */
.sel-flash{position:fixed;left:50%;bottom:calc(var(--composer-h, 90px) + 18px);
  transform:translateX(-50%);z-index:240;pointer-events:none;
  padding:5px 12px;border-radius:999px;font-size:12px;font-weight:600;
  background:var(--bg-2);color:var(--tx);border:1px solid var(--line-2);
  box-shadow:0 6px 20px rgba(0,0,0,.35);animation:sel-flash .9s ease forwards}
@keyframes sel-flash{0%{opacity:0;transform:translate(-50%,6px)}
  18%{opacity:1;transform:translate(-50%,0)}75%{opacity:1}100%{opacity:0}}
/* THE STRIP OVER THE SELECTION — «first you select, then you are offered» (the owner, 27.08). It
   arrives only when the selection has settled, and it leaves at any movement of it; `placeSelBar`
   finds it a place so that it does not cover the letters themselves. z-index below the menu
   (200): «⋯» opens the menu, and the menu is obliged to lie on top. The width has its ceiling
   from the SCREEN, so it is divided by --ui-scale, and what does not fit scrolls away rather than
   wrapping onto a line: a second line would turn the strip into a chip, that is, into exactly
   what he complains about. */
.sel-bar{position:fixed;z-index:190;display:flex;gap:2px;align-items:center;
  max-width:calc(96vw / var(--ui-scale,1));overflow-x:auto;scrollbar-width:none;
  background:var(--bg-3);border:1px solid var(--line-2);border-radius:12px;padding:3px;
  box-shadow:0 12px 34px var(--shadow);user-select:none;-webkit-user-select:none;
  animation:sel-bar-in .13s ease both}
.sel-bar::-webkit-scrollbar{display:none}
/* ⚠ THE HORIZONTAL PADDING IS THE ONLY SLACK THIS STRIP HAS. Five words at 11px of padding
   measure 297 local px, and at his real --ui-scale 1.4 a 430px phone caps the strip at
   96vw/1.4 = 295 — so the last button, «⋯», the only door into the full menu, was clipped by
   the scroll. It fitted when the labels were shorter; the widths of the WORDS are not ours to
   fix, the padding is. 9px brings the content to ~287 and leaves 8px of room for the next
   label that grows. The gate measures the CLIPPING (scrollWidth > clientWidth) at 1.4. */
.sel-bar button{flex:none;background:none;border:none;color:var(--tx);
  font:600 12.5px var(--sans);padding:10px 9px;border-radius:9px;cursor:pointer;white-space:nowrap}
.sel-bar button:active{background:var(--bg-4);color:var(--acc)}
.sel-bar .sel-more{font-size:16px;line-height:1;padding:9px 12px;color:var(--tx-2)}
@keyframes sel-bar-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
/* The «copied» pill does not hide under the strip when the strip is pressed to the bottom of the feed. */
html.sel-on .sel-flash{bottom:calc(var(--composer-h, 90px) + 74px)}
/* The task's spend: the figures the page is opened for are set large — «what you wrote up at the
   top there is very small». */
.big-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:9px;margin:10px 0 4px}
.big-tile{background:var(--bg-2);border:1px solid var(--line-2);border-radius:11px;padding:11px 13px}
.big-tile .k{font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--tx-3)}
.big-tile .v{font-size:27px;line-height:1.15;font-weight:700;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.big-tile .n{font-size:11.5px;color:var(--tx-3);margin-top:3px;line-height:1.35}
.left-at{color:var(--tx-3);font-weight:400}
/* How much written text is waiting for this row to free up. It lives on the task row and not in
   the neighbouring row's composer: the queue belongs to the task. */
.pill.queued{color:var(--warn);background:var(--warn-bg);letter-spacing:0}
/* ✎ «something unfinished lies here» — in the accent colour, but with no background: this is not
   an alarm but a reminder, and next to ⏳ (yellow) two equally loud signs would argue over one
   glance. */
.pill.undone{color:var(--acc);letter-spacing:0}
/* ⚑ «there is work in here that is saved nowhere else». Bare INK, no fill — the loudest thing this
   sign is allowed to be. ⛔ It may never take `--acc`: that token has exactly one job in this rail,
   «I am standing here», and the project band that spent it on every project row was built on 04.09
   and revoked on 05.09 («an accent spent eleven times is not an accent»). It may never take `--err`
   either — unfinished work is the NORMAL state of a working machine, not a fault, and a rail with
   five red flags on it every morning is a rail nobody reads. `--warn` ink says «worth a look»,
   which is exactly what it is. The glyph sizes itself off the pill, `currentColor` does the rest. */
.pill.unfin{color:var(--warn);letter-spacing:0;display:inline-flex;align-items:center;gap:2px}
.pill.unfin .unfinmark{width:9px;height:9px;flex:0 0 auto}
/* ✉ «something came back here and I have not read it» — AND IT TAKES `--ok`, THE GREEN OF THE ✓,
   DELIBERATELY. The envelope and the «finished, unread» beacon answer one and the same question;
   they differ only in how long the panel has remembered it (the beacon is a live alert capped at
   60, the envelope is a stored number with no cap). They are mutually exclusive by construction —
   `unreadPill` draws nothing while the beacon is lit — so the green is never said twice about one
   row, and the rail gains one plain reading instead of two competing ones: GREEN ANYWHERE ON A
   ROW MEANS «there is something here for you», on the left while it is fresh news and on the
   right once it is old news.
   ⛔ NOT `--acc`: that token has one job in this rail, «I am standing here» (05.09). ⛔ NOT `--err`:
   an unread answer is the NORMAL state of a working machine — a rail with forty red marks every
   morning is a rail nobody reads, which is the wall this feature exists to clear, not to rebuild.
   ⚠ It does NOT pulse. A light no press can extinguish is what makes a working panel feel broken
   (canon 30.08), and this one is put out by READING the row, which can take a minute.
   The glyph sizes itself off the pill and `currentColor` does the rest. */
.pill.unread{color:var(--ok);background:color-mix(in srgb, var(--ok) 13%, transparent);
  letter-spacing:0;display:inline-flex;align-items:center;gap:2px}
.pill.unread .mailmark{width:10px;height:10px;flex:0 0 auto}
/* The rollup on a shut project carries a COUNT beside the same glyph — «3 rows in here are
   waiting». Slightly larger, because it is answering for a whole branch and is read from further
   away than a row's own mark. */
.row-project .pill.unread{font-size:10px}
.row-project .pill.unread .mailmark{width:11px;height:11px}
/* 🔁 — the row sends itself the same text on a timer. It is the one badge in the rail that means
   MONEY IS BEING SPENT while nobody is looking, so it carries a fill like ⏳ rather than bare ink,
   and it does not blink: it is switched off by a press, and a light no press can extinguish is
   what makes a working panel feel broken (canon 30.08). */
.pill.loop{color:var(--ok);background:color-mix(in srgb, var(--ok) 14%, transparent);letter-spacing:0}
/* ⛔ No `.pill.bug` here any more — the rail draws no 🐞 (owner, 11.09). The count lives at the
   top of the open task, `.bug-chip`. */
@keyframes beacon{0%,100%{opacity:1;box-shadow:0 0 8px var(--ok)}50%{opacity:.2;box-shadow:0 0 0 var(--ok)}}
/* An unread row stops being grey: its name is read, not skimmed. */
.row-task.has-dot .row-name{color:var(--tx)}

/* The disclosure arrow is also the node's heat indicator. It is drawn as a triangle and not as a
   glyph: the size and the colour are then ours and not the font's, and it can no longer come out
   small. */
.twist{width:13px;height:13px;flex:none;display:flex;align-items:center;justify-content:center;
  transition:transform .16s ease;color:var(--tx-3)}
.twist i{display:block;width:0;height:0;border-left:7px solid currentColor;
  border-top:5px solid transparent;border-bottom:5px solid transparent}
.twist.open{transform:rotate(90deg)}
.twist.w4{color:var(--ok)}
.twist.w3{color:var(--warn)}
.twist.w2{color:var(--acc)}
.twist.w1{color:var(--err)}
.twist.cold{color:var(--cold);opacity:.75}
.row:hover .twist{filter:brightness(1.3);opacity:1}

/* The period is the rail's main switch: what to show at all. */
/* The period moved into the «Projects» heading as a single chip — a strip of five buttons took up
   a row of the rail always, and it is changed once a week. */
/* The period chip was removed from the head altogether (27.08) — it lived here and held the
   `margin-left:auto` with which the right-hand group of buttons was pushed to the edge. The right
   to push passed to the search: without it the ☰ group slid towards the name and the heading read
   as a random heap. */
#tree-head{cursor:pointer}
/* ⚠ THE `auto` MARGIN MOVED ONTO THE FIRST BUTTON OF THE RIGHT-HAND GROUP, NOT COPIED ONTO BOTH.
   It is what pushes that group to the rail's edge; written on both buttons it would push them
   APART — the filter against the machine strip and the search against the edge — and the header
   would grow a gap that changes width with the numbers beside it. `#btn-focus` is first in the
   markup, so it carries it and `#btn-search` goes back to the ordinary 4px. Both are IDs, so they
   outrank `.side-label .mini-btn` below whatever the order of the file. */
#btn-focus{margin-left:auto}
#btn-search{margin-left:4px}
/* ═══ RECENT ONLY: THE FUNNEL IS INK, AND ONLY THE «ON» STATE IS ALLOWED A COLOUR ═══
   Off, it is one more quiet glyph in a line of quiet glyphs (`--tx-3`, like every `.mini-btn`).
   On, it takes `--acc` AND says its span in words — and that pair is the point: a filter that
   hides rows must be impossible to leave switched on by accident, because from the inside a
   filtered rail and an emptied rail look exactly the same. ⛔ The accent is allowed here, and it
   does not break the 05.09 law («an accent spent eleven times is not an accent»): that law is
   about rows, of which there are hundreds, and this is ONE control in the header that is coloured
   only while it is actively holding rows off the screen. */
.focus-btn{display:inline-flex;align-items:center;gap:3px;padding:0;line-height:1}
.focus-btn .focusmark{width:12px;height:12px;flex:0 0 auto}
.focus-btn .focus-span{font:10px var(--mono);letter-spacing:0}
.focus-btn.on{color:var(--acc)}
.focus-btn.on .focusmark{filter:drop-shadow(0 0 3px color-mix(in srgb, var(--acc) 55%, transparent))}
@media (max-width:860px){.focus-btn .focusmark{width:16px;height:16px}.focus-btn .focus-span{font-size:12px}}
/* ⚠ EVERY BUTTON ON THE HEADING'S LINE IS THE SAME BOX, OR THEY ARE NOT ON ONE LINE (24.09).
   `.focus-btn` is a flex box 12px high (its funnel); `#btn-search` was a bare inline button whose
   height was the LINE BOX of its glyph — 17px at the same 13px font. The heading centres its
   children, so two boxes of different heights put the two glyphs 2.5px apart vertically: not a
   wrap, just a crooked line, and the pair drifts further apart at every font the machine happens
   to have. Measured: focus top 49.5 · search top 47 at the default rail. `inline-flex` +
   `line-height:1` gives every glyph here the same box, so the row is straight AND the gate's
   ratchet («the search button has not stepped onto a second line», `npm run rail`) measures a
   wrap instead of measuring rounding — it compares rounded tops within 2px, and 2.5px of
   intrinsic offset made it a coin toss on the sub-pixel position of the heading. */
.side-label .mini-btn{margin-left:4px;display:inline-flex;align-items:center;justify-content:center;line-height:1}
/* The voice assistant is taken rarely, but it is taken WITH THE HANDS and without looking — so it
   is larger than its neighbours: a small button on a phone is hunted for with a finger on the
   third try. */
.side-label .mini-btn.big{font-size:19px;line-height:1}
@media (max-width:860px){.side-label .mini-btn.big{font-size:24px}}
/* == A PROJECT ROW IS A HEADING, AND A HEADING IS WEIGHT, NOT PAINT (the owner, 05.09) ==
   «Оформление вот этих оранжевого обводки проектов точно не было. Оно очень неудобно сейчас…
   верни именно только схему выделения, как раньше… чтобы можно было просто сразу увидеть,
   какая задача является актуальной, когда работает.»
   The band drawn here on 04.09 — two accent hairlines, a 7% accent fill and 1.5x the height —
   is REVOKED, together with the `dep1..3` ladder of 05.09 that only faded those three devices
   per floor. It was asked for, and it was built to his words, and it still cost the rail the
   thing the rail is for: the accent is the SAME token that says «you are standing here»
   (`.row.current` → `--acc`), so a screen holding eleven projects held eleven accent frames,
   and the one row that had earned the accent — the task running right now — stopped standing
   out among them. A sign spent a dozen times on one screen is not a sign.
   So the rail is back to ONE accented object: the current task and the project it lives in,
   both wearing `.row.current`. A project is told apart from a task by what costs no accent —
   the font weight, the indent and the twist glyph on the left. The height goes back to the
   ordinary row's, which is what he means by «у нас все чуть-чуть плотнее было».
   ⚠ Do not re-add a fill, a hairline or a height here unless he asks a second time: the only
   two things this rail may paint across a WHOLE row are a status (set by hand, once or twice a
   screen) and «I am standing here». A heading is neither. */
/* ══ THE FOLDER IS LIT LIGHTLY, THE TASK HEAVILY (the owner, 06.09) ══
   «Хотелось бы, чтобы активная задача подсвечивалась лёгким выделением, и сам проект легко
   подсвечивался, а жирнее выделялась сама задача, на которой я стою, чтобы можно было хоть
   понять, на какой же задаче я сейчас стою.» Both rows still wear `.current` — one accented
   object plus the folder it lives in, exactly as 05.09 left it — but they may not read the
   same: the project answers «which branch», the task answers «which work», and only the second
   one is the question the rail exists for. So the folder keeps a step of fill and half a bar,
   and the accent itself is spent on the task alone. This is NOT the revoked band of 04.09: it
   is drawn on exactly ONE project row on the screen, the one holding the open task. */
.row-project.current{--row-bg:var(--bg-3);box-shadow:inset 2px 0 0 color-mix(in srgb,var(--acc) 45%,transparent)}
.row-project{--pp:10px;font-weight:600}
/* What is left of the 05.09 hierarchy work, and it is all that was ever needed: a nested
   project's count pill is a shade smaller than its parent's, and its indent (`web/tree.js →
   projectRows`, `margin-left`) steps the whole row in — so a child reads as standing INSIDE its
   parent with no colour spent at all. */
.row-project.dep1{--pp:9.4px}
.row-project.dep2{--pp:8.8px}
.row-project.dep3{--pp:8.4px}
.row-project .pill{font-size:var(--pp)}
.row-project .row-name{color:var(--tx);font-weight:600;letter-spacing:.1px}
/* The first child project revealed under a run of the node's OWN tasks: a little more air, so the
   two kinds in one fold are not read as one list. `.swipe` wraps the row on a phone, hence both. */
.row-project.fold-cut,.fold-cut .row-project{margin-top:6px}

/* ══ THE BRANCH YOU ARE READING STAYS ON SCREEN (the owner, 06.09) ══
   «Если я подлистываю, то проект остаётся фиксированным, пока я подлистываю другие проекты…
   Как только я прилистываю последние задачи этого проекта и прихожу на другой проект, этот
   проект становится закреплённым… Чтобы было понятно всегда, в какой ветке дерева я нахожусь.»
   A rail scrolled past its first screen stops answering the one question it exists for: the
   name of the folder these thirty task rows belong to is somewhere above the top edge.
   ⚠ THE CLASS LANDS ON `#tree`'S DIRECT CHILD, whatever shape it has. On a phone a project row
   lives inside a `.swipe` wrapper, and `position:sticky` is CLIPPED by the nearest block-level
   ancestor: put on the row inside the wrapper it would have exactly the row's own height to
   travel in, i.e. it would never stick at all.
   ⚠ Flat siblings do not push each other out — a stuck heading of a finished project would hang
   over the next one for ever — so WHICH rows may stick, at what `top` and with what push is
   decided in `web/tree.js → stickApply`; this rule only supplies the mechanism and the paint. */
.tree>.rail-stick{position:sticky;z-index:20}
/* An opaque backing, or the rows travelling underneath show straight through the heading. It
   reads the row's own decision (`--row-bg`) first, so the folder holding the open task keeps
   its highlight while it is stuck, and falls back to the rail's own background. */
.tree>.rail-stick.row,.tree>.rail-stick .row{background-color:var(--row-bg,var(--bg-2))}
/* The hairline appears only while the heading is standing over other rows: it is the edge of a
   heading that has left its place, not a permanent border (the band of 04.09 was revoked). */
.tree>.rail-stick.stick-over::after{content:'';position:absolute;left:0;right:0;bottom:-1px;
  height:1px;background:var(--line);pointer-events:none}


/* ══ «ARCHIVED» — THE RAIL'S LAST LINE ══
   It is a HEADING, so it is built like the project rows (the same weight, the same letter spacing)
   and NOT like the fold strips: a fold is a state of some other rows, while this is a place of its
   own that always stands in the same spot. What separates it from the list is a hairline above and
   a little air — the shelf must read as the bottom of the rail and not as one more project — and
   its name is a shade quieter than a project's: it is not work, it is where work is put away. */
.row-arch{font-weight:600;border-top:1px solid var(--line);margin-top:6px}
.row-arch .row-name{color:var(--tx-2);font-weight:600;letter-spacing:.1px}
.row-arch.open .row-name,.row-arch:hover .row-name{color:var(--tx)}
/* The glyph sets its own size and takes the row's colour, exactly like every other glyph here. */
.row-arch .arch-ico{font-size:12px;line-height:1;opacity:.8;margin-right:2px;flex:none}

/* ══ A BOX — A NAMED FOLDER OF ONE PROJECT, AT THE TOP OF IT ══
   Built exactly like the «Archived» line, because it is the same kind of thing: a heading for a
   place, not a state of other rows. Two deliberate differences, and both are about WHERE it stands.
   No hairline and no air above it: the shelf wears them to read as the BOTTOM of the rail, while a
   box is the project's first line and a rule there would draw a second border right under the
   project's own heading. And its name is at full strength (`--tx`), not the shelf's quieter
   `--tx-2`: the shelf holds work that is over, a box holds the work you are doing.
   ⛔ NO FILL AND NO ACCENT. The project band was built with one on 04.09 and revoked on 05.09 —
   «an accent spent eleven times is not an accent»: `--acc` in this rail has exactly one job, «I am
   standing here», and a colour drawn on every box line would take the answer away from the one row
   that needs it. Weight, an icon and an indent are free; colour is not. */
.row-box{font-weight:600}
.row-box .row-name{color:var(--tx);font-weight:600;letter-spacing:.1px}
.row-box .box-ico{font-size:12px;line-height:1;opacity:.8;margin-right:2px;flex:none}

/* The task row is the only kind of row under a project (no more branches or sessions in the rail). */
.row-task{font-size:12px}
.row-task .row-name{color:var(--tx-2)}
.row-task.current .row-name{color:var(--tx)}
.row-task.cold .row-name{color:var(--tx-3)}
/* A fresh row is not read as a cooled one. Without this the tint would sit under the dimmest
   `--tx-3` and the row would be lit and hard to read at the same time — the exact pair of layers
   that made a status unreadable on 28.08. `--tx-2` is the ordinary task colour, not the open
   row's full `--tx`: fresh means «still near», not «this is where you are». Same weight as
   `.cold` and placed after it, so the clock that is nearer wins; the status rule below is later
   still, so a hand beats both. */
.row-task.fresh .row-name{color:var(--tx-2)}
/* ⚠ THE TEXT OF A ROW WITH A STATUS IS BRIGHTER THAN AN ORDINARY ONE, NOT PALER («very faded and
   hard to read», the owner, 28.08). A status is set BY HAND: such a row is going to be read, and
   the cooling of the cache has nothing to do with it — «done» a week ago means exactly the same
   as done an hour ago. It came out the other way round and in two layers: `.row-task.cold`
   painted the name in the dimmest `--tx-3`, and on top of it lay a separate paleness of «done» —
   on a coloured fill there was nothing left to read with.
   ⚠ The rule stands EXACTLY HERE, right after `.cold`, and deliberately at the same weight (two
   classes + a descendant): the status is obliged to outrank the cooling, and at equal specificity
   the order in the file decides. Up at the top, next to the fill, it would lose to `.cold` — and
   an open row would lose its brightness exactly when work is being done in it. It also covers
   `.current`: an open row has the same `--tx` colour, and its boldness is added by its own rule. */
.row-task.st-urgent .row-name,.row-task.st-almost .row-name,.row-task.st-done .row-name{color:var(--tx)}
/* The tail of the name stays quieter than the head, but on a row with a status it is `--tx-2` and
   not `--tx-3`: on the fill the dimmest tone merges with the background. */
.row-task.st-urgent .row-tail,.row-task.st-almost .row-tail,.row-task.st-done .row-tail{color:var(--tx-2)}
/* ⚠ `--row-bg`, never `background:` — the shorthand would wipe the status tint that lives in
   `background-image` right above. The open TASK is the loudest row in the rail: a wash of the
   accent over `--bg-4`, against the folder's bare `--bg-3` one line up. */
.row-task.current{--row-bg:color-mix(in srgb,var(--acc) 13%,var(--bg-4))}
/* ⛓N is not a property of the work but a trace of the task surviving a couple of cache coolings. */
.pill.chain{color:var(--acc-2);border-color:var(--line-2);letter-spacing:0}
/* A row standing on a folder that is not there: nothing can be sent from it until the folder is
   picked, so the mark is a warning colour and not a counter. */
.pill.gone{color:#e0a33a;border-color:#8a6520;letter-spacing:0}
/* ↳N — how many sub-tasks hang under this row, and the one pill in the rail that is a CONTROL:
   it folds the family. So it says so with the cursor and a hover, which is the only budget it has
   — a task row has no twist glyph, and giving it one would move every beacon, name and number in
   the rail one column to the right for the sake of the few rows that have children.
   ⛔ NO FILL, and that is the project band's lesson of 05.09 («an accent spent eleven times is not
   an accent»): `--acc` in this rail has exactly one job, «I am standing here». A tinted pill on
   every parent row would take that answer away from the one row that needs it. Ink only. */
.row .pill.subs{color:var(--acc-2);letter-spacing:0;cursor:pointer}
.row .pill.subs:hover{color:var(--acc);background:var(--bg-5,var(--bg-4))}
/* Folded: the count is still there — a family you cannot see is exactly when the number matters —
   and the pill is dimmed, the same way every folded thing in this panel is quieter than an open one. */
.row .pill.subs.shut{color:var(--tx-3);text-decoration:underline dotted}
/* A row that is written into from outside (the Telegram guest). The colour is the theme's accent
   and not «Telegram blue»: the rail is painted with tokens only, otherwise one theme in ten gets
   an invisible sign. */
.pill.chan{color:var(--acc);background:var(--warn-bg);letter-spacing:0}
/* WHOSE ROW THIS IS, in a project worked by the team. Drawn only on somebody ELSE's row, in the
   same violet a guest's message wears in the feed (`--think`) — one colour for «not you» in both
   places, so the rail and the feed do not teach two different alphabets. Tokens only: a hard-coded
   colour would go invisible in one theme of the eleven. */
.pill.who{color:var(--think-2,#a99fe0);background:color-mix(in srgb, var(--think,#8b7fd4) 14%, transparent);
  letter-spacing:0;max-width:9ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ══ A TASK IS PICKED UP BY HAND AND PUT ONTO A TASK (merging rows) ══
   The filling strip on the left is the only way to learn about the gesture without reading the
   instructions: it runs exactly the second you have to hold it for, and it breaks off if the hand
   trembled.
   The gesture is a mouse one: on a phone the row already has its swipes, and a second meaning for
   the same movement would be a lottery, so none of this is switched on there. */
.row-hold{position:relative;background:var(--bg-3)}
.row-hold::after{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
  border-radius:7px 0 0 7px;background:var(--acc);transform-origin:top;
  animation:row-hold 1s linear both}
@keyframes row-hold{from{transform:scaleY(0);opacity:.45}to{transform:scaleY(1);opacity:1}}
.rail-dragging,.rail-dragging .row{cursor:grabbing}
.rail-dragging{user-select:none}
/* The lifted row stays in place as a pale shadow of itself: the list must not collapse under the
   hand, or the target moves away at exactly the moment it is being aimed at. */
.row-lifted{opacity:.4;filter:grayscale(.45)}
.row-task.drop-into{background:var(--bg-4);box-shadow:inset 0 0 0 1px var(--acc)}
.row-task.drop-into .row-name{color:var(--acc);font-weight:600}
/* ══ THE SHELF'S OWN TWO TARGETS ══
   The «🗄 Archived» line takes a row IN, any project row lets one back OUT — so both of them light
   up exactly as a merge target does. One rule, not a copy of the one above: two highlights that are
   meant to look identical and are written twice stop being identical at the first edit. */
/* A box line is the third target of the same gesture, so it joins the same rule rather than getting
   a copy of it: two highlights meant to look identical and written twice stop being identical at
   the first edit. It is also the ONLY one of the three that can refuse (a box of another project,
   or the box the row is already in) — a refusal wears `.drop-no`, which is styled with the merge's. */
.row-arch.drop-into,.row-project.drop-into,.row-box.drop-into{background:var(--bg-4);box-shadow:inset 0 0 0 1px var(--acc)}
.row-arch.drop-into .row-name,.row-project.drop-into .row-name,.row-box.drop-into .row-name{color:var(--acc);font-weight:600}
/* THE PROJECT IN THE HAND, DRAWN WHOLE (11.09). The rail re-orders live while a project is carried,
   so the row is already in its new place — what could not be seen was WHICH rows travel with it: a
   project moves with its boxes, its tasks, its fold strip and its shelf. An insertion line answers
   «between which two», which was never the question. Clipped to the rail by JS, so a block scrolled
   half off is drawn half. ⚠ This is a transient drag mark, not decoration on `.row-project` — the
   band built there on 04.09 was revoked on 05.09 and must not come back. */
.proj-block{position:fixed;z-index:204;pointer-events:none;border-radius:9px;
  background:color-mix(in srgb,var(--acc) 8%,transparent);
  box-shadow:inset 0 0 0 2px var(--acc),0 6px 20px rgba(0,0,0,.18)}
.row-ghost{position:fixed;z-index:205;pointer-events:none;display:flex;align-items:baseline;gap:8px;
  max-width:min(calc(46vw / var(--ui-scale,1)),380px);padding:5px 10px;border-radius:8px;border:1px solid var(--line-2);
  background:var(--bg-2);color:var(--tx);font-size:12px;box-shadow:0 8px 24px var(--shadow)}
.row-ghost .ghost-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
.row-ghost .ghost-hint{font-size:11px;color:var(--tx-3);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;flex:none;max-width:52%}
.row-ghost.armed{border-color:var(--acc)}
.row-ghost.armed .ghost-hint{color:var(--acc)}
/* THE REFUSAL IS VISIBLE BEFORE THE HAND IS LIFTED. A row taken hold of during its own turn
   pushes back and reddens for a second (`row-nope`) — the gesture did not start and it is clear
   why; a target that cannot accept is highlighted in that same red, and the ghost is struck through. */
/* A BOX LINE JOINS THIS RULE, because it is the one drop target in the rail that can say no — a box
   of another project (a task cannot leave its folder), or the box the row is already in. Without
   this the refusal would live only in the ghost's caption, i.e. only for whoever read it, while the
   line under the cursor went on looking exactly like one that would accept. */
.row-task.drop-no,.row-box.drop-no{background:color-mix(in srgb,var(--err) 14%,transparent);
  box-shadow:inset 0 0 0 1px var(--err)}
.row-task.drop-no .row-name,.row-box.drop-no .row-name{color:var(--err)}
.row-ghost.barred{border-color:var(--err)}
.row-ghost.barred .ghost-hint{color:var(--err)}
.row-nope{background:color-mix(in srgb,var(--err) 16%,transparent);
  box-shadow:inset 0 0 0 1px var(--err);animation:row-nope .42s ease both}
@keyframes row-nope{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}

/* ⚠ THE WIDTH IS DIVIDED BY --ui-scale TOO: `vw` is SCREEN pixels, while the menu lives inside a
   `zoom`-ed root, and `min-width:200px` at 1.4 is 280 screen pixels. `menu()` in web/tree.js puts
   the menu in its place, but the width ceiling is obliged to stand in the markup as well. */
.ctx-menu{position:fixed;z-index:200;background:var(--bg-3);border:1px solid var(--line-2);
  border-radius:9px;padding:4px;min-width:200px;max-width:calc(92vw / var(--ui-scale,1));
  box-shadow:0 14px 40px var(--shadow)}
.ctx-menu button{display:block;width:100%;text-align:left;background:none;border:none;color:var(--tx);
  font:12.5px var(--sans);padding:6px 10px;border-radius:6px;cursor:pointer}
.ctx-menu button:hover{background:var(--bg-4);color:var(--acc)}
.ctx-menu button.danger:hover{background:var(--err-bg);color:var(--err)}
.ctx-sep{height:1px;background:var(--line);margin:4px 6px}
/* An item that unfolds to the right: twenty reports in the common list would turn the menu into a
   bedsheet in which the commands themselves cannot be seen. */
.ctx-menu button.has-sub{display:flex;align-items:center;justify-content:space-between;gap:8px}
.ctx-menu button.has-sub::after{content:'▸';color:var(--tx-3);font-size:11px}
.ctx-menu button.has-sub:hover::after{color:var(--acc)}
/* A SPLIT ROW ON A PHONE (`extra.split`, web/tree.js -> menu): the arrow is its own target -- 15% of
   the row, never under 44 px, a bigger arrow behind a hairline -- and the rest of the row takes
   the default. Same media test as the JS (`not (hover:hover)`), so the drawn zone and the hit
   zone cannot disagree about which device this is. */
@media not all and (hover:hover){
  .ctx-menu button.has-sub.split-sub{padding-right:0}
  .ctx-menu button.has-sub.split-sub::after{flex:none;align-self:stretch;display:flex;align-items:center;
    justify-content:center;width:15%;min-width:44px;margin:-6px 0;content:'▶';font-size:16px;border-left:1px solid var(--line)}
}
.ctx-sub{max-height:min(70vh,560px);overflow:auto;min-width:260px}
.ctx-menu button.ctx-dim{color:var(--tx-3);cursor:default}
/* A live preview under the cursor: the page inside is laid out at its own normal width and shown
   as a card — picking a report by eye is faster than reading twenty names. */
.ctx-peek{position:fixed;z-index:210;width:640px;height:478px;overflow:hidden;pointer-events:none;
  border:1px solid var(--line-2);border-radius:10px;background:Canvas;color-scheme:light dark;box-shadow:0 16px 44px var(--shadow)}
/* The size and the scale are set by JS (web/tree.js → peek): the side and the magnitude depend on
   how much room is left to the right of the menu. Here there are only the fallback values. */
.peek-frame{width:1180px;height:880px;border:0;transform:scale(.542);transform-origin:0 0;background:Canvas;color-scheme:light dark}

/* ═══ the tooltip ═══
   Our own, not the system's. The system tooltip arrives after a second and a half, is drawn in
   the OS font, and can do neither a heading nor columns nor colour — that is, exactly what a rail
   row differs by: heat, price, state. Here it is a card on the theme's tokens: a heading,
   «what → how much» pairs, a footnote. */
.tip{position:fixed;z-index:400;max-width:340px;pointer-events:none;opacity:0;transform:translateY(3px);
  background:var(--bg-3);border:1px solid var(--line-2);border-radius:var(--r);padding:9px 11px;
  box-shadow:0 8px 26px var(--shadow);transition:opacity .12s ease,transform .12s ease}
.tip.on{opacity:1;transform:none}
.tip-t{font:600 12.5px var(--sans);color:var(--tx);margin-bottom:3px;overflow-wrap:anywhere}
.tip-sub{font:11px var(--mono);color:var(--tx-3);margin-bottom:6px;overflow-wrap:anywhere}
.tip-rows{display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr);gap:2px 10px;font-size:11.5px}
/* A key is a short label everywhere else, so it never wraps — but when something long is
   handed in as one, it is clipped instead of stretching the card into a wall. */
.tip-k{color:var(--tx-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
/* One row per task: the name takes what is left and is clipped, the project stays whole. */
.tip-item{display:flex;align-items:baseline;gap:8px;font-size:11.5px;line-height:1.5;min-width:0}
.tip-item+.tip-item{margin-top:1px}
.tip-item-n{flex:1 1 auto;min-width:0;color:var(--tx-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tip-item-at{flex:none;color:var(--tx-3);font:10.5px var(--mono);opacity:.85}
.tip-more{margin-top:3px;font-size:11px;color:var(--tx-3)}
.tip-v{color:var(--tx-2);text-align:right;overflow-wrap:anywhere}
.tip-v.hot{color:var(--warn)}
.tip-v.ok{color:var(--ok)}
.tip-v.bad{color:var(--err)}
.tip-v.cold{color:var(--cold)}
.tip-note{margin-top:7px;padding-top:6px;border-top:1px solid var(--line);font-size:11px;color:var(--tx-3);line-height:1.45}
.tip-hint{margin-top:6px;font-size:11px;color:var(--acc)}

/* ═══════════════════ flow overlay ═══════════════════ */
.flow-overlay{position:fixed;inset:auto 0 0 0;height:min(62vh,560px);background:var(--bg-2);
  border-top:1px solid var(--line-2);box-shadow:0 -20px 60px var(--shadow);z-index:60;
  transform:translateY(100%);transition:transform .22s cubic-bezier(.2,.8,.2,1);
  display:flex;flex-direction:column}
.flow-overlay.open{transform:none}
.flow-head{display:flex;align-items:center;justify-content:space-between;padding:11px 18px;
  border-bottom:1px solid var(--line)}
.flow-title{font:600 13.5px var(--sans)}
.flow-sub{color:var(--tx-3);font:11.5px var(--mono);font-weight:400}
.flow-tools{display:flex;align-items:center;gap:12px}
.flow-body{flex:1;overflow-y:auto;padding:14px 18px}
.flow-grid{display:flex;flex-direction:column;gap:9px;margin-bottom:18px}
.flow-lane{display:grid;grid-template-columns:150px 1fr;gap:10px;align-items:center}
.lane-head{display:flex;align-items:center;gap:6px;min-width:0}
.lane-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--acc)}
.lane-dot.live{animation:pulse 1.1s infinite}
.lane-dot.done{background:var(--ok);animation:none}
.lane-name{font:600 11.5px var(--mono);color:var(--tx-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lane-tok{font:10.5px var(--mono);color:var(--tx-3);margin-left:auto}
.lane-track{position:relative;height:20px;background:var(--bg);border:1px solid var(--line);
  border-radius:6px;overflow:hidden}
.blk{position:absolute;top:3px;bottom:3px;min-width:3px;border-radius:3px;opacity:.9;cursor:pointer}
.blk:hover{opacity:1;outline:1px solid var(--hair)}
.blk.running{animation:pulse 1.2s infinite}
.blk.bad{outline:1px solid var(--err)}
.lane-desc{grid-column:2;font-size:11px;color:var(--tx-3);margin-top:-4px}
.lane-cur{color:var(--acc-2);font-family:var(--mono)}
.flow-tally{display:grid;grid-template-columns:1fr 1fr;gap:22px}
.tally-col{min-width:0}
.flow-legend{display:flex;gap:14px;padding:9px 18px;border-top:1px solid var(--line);flex-wrap:wrap}
.leg{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--tx-3)}
.leg i{width:9px;height:9px;border-radius:3px;display:block}

/* mcp editor */
.modal-back{position:fixed;inset:0;background:var(--veil);z-index:150;display:grid;place-items:center;padding:20px}
.modal{width:min(680px,calc(94vw / var(--ui-scale,1)));max-height:86vh;background:var(--bg-2);border:1px solid var(--line-2);
  border-radius:14px;display:flex;flex-direction:column;box-shadow:0 30px 80px var(--shadow)}
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--line)}
.modal-title{font:600 14px var(--sans)}
.modal-file{font:11px var(--mono);color:var(--tx-3)}
.modal-body{padding:14px 16px;overflow:auto;flex:1}
.modal-body textarea{width:100%;min-height:300px;background:var(--bg);border:1px solid var(--line);
  border-radius:9px;padding:12px;color:var(--tx-2);font:12.2px/1.55 var(--mono);resize:vertical;outline:none}
.modal-body textarea:focus{border-color:var(--acc)}
.modal-foot{display:flex;align-items:center;gap:10px;padding:12px 16px;border-top:1px solid var(--line)}
.modal-note{font-size:11.5px;color:var(--tx-3);flex:1;line-height:1.5}
@media(max-width:860px){.flow-tally{grid-template-columns:1fr}.flow-lane{grid-template-columns:96px 1fr}}

/* ═══ account ═══ */
.avatar{width:28px;height:28px;border-radius:50%;background:var(--acc);color:var(--on-acc);
  border:none;cursor:pointer;font:600 12px var(--sans);display:grid;place-items:center;flex:0 0 auto;transition:.15s}
.avatar:hover{background:var(--acc-2)}
.popover{position:fixed;top:52px;right:14px;width:330px;max-width:calc((100vw - 28px) / var(--ui-scale,1));z-index:60;
  background:var(--bg-2);border:1px solid var(--line-2);border-radius:14px;
  box-shadow:0 18px 50px rgba(0,0,0,.55);opacity:0;transform:translateY(-6px) scale(.98);
  pointer-events:none;transition:.16s ease}
.popover.open{opacity:1;transform:none;pointer-events:auto}
.pop-head{display:flex;align-items:flex-start;gap:10px;padding:13px 13px 10px;border-bottom:1px solid var(--line)}
.pop-who{flex:1;min-width:0}
.pop-name{font:600 14px var(--sans)}
.pop-note{font:12px/1.5 var(--sans);color:var(--tx-3)}
.pop-body{padding:12px 13px 13px;display:flex;flex-direction:column;gap:12px}
.pop-card{background:var(--bg-3);border:1px solid var(--line);border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:6px}
.pop-ok{font:12px/1.5 var(--sans);color:var(--ok)}
.pop-warn{font:12px/1.5 var(--sans);color:var(--warn)}
.pop-section{display:flex;flex-direction:column;gap:7px}
.pop-label{font:600 10.5px var(--sans);letter-spacing:.9px;text-transform:uppercase;color:var(--tx-3)}
.popover input{background:var(--bg-3);border:1px solid var(--line);border-radius:8px;padding:8px 10px;
  color:var(--tx);font:13px var(--sans);outline:none;width:100%}
.popover input:focus{border-color:var(--acc)}
.pop-actions{display:flex;align-items:center;gap:10px}
.pop-msg{font:12px var(--sans);color:var(--tx-3)}
.pop-msg.good{color:var(--ok)}
.pop-msg.bad{color:var(--err)}
.btn-primary{background:var(--acc);color:var(--on-acc);border:none;border-radius:8px;padding:8px 14px;
  font:600 13px var(--sans);cursor:pointer;transition:.15s}
.btn-primary:hover{background:var(--acc-2)}
.btn-ghost{background:var(--bg-3);color:var(--tx-2);border:1px solid var(--line-2);border-radius:8px;
  padding:8px 14px;font:500 13px var(--sans);cursor:pointer;transition:.15s}
.btn-ghost:hover{color:var(--err);border-color:var(--err)}
.btn-ghost.wide{width:100%}
/* NOT «Cancel», but not the main path either: `.btn-ghost` has a red hover — that is the language
   of refusal, and as an option of choice («when it frees up») it would lie. */
.btn-soft{background:var(--bg-3);color:var(--tx);border:1px solid var(--line-2);border-radius:8px;
  padding:8px 14px;font:500 13px var(--sans);cursor:pointer;transition:.15s}
.btn-soft:hover{color:var(--acc);border-color:var(--acc)}
/* An action that BREAKS SOMETHING (restart now, interrupting turns): the warning colour and not
   the accent one — the hand must see the difference before it presses. */
.btn-warn{background:var(--warn);color:var(--on-warn);border:none;border-radius:8px;
  padding:8px 14px;font:600 13px var(--sans);cursor:pointer;transition:.15s}
.btn-warn:hover{filter:brightness(1.08)}

/* ═══ flow: modes, graph, files ═══ */
.seg{display:flex;background:var(--bg-3);border:1px solid var(--line);border-radius:8px;overflow:hidden}
.seg button{background:none;border:none;color:var(--tx-3);cursor:pointer;font:500 12px var(--sans);padding:5px 11px;transition:.14s}
.seg button:hover{color:var(--tx-2)}
.seg button.on{background:var(--bg-4);color:var(--acc)}

.graph-wrap{position:relative;display:flex;flex-direction:column;min-height:320px}
.graph{display:block;width:100%;cursor:grab;touch-action:none;background:
  radial-gradient(circle at 22% 18%,rgba(217,119,87,.055),transparent 58%)}
.graph:active{cursor:grabbing}
.g-lab{fill:var(--tx-2);font:11.5px var(--sans)}
.g-note{fill:var(--tx-3);font:10px var(--mono)}
.g-edge{fill:var(--tx-3);font:9.5px var(--mono)}
.g-node{cursor:default}
.g-node:hover circle{stroke:var(--tx);stroke-width:1.5}
.graph-hint{display:flex;align-items:center;gap:12px;padding:6px 2px 0;font:11px var(--sans);color:var(--tx-3)}

.files-wrap{display:grid;grid-template-columns:1fr 1fr;gap:22px;align-items:start}
.heat{display:flex;flex-direction:column;gap:4px}
.heat-row{display:grid;grid-template-columns:minmax(0,1fr) 96px 54px;align-items:center;gap:9px}
.heat-name{font:11.5px var(--mono);color:var(--tx-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.heat-bar{display:flex;height:7px;background:var(--bg-4);border-radius:4px;overflow:hidden}
.heat-bar i{display:block;height:100%}
.heat-bar i.r{background:var(--cold)}
.heat-bar i.w{background:var(--warn)}
.heat-n{font:10.5px var(--mono);color:var(--tx-3);text-align:right}
.pane-empty.small{padding:10px 0;font-size:12px}
.mini-n.mono{font-family:var(--mono);font-size:11.5px}
@media (max-width:860px){.files-wrap{grid-template-columns:1fr}}

/* ═══ MCP: the price of a server and switching tools off one by one ═══ */
.mcp-chip{display:flex;align-items:center;gap:7px;margin:0 12px 10px;padding:7px 10px;background:var(--bg-3);border:1px solid var(--line);border-radius:var(--r);color:var(--tx-2);font:11.5px var(--sans);cursor:pointer;text-align:left;transition:.15s}
.mcp-chip:hover{border-color:var(--acc);color:var(--tx)}
.mcp-chip .chip-ico{color:var(--acc)}
.mcp-chip .chip-tok{margin-left:auto;font:11px var(--mono);color:var(--acc-2)}
.mcp-chip.hot{border-color:var(--err)}
.mcp-chip.hot .chip-tok{color:var(--err)}

.mcp-sum{display:flex;align-items:center;gap:12px;flex-wrap:nowrap;margin-bottom:11px;padding:10px 12px;background:var(--bg-3);border:1px solid var(--line-2);border-radius:10px}
.mcp-sum-big{font:600 24px var(--mono);color:var(--acc-2);line-height:1;flex:none}
.mcp-sum-txt{font-size:11.5px;color:var(--tx-2);line-height:1.5;min-width:0}
.mcp-sum-txt .muted{color:var(--tx-3);font-size:10.8px}
.mcp-row.plugin{border-style:dashed}
.mcp-est{color:var(--warn)}
.pane-note.hidden{display:none}
.pane-note.bad{border-color:var(--err);color:var(--err)}
.bar-row.deferred .bar-track i{opacity:.4;background:var(--tx-3)}
.mini-row.muted{color:var(--tx-3);justify-content:center}

.tool-grid{display:grid;grid-template-columns:1fr 1fr;gap:2px 10px}
.tool-pick{display:flex;align-items:center;gap:6px;font-size:11px;padding:2px 4px;border-radius:5px;cursor:pointer;color:var(--tx-2)}
.tool-pick:hover{background:var(--bg-4)}
.tool-pick.off{opacity:.45;text-decoration:line-through}
.tool-pick input{margin:0;accent-color:var(--acc)}
.tool-pick .tool-n{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono)}
.tool-pick .tool-v{font:10.5px var(--mono);color:var(--tx-3)}
@media (max-width:860px){.tool-grid{grid-template-columns:1fr}}

/* ═══ cutting the context right in the feed ═══ */
.cut-btn{margin-left:auto;background:var(--bg-3);border:1px solid var(--line-2);color:var(--tx-2);border-radius:7px;padding:3px 10px;font:11px var(--sans);cursor:pointer;transition:.15s}
.cut-btn:hover{border-color:var(--acc);color:var(--acc)}
.ctx-cut{display:flex;align-items:center;gap:10px;margin:16px 0;font:10.5px var(--mono);letter-spacing:.6px;text-transform:uppercase;color:var(--acc)}
.ctx-cut::before,.ctx-cut::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,transparent,var(--line-2),transparent)}
/* The bar of a session shown from the middle carries the door up. It is the same size as the bar's
   own letters — the bar is a place mark, not a toolbar — but it is a button, so it is underlined:
   nothing else on that line can be pressed. */
.ctx-cut .linkish{background:none;border:0;padding:0;font:inherit;letter-spacing:inherit;
  color:var(--blue);cursor:pointer;text-decoration:underline dotted;flex:none}
.ctx-cut .linkish:hover{color:var(--tx-1)}

/* ═══════════════════════════════════════════════════════════════════════════
   THE MOBILE LAYER: one settings button, attachments, reading mode, reading aloud.
   The rule for a screen in the car / in the hand: the answer is visible, not the controls.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the header: one large key percent, the reset time in brackets ── */
.key-chip{display:flex;align-items:baseline;gap:6px;background:none;border:none;cursor:pointer;
  padding:2px 6px;border-radius:8px;position:relative;color:var(--tx);transition:.15s}
.key-chip:hover{background:var(--bg-3)}
/* «~42%» in a muted tone — this key's number was measured long ago (on a non-main subscription
   the numbers are refreshed only by a turn run on it). A bright figure with no caveat reads as
   «this is how much there is now», and that is exactly how the header lied about somebody else's
   key. */
#week-val.stale{opacity:.68;font-style:italic}
.key-pct{font:600 19px var(--mono);line-height:1.1}
.key-left{font:12px var(--mono);color:var(--tx-3)}
/* The account mark: the letter of the name of the subscription THIS row is running on. It exists
   only while the work is not on the main one, so it is allowed very little room in the header. */
.key-acct{font:700 10px var(--sans);color:var(--acc);border:1px solid var(--acc);border-radius:4px;
  padding:0 3px;line-height:1.3;align-self:center;opacity:.85;text-transform:uppercase}
.key-chip .bar{position:absolute;left:6px;right:6px;bottom:1px;margin:0;height:2px}
.cache{font:13px var(--mono);color:var(--tx-2);white-space:nowrap}

/* ── the task clocks: the owner's time on the left, Claude's time on the right ──
   Two figures side by side read as a pair only when they are of one height and one font: a
   different type size would turn them into «the main one and a footnote», and it is precisely
   them that have to be compared. */
/* TWO LINES AND NOT ONE (the owner, 25.08: «put the time spent working on the task in two
   lines»). In one line the pair «🧑 25m · 🤖 1h 24m» stretches across a third of the header and
   takes width away from the key percent; one under the other the same two figures take the width
   of one and read as a pair — their height and font are the same, and the separator between them
   is no longer needed. */
.time-chip{display:flex;flex-direction:column;align-items:flex-start;gap:0;background:var(--bg-3);
  border:1px solid var(--line);cursor:default;padding:1px 6px;border-radius:8px;
  font:11.5px/1.32 var(--mono);color:var(--tx-2);white-space:nowrap}
.time-part{display:flex;align-items:center;gap:3px}
.time-ico{font-size:11px;opacity:.7}
.time-sep{display:none}
/* A turn is running — it is the agent's seconds that tick, and it is visible that they tick. */
.time-chip.ticking .time-part:last-of-type{color:var(--acc)}
/* ── AND THE OWNER'S HALF SAYS WHETHER IT IS COUNTING RIGHT NOW (the owner, 08.10) ──
   A figure standing still is indistinguishable from a broken counter, which is how this errand
   started. Resting = nothing has moved for a couple of minutes, or another window is active:
   dimmed, no alarm, it comes back by itself at the first movement. Held = the turn is parked
   behind a refused key and the clock is deliberately on pause: amber, because that one is worth
   noticing. The reason is in the tooltip; `.time-chip` has `cursor:default`, so this is the whole
   signal a glance gets. */
.time-chip.resting .time-part:first-of-type{opacity:.45}
.time-chip.held{border-color:var(--warn, #c9a227)}
.time-chip.held .time-part:first-of-type{color:var(--warn, #c9a227);opacity:.8}
@media(max-width:860px){
  .time-chip{font-size:11.5px;gap:4px;padding:2px 3px}
  .time-ico{font-size:10px}
}

/* ── the composer: 📎/⚙ on the left, the microphone above Send on the right ── */
.composer-row{align-items:stretch}
.composer-side{display:flex;flex-direction:column;gap:6px;flex:none;justify-content:flex-end}
/* ═══ THE FIELD HAS NO LEFT COLUMN IN ANY WINDOW (the owner, 25.08) ═══
   «Let us hide these buttons from every window and leave them only in the right-button menu on
   send.» 📎 and ⚙ move there for good — they are needed once in ten turns, and they took width
   away from the field on every one of them. ↑/↓ through your own prompts is brought back by a
   checkbox in the settings: it puts `hist-on` on the document, and only then does the column take
   up room again — IN EVERY window. */
.composer-row > .composer-side:first-child{display:none}
html.hist-on .composer-row > .composer-side:first-child{display:flex}
#btn-attach,#btn-gear{display:none!important}
/* A button taken out of sight but not out of the panel: it is pressed not with a finger but with
   a menu item and a hotkey, and the state (reading aloud) is painted on it as well. */
.only-menu{display:none!important}
/* ═══ THE SMALL COG UNDER «SEND» ═══
   The right-button menu is invisible to the eye: «let us make a small cog at the bottom right, so
   that it is clear there is something more there». `order:9` puts it last in the column — below
   ➤/■ — and not first, the way ⋯ is on the phone. */
.more-btn{order:9;flex:none;width:44px;min-height:24px;height:24px;padding:0;display:flex;
  align-items:center;justify-content:center;border:1px solid var(--line);border-radius:8px;
  background:var(--bg-3);color:var(--tx-3);cursor:pointer;transition:.15s}
.more-btn:hover{color:var(--tx);border-color:var(--line-2)}
.more-cog{width:14px;height:14px}
.more-dots{display:none}
/* ── stepping through your own messages: two thin arrows next to the field ──
   Not «two more buttons» but a replacement for the keyboard ↑/↓, which a phone does not have.
   Thin, with no fill, fading to a third when there is nowhere left to go: a button that will do
   nothing is obliged to say so before it is pressed. */
.hist-btn{width:30px;height:26px;border:1px solid var(--line-2);border-radius:8px;
  background:var(--bg-3);color:var(--tx-2);display:grid;place-items:center;cursor:pointer;
  padding:0;transition:opacity .12s ease,border-color .12s ease,color .12s ease}
.hist-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2.1;
  stroke-linecap:round;stroke-linejoin:round}
.hist-btn:hover:not(:disabled){color:var(--acc);border-color:var(--acc)}
.hist-btn:active:not(:disabled){transform:scale(.92)}
.hist-btn:disabled{opacity:.28;cursor:default}
.side-btn{width:40px;height:40px;flex:none;border-radius:var(--r);border:1px solid var(--line);
  background:var(--bg-3);color:var(--tx-2);cursor:pointer;font-size:16px;line-height:1;transition:.15s}
.side-btn:hover,.side-btn.on{border-color:var(--acc);color:var(--acc)}
.input{min-height:66px}
.composer-sum{background:none;border:none;color:var(--tx-3);font:11.5px var(--sans);cursor:pointer;
  padding:2px 6px;border-radius:6px;text-align:left}
.composer-sum:hover{color:var(--acc);background:var(--bg-3)}

/* ── the attachments: large tiles above the input field ──
   An attachment is checked with the eyes and not by the file's name: the question is always the
   same — «is this the right screenshot». So it is not a chip with a 30×30 icon but the snapshot
   itself; four in a row, more than that is not thrown in at once anyway. A tap on a tile opens it
   whole. */
.attach-strip{display:flex;gap:8px;flex-wrap:wrap;max-width:var(--read-w);margin:0 auto 9px}
.attach-strip:empty{display:none}
.att{position:relative;flex:none;width:132px;height:99px;border-radius:10px;overflow:hidden;
  border:1px solid var(--line-2);background:var(--bg-3);cursor:zoom-in}
.att:hover{border-color:var(--acc)}
.att img{width:100%;height:100%;object-fit:cover;display:block}
.att .att-n{position:absolute;left:0;right:0;bottom:0;padding:3px 7px;font:10px var(--mono);
  color:var(--tx);background:linear-gradient(transparent,var(--veil) 60%);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.att .att-x{position:absolute;top:4px;right:4px;width:20px;height:20px;border:none;border-radius:50%;
  background:var(--veil);color:var(--tx);font-size:11px;line-height:1;cursor:pointer;
  display:grid;place-items:center;opacity:.6;transition:opacity .15s,background .15s}
.att:hover .att-x{opacity:1}
.att .att-x:hover{background:var(--err);color:var(--on-err);opacity:1}
/* Not a picture — a glyph filling the whole tile: the file type reads as fast as the snapshot itself. */
.att.att-file{display:grid;place-items:center;background:var(--bg-4)}
.att .att-ico{font-size:30px;line-height:1;margin-bottom:14px}

/* ── the attachment whole: the only question is «is it THIS screenshot?» ── */
#lightbox{display:none;z-index:240}
#lightbox.open{display:grid}
/* The measures are taken from the veil's BOX and not from the screen: `.modal-back` is already
   divided by --ui-scale while `vw/vh` are not; that is exactly what once made the preview window
   run off to the right. */
.lightbox{position:relative;width:min(1100px,100%);height:min(900px,100%);
  display:flex;flex-direction:column;gap:9px}
/* ── THE WINDOW HE SIZED HIMSELF (owner, 15.09) ──
   The margin left on each side is the setting, in percent of the veil's own box: that is the one
   form of it that means the same thing on a 2560px monitor and in a hand. Absolute positioning
   here, because `inset` percentages are the only ones that resolve against the BOX on all four
   sides — a percentage `margin` resolves against the width even at the top and the bottom, which
   would make a window picked at the desk a different shape on the phone. Nothing is applied until
   he has actually dragged something: with no saved box the rule above stands, untouched. */
#lightbox.lb-boxed .lightbox{position:absolute;width:auto;height:auto;
  top:var(--lb-t,0);right:var(--lb-r,0);bottom:var(--lb-b,0);left:var(--lb-l,0)}
/* No picture — the window is only its bar, and it may not be stretched down the screen. */
#lightbox.lb-boxed.nopic .lightbox{bottom:auto}
/* The cross hangs OUTSIDE the window by 8px, which is off the screen once the top margin is 0. */
#lightbox.lb-boxed .lightbox-x{top:2px;right:2px}
/* Invisible until the hand comes near: this is a viewer, and eight permanent bars around a
   screenshot are eight things to look at instead of the picture. `touch-action:none` for the same
   reason as `.lb-view` — without it the browser takes the drag and does nothing with it. */
.lb-grip{position:absolute;z-index:1;touch-action:none;display:block}
.lb-grip::after{content:'';position:absolute;inset:0;border-radius:6px;background:var(--acc);
  opacity:0;transition:opacity .12s}
.lb-grip:hover::after,#lightbox.lb-sizing .lb-grip::after{opacity:.4}
.lb-grip[data-edge="n"]{top:-5px;left:16px;right:16px;height:12px;cursor:ns-resize}
.lb-grip[data-edge="s"]{bottom:-5px;left:16px;right:16px;height:12px;cursor:ns-resize}
.lb-grip[data-edge="w"]{left:-5px;top:16px;bottom:16px;width:12px;cursor:ew-resize}
.lb-grip[data-edge="e"]{right:-5px;top:16px;bottom:16px;width:12px;cursor:ew-resize}
.lb-grip[data-edge="nw"]{top:-5px;left:-5px;width:20px;height:20px;cursor:nwse-resize}
.lb-grip[data-edge="ne"]{top:-5px;right:-5px;width:20px;height:20px;cursor:nesw-resize}
.lb-grip[data-edge="sw"]{bottom:-5px;left:-5px;width:20px;height:20px;cursor:nesw-resize}
.lb-grip[data-edge="se"]{bottom:-5px;right:-5px;width:20px;height:20px;cursor:nwse-resize}
/* One corner says out loud that the window can be taken by its edges — on a phone there is no
   hover to discover it with. */
.lb-grip[data-edge="se"]::before{content:'';position:absolute;right:4px;bottom:4px;width:9px;height:9px;
  border-right:2px solid var(--tx-3);border-bottom:2px solid var(--tx-3);opacity:.55;border-radius:0 0 3px 0}
/* While the size is being dragged the picture underneath is not a target: the same argument as
   `.hv-win.sizing .hv-frame` — a player's seek bar would otherwise eat a gesture aimed at the edge. */
#lightbox.lb-sizing .lb-view{pointer-events:none}
#lightbox.lb-sizing{cursor:inherit;user-select:none}
/* THE PICTURE'S WINDOW CLIPS, THE PICTURE INSIDE PANS AND GROWS (web/zoom.js).
   `touch-action:none` is mandatory: without it the pinch goes to the browser, and on this page
   the browser cannot zoom at all (user-scalable=no) — that is, the gesture is lost outright. */
.lb-view{position:relative;flex:1;min-height:0;overflow:hidden;touch-action:none;
  overscroll-behavior:contain;border-radius:12px;border:1px solid var(--line-2);
  background:var(--bg-2);box-shadow:0 30px 80px var(--shadow);cursor:zoom-in}
.lb-view.zoomed{cursor:grab}
.lb-view.zoomed.dragging{cursor:grabbing}
.lightbox img{position:absolute;top:0;left:0;transform-origin:0 0;max-width:none;max-height:none;
  will-change:transform;user-select:none;-webkit-user-drag:none}
#lightbox.nopic .lb-view{display:none}
/* A VIDEO IS FITTED, NOT PANNED. `.lightbox img` above is absolutely positioned because the zoom
   engine drives it by `transform`; a player must do the opposite — sit still, keep its control bar
   reachable and never leave the window under a drag. Hence its own rules rather than a shared
   selector, and `touch-action:auto`, or the seek bar could not be dragged with a thumb: `.lb-view`
   kills touch actions on purpose, for the pinch. */
.lb-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;
  touch-action:auto}
.lb-vid.hidden{display:none}
/* Zoom, «Copy» and «↗ Tab» are all questions about a picture. They are HIDDEN rather than
   disabled here because they are not «unavailable for this file» — they are not this file's
   questions at all, and a row of dead buttons only asks to be pressed. The arrows stay: the row
   they walk holds recordings too. */
#lightbox.lb-video .lb-z,#lightbox.lb-video #lb-copy{display:none}
#lightbox.lb-video .lb-view{cursor:default}
/* The zoom figure is not a caption but a reading: monospaced digits, so that 100% and 940% do not
   twitch the neighbouring buttons. */
.lb-z{min-width:40px;padding:5px 8px;font-variant-numeric:tabular-nums;text-align:center}
.lightbox-note{font:11px var(--mono);color:var(--tx-3);text-align:center;word-break:break-all}
/* Five buttons in a row is already too much width on a phone: let them wrap rather than squeeze
   each other over the edge. */
.lightbox-bar{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
/* No picture (a path with no snapshot) — the window may not fill the whole screen with emptiness. */
#lightbox.nopic .lightbox{height:auto}
.lb-act{width:auto;height:auto;padding:5px 12px;font:12px var(--sans);color:var(--tx-2);
  background:var(--bg-3);border:1px solid var(--line-2);border-radius:9px}
.lb-act:hover{color:var(--tx);border-color:var(--acc)}
/* «⬇ Save» IS AN <a download>, NOT A <button> — the browser's own save needs no script, and on a
   phone it is what raises the proper «save / share» sheet. An anchor arrives underlined and with
   the text cursor, so it is told to look like its neighbours in the row; `display:none` is what
   `.hidden` means for it, since the button's rule cannot reach a link. */
a.lb-act{display:inline-flex;align-items:center;text-decoration:none;cursor:pointer}
a.lb-act.hidden{display:none}
#lightbox.nopic .lightbox-bar{display:none}
/* ── moving between the task's pictures ──
   The pair of arrows is soldered around the counter (as in the search): three separate buttons
   would read as three unrelated actions, and this is one — «I am flipping through». The counter
   is not decoration, it is the only thing that answers «and how many are there in all» and «how
   far is it to the edge». */
.lb-nav{display:inline-flex;align-items:center;gap:0}
.lb-nav.hidden{display:none}
/* The arrow's ink is full (`--tx`) and not muted: a grey glyph on a grey tile has already been
   judged «not a control» in the search bar, and there is no reason to pay for that lesson twice. */
.lb-arrow{min-width:34px;padding:5px 9px;font-size:16px;line-height:1;text-align:center;
  color:var(--tx);font-weight:600}
#lb-prev{border-radius:9px 0 0 9px;border-right:none}
#lb-next{border-radius:0 9px 9px 0;border-left:none}
.lb-count{min-width:64px;padding:5px 6px;font:11.5px var(--sans);color:var(--tx-2);
  text-align:center;background:var(--bg-3);border:1px solid var(--line-2);
  font-variant-numeric:tabular-nums}
.lb-act:disabled{opacity:.45;cursor:default}
.lb-act:disabled:hover{color:var(--tx-2);border-color:var(--line-2)}
/* ON A PHONE YOU ZOOM WITH YOUR FINGERS. «−/percent/+» duplicate the pinch and the double tap —
   that is, gestures that work here anyway — and the price for them was a second row of buttons
   and a run past the right edge. Their place is a mouse one; on a narrow screen only those two
   remain that cannot be done with a finger, and they stand in one row. */
@media(max-width:860px){
  .lb-z{display:none}
  .lightbox-bar{flex-wrap:nowrap;gap:6px}
  .lb-act{padding:5px 10px;font-size:11.5px}
  /* The arrows are NOT `.lb-z`: a finger is exactly what does them. A swipe over the picture
     can do the same, but a gesture is invisible while a button is visible; we shrink, we do not hide. */
  .lb-arrow{min-width:30px;padding:5px 7px;font-size:15px}
  .lb-count{min-width:52px;padding:5px 3px;font-size:10.5px}
}

/* ── a picture right in the reply ──
   The path to a snapshot is not a string of characters but the snapshot itself; the chip above it
   stays a door. */
.imgcard{position:relative;display:inline-block;line-height:0;margin:8px 0 2px;
  max-width:min(560px,100%);cursor:zoom-in;border:1px solid var(--line);border-radius:12px;
  overflow:hidden;background:var(--bg-2);transition:opacity .22s ease,transform .22s ease}
/* IT COMES OUT ONCE AND WITHOUT TOUCHING THE LAYOUT: the class is removed on load (picCard) and
   never comes back, and only the opacity and the transform move — the card's height is its own
   the whole time, so the text under it does not creep. */
.imgcard.hatch{opacity:0;transform:translateY(6px)}
@media (prefers-reduced-motion:reduce){
  .imgcard{transition:none}
  .imgcard.hatch{opacity:1;transform:none}
}
.imgcard:hover{border-color:var(--acc)}
/* `height:auto` — so that width/height known in advance set the BOX by the aspect ratio rather
   than stretching the snapshot: without it a picture with the attributes 1920×1080 would stand
   1080 tall and hit max-height. */
.imgcard-pic{display:block;max-width:100%;height:auto;max-height:340px;object-fit:contain}
/* A PICTURE THAT NEVER ARRIVED SAYS SO. Without this the card is an empty rectangle — «бросаю
   скриншот, а он пустой» — and there is nothing to press, because nothing failed. */
.imgcard-deaf{min-width:220px;min-height:64px;line-height:1.4;padding:14px 16px;
  border-style:dashed;cursor:pointer;color:var(--tx-2);font:13px var(--sans)}
.imgcard-deaf .imgcard-pic{min-height:32px}
.imgcard-bar{position:absolute;top:6px;right:6px;display:flex;gap:4px;opacity:0;transition:opacity .12s}
.imgcard:hover .imgcard-bar,.imgcard-bar:focus-within{opacity:1}
.imgcard-btn{width:26px;height:26px;padding:0;font-size:13px;line-height:1;cursor:pointer;
  color:var(--tx-2);background:var(--bg-4);border:1px solid var(--line-2);border-radius:7px}
.imgcard-btn:hover{color:var(--tx);border-color:var(--acc)}
/* On a phone there is no «hover» — the buttons are visible straight away. */
@media (hover:none){.imgcard-bar{opacity:1}}

/* ── a REPORT right in the reply (17.09) ──
   «When you give me a link to an HTML, give it the way you give a picture — a little preview I
   press.» The page itself is drawn small in the same sandboxed frame the preview window uses; the
   frame is laid out at a DESKTOP width and scaled down, so the thumbnail is the document he is
   about to open and not a phone-width re-flow of it. */
.repcard{position:relative;display:block;width:min(520px,100%);margin:8px 0 2px;cursor:pointer;
  border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--bg-2);
  transition:opacity .22s ease,transform .22s ease,border-color .12s}
.repcard.hatch{opacity:0;transform:translateY(6px)}
@media (prefers-reduced-motion:reduce){
  .repcard{transition:none}
  .repcard.hatch{opacity:1;transform:none}
}
.repcard:hover{border-color:var(--acc)}
.rc-shot{position:relative;height:196px;overflow:hidden;background:Canvas;color-scheme:light dark}
/* THE FRAME IS INERT. It is a foreign origin: left clickable it would swallow the press meant for
   the card, and there is nothing inside a thumbnail worth pressing anyway. */
.rc-frame{position:absolute;left:0;top:0;width:1100px;height:1450px;border:0;
  transform:scale(.4);transform-origin:0 0;pointer-events:none}
/* The page fades into the card's own floor instead of being cut off mid-sentence. */
.rc-veil{position:absolute;inset:0;background:linear-gradient(to bottom,transparent 55%,var(--bg-2))}
.rc-bar{position:relative;display:flex;align-items:baseline;gap:8px;padding:6px 10px;
  border-top:1px solid var(--line);font:12.5px/1.35 var(--sans)}
.rc-name{color:var(--tx);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rc-when{margin-left:auto;color:var(--tx-3);font-style:normal;white-space:nowrap;font-family:var(--mono);font-size:11px}
@media(max-width:700px){.rc-shot{height:158px}.rc-frame{transform:scale(.32)}}

/* ── THE REPORT STANDS IN FOR THE ANSWER (17.09) ──
   «Instead of this text report — only this HTML report, right inside our feed.» So the card of a
   finished answer is not a thumbnail: full width, its own height (the frame says it by
   postMessage — a foreign origin cannot be measured from outside), its own links live under the
   finger, and the head keeps ⛶ for the full window. */
/* ⚠ AND THE CARD HAS TO BE A FLEX COLUMN FOR ANY OF THAT ORDER TO MEAN ANYTHING. `.repcard` is
   `display:block`, where `order` is a declaration the engine ignores in silence — the head used to
   carry `order:2` and sat under the page purely because it is last in the markup, so the first
   attempt to lift it to the top changed exactly nothing and the gate said so. */
.repcard.rc-open{width:100%;cursor:zoom-in;background:var(--bg-2);border-color:var(--line-2);
  display:flex;flex-direction:column}
.repcard.rc-open .rc-shot{order:1}
/* ── THE WINDOW IS SMALL UNTIL HE SAYS OTHERWISE, AND IT NEVER SCROLLS INSIDE ITSELF (17.09) ──
   «Всё равно прокрутка полосы внутри окна есть… пусть окно будет свёрнуто, а кнопочка разворачивает
   его полностью.» The CLIP lives here, on the shot; the frame keeps the full height of its own
   document (`studioFrameH`), so there is never a second scrollbar and the feed's search still
   reaches every word below the fold. */
.repcard.rc-open .rc-shot{height:auto;max-height:var(--rc-peek,460px);background:Canvas;color-scheme:light dark}
.repcard.rc-open.rc-tall .rc-shot{max-height:none}
.repcard.rc-open .rc-frame{position:relative;width:100%;height:520px;transform:none;pointer-events:auto;display:block}
/* A clipped page says so: the last visible line fades into the card's own floor instead of being
   cut mid-sentence. It is a strip at the BOTTOM and eats no press — the report's own links live
   under it. */
.repcard.rc-open .rc-veil{top:auto;bottom:0;height:88px;pointer-events:none;
  background:linear-gradient(to bottom,transparent,var(--bg-2))}
.repcard.rc-open.rc-tall .rc-veil{display:none}
/* ⛔ THE TWO CONTROLS THAT ARE **NOT** «OPEN THE WINDOW» HAVE TO BE FINDABLE BY A THUMB (17.09).
   The whole card is now the door into the full preview, so these are the exceptions to it — and a
   12px glyph with 6px of padding is an exception nobody can hit on a phone («сделай её чуть
   покрупнее, чтобы она была»). 36x30 is the smallest that is still comfortably pressed, and the
   expander wears the accent because it is the one control the rest of the card does not do. */
.rc-btn{display:none;flex:0 0 auto;margin-left:6px;min-width:36px;height:30px;padding:0 9px;
  font-size:16px;line-height:28px;text-align:center;cursor:pointer;
  color:var(--tx-2);background:var(--bg-4);border:1px solid var(--line-2);border-radius:8px}
.repcard.rc-open .rc-btn{display:inline-block}
.rc-btn:hover{color:var(--tx);border-color:var(--acc);background:var(--bg-3)}
.rc-grow{color:var(--acc)}
.rc-grow[aria-expanded="true"]{background:color-mix(in srgb,var(--acc) 16%,var(--bg-4));border-color:var(--acc)}
/* The head stands ABOVE the page in an open card: under a document drawn at its own height it is
   a screen and a half away from the eye, and a control you have to scroll to find is a control
   nobody presses. */
.repcard.rc-open .rc-bar{order:0;align-items:center;border-top:0;border-bottom:1px solid var(--line)}
.repcard.rc-open .rc-when{margin-left:auto}
/* The card the search jumped into names itself for a moment, exactly like a message does. */
.repcard.find-jump{border-color:var(--acc);box-shadow:0 0 0 2px color-mix(in srgb,var(--acc) 35%,transparent)}
@media(max-width:700px){.repcard.rc-open .rc-shot{max-height:var(--rc-peek,320px)}}

/* What the fold hides, and the one press that brings it back. `display:none` only while the
   block is closed, so the toggle needs no second rule and nothing is ever deleted. */
.md.rep-answer:not(.rep-open) > .rep-hide{display:none}
.rep-more{display:block;margin:0 0 6px;padding:2px 9px;font:500 12px/1.7 var(--sans);cursor:pointer;
  color:var(--tx-3);background:transparent;border:1px dashed var(--line-2);border-radius:999px}
.rep-more:hover{color:var(--acc);border-color:var(--acc)}
.md.rep-open .rep-more{color:var(--acc);border-style:solid}
/* An ending a Stop hook sent back (supersedeEndings in web/app.js): one muted line, the rest one press away. */
.md.redo:not(.redo-open) > :not(.redo-pill){display:none}
.redo-pill{display:block;margin:0 0 6px;padding:2px 9px;font:500 12px/1.6 var(--sans);cursor:pointer;text-align:left;
  color:var(--mut);background:transparent;border:1px dashed var(--line);border-radius:10px}
.redo-pill:hover{color:var(--acc);border-color:var(--acc)}
.md.redo-open .redo-pill{color:var(--acc);border-style:solid}

/* ══════════════ A VIDEO IN THE FEED (08.09) ══════════════
   The same box as a picture — same width ceiling, same appearance, same size memory — because
   they answer the same question and standing in one column they must not disagree about it.
   `cursor:default`: the card is not «zoom in», the player under the finger is the point. */
.vidcard{position:relative;display:inline-block;line-height:0;margin:8px 0 2px;
  max-width:min(560px,100%);border:1px solid var(--line);border-radius:12px;
  overflow:hidden;background:var(--bg-2);transition:opacity .22s ease,transform .22s ease}
.vidcard.hatch{opacity:0;transform:translateY(6px)}
@media (prefers-reduced-motion:reduce){
  .vidcard{transition:none}
  .vidcard.hatch{opacity:1;transform:none}
}
.vidcard:hover{border-color:var(--acc)}
/* `height:auto` for the same reason as the picture: the attributes describe the BOX by aspect
   ratio, so the feed does not jump when the metadata finally lands. */
.vidcard-vid{display:block;max-width:100%;height:auto;max-height:340px;background:#000}

/* THE VEIL IS THE SENTENCE «THIS HAS SOUND», AND IT IS ALSO THE BUTTON THAT TURNS IT ON.
   It stops 44px short of the bottom so the player's own control bar is never covered: the native
   buttons are the ones a thumb already knows, and taking them away to add one of ours would be a
   worse panel. It goes away with the sound — a badge that stays after it has been obeyed is
   noise, and worse, it reads as «still muted». */
.vidcard-veil{position:absolute;left:0;right:0;top:0;bottom:44px;display:flex;gap:7px;
  align-items:center;justify-content:center;cursor:pointer;border:0;background:transparent;
  color:var(--tx);font:600 12.5px var(--sans);transition:opacity .15s}
.vidcard-veil::before{content:'';position:absolute;left:50%;top:50%;translate:-50% -50%;
  width:170px;height:42px;border-radius:21px;background:var(--bg-2);opacity:.82;
  border:1px solid var(--line-2)}
.vidcard-veil-ico,.vidcard-veil-txt{position:relative}
.vidcard-veil-ico{font-size:15px;line-height:1}
.vidcard-veil:hover::before{border-color:var(--acc);opacity:.92}
.vidcard.loud .vidcard-veil{display:none}

.vidcard-bar{position:absolute;top:6px;right:6px;display:flex;gap:4px;align-items:center;
  opacity:0;transition:opacity .12s}
.vidcard:hover .vidcard-bar,.vidcard-bar:focus-within{opacity:1}
/* The length is a fact you want BEFORE pressing: «is this six seconds or six minutes». Tabular
   digits so 0:07 and 12:40 do not twitch the button beside them. */
.vidcard-time{padding:0 7px;height:26px;display:inline-flex;align-items:center;
  font:11.5px var(--mono);font-variant-numeric:tabular-nums;color:var(--tx-2);
  background:var(--bg-4);border:1px solid var(--line-2);border-radius:7px}
.vidcard-time:empty{display:none}
.vidcard-btn{width:26px;height:26px;padding:0;font-size:13px;line-height:1;cursor:pointer;
  color:var(--tx-2);background:var(--bg-4);border:1px solid var(--line-2);border-radius:7px}
.vidcard-btn:hover{color:var(--tx);border-color:var(--acc)}
@media (hover:none){.vidcard-bar{opacity:1}}

/* A VIDEO TILE IS A FRAME PLUS A ▶. A still frame with nothing on it reads as a photograph, and
   then the tile lies about what was sent. Same geometry as `.att-img`, so a screenshot and a
   recording stand level in one strip. */
.att-vid{width:100%;height:98px;object-fit:cover;display:block;background:#000}
.att .att-vid{height:100%}
/* `.att` already stands `relative` (the ✕ hangs on it); the feed's tile did not need to until
   now — the ▶ is the first thing inside it that is positioned. */
.att-cell{position:relative}
.att-play{position:absolute;left:50%;top:49px;translate:-50% -50%;pointer-events:none;
  width:34px;height:34px;display:grid;place-items:center;border-radius:50%;
  font-size:13px;color:var(--tx);background:var(--bg-2);opacity:.85;border:1px solid var(--line-2)}
.att .att-play{top:50%}
.lightbox-x{position:absolute;top:-8px;right:-8px;z-index:2;background:var(--bg-4);
  border:1px solid var(--line-2);border-radius:50%}

/* ── dragging a file into the window ── */
.drop-veil{position:fixed;inset:0;z-index:260;background:var(--veil);display:grid;place-items:center;
  pointer-events:none}
.drop-msg{border:2px dashed var(--acc);border-radius:16px;padding:26px 34px;color:var(--acc);
  font:600 15px var(--sans);background:var(--bg-2)}

/* ── the sheets from below: settings and attachments ── */
.sheet-back{position:fixed;inset:0;background:var(--veil);z-index:120;display:flex;align-items:flex-end;
  justify-content:center;opacity:0;pointer-events:none;transition:opacity .18s}
.sheet-back.open{opacity:1;pointer-events:auto}
.sheet{width:min(560px,100%);max-height:86vh;background:var(--bg-2);border:1px solid var(--line-2);
  border-radius:16px 16px 0 0;display:flex;flex-direction:column;transform:translateY(12px);
  transition:transform .2s cubic-bezier(.2,.8,.2,1);box-shadow:0 -20px 60px var(--shadow)}
.sheet-back.open .sheet{transform:none}
.sheet-head{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-bottom:1px solid var(--line)}
.sheet-title{font:600 14px var(--sans)}
/* The turn's settings and the panel's settings are not mixed into one list. */
.sheet-tabs{display:flex;gap:4px;padding:8px 16px 0;border-bottom:1px solid var(--line);flex:none}
.sheet-tab{flex:1;background:none;border:none;border-bottom:2px solid transparent;color:var(--tx-3);
  font:600 12.5px var(--sans);padding:9px 6px;cursor:pointer;border-radius:8px 8px 0 0;transition:.15s}
.sheet-tab:hover{color:var(--tx-2);background:var(--bg-3)}
.sheet-tab.active{color:var(--acc);border-bottom-color:var(--acc)}
.sheet-pane{display:none;flex-direction:column;gap:11px}
.sheet-pane.active{display:flex}
.theme-auto.off{opacity:.6}
/* `overflow-y:auto` makes the other axis `auto` BY ITSELF (by the specification visible+auto
   cannot happen): any child that did not fit by width immediately added a horizontal scroll and
   ran off the screen. The sheet does not scroll sideways — it narrows its content. */
.sheet-body{padding:14px 16px calc(16px + env(safe-area-inset-bottom));overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:11px}
/* ⛔ A ROW WRAPS RATHER THAN CLIPS, AND IT MAY NOT BE SCOPED TO A MEDIA QUERY (09.09).
   The room this drawer draws in is the screen divided by --ui-scale, so at the 200% the owner
   asked for it is ~262px on his phone rather than 374. `#model-default-pick` carries
   min-width:190px and white-space:nowrap, i.e. it cannot yield: with the label beside it the row
   needed ~302px, and because .sheet-body already refuses to pan sideways (the belt below) the
   surplus was not scrolled — it was CUT, so «opus[1m] · high ▾» lost its tail and the row stopped
   answering the one question it exists for.
   ⛔ MEASURED 09.09: a media query CANNOT see this. matchMedia('(max-width:400px)') is false at
   --ui-scale 2 on a 524px screen — queries are answered from the screen width and are blind to
   the zoom, so `@media(max-width:300px)` would never fire however small the room really got.
   The cure therefore has to hold at every scale: the row wraps. While the contents fit, wrapping
   changes nothing at all; when they do not, the control drops to a second line whole instead of
   losing its right-hand end. */
.sheet-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.sheet-row label{font-size:13px;color:var(--tx-2);flex:none}
.sheet-row .chip-select{width:auto;min-width:190px;font-size:13px;padding:7px 10px}
/* The model-picking button in the sheet is of exactly the height and weight of the list beside
   it, otherwise the «Model» row stands higher than the «Permissions» row and reads as a different
   class of setting. */
.sheet-row .pick-btn{margin-left:auto;min-width:190px;max-width:60%;text-align:left;
  background:var(--bg-3);color:var(--tx);border:1px solid var(--line);border-radius:8px;
  font:13px var(--sans);padding:7px 10px;cursor:pointer;outline:none;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sheet-row .pick-btn:hover{border-color:var(--acc)}
/* The value's home stays a `<select>` — simply an invisible one: all the memory hangs on it. */
.hidden-input{display:none !important}
/* The advanced mode: the effort level and the thinking are shown only when they have been asked
   for. By default the model decides, and that is the right answer to almost every turn. */
.adv-only{display:none}
body.adv .adv-only{display:flex}
/* The only text field in the sheet is the send key phrase. It looks like the select beside it, so
   that the settings row does not fall apart. */
.chip-input{width:auto;min-width:190px;font:13px var(--sans);padding:7px 10px;border-radius:7px;
  background:var(--bg-3);border:1px solid var(--line);color:var(--tx);outline:none}
.chip-input:focus{border-color:var(--acc)}
.sheet-toggle{display:flex;align-items:center;gap:9px;font-size:13.5px;color:var(--tx-2);cursor:pointer;
  padding:7px 2px;user-select:none}
.sheet-toggle input{width:17px;height:17px;accent-color:var(--acc)}
/* A SETTING THAT CONTROLS NOTHING ON THIS SCREEN IS A LIE. The spoken announcement happens only
   on a phone, so at the desk the switch is simply not there: a checkbox that changes nothing reads
   as a broken one. It has its own key per screen class anyway. */
.only-phone{display:none}
@media (max-width:860px){.sheet-toggle.only-phone{display:flex}}
.sheet-acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:2px}
.sheet-acts .act-btn{padding:9px 13px;font-size:13px}
.sheet-label{font:600 10.5px var(--sans);letter-spacing:.9px;text-transform:uppercase;color:var(--tx-3);margin-top:4px}
.sheet-note{font-size:11.5px;line-height:1.55;color:var(--tx-3)}
.sheet-note b{color:var(--tx-2)}

/* ── WHERE WE TAKE AN ATTACHMENT FROM ──
   The button row (`.sheet-acts`) on a phone turned into a fine mush of three pills, in which
   «gallery / file» promised nothing: you tapped it and the file manager opened. Here a source is
   a large full-width row with a caption saying WHAT will open behind it, and a finger hits it
   without looking. */
/* THE COLUMN IS SET TO `minmax(0,1fr)` AND NOT LEFT AS `auto`. An auto track's minimum equals the
   content's min-content, and inside the button lies a caption with `nowrap`: the line «Downloads
   and everything else on the phone» stretched the button WIDER than the sheet. `.sheet-body` has
   `overflow-y:auto`, that is, auto horizontally too — and the buttons ran off the right edge of
   the screen together with a horizontal scroll (the owner's screenshot, 22.08). */
.att-src{display:grid;grid-template-columns:minmax(0,1fr);gap:8px;margin-top:2px}
.src-btn{display:flex;align-items:center;gap:11px;width:100%;min-width:0;text-align:left;
  background:var(--bg-4);border:1px solid var(--line-2);color:var(--tx);border-radius:11px;
  padding:11px 13px;font:500 14px var(--sans);cursor:pointer}
.src-btn:hover,.src-btn:active{border-color:var(--acc);color:var(--acc)}
.src-ico{font-size:21px;line-height:1;flex:0 0 auto}
.src-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
.src-txt i{font-style:normal;font-weight:400;font-size:11.5px;color:var(--tx-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The computer's disk is needed rarely from a phone — it lives as a fold and IS NOT READ FROM THE
   DISK until the fold has been opened: the list of another machine's screenshots must not be the
   first thing a person reaching for his own gallery sees. */
.pc-toggle{display:flex;align-items:center;gap:8px;width:100%;margin-top:11px;
  background:none;border:1px solid var(--line);color:var(--tx-2);border-radius:9px;
  padding:9px 12px;font:500 12.5px var(--sans);cursor:pointer}
.pc-toggle:hover{border-color:var(--acc);color:var(--acc)}
.pc-caret{margin-left:auto;transition:transform .16s}
.pc-toggle[aria-expanded="true"] .pc-caret{transform:rotate(180deg)}
.pc-wrap{display:none;margin-top:9px}
.pc-wrap.open{display:block}
.pc-wrap .sheet-label{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:7px}

/* ── the grid of the machine's recent screenshots ── */
.shots-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:8px}
.shot{position:relative;padding:0;border:1px solid var(--line);border-radius:9px;overflow:hidden;
  background:var(--bg-3);cursor:pointer;aspect-ratio:4/3}
.shot:hover{border-color:var(--acc)}
.shot img{width:100%;height:100%;object-fit:cover;display:block}
.shot .shot-n{position:absolute;left:0;right:0;bottom:0;background:var(--veil);color:var(--tx-2);
  font:10px var(--mono);padding:2px 5px;text-align:left}

/* ── the statistics behind a tap on the key percent ── */
#stats{display:none}
#stats.open{display:grid}
/* ONE AND A HALF TIMES WIDER (28.08): «make it wider, about one and a half times, so that more
   information fits». 520 → 780 — on a phone nothing changes (it is 94vw there anyway), while at
   the desk four tiles stand in a row instead of two and the captions stop wrapping. */
.stats-modal{width:min(780px,calc(94vw / var(--ui-scale,1)))}

/* ═══════════ THE «DAY BY DAY» SHEET (a click on the clock in the header) ═══════════
   Wider than the spend page: here it is not a column of figures that is read but the SHAPE —
   where the spike is, where it is flat. On a narrow chart a hundred and fifty days turn into a
   picket fence from which nothing can be seen. */
#tl{display:none}
#tl.open{display:grid}
.tl-modal{width:min(1020px,calc(96vw / var(--ui-scale,1)))}
/* THE FOUR SCOPES STAND SIDE BY SIDE AND NOT IN A DROP-DOWN LIST. They are compared by eye («the
   task — 4 hours, the project — 40, the whole workshop — 300»), and a list that shows one row at
   a time takes away precisely that comparison. */
.tl-scopes{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));gap:7px;margin-bottom:9px}
.tl-scope{text-align:left;padding:8px 10px;border-radius:11px;border:1px solid var(--line);
  background:var(--bg-3);color:var(--tx-2);cursor:pointer;transition:border-color .13s,background .13s}
.tl-scope:hover{border-color:var(--acc)}
.tl-scope.on{border-color:var(--acc);background:var(--bg-4);color:var(--tx)}
.tl-scope-k{font:10px var(--mono);letter-spacing:.5px;text-transform:uppercase;color:var(--tx-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A first-level project is its own parent project: the button stays (the level exists) but is
   dimmed — so that identical figures side by side read as «the same thing» and not as a counting
   failure. */
.tl-scope.tl-twin{opacity:.62}
.tl-scope.tl-twin.on{opacity:1}
.tl-scope-n{font:600 12.5px var(--sans);margin:1px 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tl-scope-v{display:flex;flex-wrap:wrap;gap:7px;font:11px var(--mono)}
.tl-me{color:var(--acc)} .tl-ai{color:var(--ok)} .tl-key{color:var(--warn)}
.tl-periods{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.tl-per{padding:3px 11px;border-radius:999px;border:1px solid var(--line);background:transparent;
  color:var(--tx-2);font:11.5px var(--sans);cursor:pointer}
.tl-per:hover{border-color:var(--acc)}
.tl-per.on{background:var(--bg-4);border-color:var(--acc);color:var(--tx)}
.tl-chart{margin-bottom:12px}
/* The total is under the chart and over THE SAME window: a sum that does not match the picture
   above it is worse than a missing sum. */
.tl-tot{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:7px}
.tl-tile{background:var(--bg-3);border:1px solid var(--line);border-radius:10px;padding:7px 9px}
.tl-tile-v{font:600 14px var(--mono)}
.tl-tile-k{font:10px var(--sans);color:var(--tx-3);margin-top:1px}
.tl-tile.me .tl-tile-v{color:var(--acc)}
.tl-tile.ai .tl-tile-v{color:var(--ok)}
.tl-tile.key .tl-tile-v{color:var(--warn)}
.tl-note{margin-top:10px;font:11px/1.55 var(--sans);color:var(--tx-3)}
@media (max-width:860px){
  .tl-scopes{grid-template-columns:1fr 1fr}
  .tl-tot{grid-template-columns:1fr 1fr 1fr}
}
/* ── 🔑 the key by hours (a section of the spend page, web/keyhours.js) ──────────────
   The same language as the «time day by day» sheet: the `.tl-per` switches, the `.tl-tile` tiles,
   the `.tl-note` footnote — the section is obliged to read as part of one system and not as a
   second page about the same thing. The only thing of its own here is the card of the chosen hour. */
.kh-host{margin-bottom:14px}
.kh-chart{margin-bottom:10px}
/* THE HEADING OF THE SECOND AND THE THIRD CHART. Three pictures in a row with not a single word
   between them read as one that has gone wrong: each has its own question, and the question is
   written above it. */
.kh-h2{margin:16px 0 4px;font:600 12.5px var(--sans);color:var(--tx-2);
  display:flex;gap:7px;align-items:baseline}
.kh-h2n{margin:0 0 7px;font:11px/1.55 var(--sans);color:var(--tx-3);max-width:78ch}
/* The breakdown of the third chart — by project or by task. It stands IN THE HEADING and not on a
   line of its own: it is a refinement of the question «who ate it» and not a second question. */
.kh-switch{display:inline-flex;gap:4px;margin-left:auto}
.kh-sw{border:1px solid var(--line);background:transparent;color:var(--tx-3);
  border-radius:999px;padding:1px 9px;font:11px var(--sans);cursor:pointer}
.kh-sw:hover{border-color:var(--acc)}
.kh-sw.on{color:var(--tx);border-color:var(--acc);background:rgba(217,119,87,.14)}
.kh-span{font:11px var(--mono);color:var(--tx-3);margin:2px 0 6px;display:flex;gap:10px;flex-wrap:wrap;align-items:baseline}
.kh-zoom{color:var(--acc);font:11px var(--sans)}
/* The legend of the series: a dot of the same colour, the series' name, and what it is at all. As
   a solid footnote it does not read — there is nothing to match the colour against. */
.kh-keys{margin:2px 0 8px;display:flex;flex-direction:column;gap:3px}
.kh-key{display:flex;gap:7px;align-items:baseline;font:11px/1.5 var(--sans);color:var(--tx-3)}
.kh-key b{color:var(--tx-2);font:600 11px var(--mono);flex:0 0 auto}
.kh-kdot{flex:0 0 auto;width:9px;height:9px;border-radius:3px;margin-top:2px}
.kh-kdot.line{height:3px;width:13px;border-radius:2px;margin-top:5px}
.tl-tile.gauge .tl-tile-v{color:var(--think)}
.kh-hint{font:11px var(--sans);color:var(--tx-3);text-align:center;padding:8px 0 2px;opacity:.85}
.kh-detail{margin-top:11px;border:1px solid var(--line);border-radius:12px;
  background:var(--bg-3);padding:10px 12px}
.kh-dhead{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
.kh-dwhen{font:600 13px var(--sans);flex:1 1 auto;min-width:120px}
.kh-dpct{font:600 15px var(--mono);color:var(--warn)}
.kh-daud{font:12px var(--mono);color:var(--tx-3)}
.kh-dfacts{margin-top:5px;font:11.5px/1.6 var(--mono);color:var(--tx-2)}
.kh-dgauge{margin-top:4px;font:11px/1.6 var(--sans);color:var(--tx-3)}
.kh-dgauge b{color:var(--think);font-weight:600}
.kh-rows{margin-top:10px;display:flex;flex-direction:column;gap:9px}
.kh-row{border-top:1px solid var(--line);padding-top:8px}
.kh-rhead{display:flex;align-items:baseline;gap:8px}
/* The task's name is cut with an ellipsis rather than wrapped: there are up to twelve rows here,
   and two lines for each turn the card into a bedsheet that can no longer be compared by eye. */
.kh-rname{flex:1 1 auto;min-width:0;font:12.5px var(--sans);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.kh-rpct{font:600 12px var(--mono);color:var(--warn);flex:0 0 auto}
.kh-more{margin-top:9px;font:11px var(--sans);color:var(--tx-3)}
@media (max-width:860px){ .kh-dfacts{font-size:11px} }
.stat-hero{text-align:center;padding:6px 0 14px}
.stat-hero .big{font:600 54px var(--mono);color:var(--acc-2);line-height:1}
.stat-hero .sub{font-size:12.5px;color:var(--tx-3);margin-top:6px}
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
.stat-tile{background:var(--bg-3);border:1px solid var(--line);border-radius:10px;padding:9px 11px}
.stat-tile .k{font:600 10px var(--sans);letter-spacing:.9px;text-transform:uppercase;color:var(--tx-3)}
.stat-tile .v{font:600 17px var(--mono);color:var(--tx);margin-top:3px}
.stat-tile .n{font-size:11px;color:var(--tx-3);margin-top:2px}

/* ── reading aloud ── */
.speak-btn{background:none;border:1px solid var(--line-2);color:var(--tx-3);border-radius:7px;
  padding:3px 9px;font:11px var(--sans);cursor:pointer;transition:.15s}
.speak-btn:hover,.speak-btn.on{border-color:var(--acc);color:var(--acc)}
.speak-btn.on{background:var(--warn-bg)}

/* ── reading mode: the screen is given over to the text ── */
/* The composer's height is measured live (mobile.js) and arrives here: the floating buttons are
   obliged to stand ABOVE the input field and not on top of «Send». In reading mode the composer
   is collapsed, the variable becomes zero — and the buttons drop down by themselves. */
/* READING ALOUD DOES NOT BELONG TO READING MODE, AND IT DOES NOT BELONG TO THE PHONE EITHER.
   🔊 lived INSIDE .read-fab, and that one only showed when the header and the composer went
   away. As soon as that creeping-away was switched off by default (at the owner's request),
   there was physically nothing left to speak a reply with on the phone: «the mobile version
   does not work, read the last reply aloud».
   ⛔ THE SECOND HALF OF THE SAME DEFECT LIVED ON FOR ANOTHER FORTNIGHT: the rule that turned the
   buttons on stood INSIDE `@media(max-width:860px)`, so on a wide screen the disc was drawn,
   sized, animated — and invisible, at `opacity:0`. The desktop's only door to the voice was the
   text button «🔊 read aloud» at the foot of a finished answer, i.e. nothing at all while the
   turn is still running, and nothing on an answer that has scrolled away. «Make this button
   available for desktop mode too — not only for mobile» (the owner, 07.09). The visible state is
   the BASE state now, on every width; the fade-out below is only what hides it while the rail
   curtain is open. */
/* ⚠ `--win-r` / `--win-b` / `--win-l` ARE THE EDGES OF WINDOW 1'S OWN COLUMN, and they are how
   every floating thing here stops being a thing of the SCREEN and becomes a thing of the WINDOW
   (web/workspace.js → anchor(), 11.09). `position:fixed` is measured against the viewport, and a
   column window is an <iframe> whose viewport IS the column — so a pane was always right and only
   window 1 was wrong, drawing its speaker on the right edge of the whole layout, on top of the
   last column's speaker. Unset = one window = exactly the old numbers. */
.read-fab{position:fixed;right:calc(var(--win-r, 0px) + 14px);
  bottom:calc(var(--win-b, 0px) + var(--composer-h, 96px) + 12px);z-index:70;
  display:flex;flex-direction:column;gap:9px;opacity:1;pointer-events:auto;
  transform:none;transition:.18s}
/* ▼ «down, to the input» — the other way round, only when there is something below: at the
   very bottom of the feed it is a button that says «scroll to where I am standing anyway». */
.read-fab.at-end #fab-down{display:none}
.read-fab button{width:46px;height:46px;border-radius:50%;border:1px solid var(--line-2);
  background:var(--bg-4);color:var(--tx);font-size:18px;cursor:pointer;box-shadow:0 8px 24px var(--shadow)}
.read-fab button:active{background:var(--acc);color:var(--on-acc)}
.read-fab button.on{background:var(--acc);color:var(--on-acc);border-color:var(--acc)}

@media(max-width:860px){
  .only-wide{display:none!important}
  /* ⛔ THE OFF LOOP CHIP STAYS HIDDEN HERE. On a phone it lives in `#gauge-row`, which is a LINE
     OF ITS OWN under the bottom bar, and `.gauge-row:empty` is what keeps that line off a row
     with nothing to show — an always-present icon would hand every brand-new empty row a line it
     does not have today, in the block that carries the composer's height. The phone's door into
     the loop is the ⋯ button: full height, under the thumb, and it already carries the item.
     ⛔ A SECOND WINDOW IS NOT A PHONE, AND THIS RULE USED TO SAY IT WAS (the owner, 04.10: «check
     that button appears on multi-window mode»). A workspace window is `<iframe src="/?pane=…">`
     and a column IS that iframe's whole viewport, so two columns on a 1920-px desk make every
     pane 828 px — under this breakpoint, with a mouse, a hover and a full bottom bar. Measured:
     window 1 drew the ↻ chip, window 2 drew nothing, which is exactly the half of his screenshot
     the arrow points at. The reason written above does not even apply there: `placeGauge` in
     web/app.js ALREADY carves the pane out of the phone layout (`… && !documentElement.classList
     .contains('pane')`), so in a pane the chip never moves to `#gauge-row` and there is no line
     of its own to keep empty — one rule, two opinions, and the CSS held the older one. A `.pane`
     narrower than this breakpoint is by construction a desk column and never a phone: the
     workspace refuses to exist under 860 px (`narrow()` in web/workspace.js returns early, and
     `addPane` refuses), so a phone has no panes to apply this to. */
  html:not(.pane) .loop-left.loop-left.off{display:none}
  /* THE MODEL IS CHANGED EVERY TURN — SO IT IS NEEDED ON THE PHONE TOO (the owner, 30.08).
     «There is no way in the mobile version to choose which model to send the task with… I would
     like it right down at the bottom, as on the desktop, to pick a model quickly.» The strip under
     the field was hidden WHOLE on a narrow screen (`only-wide`) — together with the only fast door
     to the model, and only the settings sheet was left. We bring the ROW back, but not all of its
     content: «🙈 no thoughts» stays in the sheet — its label takes 160px out of the three hundred,
     and a model list squeezed into the remainder would read as an ellipsis, that is, it would stop
     answering its only question.
     Two classes against one — the rule is stronger than `.only-wide{display:none!important}`; in a
     window of the working field (`html.pane`) everything stays as it was, there is width there.

     ⚠ CORRECTED 09.09: the whole switch used to be hidden here, and the owner could not find it
     («this checkbox is not visible on mobile layout — fix it… must be visible, no text maybe,
     only icon, and details»). The 160px that made it leave were the WORDS, not the control: the
     eye already says the state (open/shut swap on `:checked`), so only `.flag-txt` goes. Measured
     at 430px / --ui-scale 1.4: the whole switch is now ~46 local px of the ~300 there are. The
     caption is not lost — a 480 ms hold prints `data-hint` above it (`holdHint` in web/app.js),
     which is the same answer the findbar's buttons give to «what is this little button». */
  .composer-flags.only-wide{display:flex!important}
  html:not(.pane) .composer-flags .flag .flag-txt{display:none}
  /* A finger, not a glyph — the same reason #model-quick grows below. The switch keeps the
     13px box plus the eye, and it may not wrap: this row carries the height of the whole
     composer bottom, and one wrap slides «send» under the thumb. */
  html:not(.pane) .composer-flags .flag{padding:6px 8px;min-height:32px;gap:4px}
  /* A finger, not a glyph: the model list is as much a button of the turn as «send» is. */
  html:not(.pane) #model-quick{font-size:13px;padding:6px 9px;min-height:32px;
    max-width:calc(52vw / var(--ui-scale,1))}
  /* A FIELD THREE BUTTONS TALL — AND IT STAYS THAT WAY AFTER A DICTATION. 88px is two lines:
     what had been dictated hid behind an inner scroll at once, and a second press of the
     microphone collapsed the window «to zero» at exactly the instant the text finally appeared
     in it. The height is counted from the column of buttons: 3 × 44 + two 6px gaps = 144, plus
     the field's own border. */
  .input{min-height:150px;font-size:16px}   /* 16px — otherwise iOS zooms the page on focus */
  .side-btn,.mic-btn,.send-btn,.stop-btn{width:44px;height:44px}
  /* The gauge's own line under the flags row. 14px is the bar plus its labels and not a pixel
     more — the composer's height is load-bearing geometry on a phone. */
  .gauge-row{display:flex;align-items:center;gap:7px;max-width:var(--read-w);margin:5px auto 0;height:14px}
  /* The track is pushed 3px down inside its own 14px line — the row does not grow, and the
     minutes above it gain that clearance from the flags row overhead. */
  .gauge-row .ctx-gauge{flex:1 1 auto;width:auto;height:14px;padding-top:3px;box-sizing:border-box}
  .gauge-row .cg-zero,.gauge-row .cg-scale{font-size:9.5px}
  /* The phone line is 14px and the flags row stands 5px above it: the number over the track is
     given a smaller face and a thinner gap so its ink lands INSIDE that gap and touches nothing. */
  .gauge-row .cg-now{font-size:8.5px;bottom:calc(100% + 1.5px)}
  .key-pct{font-size:22px}
  .cache{font-size:13.5px}
  .meters{gap:9px}
  .composer-sum{display:none}
  .sheet{width:100%;max-height:88vh}
  .sheet-row .chip-select{min-width:0;flex:1}

  /* I scroll the reply — the header and the composer leave, the whole screen becomes text.
     THE GEOMETRY CHANGES INSTANTLY, only the colour fades. The height may not be animated: for
     all 220 ms the feed grows under the text and the text travels after it — that is exactly
     «the page twitches, the bottom panel now swims». The jump of the header's top edge is
     compensated by mobile.js in the same frame, so nothing at all moves on screen except the
     panels themselves. */
  .topbar,.composer{transition:opacity .14s ease}
  /* …AND THEY COME BACK BY SLIDING IN. The geometry still changes instantly (otherwise the feed
     grows under the text for all 240 ms — that same «twitching page»), but the panels themselves
     arrive at their place with a shift: the header from the top, the composer from below. The
     animation is one-shot, the class is removed on animationend — see mobile.js → setReading. */
  @keyframes panel-up{from{transform:translateY(112%);opacity:0}to{transform:none;opacity:1}}
  @keyframes panel-down{from{transform:translateY(-112%);opacity:0}to{transform:none;opacity:1}}
  .composer.slide-in{animation:panel-up .26s cubic-bezier(.22,.61,.36,1)}
  .topbar.slide-in{animation:panel-down .22s cubic-bezier(.22,.61,.36,1)}
  html.reading .topbar{margin-top:calc(-1 * var(--topbar-h, 54px));opacity:0;pointer-events:none}
  html.reading .composer{max-height:0;opacity:0;padding-top:0;padding-bottom:0;overflow:hidden;
    pointer-events:none;border-top-color:transparent}
  html.reading .agent-strip{display:none}
}

/* ═══════════════════════════════════════════════════════════════════════════
   ADDED: the zoom, the header, the «are you sure?» question, picking a project, the key's rest
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── everything measured from the viewport is divided back by the scale ── */
.sheet{max-height:calc(86vh / var(--ui-scale))}
.modal{max-height:calc(86vh / var(--ui-scale))}
.flow-overlay{height:min(calc(62vh / var(--ui-scale)),560px)}
@media(max-width:860px){.sheet{max-height:calc(88vh / var(--ui-scale))}}

/* ── +/− of the font size ── */
.scale-ctl{display:flex;align-items:center;gap:6px}
.scale-ctl button{width:30px;height:30px;border-radius:8px;border:1px solid var(--line-2);
  background:var(--bg-3);color:var(--tx);font:600 15px/1 var(--sans);cursor:pointer;transition:.15s}
.scale-ctl button:hover{border-color:var(--acc);color:var(--acc)}
.scale-ctl button.ghost{width:auto;padding:0 9px;font:11.5px var(--sans);color:var(--tx-3)}
.scale-ctl #fs-val{min-width:46px;text-align:center;font:12.5px var(--mono);color:var(--tx-2)}
/* The phone's own reading, and the state where there is nothing to obey. A disabled «match» is
   dimmed and loses the hover promise: the reason is spelled out in the reading beside it
   («not reported»), so the button must not look pressable while it is not. */
.scale-ctl #fs-phone{text-align:right;font:12.5px var(--mono);color:var(--tx-2)}
.scale-ctl #fs-phone.dim{font-size:11.5px;font-family:var(--sans);color:var(--tx-3)}
.scale-ctl button:disabled{opacity:.45;cursor:default}
.scale-ctl button:disabled:hover{border-color:var(--line-2);color:var(--tx-3)}

/* ── the header: the context in one figure, the key percent large and steady ── */
/* NARROWER THAN IT WAS (the owner, 25.08: «the token count — make it narrower»): a figure like
   «259k» takes forty pixels at most, and the rest was air in the middle of the most expensive
   strip of the screen. */
/* THE WIDTH OF THE CONTEXT CHIP IS EXACTLY THAT OF THE LONGEST NUMBER IT EVER HOLDS.
   «Make 303k the maximum width of this chip — it does not need to be that long» (the owner,
   25.08). The context tops out at 300k and by definition grows no higher, so four characters are
   the ceiling; 54px held room for a fifth that will never exist, and that air stood in the middle
   of the most expensive strip of the screen. The strip under the figure stretches across the chip
   (`width:100%`) rather than living by its own number — otherwise it would set the width itself. */
.ctx-chip{display:flex;flex-direction:column;justify-content:center;gap:2px;width:40px;
  background:none;border:none;cursor:pointer;padding:3px 4px;border-radius:8px;transition:.15s}
.ctx-chip:hover{background:var(--bg-3)}
.ctx-num{font:600 15px/1 var(--sans);color:var(--tx-2);font-variant-numeric:tabular-nums;
  letter-spacing:-.2px;text-align:left}
.ctx-chip .bar{margin:0;width:100%}
/* ═══ THE KEY PERCENT AND ITS RESET TIME IN TWO STOREYS ═══
   «How much of the key is left — maybe move it to a second line, so that it becomes a little
   narrower» (the owner, 25.08). In one line «5% (6d 21h)» takes four times as much room as the
   percent itself, for the sake of a time that is looked at once a day. The wrap is done by
   `flex-basis:100%` on the reset time: the chip itself stays a flex row (the account letter is
   obliged to stand FLUSH against the percent), and the second storey comes out of a wrap rather
   than of a separate grid. */
.key-chip{flex-wrap:wrap;column-gap:5px;row-gap:0;max-width:78px;padding-bottom:4px}
.key-pct{font:600 21px/1 var(--sans);font-variant-numeric:tabular-nums;letter-spacing:-.6px}
.key-left{flex-basis:100%;margin-top:1px;font:11.5px var(--sans);color:var(--tx-3);
  font-variant-numeric:tabular-nums;line-height:1.1;white-space:nowrap}

/* ── «＋ New project» under the tree ── */

/* ── the «are you sure?» question ── */
#ask{display:none}
#ask.open{display:grid}
/* ⚠ 400px of LOCAL pixels at zoom 1.4 is 560 screen pixels on a 430px phone:
   this is exactly where a translation is shown, and it is exactly the one that «ran off to the
   right, and the button was cut off». Everything measured from `vw` lives in SCREEN pixels and is
   obliged to be divided by the scale. */
.ask-modal{width:min(400px,calc(94vw / var(--ui-scale,1)))}
.ask-t{font:600 15px var(--sans);margin-bottom:7px}
.ask-d{font:12.8px/1.6 var(--sans);color:var(--tx-2);margin-bottom:15px;white-space:pre-line}
/* The answer line (studioPrompt). The 16px is not for beauty: a smaller type size in an input
   field makes Safari on a phone ZOOM the page on focus — and the question window runs off the edge. */
.ask-in{width:100%;box-sizing:border-box;margin:0 0 15px;padding:10px 12px;
  font:16px/1.4 var(--sans);color:var(--tx-1);background:var(--bg-2);
  border:1px solid var(--line);border-radius:9px;outline:none}
.ask-in:focus{border-color:var(--acc);box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 22%,transparent)}
/* Three buttons in one row do not fit on a phone, and wrapping to a line is safe here: the
   question sheet holds nothing underneath it. The extra paths (studioPick) are in the middle. */
.ask-btns{display:flex;flex-wrap:wrap;gap:9px;justify-content:flex-end}
.ask-more{display:contents}
/* A button with a HEADING (studioPick + lead): two words on top, which is what the choice is made
   by, and a caption with the task's name and project below. Aligned to the left edge — the
   heading and the caption are of different lengths, and centred they «dance». */
.btn-lead{display:flex;flex-direction:column;align-items:flex-start;gap:2px;
  text-align:left;padding:8px 13px;line-height:1.3}
.ask-lead{font:800 12.5px var(--sans);letter-spacing:.7px;text-transform:uppercase}
.ask-sub{font:500 12.5px var(--sans);opacity:.82}

/* ── picking a project ── */
#picker{display:none}
#picker.open{display:grid}
.picker-modal{width:min(560px,calc(96vw / var(--ui-scale,1)))}
.picker-list{display:flex;flex-direction:column;gap:3px;margin-top:11px;max-height:52vh;overflow-y:auto}
.pick-row{display:flex;align-items:baseline;gap:9px;width:100%;text-align:left;background:none;
  border:1px solid transparent;border-radius:8px;padding:7px 9px;cursor:pointer;color:var(--tx)}
.pick-row:hover{background:var(--bg-3);border-color:var(--line-2)}
.pick-n{font:600 12.8px var(--sans);flex:none}
.pick-p{font:10.5px var(--mono);color:var(--tx-3);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pick-tag{font:10px var(--sans);color:var(--tx-3);border:1px solid var(--line-2);border-radius:99px;padding:1px 7px;flex:none}
.pick-tag.on{color:var(--acc);border-color:var(--acc)}
.pick-row.was-hidden .pick-n{color:var(--tx-3)}
/* A container folder is a shelf and not a project: the same row, but paler and with no weight,
   otherwise `BusinessApps` reads on a par with the projects inside it. */
.pick-row.pick-dir .pick-n{font-weight:500;color:var(--tx-2)}
.pick-row.pick-dir .pick-mark{color:var(--tx-3)}
.pick-tri{flex:none;width:15px;background:none;border:0;padding:0;color:var(--tx-3);cursor:pointer;
  font:11px var(--sans);line-height:1;text-align:left}
.pick-tri:hover{color:var(--acc)}
.pick-tri.hollow{cursor:default}
.pick-when{font:10.5px var(--mono);color:var(--tx-3);flex:none;min-width:26px;text-align:right}
/* The chips: «what is not in the rail yet» is this window's working question, so it comes first
   and is open by default; the counter next to the name, so as not to open a drawer for emptiness. */
.pick-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
.pick-chip{display:inline-flex;align-items:center;gap:6px;background:none;border:1px solid var(--line-2);
  border-radius:99px;color:var(--tx-2);font:11.5px var(--sans);padding:3px 10px;cursor:pointer}
.pick-chip:hover{border-color:var(--tx-3);color:var(--tx)}
.pick-chip.on{background:var(--acc);border-color:var(--acc);color:var(--on-acc)}
.pick-chip-n{font:10.5px var(--mono);opacity:.72}
.picker-top{display:flex;gap:8px;align-items:center}
.picker-top .tree-search{flex:1;min-width:0}
.pick-new-btn{flex:none;white-space:nowrap;background:var(--acc);color:var(--on-acc);border:0;
  border-radius:8px;font:600 12px var(--sans);padding:8px 12px;cursor:pointer}
.picker-hint{margin-top:9px;color:var(--tx-3);font:10.5px var(--sans)}
.pick-mark{color:var(--acc)}
.pick-add{flex:none;background:none;border:1px solid var(--line-2);border-radius:7px;color:var(--tx-3);
  font:12px/1 var(--sans);padding:4px 7px;cursor:pointer}
.pick-add:hover{color:var(--acc);border-color:var(--acc)}
.pick-new{display:flex;flex-wrap:wrap;gap:7px;align-items:center;background:var(--bg-3);
  border:1px solid var(--acc);border-radius:9px;padding:9px 10px;margin:2px 0}
.pick-new-t{width:100%;font:11.5px var(--sans);color:var(--tx-2)}
.pick-new-in{flex:1;min-width:150px;background:var(--bg);border:1px solid var(--line-2);border-radius:7px;
  color:var(--tx);font:12.5px var(--sans);padding:6px 8px}
.pick-new-go{flex:none;background:var(--acc);color:var(--on-acc);border:0;border-radius:7px;
  font:600 12px var(--sans);padding:7px 12px;cursor:pointer}
.pick-new-err{width:100%;color:var(--err);font:11px var(--sans)}
.pick-new-no{flex:none;background:none;border:1px solid var(--line-2);border-radius:7px;color:var(--tx-2);
  font:12px var(--sans);padding:7px 12px;cursor:pointer}
.pick-new-no:hover{border-color:var(--tx-3);color:var(--tx)}
#picker.pick-create .pick-add{border-color:var(--acc);color:var(--acc)}
#picker.pick-create .picker-hint{color:var(--acc)}

/* ── the spend: first what is left, then where it went ── */
.stat-project{background:var(--bg-3);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:12px}
.sp-name{font:600 14px var(--sans)}
.sp-path{font:10.5px var(--mono);color:var(--tx-3);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sp-line{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;font:11.5px var(--sans);color:var(--tx-2)}
.sp-warm{color:var(--acc)}
.left-card{display:flex;flex-direction:column;gap:13px;background:var(--bg-3);border:1px solid var(--line-2);
  border-radius:11px;padding:12px 13px;margin-bottom:6px}
.left-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.left-name{font:600 12.5px var(--sans);color:var(--tx-2)}
.left-rest{font:600 22px/1 var(--sans);font-variant-numeric:tabular-nums;letter-spacing:-.6px;color:var(--tx)}
.left-track{height:9px;background:var(--bg);border:1px solid var(--line);border-radius:5px;overflow:hidden;margin-top:6px}
.left-track i{display:block;height:100%;background:var(--ok);border-radius:5px;transition:width .4s}
.left-track i.warm{background:var(--warn)}
.left-track i.hot{background:var(--err)}
.left-note{font:11px var(--mono);color:var(--tx-3);margin-top:5px}
/* ═══ A PROJECT AND ITS TASKS ARE TWO TIERS OF ONE LIST ═══
   «Not only the tasks, but broken down by projects: the project — how much it took in all, and
   which tasks are inside it» (the owner, 27.08). A tier is read by three things at once — the
   type size of the name, the type size of the percent and the thickness of the bar — because an
   indent alone is not enough: a list of twenty rows of the same weight becomes flat again. A task
   holds on to a vertical line: on a narrow screen an indent with no line is lost in the very
   first long name. */
.left-row.proj + .left-row.proj,.left-row.kid + .left-row.proj{margin-top:4px;padding-top:12px;
  border-top:1px solid var(--line)}
.left-row.proj .left-name{font-size:13.5px;color:var(--tx)}
.left-row.proj .left-track{height:11px}
/* THE LITTLE PLUS ON A PROJECT IS FOR TAKING ITS TASKS OUT OF SIGHT (28.08): «so that it has a
   little plus, so that it can be folded, and looking through the other tasks is easier and
   faster». The button stands BEFORE the name and holds its width even when folded: the column of
   names must not travel left and right. */
.left-fold{flex:0 0 auto;width:20px;height:20px;padding:0;line-height:18px;border-radius:6px;
  border:1px solid var(--line-2);background:var(--bg-2);color:var(--tx-3);cursor:pointer;
  font:600 13px/18px var(--mono);text-align:center;align-self:center}
.left-fold:hover{color:var(--tx);border-color:var(--tx-3)}
/* ⚠ A THIRD CHILD IN `space-between` PUSHES THE NAME INTO THE MIDDLE: with the button on the left
   and the percent on the right the project's name moved to the centre of the row and stopped
   holding the column. The name takes the whole remainder — then it stands flush against the plus,
   as before. */
.left-row.proj > .left-head{cursor:pointer;gap:8px}
.left-row.proj > .left-head .left-name{flex:1 1 auto;text-align:left}
.left-row.proj > .left-head:hover .left-name{color:var(--tx)}
.left-row.kid{margin-left:10px;padding-left:10px;border-left:2px solid var(--line-2)}
.left-row.kid .left-name{font-weight:500;color:var(--tx-2)}
.left-row.kid .left-rest{font-size:15px;color:var(--tx-2)}
.left-row.kid .left-track{height:6px;margin-top:5px}
/* The sum is SEARCHED for by eye down the list, so it has its own colour, not the grey of the enumeration. */
.left-aud{color:var(--ok);font-weight:600;font-variant-numeric:tabular-nums}
/* «Expand all» under the week's list: a finger must hit it on a phone too — 40px. */
.left-more{display:block;width:100%;min-height:40px;margin:0 0 10px;padding:9px 12px;
  background:var(--bg-3);border:1px dashed var(--line-2);border-radius:10px;
  font:600 11.5px var(--sans);color:var(--tx-3);cursor:pointer;text-align:center}
.left-more:hover{color:var(--tx-2);border-color:var(--tx-3)}

/* ═══ the clean feed: only question → answer ═══ */
.stream.qa-only .think,
.stream.qa-only .tool,
.stream.qa-only .notice,
.stream.qa-only .result,
.stream.qa-only .err-box,
/* A MESSAGE FROM ANOTHER SESSION IS NEITHER THE QUESTION NOR THE REPLY (the owner, 03.09, arrow
   drawn straight at the ticked box). «🙈 no thoughts or actions — keep the question and replies» is
   a promise about WHOSE words stay, and a peer's are a third party's: FLEET announcing itself into a
   UI task is exactly the traffic this checkbox exists to get out of the way. A background agent's
   card is deliberately NOT hidden here — that one is this row's own work, launched by this task,
   and its report is part of the answer. Hiding is by CLASS on the container, so it applies to what
   is already drawn as well as to what arrives next; nothing is decided at insert time. */
.stream.qa-only .agentcard[data-via="peer"]{display:none}
.stream.qa-only .ctx-cut{display:flex}
/* ⛔ A DOOR IS NOT «AN ACTION», AND A FAILURE IS NOT A THOUGHT (11.09).
   `.notice` is two different things wearing one class: a housekeeping line (which this mode exists
   to get out of the way) AND the only controls the feed has — the door to the earlier sessions
   («session 6 continues above · 4 earlier · load them · ↥ session 1 of 12»), the way back from a
   search hit, and the line that says a read failed. Hiding the lot meant that in the clean feed the
   task simply ENDED at the top of the last chunk: the strip said «session 6 of 11 · shown from the
   middle» and above it stood nothing at all — no door, no «⟳ loading», no «the task starts here».
   That is the screen the owner photographed on 11.09 («почему я не вижу историю выше… мы же
   говорили, что он будет бесконечно листать вверх»). The session bar is already exempt right above
   for the same reason; navigation and bad news are not «thoughts or actions». */
.stream.qa-only .notice.hist-top,
.stream.qa-only .notice.hist-back,
.stream.qa-only .notice.n-warn,
.stream.qa-only .notice.n-warning,
.stream.qa-only .notice.n-error{display:block}
.stream.qa-only .msg-assistant{border-bottom:1px solid var(--line);padding-bottom:12px}

/* ═══ the queue: what was written during a turn ═══ */
/* «This text will travel into ANOTHER row» — the turn's owner (stampOwn in app.js). The strip
   appears only when the screen has moved away from the row the writing was started in, so in
   ordinary work it takes no room at all. */
.own-strip{display:flex;align-items:center;gap:6px;max-width:var(--read-w);margin:0 auto 8px}
.own-go{flex:1;min-width:0;text-align:left;background:var(--bg-3);border:1px solid var(--blue);
  border-left:3px solid var(--blue);border-radius:9px;padding:5px 8px;color:var(--blue);
  font-size:12px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.own-go:hover{border-color:var(--blue-2)}
.own-x{background:none;border:none;color:var(--tx-3);cursor:pointer;font-size:12px;padding:0 4px;flex:none}
.own-x:hover{color:var(--err)}
.queue-strip{display:flex;flex-direction:column;gap:5px;max-width:var(--read-w);margin:0 auto 8px}
.q-chip{display:flex;align-items:center;gap:8px;background:var(--bg-3);border:1px dashed var(--line-2);
  border-left:2px solid var(--blue);border-radius:9px;padding:5px 8px;cursor:pointer;font-size:12px;color:var(--tx-2)}
.q-chip:hover{border-color:var(--blue-2)}
.q-n{font:600 10px var(--mono);color:var(--blue);flex:none;min-width:13px}
.q-t{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.q-thumb{width:20px;height:20px;border-radius:3px;object-fit:cover;flex:none;border:1px solid var(--line-2);background:var(--bg-2)}
.q-att{font:600 10px var(--mono);color:var(--tx-3);flex:none;white-space:nowrap}
.q-x{background:none;border:none;color:var(--tx-3);cursor:pointer;font-size:12px;flex:none;padding:0 2px}
.q-x:hover{color:var(--err)}
/* What was said, already sent and still being transcribed. There is no cursor — there is nothing
   to touch, only seconds to wait; the pulse says «work is going on» and not «it has hung». */
.q-chip.pend{cursor:default;border-left-color:var(--warn,#c08a2e);animation:pend-pulse 1.6s ease-in-out infinite}
.q-chip.pend .q-n{color:var(--warn,#c08a2e)}
/* A turn held back for ANOTHER row: it is alive, it is waiting, but it is not what is being
   written here and now. Muted, the cursor ordinary — there is nothing to touch and something to read. */
/* HELD SINCE YESTERDAY — not «in the queue» but «waiting for a decision»: an amber border, the
   date next to the text and a send button of its own. It will no longer travel anywhere by itself
   (app.js → QUEUE_STALE_MS). */
.q-chip.held{border-style:solid;border-color:var(--warn,#c08a2e);background:color-mix(in srgb,var(--warn,#c08a2e) 8%,var(--bg-3))}
.q-chip.held .q-n{color:var(--warn,#c08a2e)}
.q-when{font:600 10px var(--mono);color:var(--warn,#c08a2e);flex:none;white-space:nowrap;opacity:.9}
.q-go{background:none;border:none;color:var(--blue);cursor:pointer;font-size:13px;flex:none;padding:0 2px;line-height:1}
.q-go:hover{color:var(--acc)}
.q-chip.away{opacity:.72;cursor:default;border-style:dotted}
.q-chip.away:hover{border-color:var(--line-2)}
.q-chip.away .q-n{color:var(--fg-3,#8b8b8b)}
/* WHAT PULSES IS WHAT IS NOT THERE YET, NOT WHAT IS ALREADY WRITTEN. The WHOLE bubble was italic
   and semi-transparent — together with the text the person typed by hand: his own words looked
   like a ghost draft at exactly the moment he is checking whether they have been eaten. What is
   his own is set in ordinary text; only the last line is waiting. */
.pend-said{white-space:pre-wrap}
.pend-att{margin-top:5px;font-size:.86em;opacity:.82;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pend-wait{margin-top:5px;opacity:.72;font-style:italic;animation:pend-pulse 1.6s ease-in-out infinite}
@keyframes pend-pulse{0%,100%{opacity:.55}50%{opacity:1}}
/* The button is still «send», but it will not leave at once — the dashed line says so without words. */
.send-btn.queueing{border-style:dashed;border-color:var(--on-blue)}

/* ── the verdict on the next turn ──
   It stands above the input field because it is read BEFORE sending: «it will continue this
   session» or «it will start a new one and why». A solid strip on the left means a continuation
   (cheap), a blue one means a new session. The colour here is not decoration: it is the only
   thing visible with peripheral vision. */
.turn-plan{display:block;width:100%;max-width:var(--read-w);margin:0 auto 8px;text-align:left;
  background:var(--bg-3);border:1px solid var(--line-2);border-left:2px solid var(--acc);
  border-radius:9px;padding:6px 30px 6px 10px;font:11.5px/1.45 var(--sans);color:var(--tx-2);cursor:pointer;
  position:relative;
  /* A NARROW STRIP THAT UNFOLDS ON A TAP.
     Three lines of permanent text are three lines of screen taken away from the conversation
     for the sake of a hint that is read once. One line with an ellipsis answers «what comes
     next» briefly; you poke it and it becomes tall, the WHOLE text and the action button are
     visible; you poke it back and it is a strip again. It may not be cut off for good: then
     «why» could never be learned at all. */
  overflow:hidden;white-space:normal}
.turn-plan.open{color:var(--tx);padding:8px 30px 8px 11px}
/* ⛔ THE CLAMP BELONGS TO THE TEXT, NOT TO THE BUTTON — AND THAT IS WHY THE STRIP TORE.
   `-webkit-line-clamp` counts the lines of the box's OWN inline content, and `.plan-txt` is a
   `display:block` child, so the clamp on `.turn-plan` clipped nothing. The button kept the height
   of one line, the second line was drawn into the bottom padding, and `overflow:hidden` cut it
   through the middle of the letters: on the phone the strip read as a torn line hanging out
   past its own rounded border (his screenshot, 09.09 — measured 56px of content in a 29px box,
   at EVERY width, 430 as much as 374). Clamping the text element itself is the whole cure.
   Specificity, not order, is what makes it hold: `.turn-plan:not(.open) .plan-txt` (0,3,0) beats
   the flat `.plan-txt{display:block}` below wherever it stands in the file — the same mine that
   lit the cog on the phone and the burger at the desk. */
.turn-plan:not(.open) .plan-txt{display:-webkit-box;-webkit-box-orient:vertical;
  -webkit-line-clamp:1;line-clamp:1;overflow:hidden}
/* THE CROSS IS THE SWIPE FOR THOSE WHO HAVE NO FINGER.
   As an absolute, and the reason survived the clamp moving off this element (09.09): folded, the
   strip is one line tall because `.plan-txt` is clamped to one line, and any sibling in the flow
   would add its own height on top of that line — pushing the text out exactly as it would have
   joined the line count before. */
.plan-x{position:absolute;top:1px;right:2px;width:24px;height:24px;display:grid;place-items:center;
  border:0;background:none;color:var(--tx-2);font:12px/1 var(--sans);cursor:pointer;
  border-radius:7px;opacity:.5;padding:0}
.plan-x:hover{opacity:1;background:var(--bg-4);color:var(--tx)}
.plan-txt{display:block}
/* The action lives INSIDE the unfolded chip and not on its backing: while the strip is folded a
   tap on it unfolds it, and starting a new session by accident with a finger is no longer possible. */
.plan-go{display:none;margin-top:7px;padding:5px 10px;border:1px solid var(--line-2);
  border-radius:8px;background:var(--bg-4);color:var(--tx);font:11.5px var(--sans);cursor:pointer}
.turn-plan.open .plan-go{display:inline-block}
.plan-go:hover{border-color:var(--acc);color:var(--acc)}
.turn-plan:hover{border-color:var(--acc);color:var(--tx)}
.turn-plan.plan-new,.turn-plan.plan-rotate{border-left-color:var(--blue);cursor:default}
/* The window is already large: continuing is still cheap for THE SAME subject, but starting a new
   subject that way means paying for somebody else's context. Not a ban but a warning — the owner
   decides. */
.turn-plan.heavy{border-left-color:var(--warn,var(--err));color:var(--tx)}
.turn-plan.hidden{display:none}

/* ═══ picking the look ═══ */
/* A tile is its own preview: its colours are taken from the theme it offers, so the choice can be
   made with the eyes and without reading the names. */
.theme-swatches{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;margin-bottom:12px}
.sw{position:relative;border:1px solid var(--line-2);border-radius:10px;padding:8px 9px 7px;cursor:pointer;
  text-align:left;overflow:hidden;transition:.15s;font:inherit}
.sw:hover{border-color:var(--acc-2)}
.sw.on{border-color:var(--acc);box-shadow:0 0 0 1px var(--acc)}
.sw-name{font:600 11.5px var(--sans);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sw-when{font:9.5px var(--mono);opacity:.7;margin-top:1px}
.sw-dots{display:flex;gap:4px;margin-top:7px}
.sw-dots i{width:10px;height:10px;border-radius:50%;display:block}
.sw-line{height:3px;border-radius:2px;margin-top:6px}
.sw.auto{display:flex;flex-direction:column;justify-content:center;background:var(--bg-3);color:var(--tx)}
.sw.auto .sw-when{color:var(--tx-3);opacity:1}
.theme-auto{border-left:2px solid var(--line-2);padding-left:11px;margin-bottom:12px}
#theme-now{font:10.5px var(--mono);text-transform:none;letter-spacing:0}

/* ═══ swipes over the rail (phone) ═══ */
.swipe{position:relative;overflow:hidden;border-radius:7px}
/* The row is obliged to lie ABOVE the backing and to catch the touches itself: the backing is
   absolute while a static row is drawn under it — a tap on the tree would go into emptiness. */
/* ⚠ `background-COLOR`, not the shorthand: the colour of a tab's status lives in
   `background-image` (see «A TASK'S STATUS» above), and the `background:` shorthand resets the
   image to none. Both rules are of one weight (two classes), but this one is LOWER in the file —
   so it is the one that wins, and on the phone the status colours disappeared entirely: «in the
   mobile interface the tabs are not shown by colour, in the desktop one they are» (the owner,
   28.08). The swipe wrapper exists only on a narrow screen — which is why the breakage was
   exactly there. */
.swipe .row{position:relative;z-index:1;background-color:var(--row-bg,var(--bg-2));
  transition:transform .18s cubic-bezier(.2,.8,.2,1)}
/* ⛔ AND A ROW THAT LOST ITS GESTURES KEEPS THE FILL THE WRAPPER USED TO GIVE IT (23.09).
   Task and draft rows have no swipe any more — right to left puts the whole list away now, and
   a task offers nothing leftwards — so they fell out of the rule above and became TRANSPARENT
   on the phone: the rail's own background showed through, and a sticky heading scrolling over
   them would show the rows through itself. Same fallback variable, so `.row.current` and the
   status `background-image` still win exactly as they do inside a wrapper. */
@media (max-width:860px){
  .tree .row{background-color:var(--row-bg,var(--bg-2))}
}
.swipe-under{position:absolute;inset:0;display:flex;align-items:center;opacity:0;pointer-events:none;
  font:600 11.5px var(--sans);color:var(--tx-3);transition:opacity .12s}
.swipe-under.at-left{opacity:1;justify-content:flex-start;padding-left:11px}
.swipe-under.at-right{opacity:1;justify-content:flex-end;padding-right:11px}
.swipe-under.armed{color:var(--acc)}
.swipe-under.at-left.armed{color:var(--ok)}

/* ═══ THE PANEL REFRESHES ITSELF, AND THIS IS ITS ONLY VOICE (owner, 05.09) ═══
   A hairline that runs the top edge while the page leaves, and a fade-in on the page that
   arrives. Both exist for one reason: a blink with no motion around it is indistinguishable
   from a dropped connection, which is what the deleted strip, countdown and feed lines were
   there to explain. Motion explains it without words, and without stealing a click.
   It sits above everything (the done plate lives at 230) and catches nothing — a fixed layer
   whose only job is to be looked at must never be able to be pressed. */
.reload-sweep{position:fixed;left:0;top:0;height:2px;width:0;z-index:400;pointer-events:none;
  border-radius:0 2px 2px 0;background:linear-gradient(90deg,transparent,var(--acc) 60%,var(--blue-2));
  box-shadow:0 0 12px var(--acc);opacity:.92;
  transition:width .44s cubic-bezier(.22,.85,.3,1)}
.reload-sweep.go{width:100%}
html.fresh-in body{animation:fresh-in .24s ease-out}
@keyframes fresh-in{from{opacity:.4}to{opacity:1}}
@media (prefers-reduced-motion:reduce){
  .reload-sweep{transition:none}
  html.fresh-in body{animation:none}
  .stale-tip{transition:none}
}

/* THE ONLY THING LEFT THAT SPEAKS ABOUT AN UPDATE — a line at the top edge, not an object on the
   screen. «Максимум сверху всплывающей строчечкой… и хватит» (owner, 05.09): the pill that used to
   stand over the composer had a reason, a button and a ✕, i.e. it had to be read and dismissed. This
   slides out of the top edge, waits seven seconds and leaves on its own; a tap on it IS the button. */
.stale-tip{position:fixed;left:50%;top:0;transform:translate(-50%,-100%);z-index:401;
  background:var(--bg-4);border:1px solid var(--line-2);border-top:none;border-radius:0 0 11px 11px;
  padding:5px 14px;font-size:12px;color:var(--tx-2);cursor:pointer;white-space:nowrap;
  max-width:calc(92vw / var(--ui-scale,1));overflow:hidden;text-overflow:ellipsis;
  box-shadow:0 8px 22px var(--shadow);opacity:0;
  transition:transform .22s cubic-bezier(.22,.85,.3,1),opacity .22s ease-out}
.stale-tip.go{transform:translate(-50%,0);opacity:1}
.stale-tip:hover{color:var(--tx);border-color:var(--acc)}

/* A way back from the swipe: the gesture is too cheap to be irreversible. */
.undo-bar{position:fixed;left:50%;bottom:calc(var(--composer-h, 84px) + 16px);transform:translateX(-50%);
  z-index:220;display:flex;align-items:center;gap:12px;background:var(--bg-4);
  border:1px solid var(--line-2);border-radius:99px;padding:8px 8px 8px 15px;
  box-shadow:0 10px 30px var(--shadow);font-size:12.5px;color:var(--tx-2);max-width:calc(92vw / var(--ui-scale,1))}
.undo-bar button{background:var(--acc);color:var(--on-acc);border:none;border-radius:99px;
  padding:5px 13px;font:600 12px var(--sans);cursor:pointer}
/* A REFUSAL IS A SENTENCE, AND A PILL IS SHAPED FOR THREE WORDS. The undo strip carries «Undone.»;
   a refusal has to say what to do instead, so on the same pill it wrapped into a squeezed blob with
   the button sitting inside the text. `.say` keeps the position (above the composer, where the hand
   is) and gives the words a rectangle: full width on a phone, the button pinned to the right. */
.undo-bar.say{border-radius:14px;padding:11px 11px 11px 15px;align-items:flex-start;
  max-width:min(560px, calc(92vw / var(--ui-scale,1)));line-height:1.4;color:var(--tx)}
.undo-bar.say span{flex:1;min-width:0}
.undo-bar.say button{flex:none;margin-top:1px}
@media (max-width:860px){
  .undo-bar.say{left:8px;right:8px;transform:none;max-width:none;font-size:13.5px}
}
/* THE WAY BACK FROM HIDING LIVES AT THE BOTTOM OF THE RAIL ITSELF.
   A row is hidden while standing in the task list — and the road back must lie there too: a chip
   over the conversation answered somewhere other than where the eye is looking, and on a phone it
   also shared the screen with the rebuild splash. The strip covers nothing (the list under it is
   alive) and leaves by itself. */
.rail-undo{position:absolute;left:8px;right:8px;bottom:8px;z-index:6;display:flex;align-items:center;gap:8px;
  background:var(--bg-4);border:1px solid var(--line-2);border-radius:12px;padding:6px 6px 6px 11px;
  box-shadow:0 8px 24px var(--shadow);font-size:12px;color:var(--tx-2);animation:live-in .16s}
.rail-undo-tx{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rail-undo-go{background:var(--acc);color:var(--on-acc);border:none;border-radius:99px;
  padding:5px 12px;font:600 12px var(--sans);cursor:pointer}
.rail-undo-x{background:none;border:none;color:var(--tx-3);font:400 13px var(--sans);cursor:pointer;padding:2px 5px}
@media (max-width:860px){
  /* The same law as for the rail's rows: a finger does not hit 12px. */
  .rail-undo{font-size:13.5px;padding:8px 8px 8px 13px;bottom:12px}
  .rail-undo-go{padding:7px 14px;font-size:13px}
}
/* A warning before leaving («recording in progress»). It lives for seconds but it covers the
   bottom of the screen — so clicks pass through it while its own buttons catch them themselves:
   the same law the «done» chip once got burned on. */
.back-bar{pointer-events:none}
.back-bar button{pointer-events:auto}
/* An attachment's fate is also a line on the screen and not a record in a hidden sheet:
   «uploading…», «it did not attach: …», «the drop brought only text/html». Silence here reads as
   a breakage. */
.att-note{padding:8px 15px;border-radius:14px;max-width:min(calc(92vw / var(--ui-scale,1)),520px);line-height:1.4;
  word-break:break-word;text-align:center}

/* ── «task finished» ──
   A voice can be missed and a notification can be forbidden; the strip always works and, unlike
   the beacon in the rail, says WHERE to go itself. It lives half a minute: it may not be seen
   straight away. */
/* THE CHIP IS NEWS, NOT A DOOR: CLICKS PASS STRAIGHT THROUGH IT.
   z-index:230 holds it above the settings sheet (120) and the modals (150) — that is by design,
   «done» must be visible over everything. But a solid pill also CAUGHT the clicks, and for a
   minute and a half (doneHere lives 90 s) the button under it silently would not press: the turn
   finished while you were in the settings — and the sheet's tab stopped answering. That is
   exactly what npm run ui caught: «<div class="done-toast"> intercepts pointer events».
   It is the BACKING that stays transparent; its own buttons take pointer-events back below. */
.done-toast{pointer-events:none;position:fixed;left:50%;bottom:calc(var(--composer-h, 84px) + 70px);transform:translateX(-50%);
  max-width:calc((100vw - 24px) / var(--ui-scale,1));flex-wrap:wrap;justify-content:center;
  z-index:230;display:flex;align-items:center;gap:10px;background:var(--bg-4);
  border:1px solid var(--ok);border-radius:99px;padding:8px 8px 8px 14px;
  box-shadow:0 10px 30px var(--shadow);font-size:12.5px;color:var(--tx);max-width:calc(92vw / var(--ui-scale,1))}
.done-toast.bad{border-color:var(--err)}
/* «Asking» is not an error, but it is not «done» either: the same red as the «?» in the rail, so
   that the strip and the row speak about the same thing in one colour. */
.done-toast.ask{border-color:var(--err)}
.done-toast .done-ico{color:var(--ok);font-weight:700;flex:none}
.done-toast.bad .done-ico,.done-toast.ask .done-ico{color:var(--err)}
.done-toast .done-t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.done-toast button{pointer-events:auto;background:var(--ok);color:var(--on-ok);border:none;border-radius:99px;
  padding:5px 13px;font:600 12px var(--sans);cursor:pointer;flex:none}
.done-toast.bad button{background:var(--err);color:var(--on-err)}
.done-toast .done-x{background:none;color:var(--tx-3);padding:5px 9px;font-size:13px}

/* The phone: the attachment tile is smaller but still a picture and not an icon — three or four in a row. */
@media (max-width:860px){
  .att{width:98px;height:76px}
  .att .att-x{opacity:1}
  .att .att-ico{font-size:24px;margin-bottom:12px}
  /* On a phone the composer is taller (a three-line field, the chips above it) — a hard 96px laid
     the strip right on top of the input field. We count from the composer's live height, as the
     floating buttons do. */
  .done-toast{bottom:calc(var(--composer-h, 96px) + 70px)}
}

/* ═══════════════════════════════════════════════════════════════════════════
   TEXT SIZE BY PINCH · THE GEOMETRY OF OVERLAYS · SEARCH BY BUTTON · THE VOICE ASSISTANT
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── two different scales, and they are not the same thing ──
   --ui-scale moves the WHOLE interface (the buttons, the rail, the header), --read-scale only the
   text of the conversation. A two-finger pinch over the feed means «let me read it larger» and
   not «blow my buttons up»: the browser's page zoom did precisely the second. */
:root{--read-scale:1}
.stream .wrap{zoom:var(--read-scale)}
/* ── and a THIRD scale, for the list of tasks alone (the owner's brother, 24.09) ──
   «Когда на мобильном телефоне открываем панель проектов, он просит, чтобы можно было пальцами
   увеличивать или уменьшать всю эту панель… именно только левой панели с задачами. А масштаб
   ленты нашей оставался своим масштабом.» Three scales, three gestures, three different elements,
   and none of them argues with the others: --ui-scale is the whole interface, --read-scale the
   conversation, --input-scale the field being dictated into, and this one the rail. It is put on
   the SCROLLER and on the search box, never on `.sidebar` itself: the drawer's own width is what
   the swipe measures its per cent against (`rail.move`), and a header whose buttons grow would
   push them off the edge of a phone. */
:root{--rail-scale:1}
.sidebar .tree,.sidebar .tree-search-box{zoom:var(--rail-scale)}
/* The pinch belongs to US: pan-y leaves the feed its scroll and takes the zoom from the browser. */
.stream{touch-action:pan-y}
/* …but code keeps its own horizontal scroll: otherwise a long line becomes unreadable. */
.md pre,.code-pre,.tool-body pre,.md table,.md .tbl-wrap{touch-action:pan-x pan-y}

/* A pinch over the interface NO LONGER zooms the page: our own enlargement of the text lives in
   the feed (--read-scale), while the browser's zoom broke the fixed rail (the letters were
   clipped by height) and made the bottom panel appear and disappear by turns. */
.app,.sidebar,.sheet,.modal-back,.topbar,.composer{touch-action:pan-x pan-y}
.read-chip{position:fixed;left:50%;top:calc(70px / var(--ui-scale));transform:translateX(-50%);
  z-index:215;display:flex;align-items:center;gap:10px;background:var(--bg-4);
  border:1px solid var(--line-2);border-radius:99px;padding:6px 8px 6px 14px;
  box-shadow:0 10px 30px var(--shadow);font:12.5px var(--sans);color:var(--tx-2);
  opacity:0;transition:opacity .15s;pointer-events:none}
.read-chip.on{opacity:1;pointer-events:auto}
.read-chip button{background:var(--bg-3);border:1px solid var(--line-2);color:var(--tx-2);
  border-radius:99px;padding:3px 11px;font:12px var(--sans);cursor:pointer}
.read-chip button:hover{border-color:var(--acc);color:var(--acc)}

/* ── AN OVERLAY HAS NO RIGHT TO RUN OFF THE SCREEN ──
   With zoom on :root, `inset:0` gives a box --ui-scale times LARGER than the screen: at 150% the
   settings sheet travels down together with the only slider that turns the scale back down, and
   the phone is left with no way out. So the viewport's measures are divided back — as in .app. */
.sheet-back,.modal-back,.drop-veil{inset:0 auto auto 0;
  width:calc(100vw / var(--ui-scale));height:calc(100svh / var(--ui-scale))}
.sheet{max-height:calc(86svh / var(--ui-scale))}
@media(max-width:860px){.sheet{max-height:calc(82svh / var(--ui-scale))}}
/* The scale slider sticks to the top of the sheet: it is also the way back out, so it is obliged
   to be on screen in any state of the scroll. */
.sheet-row.scale-row{position:sticky;top:0;z-index:3;background:var(--bg-2);
  padding:9px 0;margin:-9px 0 0;border-bottom:1px solid var(--line)}
#fs-val.nofit{color:var(--err)}

/* ── the search: a button instead of an always-open field ── */
.mini-btn.on{color:var(--acc)}
#tree-search{margin:0;padding-right:26px}
.tree-search.hidden{display:none!important}
/* The field and the cross are one row: the border belongs to the field and the button stands over
   it as an absolute, otherwise the search would take a second row of the rail away for the sake
   of twenty pixels. The native `type=search` cross is switched off — two identical signs in a row
   read as a breakage. */
.tree-search-box{position:relative;display:flex;align-items:center;margin:0 0 7px}
.tree-search-box.hidden{display:none!important}
.tree-search::-webkit-search-cancel-button{display:none}
.tree-search-x{position:absolute;right:3px;top:50%;transform:translateY(-50%);
  width:22px;height:22px;display:flex;align-items:center;justify-content:center;
  background:none;border:0;border-radius:6px;padding:0;cursor:pointer;
  color:var(--tx-3);font:12px var(--sans);line-height:1}
.tree-search-x:hover{background:var(--bg-2);color:var(--tx-1)}
/* The three chips under the field: names / content / everywhere. Same shape as the project
   window's `.pick-chip`, one size down because the rail is the narrowest column in the panel, and
   they WRAP rather than clip — a rail can be dragged narrower than any label can save, and a
   button pushed off the edge is a door that does not exist. */
.sm-chips{display:flex;flex-wrap:wrap;gap:5px;margin:-3px 0 7px}
.sm-chips.hidden{display:none!important}
.sm-chip{background:none;border:1px solid var(--line-2);border-radius:99px;
  color:var(--tx-3);font:11px var(--sans);padding:3px 9px;line-height:1.35;cursor:pointer}
.sm-chip:hover{border-color:var(--tx-3);color:var(--tx)}
.sm-chip.on{background:var(--acc);border-color:var(--acc);color:var(--on-acc)}
/* THE SPAN ROW SAYS WHAT ITS NUMBERS ARE. «1d · 2d · 7d» on their own is a row of bare numbers,
   and a bare number in this rail has been tried and removed once already (the «7d» period chip,
   27.08): standing naked it could not say which span it meant. The caption costs ~34px once,
   where spelling «2 days» on every chip costs that much six times and wraps the row onto a
   second line. It is not a button — no border, no press, and `align-self:center` so the word
   sits on the chips' own line rather than on the taller box's top edge. */
.sm-chips .sm-cap{font:10.5px var(--sans);color:var(--tx-3);align-self:center;margin-right:1px;
  flex:none;white-space:nowrap}
/* A hit found in a HIDDEN project: the row is there, but it is grey and it says why. */
.row.row-off .row-name{color:var(--tx-3)}
.row-tag{font:9.5px var(--sans);color:var(--tx-3);border:1px solid var(--line-2);
  border-radius:99px;padding:0 5px;margin-left:6px;flex:none;white-space:nowrap}

/* ═══ the voice assistant ═══
   The panel from below is the same genre as the sheets: it is looked into rarely (people talk,
   they do not read), but when they do look, the whole conversation is needed and one large button. */
/* THE BACKING CATCHES NOTHING, AND THAT IS THIS PANEL'S MAIN PROPERTY.
   A dimming with `pointer-events:auto` is a modal, that is, «until you have finished talking, do
   nothing else». A conversation with the assistant is background by its nature: you ask «what has
   finished» and, while it answers, you scroll the feed and open rows. So there is no veil at all
   any more, the layer is transparent to clicks, and clicks past the sheet reach the panel as
   usual (the sheet hides itself then — see `hidePanel` in voice-agent.js) and the conversation
   goes on.
   The height is HALF THE SCREEN: a full screen hid behind it precisely what is being asked about. */
.voice-back{position:fixed;inset:0 auto auto 0;width:calc(100vw / var(--ui-scale));
  height:calc(100svh / var(--ui-scale));z-index:130;
  display:flex;align-items:flex-end;justify-content:center;opacity:0;pointer-events:none;transition:opacity .18s}
.voice-back.open{opacity:1}
.voice-panel{width:min(560px,100%);height:calc(52svh / var(--ui-scale));min-height:260px;
  max-height:calc(72svh / var(--ui-scale));background:var(--bg-2);
  border:1px solid var(--line-2);border-radius:16px 16px 0 0;display:flex;flex-direction:column;
  box-shadow:0 -20px 60px var(--shadow);pointer-events:none;
  transform:translateY(14px);transition:transform .18s}
/* ONLY the sheet itself catches the clicks — the backing around it stays transparent. */
.voice-back.open .voice-panel{pointer-events:auto;transform:none}
.voice-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--line)}
.voice-title{font:600 14px var(--sans);flex:none}
.voice-state{flex:1;font:11.5px var(--mono);color:var(--tx-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.voice-state.live{color:var(--ok)}
.voice-state.talk{color:var(--acc)}
/* The diagnostics strip. A phone is a device with no console: «can it hear me?» must be readable
   at arm's length, so the microphone's level is shown as a live bar and not in words. */
.voice-diag{display:flex;align-items:center;gap:9px;padding:7px 14px;border-bottom:1px solid var(--line)}
.v-ears{flex:none;font:11px var(--mono);color:var(--tx-3);white-space:nowrap}
.voice-lvl{flex:1;height:6px;min-width:40px;border-radius:3px;background:var(--bg-3);
  border:1px solid var(--line);overflow:hidden}
.voice-lvl i{display:block;height:100%;width:2%;background:var(--ok);transition:width .07s linear}
.v-test{flex:none;font:11px var(--sans);color:var(--tx-2);background:var(--bg-3);
  border:1px solid var(--line-2);border-radius:7px;padding:4px 10px;cursor:pointer}
.v-test:active{background:var(--bg-2)}
.v-line.warn{color:var(--warn, var(--acc))}
.v-line.warn .v-who{color:var(--warn, var(--acc))}
.voice-log{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:7px;min-height:120px}
.v-line{font-size:13px;line-height:1.5;color:var(--tx-2);display:flex;gap:8px}
/* The width is fixed so that the lines stand in one column — and `overflow:hidden` keeps a word
   that did not fit INSIDE its own column rather than over the neighbouring one. */
.v-line .v-who{flex:none;font:600 10.5px var(--mono);letter-spacing:.6px;text-transform:uppercase;
  color:var(--tx-3);padding-top:3px;width:26px;overflow:hidden;white-space:nowrap}
.v-line.me{color:var(--tx)}
.v-line.me .v-who{color:var(--blue)}
.v-line.it .v-who{color:var(--acc)}
.voice-buf{margin:0 14px 8px;padding:8px 11px;background:var(--bg-3);border:1px dashed var(--line-2);
  border-left:2px solid var(--blue);border-radius:9px;font-size:12.5px;color:var(--tx-2);max-height:120px;overflow:auto}
#voice-cancel{margin-left:auto}
.voice-foot{display:flex;align-items:center;gap:11px;padding:8px 14px calc(10px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line);flex-wrap:wrap}
/* THE MICROPHONE IN THE SHEET'S HEAD, THE SIZE OF AN ICON. The fifty-four-pixel pulsing circle at
   the bottom answered the question «is a conversation going on» — that is now answered by the
   button in the PANEL's head, which is visible even when the sheet is hidden. What is left here
   is the switch for the ears. */
.voice-mic{width:30px;height:30px;flex:none;display:flex;align-items:center;justify-content:center;
  border-radius:50%;border:1px solid var(--line-2);background:var(--bg-3);color:var(--tx-2);cursor:pointer;transition:.15s}
.voice-mic .talk-ico{width:16px;height:16px}
.voice-mic.on{background:var(--bg-4);border-color:var(--ok);color:var(--ok)}
.voice-min svg{width:17px;height:17px;display:block}
.voice-wipe{flex:none}
.voice-wipe svg{width:16px;height:16px;display:block}
.voice-wipe:disabled{opacity:.3;cursor:default}
/* THE WRITTEN LIST OF COMMANDS (03.10) — a fold under the diagnostics strip, shut by default.
   It is capped in height and scrolls: on a phone five groups of four phrases is taller than the
   conversation they are meant to help with, and a list that pushes the feed off the screen is the
   22.08 furniture again under a new name. */
.voice-guide{max-height:38vh;overflow-y:auto;padding:8px 14px 10px;border-bottom:1px solid var(--line);
  background:var(--bg-2)}
.vg-group{margin:6px 0 0}
.vg-title{font:10px var(--sans);letter-spacing:.08em;text-transform:uppercase;color:var(--tx-3);margin:0 0 4px}
.vg-rows{display:flex;flex-wrap:wrap;gap:5px}
.vg-say{font:11.5px var(--sans);color:var(--tx-1);background:var(--bg-3);border:1px solid var(--line);
  border-radius:11px;padding:3px 9px;cursor:pointer;text-align:left}
.vg-say:active{background:var(--bg-1)}
/* A SLOT IS NOT A PHRASE ANYBODY SAYS, so it must not look pressable: «new task in project …»
   needs a name in it, and a tap on the ellipsis could only fail. */
.vg-say.slot{color:var(--tx-3);background:transparent;border-style:dashed;cursor:default}
.v-test.on{background:var(--accent-dim,var(--bg-1));color:var(--tx-1)}
/* The mark of a wipe. It is DRAWN into the log and is not a line of the conversation (clearLog),
   so it is deliberately unlike one: centred, small, no YOU/AI badge beside it. */
.v-note{font:11px var(--mono);color:var(--tx-3);text-align:center;padding:3px 0;opacity:.9}
.voice-hint{flex:1;min-width:150px;font:11px/1.55 var(--sans);color:var(--tx-3)}
.voice-hint b{color:var(--acc)}

/* ── THE PANEL TOUR (tour.js) ────────────────────────────────────────────────────
   A dimming with a «hole» over an element: a 9999px shadow around a small block is cheaper and
   more precise than four strips around a cut-out. The layer is full-screen, so it is divided by
   --ui-scale — with zoom on :root, inset:0 gives a box LARGER than the screen. The colours are
   theme tokens only: the tour is shown on the light theme and on the dark one equally often. */
.tour-back {
  position: fixed; left: 0; top: 0;
  width: calc(100vw / var(--ui-scale, 1));
  height: calc(100dvh / var(--ui-scale, 1));
  z-index: 90; font: 13px/1.5 var(--sans);
}
.tour-hole {
  position: absolute; border-radius: calc(var(--r) + 2px);
  box-shadow: 0 0 0 9999px var(--veil), 0 0 0 2px var(--acc) inset;
  outline: 2px solid var(--acc); outline-offset: -1px;
  transition: left .18s ease, top .18s ease, width .18s ease, height .18s ease;
  pointer-events: none;
}
.tour-card {
  position: absolute; width: min(340px, calc(100vw / var(--ui-scale, 1) - 24px));
  background: var(--bg-2); color: var(--tx);
  border: 1px solid var(--line); border-radius: var(--r);
  box-shadow: var(--shadow); padding: 12px 13px 10px;
}
.tour-t { font-weight: 600; font-size: 14px; margin-bottom: 5px; color: var(--tx); }
.tour-x { color: var(--tx-2); }
.tour-nav { display: flex; align-items: center; gap: 6px; margin-top: 11px; }
.tour-count { color: var(--tx-3); font-size: 11.5px; margin-right: auto; font-variant-numeric: tabular-nums; }
.tour-btn {
  font: inherit; cursor: pointer; padding: 5px 11px; border-radius: 7px;
  border: 1px solid transparent; background: var(--acc); color: var(--on-acc);
}
.tour-btn.ghost { background: transparent; color: var(--tx-2); border-color: var(--line-2); }
.tour-btn:hover { filter: brightness(1.08); }
@media (max-width: 860px) { .tour-card { font-size: 13.5px; } }

/* ═══════════════════════════════════════════════════════════════════════════
   THE MOBILE VERSION: the reading-aloud player, large floating buttons, a narrow header
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the floating buttons: the size of a finger, not the size of a glyph ── */
.read-fab button{width:38px;height:38px;border-radius:50%;font-size:0;
  display:grid;place-items:center;box-shadow:0 10px 26px var(--shadow),0 2px 6px var(--shadow);
  transition:transform .14s,background .14s,border-color .14s}
.read-fab button:active{transform:scale(.94)}
.read-fab svg{width:21px;height:21px;display:block}
/* THE RAIL IS A CURTAIN OVER THE CONVERSATION, and the floating buttons hung over IT: the task
   list is open, and on it are two discs belonging to a feed that cannot be seen right now. We
   hide them while the rail is open — together with the player's bar, which is of the same layer. */
body:has(.sidebar.open) .read-fab,body:has(.sidebar.open) .done-bar{opacity:0;pointer-events:none}
/* THE INSPECTOR IS A COLUMN ON A WIDE SCREEN AND A CURTAIN ON A NARROW ONE — so the floating
   buttons owe it two different answers. On the desktop they step aside by its width (372px, the
   same number `.app:has(.inspector.open)` uses for the grid), because `right:14px` would otherwise
   put 🔊 on top of the inspector's own text. On a phone the inspector covers the whole screen, so
   they go away entirely — exactly as they do for the rail, and for the same reason. */
/* ⚠ ONE RULE FOR BOTH CASES, and the fallback is what says so: in a split field `--win-r` is
   MEASURED (workspace.js → anchor()) and already contains the inspector, because the inspector
   is a grid column to the right of the whole working field and window 1's box therefore stops
   before it. In a single window the variable is unset and the fallback restores the old 372 + 14
   exactly. A flat `right:386px` here would have beaten the split anchor on specificity and put
   the speaker back on the last column. */
@media(min-width:861px){body:has(.inspector.open) .read-fab{right:calc(var(--win-r, 372px) + 14px)}}
@media(max-width:860px){body:has(.inspector.open) .read-fab{opacity:0;pointer-events:none}}
.ico-speak .sp-body{fill:currentColor}
/* The waves stand still while we are silent and run outward while we are reading: movement is
   visible with peripheral vision, whereas «lit / not lit» on a round button is not. */
.ico-speak .sp-waves path{opacity:.28;stroke-width:1.9}
.read-fab button.on .ico-speak .w1{animation:sp-wave 1.5s ease-out infinite}
.read-fab button.on .ico-speak .w2{animation:sp-wave 1.5s ease-out .18s infinite}
.read-fab button.on .ico-speak .w3{animation:sp-wave 1.5s ease-out .36s infinite}
@keyframes sp-wave{0%{opacity:.15;transform:translateX(-1px)}35%{opacity:1;transform:none}100%{opacity:.15;transform:none}}
/* Preparing the sound — the button BREATHES and turns slowly: «I am busy», not «I am playing». */
.read-fab button.prep{animation:sp-prep 1.25s ease-in-out infinite}
@keyframes sp-prep{
  0%{transform:rotate(0) scale(1);box-shadow:0 10px 26px var(--shadow)}
  50%{transform:rotate(11deg) scale(1.07);box-shadow:0 12px 30px var(--acc-soft,var(--shadow))}
  100%{transform:rotate(0) scale(1);box-shadow:0 10px 26px var(--shadow)}}
#fab-down{background:var(--bg-3)}
#fab-down:active{background:var(--acc);color:var(--on-acc)}

/* ── THE READING-ALOUD PLAYER ──
   One bar answers three questions at once: how much there is in all, how much is ready already
   and where I am now. Solid on the left — what has been synthesised, running diagonal stripes on
   the right — what is still being prepared (the piece shrinks to the right and disappears at the
   end), a blue fill — what has been read, the notches — the block boundaries, which are also the
   seek points. */
.tts-bar{flex:none;display:flex;align-items:center;gap:9px;padding:6px 22px;
  background:var(--bg-2);border-bottom:1px solid var(--line);animation:tts-in .18s ease}
@keyframes tts-in{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
.tts-btn{flex:none;width:30px;height:30px;border-radius:50%;border:1px solid var(--acc);
  background:var(--acc);color:var(--on-acc);display:grid;place-items:center;cursor:pointer}
.tts-btn:active{transform:scale(.93)}
.tts-ico{width:0;height:0;margin-left:3px;border-left:10px solid currentColor;
  border-top:6.5px solid transparent;border-bottom:6.5px solid transparent}
.tts-bar.playing .tts-ico{width:10px;height:12px;margin-left:0;border:0;
  background:linear-gradient(currentColor,currentColor) left/3.5px 100% no-repeat,
             linear-gradient(currentColor,currentColor) right/3.5px 100% no-repeat}
.tts-track{position:relative;flex:1;min-width:0;height:28px;border-radius:8px;overflow:hidden;
  background:var(--bg-4);border:1px solid var(--line-2);cursor:pointer;touch-action:none;user-select:none}
.tts-ready{position:absolute;left:0;top:0;bottom:0;width:0;background:var(--bg-3)}
/* The stripes run TO THE RIGHT — the same way the reading travels — and they are blue, not the
   theme's colour: «waiting for sound» must not look like «this much has been read». Light: this
   is a background, not a message. */
.tts-prep{position:absolute;top:0;bottom:0;right:0;
  background:repeating-linear-gradient(115deg,var(--blue-2,var(--blue)) 0 9px,transparent 9px 19px);
  background-size:21px 100%;opacity:.3;animation:tts-run .6s linear infinite}
@keyframes tts-run{from{background-position:0 0}to{background-position:21px 0}}
.tts-fill{position:absolute;left:0;top:0;bottom:0;width:0;background:var(--acc);opacity:.24}
.tts-ticks{position:absolute;inset:0;pointer-events:none}
.tts-ticks i{position:absolute;top:4px;bottom:4px;width:1px;background:var(--tx-3);opacity:.5}
/* The figures on this bar are read ON THE MOVE, over a running background: they need type size
   and weight, not a small monospaced whisper. On the left is the total, on the right what is left. */
.tts-cap{position:absolute;inset:0;display:flex;align-items:center;gap:8px;padding:0 66px 0 11px;
  font:11px var(--sans);color:var(--tx-2);pointer-events:none;white-space:nowrap;overflow:hidden}
.tts-cap b,.tts-total{font:700 13.5px/1 var(--mono);color:var(--tx);letter-spacing:-.2px}
.tts-kb{font:10.5px var(--mono);color:var(--tx-3)}
.tts-left{position:absolute;right:9px;top:50%;transform:translateY(-50%);
  font:700 13.5px/1 var(--mono);color:var(--tx);letter-spacing:-.2px;pointer-events:none}
.tts-track.drag{border-color:var(--acc)}
/* WE ARE WAITING FOR SOUND THAT IS NOT THERE YET (🔊 was pressed, «what is warmed up» and the
   digest are on their way). The stripes run the whole width: there is nothing to measure out, and
   they say exactly that — preparation is going on. The pause button is dimmed, there is nothing
   to pause; ✕ is in place — the wait can always be cancelled. */
.tts-bar.warmup .tts-prep{left:0!important;right:0}
.tts-bar.warmup .tts-btn{opacity:.4;pointer-events:none}
.tts-bar.warmup .tts-total{font:600 12px var(--sans);color:var(--tx-2);letter-spacing:0}
.tts-x{flex:none;width:26px;height:26px;border:none;background:none;color:var(--tx-3);
  font-size:13px;cursor:pointer;border-radius:7px}
.tts-x:hover{color:var(--err);background:var(--bg-3)}
/* The «reading the digest» mark: it is also the «and now in full» button. It lives only
   while the compressed one is being read — on the full reply it has nothing to say. */
.tts-note{display:none}
.tts-bar.gist .tts-note{flex:none;display:block;border:1px solid var(--line-2);background:var(--bg-3);
  color:var(--tx-2);font-size:11px;line-height:1;padding:6px 9px;border-radius:99px;cursor:pointer;white-space:nowrap}
.tts-bar.gist .tts-note:hover{color:var(--tx);border-color:var(--acc)}

/* ── text size in the input field: its own pinch, separate from the feed ── */
.input{font-size:calc(14px * var(--input-scale, 1))}

/* ── the verdict on the next turn: on a phone it can be swiped away ── */
.turn-plan{transition:transform .18s ease,opacity .18s ease}
.turn-plan.swiping{transition:none}

@media(max-width:860px){
  /* THE HEADER IS ONE AND A HALF TIMES NARROWER.
     It stood on desktop paddings and the desktop type size — and the first thing to go over
     the edge was the very thing it is looked into for: the warmth of the session and the
     right-hand group of buttons. The clock — the most optional thing here — is squeezed into
     two storeys, the key percent stopped being a heading. */
  .topbar{padding:4px 10px;gap:8px}
  .chat-title{font-size:12.5px;line-height:1.2}
  .chat-sub{font-size:10px;line-height:1.15}
  .meters{gap:7px}
  .key-pct{font-size:13.5px}
  .key-left{display:none}
  .key-chip .bar,.ctx-chip .bar{width:34px}
  .time-chip{flex-direction:column;align-items:flex-start;gap:0;padding:1px 3px;
    font-size:9.5px;line-height:1.1}
  .time-sep{display:none}
  .time-part{gap:2px}
  .cache{font-size:11.5px;gap:3px}
  .topbar .icon-btn{font-size:15px;padding:2px 3px}
  .topbar .avatar{width:24px;height:24px;font-size:11px}
  .input{font-size:calc(16px * var(--input-scale, 1))}   /* 16px — otherwise iOS zooms the page */

  /* An empty column has no right to hold width: 📎 and ⚙ moved into the menu. */
  .composer-side:empty{display:none}
  /* …and since 20.08 the ↑/↓ history arrows moved there too, and the column became not empty
     but INVISIBLE: `:empty` does not catch such a one (hidden children stay children), so the
     left column ate one more `gap:9px` from the field. Since 25.08 it is hidden by the BASE
     rule — in every window, not only on the phone; the «show the arrows» checkbox lights it
     back on (html.hist-on). */
  /* ⋯ IS ALWAYS LIT, IT DOES NOT APPEAR FROM THE FIELD GROWING. A button of zero height is a
     button that is not there, and behind it live «📎 Attach file» and «⚙ Settings»: there is
     nothing to hunt for them by touch with, in the middle of a dictation. Its room is held by
     min-height, and it still knows how to grow together with the field. On the PHONE it is
     still the ⋯ above the microphone: the room is freed by the grown field, and the finger
     lies exactly there. The cog under «Send» is the wide screen's form. */
  .more-btn{order:0;flex:1 1 auto;min-height:38px;height:auto;width:44px;padding:0;overflow:hidden;
    border:1px solid var(--line-2);border-radius:11px;background:var(--bg-3);color:var(--tx-2);
    font-size:15px;line-height:1;cursor:pointer}
  .more-btn:active{border-color:var(--acc);color:var(--acc)}
  .more-dots{display:block}
  /* ⚠ CLASS AGAINST CLASS IS DECIDED BY THE ORDER IN THE FILE, NOT BY MEANING.
     `.ico{display:inline-block}` stands lower (line ~2631), the specificity of both is (0,1,0)
     — so on the PHONE the cog was lit back on and hung next to the ellipsis: «a button — 3
     dots and a cog, somehow made ugly; make it simply be three dots» (the owner, 27.08). A
     media query adds no specificity, so it is cured only by a selector of two classes (0,2,0).
     The same canon is already written down about `.burger`: a rule that sets `display` on a
     component is obliged to outrank every visibility flag that lies LOWER in the file. */
  .more-btn .more-cog{display:none}

  .tts-bar{padding:5px 12px;gap:7px}
  .tts-track{height:30px}
  .tts-cap{padding:0 62px 0 9px}
  .tts-cap b,.tts-total,.tts-left{font-size:14px}

  /* The header travels up by exactly ITS OWN height — it is no longer 54px, and a hard number
     would have dragged the top of the feed off the screen. mobile.js measures the value the
     same way it measures the composer's height. */
  html.reading .topbar{margin-top:calc(-1 * var(--topbar-h, 54px))}
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE RAIL'S FOOTER, THE SESSION'S WARMTH, THE COLOURED SPEAKER, THE COMPACT SPEND
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the three bars: the size of a finger ──
   The rail on a phone is a drawer, and it is opened dozens of times a session. The button was
   as tall as its glyph (15px and three pixels of padding), i.e. one had to AIM at it. */
/* The task-list button: pressed often, but NOT the widest one in the header. The room for it
   is cut by about a third — what is freed goes to the task's name, which used to break off
   with an ellipsis on the very first word. */
/* ⚠ `display:inline-flex` STOOD HERE — AND THAT WAS THE CAUSE OF ☰ ON THE DESKTOP.
   `.only-mobile{display:none}` stands at line 734, and this rule at 2367: the same
   specificity (one class), so the last one wins. A button marked «phone only» stood all this
   time at the desk next to an already-open rail — «why do we have the three little bars in
   the first window?» (the owner, 25.08). The sizes stay here, and the layout moved to where
   it is decided whether it is visible at all. */
.burger{min-width:30px;height:38px;font-size:20px;line-height:1;border-radius:10px;padding:0;
  align-items:center;justify-content:center}
@media (max-width:860px){.burger{display:inline-flex}}
.burger:active{background:var(--bg-3)}

/* ── the rail's footer: settings, agents, who I am, sign out ──
   Everything that concerns the WHOLE panel, not the current turn. On a narrow screen these
   are their only doors, so the rows here are full-size. */
/* THE FOOTER IS SHOWN WHEN YOU HAVE SCROLLED TO THE BOTTOM.
   A pinned bar of three buttons took a row of the list away in every frame for the sake of
   what is pressed once a day. Now it is at the bottom of the LIST: you reached the end — it
   came out. */
.side-foot{flex:none;border-top:1px solid transparent;padding:0 12px;
  display:flex;align-items:center;gap:7px;background:var(--bg-2);
  max-height:0;opacity:0;pointer-events:none;overflow:hidden;transition:.18s ease}
.side-foot.on{max-height:56px;opacity:1;pointer-events:auto;padding:8px 12px 10px;
  border-top-color:var(--line)}
.foot-row{display:flex;gap:7px;flex:1;min-width:0}
.foot-btn{flex:1;padding:8px 9px;border:1px solid var(--line-2);border-radius:10px;
  background:var(--bg-3);color:var(--tx-2);font:12.5px var(--sans);cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:.15s}
.foot-btn:hover{border-color:var(--acc);color:var(--acc)}
.foot-btn:active{transform:scale(.98)}
/* The account — on the right and as one circle: the name and the way of signing in live in
   the card itself, and in the row they took two thirds of the width for the sake of what is
   known anyway. */
.foot-acct{display:flex;align-items:center;justify-content:center;padding:2px;border-radius:50%;
  border:1px solid var(--line);background:var(--bg-3);cursor:pointer;
  color:var(--tx);font:inherit;transition:.15s;flex:none}
.foot-acct:hover{border-color:var(--acc)}
.avatar.sm{width:26px;height:26px;font-size:12px;flex:none}
.foot-who{flex:1;min-width:0;display:flex;flex-direction:column;line-height:1.25}
.foot-who b{font:600 12.5px var(--sans);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.foot-who i{font:normal 10.5px var(--sans);color:var(--tx-3);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.foot-chev{color:var(--tx-3);font-size:15px;flex:none}
.foot-out{padding:7px 9px;border:1px solid var(--line-2);border-radius:10px;background:none;
  color:var(--tx-3);font:12px var(--sans);cursor:pointer;transition:.15s}
.foot-out:hover{border-color:var(--err);color:var(--err)}

/* ── the session's warmth: one sign instead of dashes ──
   Cooled down — a blue cube and NOTHING ELSE (there is nothing to say, while «--:--» takes
   room and lies about some time or other). Warm — a flame and minutes. Under ten minutes the
   flame blinks: that is the last moment to add to the same session, after it the same
   conversation starts over. */
.cache{display:inline-flex;align-items:center;gap:3px;background:none;border:none;
  padding:1px 3px;border-radius:9px;cursor:pointer;transition:.15s;line-height:1}
.cache:hover{background:var(--bg-3)}
.cache-ico{display:block;width:20px;height:20px;flex:none}
.cache-ico svg{width:100%;height:100%;display:block}
/* NOT `.ico-fire{display:none}`: the neighbouring `.cache-ico svg` is a class PLUS a type,
   i.e. a higher weight, and the flame would burn over the cube always. A selector with the
   parent gives the weight back. */
.cache .ico-fire{display:none}
.cache.warm .ico-ice{display:none}
.cache.warm .ico-fire{display:block}
.ice-top{fill:var(--blue);opacity:1}
.ice-l{fill:var(--blue);opacity:.72}
.ice-r{fill:var(--blue);opacity:.48}
.fl-out{fill:var(--warn)}
.fl-in{fill:var(--err);opacity:.8}
.cache.soon .ico-fire{animation:cache-blink 1.05s ease-in-out infinite}
@keyframes cache-blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.9)}}
.cache-left{font:600 13px/1 var(--mono);color:var(--tx-2);font-variant-numeric:tabular-nums}
.cache.soon .cache-left{color:var(--warn)}
.cache-left:empty{display:none}
@media (prefers-reduced-motion: reduce){.cache.soon .ico-fire{animation:none}}

/* ── THE COLOURED SPEAKER ──
   The body is filled with a gradient made of the theme's tokens, the waves go outward as a
   ladder: accent → cold blue. The button is round and hangs over the feed — «lit / not lit»
   on it is not read with peripheral vision, while colour and movement are. The ring around it
   is the same gradient through two backings (padding-box + border-box), so it lives in any
   theme. */
/* COLOUR IS GIVEN BY THE DISC, NOT BY THE GLYPH.
   A gradient inside the icon itself on a small button reads as a dirty dark blob — «why is it
   so black?». We fill the whole circle with a live accent→blue transition and leave the
   speaker light: the contrast is at its maximum, the button is visible with peripheral vision. */
#fab-speak{border:none;color:var(--on-acc);position:relative;touch-action:none;
  background:linear-gradient(140deg,var(--acc) 0%,var(--acc-2,var(--acc)) 42%,var(--blue) 100%);
  box-shadow:0 6px 18px var(--shadow),0 0 14px -4px var(--acc)}
/* The glyph is always over the wave: the ::after pseudo-element is drawn AFTER the children. */
.read-fab button svg{position:relative;z-index:2}
#fab-speak:active{filter:brightness(1.12)}
.ico-speak .sp-body{fill:currentColor}
.ico-speak .sp-waves path{stroke:currentColor}
.ico-speak .w1{opacity:1;stroke-width:2.2}
.ico-speak .w2{opacity:.72;stroke-width:2}
.ico-speak .w3{opacity:.44;stroke-width:1.8}
/* READING — THE WAVE GOES FROM THE CENTRE OF THE DISC AND LEAVES PAST ITS EDGE (the owner,
   02.09). «So that this dark circle does not stay, but this whole circle from zero to the
   outgoing rings pulsed — not from the speaker's little circle and slightly to the sides, but
   straight from the centre and out to the circle. I need to see better from a distance that
   recognition is running.»
   The former sp-halo began EXACTLY AT THE BUTTON'S EDGE (a box-shadow grows from the border
   outward): the disc itself stood as a motionless dark blob, and from a metre and a half the
   «reading» button was no different from the «silent» button. Now the wave is drawn by two
   pseudo-elements over the fill (z-index:1 — above the gradient, below the glyph): the ring is
   born as a point in the centre, passes the whole disc and dies out already outside. There are
   two of them, half a period apart — the wave goes continuously rather than blinking. The ring
   outside is left as a second layer: together they read as one diverging circle. */
#fab-speak::before,#fab-speak::after{content:'';position:absolute;inset:0;border-radius:50%;
  pointer-events:none;opacity:0;z-index:1;
  background:radial-gradient(circle,transparent 0 30%,
    color-mix(in srgb,var(--on-acc) 86%,transparent) 40% 60%,transparent 70%)}
#fab-speak.on{animation:sp-halo 1.45s ease-in-out infinite}
#fab-speak.on::before{animation:sp-burst 1.45s ease-out infinite}
#fab-speak.on::after{animation:sp-burst 1.45s ease-out .72s infinite}
@keyframes sp-burst{
  0%{opacity:.9;transform:scale(.05)}
  55%{opacity:.5}
  100%{opacity:0;transform:scale(2.35)}}
@keyframes sp-halo{
  0%{box-shadow:0 10px 26px var(--shadow),0 0 0 0 var(--acc)}
  70%{box-shadow:0 10px 26px var(--shadow),0 0 0 13px transparent}
  100%{box-shadow:0 10px 26px var(--shadow),0 0 0 0 transparent}}
#fab-speak.on .ico-speak .w1,#fab-speak.on .ico-speak .w2,#fab-speak.on .ico-speak .w3{opacity:1}
/* PREPARING THE SOUND — A COMET RUNS ALONG THE RIM AND THE DISC BREATHES. «Or let it rotate»:
   the speaker itself may not be rotated (the glyph lies on its side), so a mask-ring spins
   along the rim and the disc only grows slightly. The rule stands BELOW .on and with the same
   weight — while the sound is being prepared, what is seen is the waiting, not the reading. */
#fab-speak.prep{animation:sp-breathe 1.1s ease-in-out infinite}
@keyframes sp-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.1)}}
#fab-speak.prep::after{animation:none;opacity:0}
#fab-speak.prep::before{opacity:1;animation:sp-spin .95s linear infinite;
  background:conic-gradient(from 0deg,transparent 0 55%,
    color-mix(in srgb,var(--on-acc) 92%,transparent) 88%,transparent 100%);
  -webkit-mask:radial-gradient(circle,transparent 0 56%,#000 61%);
  mask:radial-gradient(circle,transparent 0 56%,#000 61%)}
@keyframes sp-spin{to{transform:rotate(360deg)}}
/* A SWIPE ON THE SPEAKER SAYS WHAT WILL HAPPEN ON RELEASE — like the search's two stages: a
   gesture whose outcome is learned only afterwards reads as an accident. */
.fab-hint{position:absolute;right:calc(100% + 10px);bottom:2px;white-space:nowrap;
  font:600 12px var(--sans);padding:5px 10px;border-radius:9px;pointer-events:none;z-index:2;
  background:var(--bg-4);color:var(--tx);border:1px solid var(--line-2);
  box-shadow:0 8px 20px var(--shadow)}
.fab-hint.full{border-color:var(--acc);color:var(--acc)}
@media (prefers-reduced-motion: reduce){
  #fab-speak.on,#fab-speak.prep{animation:none}
  #fab-speak.on::before,#fab-speak.on::after,#fab-speak.prep::before{animation:none;opacity:0}}

/* ── SPEND: one page, not one and a half screens of air ──
   At the top is what it is opened from a phone for: is the session warm and how much has
   already gone into the task. Then the limit windows — each with TWO bars: how much of the key
   is left and how much TIME is left until the reset. Two bars side by side answer the question
   that neither of them answers on its own: «am I burning faster or slower than the clock runs». */
.stat-now{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:11px}
.now-card{flex:1 1 45%;min-width:0;background:var(--bg-3);border:1px solid var(--line);
  border-radius:10px;padding:8px 10px}
.now-card .k{font:600 9.5px var(--sans);letter-spacing:.9px;text-transform:uppercase;color:var(--tx-3)}
.now-card .v{font:600 15px var(--sans);color:var(--tx);margin-top:2px;font-variant-numeric:tabular-nums}
.now-card .n{font:11px var(--sans);color:var(--tx-3);margin-top:1px}
/* The task's numbers in the caption — bolder and lighter than the rest of the list (28.08). */
.now-card .n b{font-weight:700;color:var(--tx-2)}
.now-card.warm{border-color:var(--warn)}
.now-card.warm .v{color:var(--warn)}
/* A task's name is sometimes long, and truncating it is dishonest: the card takes the whole
   row and wraps the text rather than hiding it behind an ellipsis. */
.now-card.wide{flex:1 1 100%}
.now-card.wide .v{white-space:normal;line-height:1.25}
.now-card.wide .n{white-space:normal;line-height:1.3}
.left-card{gap:11px;padding:11px 12px}
.left-row.main .left-name{font-size:14px;color:var(--tx)}
.left-row.main .left-rest{font-size:27px}
.left-row.main .left-track{height:11px}
/* The reset time is A ROW WITH A BAR, the same as the key's remainder: it is the window's
   second resource, and as a small caption in the corner it read as a footnote. */
.left-time{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-top:7px}
.left-when{font:12.5px var(--sans);color:var(--tx-2)}
.left-when b{font-weight:600;color:var(--tx);font-variant-numeric:tabular-nums}
.left-tpct{font:600 12.5px var(--sans);color:var(--tx-3);font-variant-numeric:tabular-nums}
.left-ttrack{height:4px;background:var(--bg);border:1px solid var(--line);border-radius:3px;
  overflow:hidden;margin-top:4px}
.left-ttrack i{display:block;height:100%;background:var(--tx-3);opacity:.65;border-radius:3px}
.left-note{margin-top:4px}

@media(max-width:860px){
  /* THE KEY PERCENT IS THE HEADER'S MAIN FIGURE, and it used to be smaller than the task's
     name. The hours until the reset are removed from here entirely (.key-left): on a phone
     nobody looks at them, they are on the spend page. */
  .key-pct{font-size:17.5px;font-weight:700}
  .cache-ico{width:22px;height:22px}
  .cache-left{font-size:12.5px}
  /* ◇ — the door into the agent flow, the header's second most meaningful button after the percent. */
  .topbar #btn-flow{font-size:19px;padding:2px 5px}
  /* The spend modal: the air above and at the sides ate exactly the rows it is opened for on a
     small screen. */
  .modal-back{padding:10px}
  /* A report is READ, nobody examines the frame around it: 20px of veil on each side is 10% of a
     phone's width, given away for nothing. The preview is left a hair. */
  #htmlview{padding:5px}
  .modal-head{padding:9px 12px}
  .modal-body{padding:10px 12px}
  .pane-sub{margin:11px 0 5px}
  .stat-grid{gap:6px}
  .stat-tile{padding:7px 9px}
  .stat-tile .v{font-size:15px}
  .left-card{gap:10px;padding:10px 11px}
  .left-row.main .left-rest{font-size:24px}
}


/* ── THE HEADER ON A PHONE: ONE AND A HALF TIMES LOWER ──
   Everything it is looked into for is the key percent and the session's warmth; the task's name
   is in the rail, and the caption under it is read once a day. The header's height on a phone is
   rows taken away from the feed in every frame. */
@media(max-width:860px){
  .topbar{padding:1px 8px;gap:6px}
  .title-wrap{min-width:0;max-width:calc(46vw / var(--ui-scale,1))}
  /* The task's name and the project are the header's only two lines, and they are read on the
     move: 9px grey under 11.5px grey read as one grey noise. The type size is raised, the project
     burns with the accent colour. */
  .chat-title{font-size:12.5px;line-height:1.2}
  .chat-sub{font-size:10.5px;line-height:1.15;opacity:1}
  .meters{gap:5px}
  .key-chip{padding:1px 4px}
  .key-chip .bar,.ctx-chip .bar{width:30px}
  .time-chip{font-size:9px;padding:0 2px;border:none;background:none}
  .topbar .icon-btn{padding:1px 3px}
  .burger{min-width:36px;height:34px;font-size:21px;padding:0}
}


/* ── ◇ THE AGENT FLOW: A SIGN, NOT A FOOTNOTE ──
   This is the header's second most meaningful button after the key percent, and the ◇ glyph in
   the font is thin and small: on a phone it read as a speck of dust. We saturate it with weight
   and with size. */
#btn-flow{font-weight:800;color:var(--acc);text-shadow:0 0 10px var(--acc-soft,transparent)}
#btn-flow:hover{filter:brightness(1.2)}
/* On a phone the ◇ is no longer in the header (`only-wide`): its door is the «◇ Agent flow» item
   in the ⋯ menu under the composer and the button in the rail's footer. A rule about its size
   standing here would be a deception. */

/* ── THE FLOATING BUTTONS YIELD TO THE LIVE LINE ──
   `.live-line` sticks to the bottom of the feed, and 🔊/▼ hang in the same place on the right —
   and covered it with themselves: «the speaker covers the reasoning line». While a turn is
   running we lift them by its height. */
/* `--win-b` is carried through here for the same reason as in the base rule: this one is more
   specific, so a bottom written without it would drop the buttons back onto the viewport's floor
   — i.e. onto the pane standing UNDER window 1 — for exactly as long as a turn is running. */
html.live-turn .read-fab{bottom:calc(var(--win-b, 0px) + var(--composer-h, 96px) + var(--live-h, 34px) + 14px)}

/* ═══ ICONS WITH DEPTH: ONE RECIPE FOR THE WHOLE FAMILY ═══
   The silhouette is filled with the `#icoV` ladder (light from the top left), one light sliver on
   top and a soft shadow underneath — that is enough for the icon to read as three-dimensional,
   and it stays READABLE at 17 pixels: a glossy raster at that size gives a dirty blob (measured
   right here on .read-fab and written in every system guideline). The colour is taken from the
   button through currentColor, so the theme, hover and «switched on» work by themselves. */
.ico{width:1em;height:1em;display:inline-block;vertical-align:-.14em;flex:none;
  filter:drop-shadow(0 1px .8px rgba(0,0,0,.30))}
.ico-hi{fill:#fff;opacity:.30}
.ico-vol .vw{fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;opacity:.5}
.ico-vol .vw2{opacity:.3}
/* The size is set by the BUTTON, not by the icon: 1em is tied to its type size, and everything
   already matches what the emoji had — except in the places where the emoji was a caption to the
   text. */
.foot-btn .ico,.sheet-title .ico{width:1em;height:1em;margin-right:.24em}
.side-btn .ico{width:19px;height:19px}
.icon-btn .ico{width:17px;height:17px}

/* THE ENVIRONMENT'S DECISION — the line under a sent message: «I continue this session», «I start
   a new one with a digest», «I will hold it until the end of the turn». A choice the panel makes
   for the person every turn is obliged to be visible — a silent choice can be neither checked nor
   corrected. Quieter than a reply, but more noticeable than an ordinary note: its own left bar in
   the accent colour and `pre-line`, because the reason travels on the second line. */
.notice.n-plan{
  white-space:pre-line;
  border-left:3px solid var(--acc);
  background:var(--acc-weak,rgba(94,176,255,.07));
  padding:7px 10px 7px 12px;
  border-radius:0 8px 8px 0;
  color:var(--ink-dim,inherit);
  font-size:.92em;
  opacity:.95;
}


/* ══════════════════════════════════════════════════════════════════════════
   THE THIRD SIGN IN THE RAIL'S HEAD — THE ACCOUNTS
   Next to the connection dot (the server on old code) and the arrows (the tab on old code) stands
   a triangle: «something is wrong with the accounts». It appears ONLY when there is a loud line
   and disappears as soon as everything is whole (canon 23.08): «an educational sign will always
   distract». The permanent door into the accounts lives on the ⚙ → «API keys» tab.
   ══════════════════════════════════════════════════════════════════════════ */
/* ⚠ `[hidden]` LOSES TO A CLASS WITH `display`. `[hidden]{display:none}` is the browser's rule,
   and our own `.alert-btn{display:flex}` outranks it entirely: the hidden button honestly carried
   the attribute and still hung in the header (caught by the gate 23.08). The same mine has
   already gone off on `#alert-flow` — anything switched off by an attribute and painted by a
   selector needs its own pair. */
.alert-btn[hidden]{display:none}
.alert-btn{width:30px;height:30px;flex:none;display:flex;align-items:center;justify-content:center;
  border:none!important;background:none!important;color:var(--tx-3);cursor:pointer;padding:0;
  position:relative;opacity:.4;transition:opacity .15s,color .15s}
.alert-btn:hover{opacity:1;color:var(--acc)}
.alert-btn svg{width:19px;height:19px;display:block;filter:drop-shadow(0 1px 1px var(--shadow))}
.alert-btn.warn{opacity:1;color:var(--warn)}
.alert-btn.down{opacity:1;color:var(--err)}
/* The pulse is not decoration: a muted triangle and a yellow triangle are poorly told apart on a
   phone in sunlight, while movement is always visible. */
.alert-btn.warn svg,.alert-btn.down svg{animation:alert-cry 1.7s ease-in-out infinite}
@keyframes alert-cry{0%,100%{transform:scale(1)}50%{transform:scale(1.17)}}
@media (prefers-reduced-motion: reduce){.alert-btn svg{animation:none!important}}
.alert-n{position:absolute;top:-2px;right:-3px;min-width:14px;height:14px;padding:0 3px;
  border-radius:999px;background:var(--err);color:var(--on-acc,#fff);
  font:700 9.5px/14px var(--sans);text-align:center;box-shadow:0 0 0 2px var(--bg-2)}
.alert-btn.warn .alert-n{background:var(--warn)}
@media (max-width:860px){.alert-btn{width:38px;height:38px}.alert-btn svg{width:23px;height:23px}}

/* ══════════════════════════════════════════════════════════════════════════
   «UPDATES AVAILABLE» LIVED HERE AS A SIGN OF ITS OWN (19.09 → 23.09, merged away).
   It was a ⬇ in this topbar saying «the server has newer code on disk», while the dot beside the
   panel's name said the very same thing on the other side of the header — two signs, two
   tooltips, one fact, and the owner met both in one glance. What it genuinely added is kept:
   the LENGTH of the wait (`.conn-age`) and a press that installs now both belong to the dot,
   which since 23.09 turns like a pair of arrows instead of sitting there as a still disc.
   ══════════════════════════════════════════════════════════════════════════ */

/* ☰ ON A PHONE IS A MIRROR OF THE SAME FACT.
   The rail on a narrow screen is a drawer, so the triangle inside it is not visible for most of
   the day. «A little yellow exclamation mark there too» (the owner, 22.08). */
.burger{position:relative}
.burger.has-alert::after{content:'!';position:absolute;top:0;right:-2px;min-width:14px;height:14px;
  border-radius:999px;background:var(--warn);color:var(--bg);font:700 10px/14px var(--sans);
  text-align:center;box-shadow:0 0 8px var(--warn);pointer-events:none;
  animation:alert-cry 1.7s ease-in-out infinite}
@media (prefers-reduced-motion: reduce){.burger.has-alert::after{animation:none}}

/* ── THE ACCOUNTS DRAWER ──
   The cards are the same in spirit as the keys' (.kcard): one frame, a heading, a status line, a
   row of buttons. There is one difference and it is a meaningful one — on the left stands the
   level dot, and the screen is read by it without reading the text. */
.akard{border:1px solid var(--line);border-radius:10px;padding:9px 11px;margin:6px 0;
  background:var(--bg-2);display:grid;gap:5px}
.akard.lvl-down{border-color:var(--err)}
.akard.lvl-warn{border-color:var(--warn)}
.akard.lvl-ok{opacity:.72}
/* A PERMANENT DOOR IS NOT A FADED ONE. A green row in the drawer may dim — it is a report.
   These cards in the API keys tab are a control he goes looking for on a calm day, and a
   control at .72 reads as disabled. */
.akard.conn{padding:7px 10px;margin:4px 0}
.akard.conn.lvl-ok{opacity:1}
.akard.muted{opacity:.5}
.akard.ok-all{border-color:var(--ok)}
.adot{font-size:12px;line-height:1;margin-right:1px}
.adot.ok{color:var(--ok)}
.adot.warn{color:var(--warn)}
.adot.down{color:var(--err)}
.adot.off{color:var(--tx-3)}
.asec{margin:10px 0 2px}
.asec-head{font-size:11.5px;letter-spacing:.6px;text-transform:uppercase;color:var(--tx-3);
  margin:0 0 2px 2px}
.asec-head.foldable{cursor:pointer;user-select:none}
.asec-head.foldable::before{content:'▸ ';display:inline-block;transition:transform .15s}
.asec-head.foldable.open::before{content:'▾ '}
.kacts button.primary{border-color:var(--acc);color:var(--acc);font-weight:600}
.kacts button.ghost{opacity:.7}

/* ⟳ CHECK NOW HAS TO LEAVE A TRACE. Everything that button does happens on other people's
   servers, so a healthy machine answers a press with an identical screen — and an identical
   screen is indistinguishable from a dead control («ты хоть бы моргал, что проверка только
   что была сделана», 03.09). The card blinks once, the button goes green for two seconds,
   and the line above it carries the clock of the check. */
.kacts button.ok-flash,.act-btn.ok-flash{color:var(--ok);border-color:var(--ok)}
.kwhen{opacity:.8}
.kwhen.hot{color:var(--ok);opacity:1}
.akard.just-checked{animation:akard-blink 1.2s ease-out 1}
@keyframes akard-blink{
  0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--acc) 70%,transparent)}
  18%{box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 40%,transparent)}
  100%{box-shadow:0 0 0 3px transparent}}
@media (prefers-reduced-motion: reduce){.akard.just-checked{animation:none}}

/* THE CODE IS TYPED IN FROM ANOTHER DEVICE, BY EYE. That's why it's large, monospaced, letter-
   spaced — and a tap copies it: retyping eight characters from the phone to the laptop is exactly
   the spot where people abandon the login halfway through. */
.acode-row{display:flex;justify-content:center;margin:4px 0}
.acode{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:27px;font-weight:700;
  letter-spacing:4px;padding:9px 16px;border:1px dashed var(--acc);border-radius:10px;
  background:var(--bg-3);color:var(--acc);cursor:pointer}
.acode:hover{background:var(--bg-2)}
.acode.copied{border-style:solid;color:var(--ok);border-color:var(--ok)}
.alink{display:block;text-align:center;color:var(--acc);word-break:break-all;font-size:13.5px;
  padding:2px 0}
#alert-flow{border:1px solid var(--acc);border-radius:10px;padding:11px;margin:2px 0 10px;
  background:var(--bg-2);display:grid;gap:7px}
/* ⛔ A `display` BY ID BEATS THE `hidden` ATTRIBUTE. The UA style `[hidden]{display:none}` has
   zero specificity, so the empty sign-in frame stood above the list always — it was VISIBLE on
   the gate's screenshot, even though every check was passing. */
#alert-flow[hidden]{display:none}
#alert-flow pre{margin:0;max-height:130px;overflow:auto;font-size:11.5px;opacity:.65;
  white-space:pre-wrap;word-break:break-word}

/* ── ✕ IN THE CORNER OF A CARD ──
   «Там должна быть справа такая кнопочка-крестик, закрыть» (the owner, 26.09). It carries the
   verb that used to sit in a text button at the bottom of the card — quiet for a day on a
   warning, gone for good on a finished sign-in — put where a hand reaches for it, top right,
   in both the drawer and the API keys tab. 28px so a thumb can hit it on a phone, and the
   heading is given that much room so a long title can never slide under it. */
.akard,#alert-flow,#keys-flow{position:relative}
.kard-x{position:absolute;top:4px;right:5px;width:28px;height:28px;padding:0;line-height:1;
  display:flex;align-items:center;justify-content:center;border:1px solid transparent;
  border-radius:8px;background:transparent;color:var(--tx-3);font-size:13px;cursor:pointer}
.kard-x:hover{color:var(--warn);border-color:var(--line);background:var(--bg-3)}
.akard .ktop,#alert-flow>.ktop,#keys-flow>.ktop{padding-right:30px}

/* ── THE ROUTE CARD (the «API keys» tab) ──
   Two dropdown lists in one row: WHO answers and WHAT we pay with. */
.rcard{border:1px solid var(--line);border-radius:10px;padding:9px 11px;margin:6px 0;
  background:var(--bg-2);display:grid;gap:6px}
.rcard.on{border-color:var(--ok)}
.rrow{display:flex;gap:7px;flex-wrap:wrap;align-items:center}
.rrow select{padding:5px 8px;border:1px solid var(--line);border-radius:7px;background:var(--bg-3);
  color:inherit;font:inherit;font-size:12.5px;cursor:pointer;max-width:100%}
.rrow select:hover{border-color:var(--acc)}
.rvia{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.5px;opacity:.7}
/* The caption of the second row — «OpenAI model» — so the two rows cannot be read as one pair
   of pickers. It is a label, not a control: it never takes the pointer. */
.rlab{font-size:11.5px;opacity:.6;letter-spacing:.02em;user-select:none}
/* A hidden select must not hold its slot in a flex row, or the model picker sits in the middle
   of a gap on every model that has no thinking to turn up. `[hidden]` is a zero-specificity UA
   rule and loses to anything that sets `display` — the same mine that left `#alert-flow` and
   `.alert-btn` on screen carrying the attribute honestly. */
.rrow select[hidden]{display:none}
@media (max-width:860px){.rrow select{font-size:16px;padding:7px 9px}}


/* ═══════════════ SEARCH INSIDE A TASK ═══════════════
   A strip above the feed, not a window over it: what is found is read IN THE FEED, and covering
   it with the very thing you search with means searching blind. The geometry is counted from the
   feed, so the strip stands in the flow (not `fixed`): on a phone a floating panel lands exactly
   on the first line. */
.findwrap{position:relative;flex:0 0 auto}
.findbar{position:relative;z-index:1;display:flex;align-items:center;gap:5px;padding:6px 9px;
  background:var(--bg-2);border-bottom:1px solid var(--line);flex:0 0 auto}
.findbar.hidden{display:none!important}
.find-ico{font-size:14px;opacity:.75;flex:0 0 auto}
/* The field with the counter and the ✕ INSIDE: the room for them is reserved not by the markup
   but by `paintFind` — it measures the tail and gives the field the same padding-right. */
.find-field{position:relative;flex:1;min-width:0;display:flex;align-items:center}
.find-q{width:100%;min-width:0;background:var(--bg-3);border:1px solid var(--line);border-radius:8px;
  color:var(--tx-1);font:inherit;font-size:15px;padding:6px 9px;outline:none}
.find-q:focus{border-color:var(--acc)}
.find-q::-webkit-search-cancel-button{display:none}
.find-tail{position:absolute;right:4px;top:50%;transform:translateY(-50%);
  display:flex;align-items:center;gap:2px}
.find-count{font-size:12.5px;color:var(--tx-3);font-variant-numeric:tabular-nums;
  text-align:center;flex:0 0 auto;white-space:nowrap;cursor:pointer;
  user-select:none;border-radius:7px;padding:3px 5px}
.find-count:hover{color:var(--tx-1)}
.find-count.on{background:color-mix(in srgb,var(--acc) 22%,transparent);color:var(--tx-1)}
/* The arrows are one soldered pair, not two separate buttons: «back / forward» reads as one
   switch and saves both the gap and the field's width on a phone. */
.find-nav{display:flex;flex:0 0 auto}
.find-nav .find-btn{border-radius:0}
.find-nav .find-btn:first-child{border-radius:8px 0 0 8px}
.find-nav .find-btn:last-child{border-radius:0 8px 8px 0;border-left-color:transparent}
.find-btn{flex:0 0 auto;min-width:31px;min-height:31px;padding:0 6px;border-radius:8px;
  background:var(--bg-3);border:1px solid var(--line);color:var(--tx-2);font-size:14px;
  cursor:pointer;line-height:1}
.find-btn:hover{background:var(--bg-1);color:var(--tx-1)}
.find-btn.on{background:color-mix(in srgb,var(--acc) 26%,transparent);border-color:var(--acc);color:var(--tx-1)}
.find-btn:disabled{opacity:.4;cursor:default}
/* The ✕ stands INSIDE the field, so it is not a button-tile but a glyph: a frame and a background
   here would argue with the field's own frame. The rule is obliged to stand AFTER `.find-btn` —
   the weight is the same. */
.find-x{min-width:26px;min-height:26px;padding:0 4px;background:none;border:0;color:var(--tx-3);font-size:13px}
.find-x:hover{background:var(--bg-1);color:var(--tx-1)}
.findbar.busy .find-count{opacity:.55}

/* THE SWIPE'S TWO STAGES — the same as in a mail app: while it is pulled a little, «the next
   match» is visible under the strip; past the second line the first stage shrinks to its glyph,
   «hide search» slides out and the colour changes. The layer lies UNDER the strip, and that is
   why it opens by exactly the strip's own shift. */
.find-act{position:absolute;inset:0;z-index:0;display:none;align-items:center;gap:12px;
  padding:0 14px;overflow:hidden;white-space:nowrap;font-size:12.5px;color:var(--tx-1);
  background:color-mix(in srgb,var(--acc) 24%,var(--bg-2))}
.find-act.on{display:flex}
.find-act.at-right{justify-content:flex-end}
.find-act.shut{background:color-mix(in srgb,var(--err) 26%,var(--bg-2))}
.fa-step{display:inline-flex;align-items:center;gap:6px}
.fa-step b{font-size:16px;line-height:1}
.fa-step i{font-style:normal}
.fa-2{display:none}
.find-act.shut .fa-1{opacity:.55}
.find-act.shut .fa-1 i{display:none}
.find-act.shut .fa-2{display:inline-flex;font-weight:600;animation:fa-in .16s ease}
@keyframes fa-in{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:none}}

/* The list of matches: twenty hits are picked with the eyes, not one by one with the arrows. */
.find-list{flex:0 0 auto;max-height:38vh;overflow:auto;background:var(--bg-2);
  border-bottom:1px solid var(--line)}
.find-list.hidden{display:none!important}
.find-row{display:block;width:100%;text-align:left;background:none;border:0;cursor:pointer;
  padding:7px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);
  color:var(--tx-2);font-size:13.5px;line-height:1.35}
.find-row:hover{background:var(--bg-1)}
.find-row.on{background:color-mix(in srgb,var(--acc) 14%,transparent)}
.find-row .fr-head{display:flex;gap:7px;align-items:center;font-size:11.5px;color:var(--tx-3);margin-bottom:2px}
.find-row .fr-who{font-weight:600;color:var(--tx-2)}
.find-row .fr-txt{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.find-row mark{background:color-mix(in srgb,var(--acc) 40%,transparent);color:inherit;border-radius:2px}

/* ═══ THE PINNED TOPIC: HIS FIRST SENTENCE, KEPT WHERE THE ROW'S NAME FAILED ═══
   One line and never two: it stands between the header and the conversation, and a strip that
   wraps to three lines takes the feed's space for ever in exchange for words that were already
   readable once. The text is the BUTTON — the whole line is the target, which is what a thumb
   needs — and «▾ N» beside it opens the rest. */
.topic{display:flex;align-items:center;gap:8px;padding:5px 11px;background:var(--bg-2);
  border-bottom:1px solid var(--line);min-height:0}
.topic.hidden{display:none!important}
.topic.busy{opacity:.6}
.topic-tag{color:var(--acc);font-size:11px;line-height:1;flex:0 0 auto}
.topic-first{flex:1 1 auto;min-width:0;text-align:left;background:none;border:0;padding:0;
  cursor:pointer;color:var(--tx-2);font-size:13px;line-height:1.3;font-family:inherit;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.topic-first:hover{color:var(--tx-1)}
.topic-more{flex:0 0 auto;background:var(--bg-3);border:1px solid var(--line);border-radius:6px;
  padding:2px 7px;cursor:pointer;color:var(--tx-2);font-size:12px;line-height:1.4;
  font-family:inherit;font-variant-numeric:tabular-nums}
.topic-more:hover{color:var(--tx-1);border-color:var(--line-2)}
.topic-more.on{background:color-mix(in srgb,var(--acc) 18%,transparent);color:var(--tx-1);
  border-color:var(--acc)}
/* The dropdown borrows the match list's shape on purpose: two lists in one feed that scroll and
   highlight differently are two things to learn instead of one. */
.topic-list{flex:0 0 auto;max-height:38vh;overflow:auto;background:var(--bg-2);
  border-bottom:1px solid var(--line)}
.topic-list.hidden{display:none!important}
.topic-cut{padding:5px 11px;font-size:11.5px;color:var(--tx-3);
  border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent)}
.topic-row{display:block;width:100%;text-align:left;background:none;border:0;cursor:pointer;
  padding:7px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);
  color:var(--tx-2);font-size:13.5px;line-height:1.35;font-family:inherit}
.topic-row:hover{background:var(--bg-1)}
.topic-row .tr-head{display:flex;gap:7px;align-items:center;font-size:11.5px;color:var(--tx-3);
  margin-bottom:2px}
.topic-row .tr-no{font-weight:600;color:var(--tx-2);font-variant-numeric:tabular-nums}
.topic-row .tr-txt{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* The highlight in the feed itself. `mark` is not colour for colour's sake: what is highlighted
   has to be READ, so the background is semi-transparent, while the current hit takes the accent
   whole and is visible at a glance. */
mark.find-hit{background:color-mix(in srgb,var(--acc) 32%,transparent);color:inherit;
  border-radius:3px;padding:0 1px}
mark.find-hit.on{background:var(--acc);color:var(--bg-1)}
.find-jump{box-shadow:0 0 0 2px var(--acc) inset;border-radius:8px}
/* The list's head: it is also the only place on a phone where the reasoning layer is named in
   WORDS. The 💭 glyph in a row of buttons explains itself to nobody. */
.find-head{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--bg-1);
  border-bottom:1px solid var(--line);font-size:12px;color:var(--tx-3)}
.fh-think{margin-left:auto;background:var(--bg-3);border:1px solid var(--line);color:var(--tx-2);
  border-radius:999px;padding:3px 10px;font-size:11.5px;cursor:pointer;white-space:nowrap}
.fh-think.on{background:color-mix(in srgb,var(--acc) 26%,transparent);border-color:var(--acc);color:var(--tx-1)}

/* The hint about the gestures — once per device. */
.find-tip{display:flex;align-items:center;gap:8px;padding:5px 9px 6px;background:var(--bg-2);
  border-bottom:1px solid var(--line);font-size:11.5px;line-height:1.35;color:var(--tx-3);flex:0 0 auto}
.find-tip.hidden{display:none!important}
.find-tip b{color:var(--tx-1);font-weight:700}
.find-tip .find-btn{min-width:26px;min-height:26px;font-size:12px}

/* «What is this button» — by a long press: on a phone nobody shows a `title`. */
.hold-hint{position:fixed;z-index:260;max-width:min(calc(78vw / var(--ui-scale,1)),320px);padding:7px 10px;border-radius:10px;
  background:var(--bg-1);border:1px solid var(--line);color:var(--tx-1);font-size:12.5px;line-height:1.35;
  box-shadow:0 8px 24px rgba(0,0,0,.34);pointer-events:none;animation:hold-hint-in .14s ease}
@keyframes hold-hint-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}

@media (max-width:860px){
  /* THE STRIP ON A PHONE IS A FIELD, not a panel of buttons. Five buttons and a 56px counter
     squeezed the input down to two centimetres (at --ui-scale 1.4 — down to twenty pixels), and
     the search became impossible to use. ☰ goes into the counter (which already is «how many were
     found»), 💭 into the list's head, where it is named in words, and stepping through the matches
     is duplicated by a swipe: dragging along the whole strip is easier than hitting an arrow on
     the move. */
  .findbar{gap:4px;padding:6px 7px;touch-action:pan-y;
    transition:transform .16s ease,background-color .16s ease}
  .findbar.dragging{transition:none}
  .findbar.armed{background:color-mix(in srgb,var(--acc) 20%,var(--bg-2))}
  .find-ico{display:none}
  #find-all,#find-think{display:none}
  .find-q{font-size:16px;padding:8px 9px}   /* under 16px and iOS zooms the page on focus */
  .find-count{min-width:0;padding:3px 4px;font-size:12px}
  .find-btn{min-width:32px;min-height:34px;padding:0 4px}
  /* THE ARROWS WERE SO GREY THAT THEY COULD NOT BE SEEN (22.08). A thin `--line`-coloured frame
     on a `--bg-3` background with `--tx-2` ink is a contrast of 1.5:1; the pair gets its own
     background, a `--line-2` frame and full-weight ink, while a switched-off one (nothing to
     search for) stays readable. */
  .find-nav .find-btn{min-width:34px;min-height:36px;padding:0 2px;font-size:17px;font-weight:700;
    background:var(--bg-1);border-color:var(--line-2);color:var(--tx-1)}
  .find-nav .find-btn:disabled{opacity:.6;color:var(--tx-2)}
  .find-x{min-width:30px;min-height:30px;font-size:15px;color:var(--tx-2)}
  .find-list{max-height:46vh}
  /* ⛔ THE PINNED FIRST MESSAGE IS NOT DRAWN ON A PHONE AT ALL (the owner, 01.10).
     «В мобильной вообще её убрать» — it held a permanent line above the conversation, did not
     travel with the header while reading (`auto-hide` lifts the header, not this), and never
     folded away. The tick in Settings → General is about the wide screen only, so this is not a
     knob and is `!important`: whatever a device remembers, a narrow panel draws no strip. Its own
     phone sizing is gone with it. `html.pane` is a WINDOW of the work field, not a phone — it has
     the room and keeps the strip, which is why the rule is scoped and not a bare `.topic`.
     web/app.js → topicWanted() is the other half: it also stops the request being made. */
  html:not(.pane) #topic, html:not(.pane) #topic-list{display:none!important}
}

/* ═══ WHAT WAS FOUND IN THE MESSAGES — above the rail's tree ═══ */
.deep-head{display:flex;align-items:center;gap:7px;padding:5px 8px 4px;
  font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--tx-3)}
.deep-title{font-weight:700}
.deep-n{color:var(--tx-2)}
.deep-idx{margin-left:auto;color:var(--acc);text-transform:none;letter-spacing:0}
.deep-empty{padding:3px 10px 8px;font-size:12.5px;color:var(--tx-3)}
/* A search that did not answer: the line itself is the press that asks again (web/tree.js). */
.deep-again{cursor:pointer;color:var(--accent)}
.deep-again:hover{text-decoration:underline}
.row.row-deep{align-items:flex-start;gap:6px;padding:5px 8px;min-height:0;height:auto;cursor:pointer}
.deep-ico{flex:0 0 auto;font-size:12px;opacity:.8;margin-top:1px}
.deep-mid{flex:1;min-width:0}
.deep-name{display:flex;gap:6px;align-items:baseline;min-width:0}
.deep-task{font-size:13px;color:var(--tx-1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.deep-proj{font-size:11px;color:var(--tx-3);flex:0 0 auto}
.deep-snip{font-size:12px;color:var(--tx-3);line-height:1.3;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.deep-snip mark{background:color-mix(in srgb,var(--acc) 38%,transparent);color:var(--tx-1);border-radius:2px}
.deep-cnt{flex:0 0 auto;font-size:11px;color:var(--tx-3);font-variant-numeric:tabular-nums}
/* When this was last talked about. Pressed to the right and does not let itself be squeezed: a
   project's name is many times longer than it, and without `margin-left:auto` the date went over
   the edge before anything else. */
.deep-when{flex:0 0 auto;margin-left:auto;font-size:11px;color:var(--tx-3);
  font-variant-numeric:tabular-nums;white-space:nowrap}
/* The way back from the second floor to the first — the same row, but muted: it is not a match. */
.row.row-deep.deep-back{opacity:.75}
.row.row-deep.deep-back .deep-task{color:var(--tx-2)}
/* A ROW STANDING UNDER ITS PROJECT on the search's top floor: indented and quietened so the
   project line still reads as the heading of the group, not as one more sibling. */
.row.row-deep.deep-sub{margin-left:16px;padding-left:8px;border-left:1px solid var(--ln-1,rgba(128,128,128,.22))}
.row.row-deep.deep-sub .deep-task{font-size:12px;color:var(--tx-2)}
.row.row-deep.deep-sub .deep-ico{font-size:11px;opacity:.65}
.row.row-deep.deep-more .deep-task{color:var(--tx-3);font-size:11px}
/* A link-button inside a notice row: «go back to the end of the task» is a move, not a command,
   and an ordinary framed button reads in the feed as something dangerous. */
.notice .linkish{background:none;border:0;padding:0 2px;color:var(--acc);cursor:pointer;
  font:inherit;text-decoration:underline;text-underline-offset:2px}
.notice .linkish:hover{color:var(--tx-1)}

/* ═══════════════════ THE WORKSPACE: SEVERAL WINDOWS SIDE BY SIDE ═══════════════════
   The columns are ordered by the view button (◫ in the header), the windows inside a column by
   the hand: a task is dragged from the rail under a neighbouring window, and there become two of
   them, three at most. Every window except the main one is the same panel document in an iframe
   (web/workspace.js): its own feed, its own composer, its own microphone. At the top of a window
   is the FULL name of the task and nothing else, at the bottom the project's short name. */
.workspace{display:flex;min-width:0;min-height:0;position:relative}
.ws-col{display:flex;flex-direction:column;flex:1 1 0;min-width:0;min-height:0;border-left:1px solid var(--line-2)}
.ws-col:first-child{border-left:0}
.ws-col > .main{flex:1 1 0;min-height:0}
/* The workspace does not touch a phone: the columns are not shown, and the windows themselves are
   not even created on a narrow screen (web/workspace.js → render). The rule is a belt in case the
   screen is rotated between repaints. */
@media(max-width:860px){.ws-col:not(.ws-home){display:none}#workspace .pane{display:none}}

.pane{display:flex;flex-direction:column;flex:1 1 0;min-height:0;overflow:hidden;background:var(--bg);
  border-top:1px solid var(--line-2)}
.ws-col > .pane:first-child{border-top:0}
/* ═══ A WINDOW'S HEAD IS `.topbar`, NOT «SOMETHING LIKE IT» (the owner, 26.08) ═══
   «Make the top header be the same across the tables… so that the tabs do not differ from one
   another.» A column's window carries exactly the same markup (`workspace.js → headBits`:
   `.title-wrap` + `.meters` with the same chips), so ALL the rules of the chips — the size of the
   figures, the strips, the second storey of the reset time — apply here by themselves, without a
   single copy. This head has only three things of its own left: it is slightly narrower at the
   sides (up to eight columns on screen), it is dragged (`grab`) and it holds the ✕. It is NOT
   given the `.topbar` class on purpose: the heat wash is written as `html.heat-warm .topbar`,
   i.e. against the PARENT document, and would paint all eight heads with the main window's
   warmth. */
.pane-head{display:flex;align-items:center;gap:9px;padding:7px 8px 7px 11px;background:var(--bg-2);
  border-bottom:1px solid var(--line);cursor:grab;user-select:none;overflow:hidden}
.pane-head:active{cursor:grabbing}
/* ═══ WITH FIVE COLUMNS THE HEAD IS OBLIGED TO STAY A HEAD (the owner, 28.08) ═══
   «I must be able to have 3-4-5 columns without any trouble.» At five the window is 283px wide,
   and the head lost from its edges exactly what it exists for: the task's NAME was squeezed to
   zero, and the ✕ and the arrows were pushed past the edge — the window could be neither
   recognised nor closed. The order of retreat is set explicitly: the controls are never squeezed,
   the name is squeezed last, the figures go first. `.tight` is hung by `workspace.js → mirror` by
   the window's REAL width, not by the screen's: three columns on a wide monitor and five on a
   laptop are different things. */
.pane-head>.title-wrap{flex:1 1 auto;min-width:0}
.pane-head>.meters{flex:0 1 auto;min-width:0;overflow:hidden}
.pane-head>.pane-mvs,.pane-head>.pane-x,.pane-head>.pane-aim{flex:none}
.pane-head.tight .pm-time,.pane-head.tight .pm-flow{display:none}
/* THE FIRST WINDOW'S HEAD IS THE SAME HANDLE (the owner, 27.08: «the first column cannot be
   dragged anywhere, and it should be possible»). The class is hung by `workspace.js → render`,
   and only in a split field: in one window there is nobody to trade places with, and a hand
   cursor would promise a gesture that is not there. The buttons inside the head keep their own
   cursor to themselves. */
.topbar.ws-grab{cursor:grab}
.topbar.ws-grab:active{cursor:grabbing}
.topbar.ws-grab button,.topbar.ws-grab select,.topbar.ws-grab input{cursor:pointer}
.pane-head .meters{gap:7px}
/* The «project · technical part» caption here is of the same type size as in the main window (it
   is compared by eye), but an empty tail in it is removed: `setSub` in the main window simply
   does not create the node, while the mirror always holds it — and the separator dot would hang
   in mid-air. */
.pane-head .sub-rest:empty{display:none}
/* ═══ A WINDOW'S HEAD IS THE PANEL'S HEADER IN MINIATURE (the owner, 25.08) ═══
   «The session's sign has to be duplicated here. The key the task works on — here, saying which
   key exactly this task has. The time, naturally, has to be duplicated too.»
   Eight windows side by side mean eight DIFFERENT subscriptions, sessions and clocks: a figure
   living only in the main window's header answers about the main window and says nothing about
   the one being looked at. Everything standing here is a mirror of the live elements inside the
   frame (web/workspace.js → mirror), so it cannot part ways with the truth: these are the very
   same nodes, read from outside.
   The ⋮⋮ glyph is no longer here — «that cog has to be crossed out»: a window is dragged by its
   whole head, and the `grab` cursor says so better than six dots taking width away from the name. */
/* AT THE TOP OF A WINDOW THERE IS ONLY ONE THING — THE TASK'S NAME, AND WHOLE. An ellipsis here
   would mean that eight windows are labelled with the same beginning of a phrase; two lines are
   cheaper than indistinguishable windows. The only difference from the main window's
   `.chat-title`, and it is deliberate: there the name is one for the whole screen and is cut with
   an ellipsis, here there are up to eight columns. */
.pane-title{margin:0;min-width:0;font:600 13.5px/1.32 var(--sans);color:var(--tx);white-space:normal;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
/* THE AIM: WHICH WINDOW WILL TAKE THE NEXT ROW FROM THE RAIL. The ring is drawn with an outline
   shifted inward: it takes no room (the window does not twitch) and lies OVER the iframe, which
   an inset shadow cannot be taught — the frame's content covers it. */
.pane.on{outline:2px solid var(--acc);outline-offset:-2px}
/* ═══ AND THE MAIN WINDOW IS RINGED IN EXACTLY THE SAME WAY (the owner, 26.08) ═══
   «Absolutely independent tabs have to be ringed»: while there is one window the mark is not
   needed (there is nothing to ring), but next to a second one the absence of a ring stops meaning
   «as usual» and starts meaning «unknown where». The ring is the same, of the same thickness and
   of the same colour — the tabs must not differ from one another in anything but their content. */
html.ws-split .ws-col > .main.on{outline:2px solid var(--acc);outline-offset:-2px}
/* ═══ ONE TASK — ONE WINDOW: «here it is, you already have it open here» ═══
   The panel does not open a second copy of the row (web/workspace.js → openIn/bounce) and instead
   moves the aim into the window where the row stands. One ring will not say that: it could have
   been lit before the attempt anyway. So the window also BLINKS with its thickness — the eye
   catches movement where a static colour cannot be told from the previous state. Thickness, not
   colour: the ring's colour belongs to the aim, and a second meaning on it would mean a blind
   choice. */
@keyframes ws-found{0%,100%{outline-width:2px}45%{outline-width:6px}}
.pane.ws-found,html.ws-split .ws-col > .main.ws-found{outline:2px solid var(--acc);outline-offset:-2px;
  animation:ws-found .34s ease-in-out 2}
.pane-aim{flex:none;color:var(--acc);font:12px/1.3 var(--mono);padding-top:1px;opacity:0;transition:opacity .12s ease}
.pane.on .pane-aim,html.ws-split .main.on .pane-aim{opacity:1}
/* In the MAIN window's header there is no caret while the field is not split: the forty pixels of
   a phone's header are the panel's most expensive real estate, and a sign with nothing to aim at
   has no right to take even seven pixels of it. */
#main-aim{display:none}
html.ws-split #main-aim{display:inline-block}
.ws-empty.on{outline:2px dashed var(--acc);outline-offset:-6px;border-radius:12px}
.ws-empty-aim{display:none;font:600 11.5px/1.5 var(--sans);color:var(--acc);max-width:210px}
.ws-empty.on .ws-empty-aim{display:block}
.ws-empty.on .ws-empty-s{display:none}
.pane-x{flex:none;background:none;border:0;color:var(--tx-3);font-size:13px;line-height:1;padding:2px 3px;
  border-radius:6px;cursor:pointer}
.pane-x:hover{background:var(--bg-4);color:var(--tx)}
.pane-frame{flex:1 1 0;min-height:0;width:100%;border:0;display:block;background:var(--bg)}
/* …AND AT THE BOTTOM IS THE PROJECT'S SHORT NAME: «where is this happening» is asked by eye and
   rarely, so it stands where it takes no room from the task's name. In the accent colour —
   otherwise it drowns. */
.pane-foot{flex:none;padding:3px 10px 4px;border-top:1px solid var(--line);background:var(--bg-2);
  font:11px var(--mono);color:var(--acc);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.ws-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;
  padding:16px;text-align:center;color:var(--tx-3)}
.ws-empty-mark{font-size:26px;opacity:.3}
.ws-empty-t{font:600 13px var(--sans);color:var(--tx-2)}
.ws-empty-s{font:11.5px/1.5 var(--sans);max-width:210px}
.ws-empty-b{margin-top:7px;background:var(--bg-3);border:1px solid var(--line);color:var(--tx-2);
  border-radius:9px;padding:6px 11px;font:12px var(--sans);cursor:pointer}
.ws-empty-b:hover{background:var(--bg-4);color:var(--tx)}

/* ⚠ AN IFRAME EATS THE DRAG EVENTS — while the cursor travels over a foreign document, the parent
   is blind. The mask lies over the whole field for exactly the duration of the drag and serves
   that single purpose; the strip shows the gap the window will stand in (red — the column already
   holds three). */
.ws-mask{position:absolute;inset:0;z-index:60;display:none}
html.ws-dragging .ws-mask{display:block}
/* ⛔ A FRAME EATS THE RAIL'S HAND (an outside critic, 29.08). The mask is raised lazily — in
   hand(), i.e. after the FIRST pointermove that reached the parent. But while the pointer is over
   an <iframe>, the events go into its document and do not reach the parent at all: if the first
   touch of the field landed on a column's window, hand() was never called — the ghost froze, the
   release was lost, and the cargo stayed hanging on the cursor. While a row is being carried by
   hand, the frames do not take the pointer; the mask stands up by itself after that. */
body.rail-dragging .pane-frame{pointer-events:none}
/* ⛔ THE SAME, FOR A WINDOW CARRIED BY ITS HEADER (10.09). The header takes the pointer with
   `setPointerCapture`, so the frames could not steal it anyway — but the cursor and the ban on
   selecting text must hold for the whole flight, and the window being carried has to LOOK carried:
   without a browser drag there is no translucent ghost any more, so the header itself dims. */
body.ws-heading{cursor:grabbing;user-select:none}
body.ws-heading .pane-frame{pointer-events:none}
.pane-head.ws-lift,.topbar.ws-lift{opacity:.55}
.ws-ind{position:absolute;height:4px;border-radius:3px;background:var(--acc);opacity:0;
  box-shadow:0 0 0 1px var(--bg),0 0 16px var(--acc-soft,transparent);transition:opacity .12s ease}
.ws-ind.show{opacity:1}
.ws-ind.bad{background:var(--err)}
/* «The task is going ONTO this window»: a fill over the window's whole rectangle instead of the
   gap strip — the same language the editor groups in VS Code use. The height is set by the
   handler. */
.ws-ind.into{border-radius:10px;box-shadow:none}
.ws-ind.into.show{opacity:.22}
/* «I DRAG EXACTLY ONTO THE NEIGHBOUR'S HEAD» = SWAP PLACES. The same rectangle in two meanings
   would be a blind choice, so the swap is labelled with a word and lies exactly over the foreign
   head — over the very strip that hitting it switches it on by. The fill is semi-transparent in
   the colour itself, not by the opacity of the whole node: otherwise the label over it fades
   together with it. */
.ws-ind.swap{border-radius:9px;box-shadow:0 0 0 2px var(--acc);
  background:color-mix(in srgb, var(--acc) 30%, transparent)}
.ws-ind.swap.show{opacity:1}
.ws-ind-tag{display:none}
.ws-ind.swap .ws-ind-tag,.ws-ind.move .ws-ind-tag{display:flex;align-items:center;justify-content:center;
  height:100%;font:700 12px var(--sans);color:var(--tx);letter-spacing:.3px;text-shadow:0 1px 2px var(--bg)}
/* «The row is coming HERE from the neighbouring window» — the same rectangle as the swap has, but
   with a label: a drag moves rather than copies, and silently that looks like a duplicate. */
.ws-ind.move.into{box-shadow:0 0 0 2px var(--acc);
  background:color-mix(in srgb, var(--acc) 30%, transparent)}
.ws-ind.move.into.show{opacity:1}
html.ws-dragging .row-task,html.ws-dragging .row-project{cursor:grabbing}
/* ◀ ▶ ▲ ▼ next to the ✕ — «swap the windows» with one press, without aiming at a head. The
   horizontal ones go out at the edge of the field, the vertical ones are not there at all in a
   column of one window. */
/* The four arrows stand as ONE group with no gaps: the head's `gap:9px` between each pair would
   eat 36px — with five columns that is the width of the task's name. */
.pane-mvs{display:flex;align-items:center;gap:0}
/* The same arrows in the FIRST window's head: «the first window must be the same as the others»
   (29.08). web/workspace.js draws them only in a split field, so they are not in the markup. */
.topbar>.main-mvs{flex:none;margin-inline:2px}
.pane-mv{background:none;border:0;color:var(--tx-3);cursor:pointer;font-size:10px;line-height:1;
  padding:3px 2px;border-radius:5px;opacity:.55;transition:opacity .12s ease,color .12s ease}
.pane-mv:hover:not([disabled]){color:var(--acc);opacity:1;background:var(--bg-3)}
.pane-mv[disabled]{opacity:.16;cursor:default}

/* The view button: the columns glyph and the figure «how many there are now» — as in editors. */
.lay-btn{display:inline-flex;align-items:center;gap:3px}
.lay-n{font:700 10.5px var(--mono);color:var(--tx-3)}
.lay-btn.on .lay-n,.lay-btn:hover .lay-n{color:var(--acc)}
.lay-pop{position:fixed;z-index:180;background:var(--bg-2);border:1px solid var(--line-2);border-radius:12px;
  padding:11px;box-shadow:0 14px 40px rgba(0,0,0,.45);width:min(330px,calc(94vw / var(--ui-scale,1)))}
.lay-h{font:600 12px var(--sans);color:var(--tx-2);margin-bottom:8px}
.lay-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.lay-opt{display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 4px;cursor:pointer;
  background:var(--bg-3);border:1px solid var(--line);border-radius:9px}
.lay-opt:hover{background:var(--bg-4)}
.lay-opt.on{border-color:var(--acc)}
.lay-mini{display:flex;gap:1.5px;width:100%;height:19px}
.lay-mini i{flex:1;background:var(--tx-3);border-radius:1.5px;opacity:.5}
.lay-opt.on .lay-mini i{background:var(--acc);opacity:.9}
.lay-num{font:600 10.5px var(--mono);color:var(--tx-3)}
.lay-opt.on .lay-num{color:var(--acc)}
.lay-hint{margin-top:9px;font:11.5px/1.5 var(--sans);color:var(--tx-3)}

/* ═══ THE PANE WINDOW (/?pane=…) ═══ The rail, the header and the inspector are drawn by the
   parent — they are not here. Everything else (the feed, the composer, the microphone, the
   attachments, the search) stays exactly the same. */
/* ⚠ A HIDDEN GRID ITEM DOES NOT FREE ITS COLUMN — IT FALLS OUT OF THE PLACEMENT.
   `.app` is a grid of three columns (rail · conversation · inspector). We hid the rail and the
   inspector with display:none — and the remaining #workspace automatically landed in the FIRST
   column, i.e. in zero width: the feed shrank to its min-content and the text went one letter to
   a line (visible only on the picture — the «the elements are in place» checks all pass while it
   happens). That is why in a pane window `.app` stops being a grid at all. */
html.pane .app{display:flex;grid-template-columns:none}
html.pane #workspace{flex:1 1 auto;min-width:0}
html.pane .sidebar,html.pane .inspector,html.pane .rail-grip,html.pane .topbar,
html.pane .only-mobile,html.pane #find-tip{display:none!important}
html.pane .composer{padding-left:12px;padding-right:12px}
/* ═══ A WORKSPACE WINDOW HAS THE SAME COMPOSER AS THE MAIN ONE (the owner, 25.08) ═══
   «The text input windows must not differ between the main window and the rest. All the windows
   must be the same.» Before, a window lost BOTH sets at once: `only-wide` was switched off by the
   frame (it is narrower than 860px), `only-mobile` by the `pane` class. What came out was a
   composer without a single button and with no model under the field. Now under the field stand
   the same two switches, and the menu lives in the cog. */
html.pane .only-wide.composer-flags{display:flex!important}
html.pane .more-btn{order:9;flex:none;width:44px;min-height:24px;height:24px;border-radius:8px;
  border-color:var(--line);color:var(--tx-3)}
html.pane .more-dots{display:none}
html.pane .more-cog{display:block}
/* THE FIELD IS EXACTLY TWO BUTTONS TALL — the microphone and «Send» («make it lower, so that its
   height is exactly the microphone button and Send»): 44 + 6 + 44. A floor of three buttons is
   the PHONE's rule, where the field is the whole screen; in a workspace column it eats the feed. */
html.pane .input{min-height:94px}


/* ═══════════════════════════════════════════════════════════════════════════
   THE RAIL FOLDED WHOLE · THE SESSION'S WARMTH AS A BACKGROUND · THE KEY SWITCHER  (25.08)
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the arrow that folds the rail ──
   It lives IN the rail itself, by its name: a list can be folded only while looking at the list,
   and unfolded only while not seeing it, and that is why the way back (☰) stands somewhere else. */
.rail-fold{flex:none;width:24px;height:24px;padding:0;font:600 17px/1 var(--sans);
  color:var(--tx-3);border-radius:7px}
.rail-fold:hover{background:var(--bg-4);color:var(--tx)}
/* The column collapses to ZERO, it is not hidden with `display:none`: the `.app` grid counts the
   widths itself, and an empty first column would leave a gap --rail-w wide. */
html.rail-off .app,
html.rail-off .app:has(.inspector.open){grid-template-columns:0 1fr}
html.rail-off .app:has(.inspector.open){grid-template-columns:0 1fr 372px}
/* ⚠ `display:none` ON A GRID CHILD SHIFTS EVERYTHING ELSE ONE COLUMN LEFT.
   The rail disappeared — and the workspace became the FIRST item of the grid, i.e. it landed in
   that very column of zero width: the conversation collapsed into nothing (the gate measured
   0px). The window stays in the grid and simply collapses together with its column. */
html.rail-off .sidebar{min-width:0;overflow:hidden;border-right:0;visibility:hidden}
/* ☰ COMES BACK INTO THE HEADER EXACTLY WHEN IT HAS SOMETHING TO OPEN. At the desk with the rail
   open it means nothing and therefore it is not there; the rail was folded — it appeared. */
@media (min-width:861px){
  html.rail-off .topbar #btn-open-side{display:inline-flex!important;align-items:center;justify-content:center}
}
/* The cog moved to the panel's name (app.js → placeTalk) — there it is of the same height as the
   voice button next to it, otherwise the pair reads as «one main one and one accidental one». */
/* THE PANEL'S NAME IS ONE LINE. With the cog moving in, the rail's head came to hold four
   residents in 264 pixels, and «Claude Studio» wrapped onto a second line — that is, the head
   itself said there is no more room. The name is a label, not content: in two storeys it answers
   nothing and takes height away from the task list. */
.brand{flex-wrap:nowrap;gap:4px;white-space:nowrap;overflow:hidden}
.brand > span{min-width:0;overflow:hidden;text-overflow:ellipsis}
.side-head #btn-settings-top{flex:none;width:24px;height:24px;padding:0;margin:0}
.side-head #btn-settings-top .ico{width:14px;height:14px}
/* The assistant and the cog are ONE block on the right, not two different right edges:
   `margin-left:auto` stands only on the FIRST of them. */
/* In the rail's head all four buttons are ONE block of one height, and the first of them pushes
   the group to the right edge. Different sizes would read as «one main one, the rest
   accidental». The paddings are trimmed: the rail is 264px wide and is dragged by hand, and the
   panel's name is obliged to be readable at the narrowest of those widths. */
.side-head{gap:3px;padding:12px 9px 9px 11px}
.side-head .talk-btn{margin-left:auto;width:24px;height:24px;flex:none}
.side-head .talk-btn .talk-ico{width:14px;height:14px}
.rail-fold{width:22px;height:22px;font-size:15px}
.side-head .hard-btn{flex:none;width:26px;height:26px;padding:0}

/* ── «go to the next question» (the owner, 28.09) ──
   The `?` is the ROW's own beacon (`dot('done ask')`), so it is red and it blinks here exactly as
   it does in the rail; the count beside it is the only thing this button adds. No frame and no
   fill: `--acc` in this panel has one job — «I am standing here» — and `--err` belongs to the
   glyph, not to a plate around it (canon 05.09, «an accent spent eleven times is not an accent»).
   ⛔ THE `margin-left:auto` IS HANDED OVER, NEVER WRITTEN TWICE. Two `auto` margins in one flex
   line do not stack — they SPLIT the free space, so the pair would drift apart and the head would
   grow a gap that changes width with the numbers beside it. So the push belongs to whichever of
   the two is FIRST on screen: this button when it is there, the assistant when it is not. The
   `:not([hidden])` is what makes that swap automatic — the button is absent most of the time. */
.asks-btn{display:flex;align-items:center;gap:6px;flex:none;
  height:24px;padding:0 5px;border:none;border-radius:6px;background:none;cursor:pointer;
  color:var(--err);font:800 13px/1 var(--mono)}
.asks-btn:hover{background:var(--bg-3)}
/* ⛔ AND THE `display` HERE IS EXACTLY THE TRAP index.html WARNS ABOUT, MEASURED ON THE FIRST RUN
   OF THE GATE: a flat `display:flex` outranks the browser's own `[hidden]{display:none}`, so the
   button was `hidden` and 10px wide at the same time — present, pressable, saying nothing. The
   whole first sentence of this button is that it is NOT THERE when nothing is asking, so the
   hidden state is written back in by hand and at a higher weight. */
.asks-btn[hidden]{display:none}
/* ⚠ The push is written INSIDE the rail's head only. The same button moves into the
   conversation's topbar when the rail is folded away (`placeTalk`), and an `auto` margin there
   would shove it to the far edge, away from the assistant it travels with. */
.side-head .asks-btn{margin-left:auto}
.side-head .asks-btn:not([hidden]) ~ .talk-btn{margin-left:0}
.asks-n{line-height:1}

/* ── 🧹 «what my rows are waiting for» — the door in the conversation's topbar (web/rows.js) ──
   ⛔ IT DOES NOT LIVE IN THE RAIL'S HEAD, AND THAT WAS MEASURED, NOT JUDGED BY EYE. Put there on
   26.10 it cut the panel's own name from 7px of overflow to 35px — «Claude Studio» → «Cla…» —
   because that head is a fixed 264px carrying five buttons and has no slack to give. The topbar's
   `.title-wrap` is `flex:1`, so here a 25px button is taken out of the slack and costs nothing
   until the window is genuinely narrow. `npm run rowsui` §2 holds the measurement; moving it back
   turns that check red with the two numbers printed beside it. */
.rowq-btn{display:flex;align-items:center;gap:5px;flex:none;
  height:26px;padding:0 6px;border:none;border-radius:7px;background:none;cursor:pointer;
  color:var(--tx-3);font:700 12.5px/1 var(--mono);font-variant-numeric:tabular-nums}
.rowq-btn svg{width:17px;height:17px}
.rowq-btn:hover{background:var(--bg-3);color:var(--tx-2)}
.rowq-btn.has{color:var(--tx-2)}
/* A door read at arm's length on a phone — the same ladder the buttons beside it climb. */
@media (max-width:860px){.rowq-btn{height:32px;font-size:14px;padding:0 7px}.rowq-btn svg{width:20px;height:20px}}

/* ── the board itself ──
   ⛔ `.modal-back` carries `display:grid`, so a veil built in JS stands over the WHOLE page
   while it is «closed» — every press in the panel lands on an invisible sheet and nothing
   errors anywhere. It hides by its own id, exactly like #bugs and #code. `npm run dom` refuses
   a JS-built veil without this pair, and it refused this one on its first run. */
#rowq{display:none}
#rowq.open{display:grid}
#rowq .modal-body{display:flex;flex-direction:column;gap:2px}
/* The wait, drawn: a turning ring, «reading row N of M · S s», and a bar that fills.
   Until the first count arrives the bar sweeps instead of lying about a percentage. */
.rowq-load-wrap{display:flex;flex-direction:column;gap:10px;padding:22px 4px}
.rowq-load{display:flex;align-items:center;gap:10px;font:13px var(--sans);color:var(--tx-2)}
.rowq-load-t{font-variant-numeric:tabular-nums}
.rowq-spin{width:16px;height:16px;flex:0 0 auto;border-radius:50%;
  border:2px solid color-mix(in srgb,var(--tx-3) 40%,transparent);
  border-top-color:var(--tx-2);animation:tree-spin 1.15s linear infinite}
.rowq-bar{height:4px;border-radius:2px;background:var(--bg-3);overflow:hidden;position:relative}
.rowq-bar-fill{height:100%;width:0;background:var(--acc,var(--tx-2));transition:width .3s ease}
.rowq-bar:not(.known) .rowq-bar-fill{width:35%;animation:rowq-sweep 1.4s ease-in-out infinite}
@keyframes rowq-sweep{from{transform:translateX(-100%)}to{transform:translateX(300%)}}
@media (prefers-reduced-motion:reduce){.rowq-spin,.rowq-bar:not(.known) .rowq-bar-fill{animation:none}}
.rowq-days{background:var(--bg-2);border:1px solid var(--bd);color:var(--tx-2);
  border-radius:7px;padding:4px 8px;font:12px var(--sans);cursor:pointer}
.rowq-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:8px;margin:12px 0 4px}
.rowq-big{font:700 30px/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--tx)}
.rowq-cap{font:13px var(--sans);color:var(--tx-3)}
/* ⛔ The denominator is not decoration: without it four trays read as «all your tasks». */
.rowq-den{font:11.5px var(--sans);color:var(--tx-3);flex-basis:100%;line-height:1.5}
.rowq-tray{margin-top:18px}
.rowq-h{display:flex;align-items:baseline;gap:8px}
.rowq-h-t{font:600 13px var(--sans);color:var(--tx-2)}
.rowq-h-n{font:700 12px/1 var(--mono);color:var(--tx-3);font-variant-numeric:tabular-nums}
.rowq-note{font:11.5px var(--sans);color:var(--tx-3);line-height:1.55;margin:3px 0 8px}
.rowq-items{display:flex;flex-direction:column;gap:8px}
.rowq-card{border:1px solid var(--line);border-radius:9px;padding:9px 11px;background:var(--bg-2)}
.rowq-card.sent{opacity:.6}
.rowq-line{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}
.rowq-name{background:none;border:none;padding:0;cursor:pointer;text-align:left;
  font:600 13px var(--sans);color:var(--tx);text-decoration:underline;text-decoration-color:var(--line-2);
  text-underline-offset:3px}
.rowq-name:hover{color:var(--acc)}
.rowq-tag{font:10.5px var(--sans);color:var(--tx-3);border:1px solid var(--line-2);
  border-radius:5px;padding:1px 5px}
.rowq-tag.hot{color:var(--err);border-color:var(--err)}
.rowq-when{margin-left:auto;font:11px var(--mono);color:var(--tx-3)}
.rowq-proj{font:11px var(--mono);color:var(--tx-3);margin-top:2px}
.rowq-why{font:12px var(--sans);color:var(--tx-2);margin-top:5px;line-height:1.5}
/* The context a card carries (27.10): a small label column, then the words. */
.rowq-ctx{display:grid;grid-template-columns:76px 1fr;gap:8px;margin-top:5px;
  font:12px/1.5 var(--sans);color:var(--tx-2)}
.rowq-ctx-l{font:600 10.5px/1.9 var(--sans);color:var(--tx-3);text-transform:uppercase;letter-spacing:.03em}
.rowq-ctx-t{overflow-wrap:anywhere;white-space:pre-line}
.rowq-ctx.asked .rowq-ctx-t{color:var(--tx)}
.rowq-ctx.needs .rowq-ctx-t{color:var(--tx);font-weight:500}
.rowq-ctx.needs .rowq-ctx-l{color:var(--acc)}
.rowq-hint{font:11.5px var(--sans);color:var(--tx-3);margin-top:4px}
.rowq-whole{margin-top:7px;font:12px var(--sans);color:var(--tx-3)}
.rowq-whole summary{cursor:pointer;color:var(--tx-2)}
.rowq-whole-t{margin-top:5px;padding:7px 9px;border:1px solid var(--line);border-radius:7px;
  max-height:340px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;
  font:12px/1.55 var(--sans);color:var(--tx-2);background:var(--bg)}
.rowq-opts{display:flex;flex-direction:column;gap:3px;margin-top:7px}
/* Every option is a button since 27.10 — a press chooses it, Send sends it. */
.rowq-opt{display:grid;grid-template-columns:18px 1fr;gap:6px;align-items:baseline;text-align:left;
  font:12px var(--sans);color:var(--tx-3);line-height:1.45;width:100%;cursor:pointer;
  background:none;border:1px solid transparent;border-radius:7px;padding:4px 6px}
.rowq-opt:hover:not(:disabled){border-color:var(--line-2);background:var(--bg-3)}
.rowq-opt:disabled{cursor:default}
.rowq-opt.picked{color:var(--tx);border-color:var(--acc)}
.rowq-ohead{color:var(--tx-2)}
.rowq-opt.picked .rowq-ohead{color:var(--tx)}
.rowq-omore{color:var(--tx-3)}
.rowq-let{font:700 11px var(--mono)}
.rowq-opt.picked .rowq-let{color:var(--acc)}
.rowq-draft{margin:7px 0 0;padding:7px 9px;background:var(--bg);border:1px solid var(--line);
  border-radius:7px;font:12px/1.5 var(--mono);color:var(--tx);white-space:pre-wrap;word-break:break-word;
  display:block;width:100%;box-sizing:border-box;resize:vertical;min-height:2.8em;max-height:260px}
/* `.modal-body textarea` gives every field in a sheet 300px (it was written for the settings
   editors); a one-line reply must not inherit that. Id-scoped so it outranks the generic rule. */
#rowq .modal-body .rowq-draft{min-height:2.8em}
.rowq-draft:focus{outline:none;border-color:var(--acc)}
.rowq-draft::placeholder{color:var(--tx-3)}
.rowq-draft[readonly]{opacity:.75}
/* His own words, not the board's: the box says so with the accent edge, the button with its label. */
.rowq-card.own .rowq-draft{border-color:var(--acc);border-left-width:3px}
/* 16px on a phone: a smaller field makes iOS zoom the whole page in on the first tap. */
@media (max-width:860px){.rowq-draft{font-size:16px}}
.rowq-acts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}
.rowq-send{background:var(--acc);border:1px solid var(--acc);color:var(--acc-tx,#fff);
  border-radius:7px;padding:5px 13px;font:600 12px var(--sans);cursor:pointer}
.rowq-send:hover:not(:disabled){filter:brightness(1.08)}
.rowq-send:disabled{opacity:.45;cursor:not-allowed}
.rowq-open{background:var(--bg-3);border:1px solid var(--bd);color:var(--tx-2);
  border-radius:7px;padding:5px 11px;font:12px var(--sans);cursor:pointer}
.rowq-open:hover{background:var(--bg-4);color:var(--tx)}
.rowq-ok{font:11.5px var(--sans);color:var(--ok)}
/* A refusal is drawn where the press was. A press that answers nothing is what makes a person
   press again (the ■ lesson, 24.09). */
.rowq-no{font:11.5px var(--sans);color:var(--err);line-height:1.45}
.rowq-quiet{display:flex;flex-wrap:wrap;gap:4px 10px}
.rowq-qname{background:none;border:none;padding:0;cursor:pointer;text-align:left;
  font:11.5px var(--sans);color:var(--tx-3)}
.rowq-qname:hover{color:var(--tx)}
/* On a phone the card is read thumb-first: the press is full width and the age drops under. */
@media (max-width:640px){
  .rowq-when{margin-left:0}
  .rowq-send,.rowq-open{padding:8px 14px;font-size:13px;flex:1 1 auto}
}
/* A question reads at arm's length on a phone or it is not read at all — the same ladder the
   counters beside it climb in the narrow layout. */
@media (max-width:860px){.asks-btn{height:28px;font-size:15px;padding:0 6px}}

/* ── the session's warmth as the header's BACKGROUND ──
   Orange = the cache is still warm, adding to the same session is cheap. Blue = it has cooled
   down, the next turn will start a new session with a digest. Both washes are WEAK and fade away
   to the left as a gradient: a bar in full colour would argue with the conversation itself, and
   this is read with peripheral vision, not with the eyes. `color-mix` takes the tone from the
   theme, so all ten palettes stay themselves. */
/* ⚠ THE WASH IS MIXED INTO `--bg-2`, IT IS NOT LAID OVER TRANSPARENCY: `.topbar` carries
   `background:var(--bg-2)` in one property, and a gradient with `transparent` in its tail would
   not «let the background show through» but would ERASE it — the bar would become a hole into
   the feed beneath it. */
/* ⚠ COLD IS PAINTED WITH `--cold`, NOT WITH `--blue-2` (the owner, 27.08): «the colour is already
   very blue in this project's palette — whether it is blue, whether it has cooled down».
   `--blue-2` is the colour of the «Send» BUTTON, and of the links, and of the waiting strips, and
   in half of the ten palettes it is warm outright (`#e3ce97` in one of them) — that is, «cooled
   down» was drawn in a colour that says nothing about cold in that theme. `--cold` (#5f93b8) is
   overridden by no theme: one steel tone for all ten, and it cannot be confused with anything.
   The percentages are raised — «a little brighter» for both washes. */
html.heat-warm .topbar{background:linear-gradient(90deg,
  color-mix(in srgb, var(--warn) 26%, var(--bg-2)) 0%,
  color-mix(in srgb, var(--warn) 8%, var(--bg-2)) 46%, var(--bg-2) 78%)}
html.heat-cold .topbar{background:linear-gradient(90deg,
  color-mix(in srgb, var(--cold) 28%, var(--bg-2)) 0%,
  color-mix(in srgb, var(--cold) 8%, var(--bg-2)) 46%, var(--bg-2) 78%)}
/* The last ten minutes are the only moment when the colour is obliged to CALL, not to report. */
html.heat-soon .topbar{background:linear-gradient(90deg,
  color-mix(in srgb, var(--warn) 40%, var(--bg-2)) 0%,
  color-mix(in srgb, var(--warn) 13%, var(--bg-2)) 52%, var(--bg-2) 82%)}
/* The cube/flame glyph is removed from the header: the whole bar now says the same thing, and
   «get away from the glyph» was the request itself. The minutes stayed — they are asked for as a
   number. */
.cache-ico{display:none}
.cache{padding-left:0}

/* The head of every workspace window carries the same wash — the same two states, the same tone.
   The classes are set by `workspace.js → mirror`, reading the document INSIDE the frame. */
.pane-head.warm{background:linear-gradient(90deg,
  color-mix(in srgb, var(--warn) 28%, var(--bg-2)) 0%,
  color-mix(in srgb, var(--warn) 9%, var(--bg-2)) 55%, var(--bg-2) 88%)}
.pane-head.soon{background:linear-gradient(90deg,
  color-mix(in srgb, var(--warn) 42%, var(--bg-2)) 0%,
  color-mix(in srgb, var(--warn) 14%, var(--bg-2)) 58%, var(--bg-2) 90%)}
.pane-head.cold{background:linear-gradient(90deg,
  color-mix(in srgb, var(--cold) 30%, var(--bg-2)) 0%,
  color-mix(in srgb, var(--cold) 9%, var(--bg-2)) 55%, var(--bg-2) 88%)}
/* ⚠ THE AIM NO LONGER ERASES THE WARMTH. `.pane.on .pane-head{background:var(--bg-3)}` stood here
   — «the aim is obliged to be more visible than the warmth». The ring in the main window is drawn
   in the same colour and with the same thickness (26.08), so the question «where will I press» is
   already answered by it; while the grey wash made the selected tab the ONLY one without a sign
   of the session — the very difference between tabs that must not exist. The key's reset time
   moved out of here too: it is now the second storey of `.key-chip`, by exactly the same rule as
   in the main window's header. */

/* ── the key switcher on the spend page ──
   A row of one button is not a choice, so it is drawn only when there are two or more
   subscriptions (mobile.js). The buttons are of one height and the full width of the row: they
   are read, not hunted for. */
.key-tabs{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 4px}
.key-tab{display:flex;flex-direction:column;align-items:flex-start;gap:1px;flex:1 1 120px;
  background:var(--bg-3);border:1px solid var(--line-2);border-radius:9px;padding:7px 10px;
  cursor:pointer;color:var(--tx-2);text-align:left;transition:.15s}
.key-tab:hover{border-color:var(--line);color:var(--tx)}
.key-tab.on{border-color:var(--acc);color:var(--tx);background:color-mix(in srgb, var(--acc) 12%, var(--bg-3))}
.kt-n{font:600 12.5px var(--sans)}
.kt-s{font:10.5px var(--mono);color:var(--tx-3)}
.key-tab.on .kt-s{color:var(--tx-2)}

/* The panel's name competes for 264 pixels with four buttons and the connection dot: half a point
   of type size and the removed letter-spacing are the very ten pixels after which «Claude Studio»
   stops being «Claude Stu…» at the rail's narrowest width. */
.side-head .brand{font-size:12.5px;letter-spacing:0}

/* ── what this machine costs the panel (the spend sheet, the last section) ──
   Tiles, bars and a breakdown by groups. A chart's bar carries TWO values: the processor (in the
   accent colour, on top) and the memory (in the cold colour, behind it) — they are compared with
   each other by eye, so they are in one column rather than in two charts between which the gaze
   has to travel. */
.res-box{margin-bottom:6px}
.res-grid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
/* ═══ TWO PLOTS, WHERE THERE USED TO BE ONE STACK OF BARS (05.09) ═══
   The columns are gone, and with them ~40 lines of CSS that positioned a memory bar behind a
   processor bar on a shared height. The processor and the memory have a chart each now, drawn by
   the workshop's shared library (`/lib/mpchart.js`), which brings its own SVG styling with it —
   so what is left here is the frame around a plot and the caption over it. The caption carries
   the UNIT, and that is the one thing a shared library cannot know for us. */
.res-cap{font:600 10px var(--sans);letter-spacing:.9px;text-transform:uppercase;
  color:var(--tx-3);margin:11px 0 3px}
/* ⚠ NO FIXED HEIGHT HERE. The chart is told its height in the spec and lays the legend out under
   it; a host clipped to a round number would cut the legend off on exactly the stretches that
   have two series. */
.res-plot{background:var(--bg-3);border:1px solid var(--line);border-radius:10px;
  padding:5px 7px 6px;margin-bottom:4px}
.res-groups{display:flex;flex-direction:column;gap:5px;margin-bottom:10px}
.res-row{display:grid;grid-template-columns:96px 1fr 66px;grid-template-areas:'n t v' 'n s s';
  align-items:center;gap:2px 8px}
.res-name{grid-area:n;font-size:11.5px;color:var(--tx-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.res-track{grid-area:t;height:6px;background:var(--bg-4);border-radius:4px;overflow:hidden}
.res-track i{display:block;height:100%;background:var(--acc);opacity:.65;border-radius:4px}
.res-val{grid-area:v;font:600 11.5px var(--mono);color:var(--tx);text-align:right}
.res-sub{grid-area:s;font-size:10.5px;color:var(--tx-3)}


/* RECORDING ON A COLLAPSED WINDOW (03.09). The composer now goes away in the middle of a
   dictation too, and the fact of the recording is obliged to stay on screen: the dot, the clock
   and the way back. It stands above the feed but below the lightbox and the drawer — it is a
   status, not a layer. */
.rec-mini{position:fixed;left:50%;transform:translateX(-50%);
  bottom:calc(10px + env(safe-area-inset-bottom, 0px));z-index:118;
  display:flex;align-items:center;gap:8px;padding:7px 15px;border-radius:999px;
  border:1px solid var(--err);background:var(--bg-2);color:var(--err);
  font:700 15px/1 var(--mono);letter-spacing:.5px;
  box-shadow:0 6px 20px rgba(0,0,0,.35);cursor:pointer}
.rec-mini .rm-dot{width:9px;height:9px;border-radius:50%;background:var(--err);
  animation:rm-blink 1.15s ease-in-out infinite}
@keyframes rm-blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.75)}}
@media (min-width:861px){ .rec-mini{display:none} }


/* ── the caption by the «Projects» heading: four figures, two of them buttons ──
   There is no separating dot between the figures: the tiles stand flush (the owner, 03.09 — «the
   dot between them can be removed, to make it a little tighter»). ▸ and ✓ are DOORS: a press
   moves to the next running / next finished row rather than merely reporting a number. */
#tree-total{display:flex;align-items:center;gap:4px}
.tot-n{color:var(--tx-3)}
.tot-pill{display:flex;align-items:center;gap:5px;font:700 11.5px var(--mono);line-height:1;
  padding:2px 3px;border-radius:6px;border:none;background:none;color:var(--tx-2);cursor:default}
/* 🔥 IS A DOOR TOO (the owner, 23.09): a press walks to the next warm task, round and round, so
   it carries the pointer, the hover and the big digit of the two buttons beside it. */
.tot-pill.warm{color:var(--warn);cursor:pointer}
.tot-flame{font-size:11px;line-height:1}
/* There are no frames: an outline around a figure reads as a field, not as a button (the owner,
   03.09 — «do not put frames round either of them, it is not necessary»). They are told apart by
   the glyph and by the colour of the figure. */
.tot-pill.live{color:var(--ok);cursor:pointer}
.tot-pill.done{color:var(--ok);cursor:pointer}
/* THE GAP BETWEEN THE TWO BUTTONS. «Space the finished button and the running button apart a
   little»: they answer DIFFERENT questions, and they stood flush and read as one group of four
   glyphs. */
.tot-pill.done{margin-left:7px}
/* The instrument figures — bolder and bigger than the total session counter: read from a distance. */
.tot-pill.live .tot-num,.tot-pill.done .tot-num,.tot-pill.warm .tot-num{font:800 13px/1 var(--mono)}
.tot-pill.live:hover,.tot-pill.done:hover,.tot-pill.warm:hover{background:var(--bg-3)}

/* ── WHAT THE MACHINE HAS LEFT: CPU, FREE RAM, FREE DISK ──
   Three numbers on the heading's own line, between the counters and the search button, exactly
   where the owner drew them (03.09: «same line where word PROJECTS is»).
   ⚠ WHY THE LABELS ARE ICONS. Spelled out — `CPU 22% RAM 10.1G Disk 103G` — the strip is 239px
   wide and the heading overflowed at the default rail width of 264px: the numbers ran into the
   counters and the search glyph was clipped by the rail's edge. An icon costs 12px where the word
   costs ~30, and that difference is the whole line. Each icon still says its name in `aria-label`
   and again in the card, because an icon nobody can read is a bare number, and a bare number in
   this header was removed once already (the «7d» period chip, 27.08).
   ⚠ THE STRIP IS THE LAST TO SURVIVE, NOT THE FIRST. `.side-label` may WRAP and the strip is
   ordered after everything else, so on a rail dragged narrower than the icons can save it steps
   down onto a line of its own — it never pushes the search button off the edge, and it never
   silently disappears. The wrap is possible only because `#tree-total` shrinks with an ellipsis
   instead of wrapping; a control may not be squeezed, text may. */
.side-label{flex-wrap:wrap;container-type:inline-size}
/* ⛔ THE ORDER IS THE WHOLE MECHANISM, AND IT IS BACK TO FRONT ON PURPOSE. Wrapping follows the
   ORDERED items, not the markup: with the strip ordered before the search button, a strip that
   did not fit dragged the button down onto the second line with it. So by default the strip is
   ordered LAST and takes a full line of its own (`flex-basis:100%`), which leaves the button
   where it has always been — and only when the container proves there is room does the strip come
   back into the line, in its markup position, right after the counters. */
/* TWO HALVES, PUSHED APART. The three numbers on the left are the machine's, the two on the right
   are ours out of them — so when the strip owns a line of its own, `space-between` puts ours
   against the rail's right edge and the sentence «left … right» is literally true on screen. When
   the strip is inline in the heading the box shrinks to its content and the two groups sit side by
   side, told apart by the hairline and by the chips. */
.mach{display:flex;align-items:center;gap:10px;justify-content:space-between;min-width:0;order:9;
  flex-basis:100%;margin:1px 0 -2px;font:700 11px/1 var(--mono);color:var(--tx-3);letter-spacing:0;
  text-transform:none;white-space:nowrap;cursor:default}
.mach[hidden]{display:none}
.mach-g{display:flex;align-items:center;gap:9px;min-width:0}
/* ⛔ THE HALVES MUST NOT LOOK LIKE ONE ROW OF FIVE. Five bare pills read as one list, and then our
   own 3% is read as the machine's — a computer on fire looks calm. Hence a hairline between them
   AND a chip under ours: two separations, because a person glancing at this line does not stop to
   work out which number belongs to whom. */
/* ONE CHIP AROUND BOTH, not a chip on each: the capsule means «the panel», and what is inside it
   is the two quantities. Two chips said the same thing twice and cost 23 pixels of a line that has
   none to spare — measured, on the 420px rail where the second chip pushed the strip past the
   search button. */
.mach-us{gap:9px;padding:2px 7px;background:var(--bg-3);border:1px solid var(--line);
  border-radius:8px}
/* The threshold is measured, not guessed, and it MOVED when the strip gained its second half: three
   pills fitted from a 296px heading, five need 408 (the two ours pills plus their capsule are
   110px). ⛔ It is not a decoration — with the old number the strip went inline at a width it could
   not fit, and what it pushed off the line was the SEARCH BUTTON, down onto a second row. Above the
   threshold the strip sits on the «Projects» line; below it, it takes a line of its own and the
   halves stand at the two edges of the rail. `npm run machine` asserts both sides. */
@container (min-width: 408px){
  .mach{order:0;flex-basis:auto;margin:0}
}
.mach-pill{display:flex;align-items:center;gap:4px;min-width:0}
/* A number that opens the breakdown says so by being pressable — the hand, and a lit background
   under the finger. Everything else in this strip stays exactly as it was. */
.mach-pill.tap{cursor:pointer;border-radius:6px;padding:1px 3px;margin:-1px -3px}
.mach-pill.tap:hover,.mach-pill.tap:focus-visible{background:var(--bg-3);outline:none}
.mach-k{display:flex}
/* 13px, not 11 — and the unit letter that used to sit on every number paid for it («сделаем
   чуть-чуть крупнее цифры… и процессор тоже, чуть-чуть крупнее иконку, чтобы как-то видно
   было», 05.09). The digits went up with them: they are the thing actually read, the icon only
   says WHICH number it is. */
.mach-k svg{width:13px;height:13px;display:block}
.mach-v{color:var(--tx-2);font-weight:800;line-height:1;font-size:12.5px}
/* EACH OF THE THREE HAS ITS OWN COLOUR, from the theme's own palette — never a hard-coded hex, or
   the strip would be the one thing in the panel that does not follow a palette switch. Colour is
   the second thing that tells them apart after the silhouette, and at this size it works from
   further away than the outline does. */
.mach-pill.cpu .mach-k{color:var(--acc)}
.mach-pill.ram .mach-k{color:var(--ok)}
.mach-pill.disk .mach-k{color:var(--cold)}
/* Room is running out — amber; almost gone — red. The whole PILL changes colour, icon included:
   the icon is what the eye lands on first, and an alarm that colours only the digits is read a
   second later than one that colours the shape beside them. The number does not move — a value
   that jumps sideways when it goes bad is read as a layout glitch.
   ⚠ These come AFTER the three palette colours above and match them in specificity, so the alarm
   wins by document order: an amber pill with a still-green icon would be an alarm arguing with
   itself. */
.mach-pill.warn{color:var(--warn)}
.mach-pill.warn .mach-v,.mach-pill.warn .mach-k{color:var(--warn)}
.mach-pill.bad{color:var(--err)}
.mach-pill.bad .mach-v,.mach-pill.bad .mach-k{color:var(--err)}

@media (max-width:860px){
  /* THE PANEL'S NAME IS LARGER. 12.5px was the price paid for the 264 pixels of the rail as a
     COLUMN; on a phone the rail is a full-screen drawer, and a small font buys nothing there
     (the owner, 03.09). */
  .side-head .brand{font-size:16px}
  /* «PROJECTS» IS NO LONGER LETTER-SPACED CAPS. Caps with letter-spacing 1.1px read as a FOREIGN
     font — «as if it were written in another font, this word project». The same --sans and the
     same case as the rows under it, only slightly larger: it is a heading, not a label. */
  #tree-head{font:600 16px var(--sans);letter-spacing:0;text-transform:none;
    color:var(--tx-2);margin-bottom:5px}
  /* THE FIGURES ARE LARGER — AND ALL OF THEM AT ONCE. «For the warm sessions, and for the projects,
     and for these ones as well, make the figures a little larger than the rest» (the owner, 03.09):
     the caption is read with peripheral vision from a phone in the hand, and 11px of monospace is
     the size of a footnote, not of a reading. */
  #tree-total{font-size:13px}
  .tot-pill{font-size:13px;padding:4px 3px;gap:6px}
  .tot-pill.done{margin-left:9px}
  /* TWO FIGURES THAT ARE READ FROM A DISTANCE. «Leave the number of sessions, it is not the most
     interesting thing. But make the number of running ones and finished ones larger and bold — I
     need to see clearly from a distance what is running there and how many more tasks are running»
     (the owner, 03.09). The total number of sessions is a reference, these two are an instrument's
     reading, and the difference in type size is obliged to say so. */
  .tot-pill.live .tot-num,.tot-pill.done .tot-num{font:800 16px/1 var(--mono)}
  /* The head's height is trimmed by the same few pixels that the type size took. */
  .side-head{padding:10px 9px 7px 11px}
  .side-block{padding:0 10px 10px}
}

/* ═════════════ THE LINK CURTAIN — the panel cannot reach its server ═════════════
   Sized against the screen means divided by --ui-scale: everything taken from vw has to
   be, or at his 1.4 the card is wider than the phone (the mine that once drove the
   translate window off the right edge). */
.link-out{position:fixed;inset:0;z-index:260;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(8,10,14,.55);backdrop-filter:blur(3px)}
.link-out.on{display:flex}
.link-card{width:min(400px, calc(94vw / var(--ui-scale,1)));background:var(--bg-2);color:var(--tx-1);border:1px solid var(--line);border-radius:16px;padding:20px 18px 16px;box-shadow:0 18px 48px rgba(0,0,0,.42);text-align:center}
.link-ico{font-size:30px;line-height:1;margin:0 0 8px;color:var(--cold);display:inline-block;animation:link-spin 1.8s linear infinite}
.link-out.warn .link-ico{color:var(--err);animation:link-pulse 1.2s ease-in-out infinite}
.link-card h3{margin:0 0 8px;font-size:17px;font-weight:700;line-height:1.25}
.link-why{margin:0 0 10px;font-size:13.5px;line-height:1.45;color:var(--tx-2)}
.link-state{margin:0 0 11px;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--tx-1)}
.link-safe{margin:0 0 14px;font-size:12.5px;line-height:1.45;color:var(--tx-2);background:var(--bg-3);border-radius:10px;padding:9px 10px;text-align:left}
.link-acts{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
.link-acts button{min-height:38px;padding:0 15px;border-radius:10px;border:1px solid var(--line);background:var(--bg-3);color:var(--tx-1);font:inherit;font-size:13.5px;cursor:pointer}
.link-acts #link-reload{background:var(--blue-2);border-color:transparent;color:#fff}
.link-strip{position:fixed;left:50%;top:6px;transform:translateX(-50%);z-index:255;display:block;max-width:calc(94vw / var(--ui-scale,1));padding:7px 13px;border-radius:999px;background:var(--err);color:#fff;font-size:12.5px;font-weight:700;box-shadow:0 6px 18px rgba(0,0,0,.32);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.link-strip.ok{background:var(--ok,#2e9e5b)}
/* A PLANNED RESTART IS NOT AN ALARM. Red is spent on «something is wrong»; the panel going for
   fresh code is the most ordinary event there is, and painting it in `--err` is what made a
   twelve-second update look like a fault worth a full-screen modal (the owner, 06.09). */
.link-strip.calm{background:var(--cold,#5f93b8)}
.link-strip:hover{filter:brightness(1.08)}
.link-strip[hidden]{display:none}
html.link-off .composer{opacity:.72}
@keyframes link-spin{to{transform:rotate(360deg)}}
@keyframes link-pulse{0%,100%{opacity:1}50%{opacity:.32}}

/* ── FOLDERS IN «GENERAL»: the paths this machine writes into (web/folders.js) ─────────────
   The label stands ABOVE its field, never beside it: a path is forty characters and a two-column
   settings row leaves about ten. Every selector that sets a `display` gets its own `[hidden]`
   pair — `[hidden]{display:none}` is a zero-specificity UA rule and loses to a class, which is
   how the attention triangle once stayed on screen while honestly carrying the attribute. */
#dirs-card{display:grid;gap:7px;margin:10px 0 4px}
#dirs-card[hidden]{display:none}
.dirs-row{display:grid;gap:3px}
.dirs-row label{font-size:12px;color:var(--tx-2)}
.dirs-row label .muted{font-size:11px}
.dirs-in{width:100%;box-sizing:border-box;font:12px/1.45 var(--mono);padding:6px 8px;border:1px solid var(--line);border-radius:8px;background:var(--bg-2);color:var(--tx-1)}
.dirs-in:focus{outline:none;border-color:var(--acc)}
.dirs-in:disabled{opacity:.6;cursor:not-allowed}
.dirs-in.bad{border-color:var(--err)}
.dirs-hint{font-size:11px;line-height:1.5;color:var(--tx-3);word-break:break-all}
.dirs-hint.warn{color:var(--warn)}
.dirs-derived{font:11px/1.6 var(--mono);color:var(--tx-3);white-space:pre-wrap;word-break:break-all;border-left:2px solid var(--line);padding-left:9px}
.dirs-msg{font-size:11.5px;line-height:1.5;border-radius:8px;padding:7px 9px;background:var(--bg-3);color:var(--tx-2)}
.dirs-msg[hidden]{display:none}
.dirs-msg.bad{color:var(--err)}
.dirs-msg.ok{color:var(--ok)}
.dirs-msg.warn{color:var(--warn)}


/* ═══════════ LINES OF CODE: the Spend section and the changed-lines window ═══════════
   The diff is read on a phone as often as on the desktop, so the patch wraps rather than
   scrolls sideways, and the three colours carry the meaning by themselves. */
.code-total{margin:6px 2px 2px;color:var(--fg);font-size:13px}
.code-total b{font-size:15px}
.code-langs{margin:2px 2px 8px;color:var(--dim);font-size:12px}
.code-open{display:block;width:100%;margin:8px 0 4px;padding:9px 12px;background:var(--card);
  border:1px solid var(--line);border-radius:10px;color:var(--fg);font-size:13px;cursor:pointer}
.code-open:hover{border-color:var(--acc);color:var(--acc)}
/* A CLOSED WINDOW MUST NOT STAND OVER THE PAGE. `.modal-back` is display:grid by default, so a
   veil carrying no rule of its own covers the whole viewport at z-index 150 and swallows every
   click — the panel looks alive and answers nothing. Every modal hides by ITS OWN id; this one
   was born without the rule and blocked the page from the moment it was added (05.09). */
#code{display:none}
#code.open{display:grid}
.code-modal{width:min(900px,calc(96vw / var(--ui-scale,1)))}
.cc-ses{margin:10px 0 14px;border:1px solid var(--line);border-radius:12px;overflow:hidden}
.cc-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  padding:9px 12px 4px;background:var(--card)}
.cc-name{font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cc-nums{font-size:12px;color:var(--dim);white-space:nowrap}
.cc-when{padding:0 12px 9px;background:var(--card);color:var(--dim);font-size:11.5px}
.cc-turn{border-top:1px solid var(--line);padding:6px 0}
.cc-th{display:flex;gap:8px;align-items:baseline;padding:2px 12px 6px;font-size:11.5px;color:var(--dim)}
.cc-time{white-space:nowrap}
.cc-prompt{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cc-tnums{white-space:nowrap}
.cc-file{padding:2px 12px}
.cc-file.has .cc-fl{cursor:pointer}
.cc-file.has .cc-fl:hover .cc-path{color:var(--acc)}
.cc-fl{display:flex;gap:8px;align-items:baseline;font-size:12.5px}
.cc-st{width:14px;text-align:center;border-radius:4px;font-size:10px;line-height:14px;color:var(--bg)}
.cc-st.st-A{background:var(--ok,#3fb950)}
.cc-st.st-M{background:var(--acc)}
.cc-st.st-D{background:var(--bad,#f85149)}
.cc-path{flex:1;font-family:var(--mono,ui-monospace,Consolas,monospace);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
.cc-plus{color:var(--ok,#3fb950);white-space:nowrap}
.cc-minus{color:var(--bad,#f85149);white-space:nowrap}
.cc-gist{margin:0 0 4px 22px;color:var(--dim);font-size:11.5px}
.cc-more{padding:2px 12px;color:var(--dim);font-size:11.5px}
.cc-diff{margin:4px 0 8px;border:1px solid var(--line);border-radius:8px;overflow:auto;max-height:52vh}
.cc-pre{font-family:var(--mono,ui-monospace,Consolas,monospace);font-size:11.5px;line-height:1.45}
.cc-pre .dl{padding:0 8px;white-space:pre-wrap;word-break:break-word}
.cc-pre .dl.a{background:color-mix(in srgb,var(--ok,#3fb950) 16%,transparent)}
.cc-pre .dl.d{background:color-mix(in srgb,var(--bad,#f85149) 14%,transparent)}
.cc-pre .dl.h{background:var(--card);color:var(--dim)}
.cc-pre .dl.c{color:var(--dim)}
.cc-cut{padding:4px 8px;color:var(--dim);font-size:11px}


/* ═══ 🐞 THE LADYBUG: FAILED TOOL CALLS ═══
   A chip in the header and the sheet behind it. It sits next to the money on purpose: both
   answer «what did this turn actually cost», and one of the two costs is not paid in tokens. */
.bug-chip{display:flex;align-items:center;gap:3px;background:none;border:none;cursor:pointer;
  padding:2px 7px;border-radius:8px;color:var(--tx-2);transition:.15s}
.bug-chip:hover{background:var(--bg-3)}
.bug-chip[hidden]{display:none}
.bug-ico{font-size:13px;line-height:1;filter:saturate(.85)}
.bug-num{font:600 14px/1 var(--mono);font-variant-numeric:tabular-nums}
/* A THRESHOLD, NOT A GRADIENT. A handful of failures is the weather; a heap means this row is
   fighting its tooling instead of doing the work, and that is a different sentence. */
.bug-chip.hot{color:var(--warn,#c9793a)}
.bug-chip.hot .bug-ico{filter:none}
/* A CLOSED WINDOW MUST NOT STAND OVER THE PAGE. This sheet was born without the line, exactly
   as #code was on 05.09: `.modal-back` is display:grid in the stylesheet, so the veil covered the
   whole viewport at z-index 150 from the moment bugs.js built it. close() takes `.open` off, which
   changed NOTHING visible, so the window could not be dismissed and every click behind it was
   swallowed — the owner met it on 07.09 and had to reload the page to get his panel back.
   `npm run dom` reads veils built in JS now, so the next one cannot be born mute. */
#bugs{display:none}
#bugs.open{display:grid}

#bugs .modal-body{display:flex;flex-direction:column;gap:14px}
.bug-tabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.bug-tab{background:var(--bg-2);border:1px solid var(--bd);color:var(--tx-2);cursor:pointer;
  padding:4px 11px;border-radius:14px;font:13px var(--sans);transition:.15s}
.bug-tab:hover{background:var(--bg-3)}
.bug-tab.on{background:var(--acc);border-color:var(--acc);color:var(--acc-tx,#fff)}
.bug-days{margin-left:auto;background:var(--bg-2);border:1px solid var(--bd);color:var(--tx-2);
  border-radius:8px;padding:3px 6px;font:13px var(--sans)}

.bug-top{display:flex;gap:26px;align-items:baseline}
.bug-big{display:flex;align-items:baseline;gap:7px}
.bug-big.muted{opacity:.55}
.bug-n{font:700 30px/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--tx)}
.bug-cap{font:13px var(--sans);color:var(--tx-3)}

.bug-h{font:600 13px var(--sans);color:var(--tx-2);margin-top:2px}
.bug-h.muted{opacity:.6}
.bug-list{display:flex;flex-direction:column;gap:2px}
.bug-cls{display:grid;grid-template-columns:44px 130px 1fr;gap:9px;align-items:baseline;
  padding:3px 6px;border-radius:6px}
.bug-cls:hover{background:var(--bg-2)}
.bug-cls.muted{opacity:.55}
.bug-cnt{font:600 14px var(--mono);text-align:right;font-variant-numeric:tabular-nums;color:var(--tx)}
.bug-kind{font:13px var(--mono);color:var(--tx-2);overflow:hidden;text-overflow:ellipsis}
.bug-fix{font:12px var(--sans);color:var(--tx-3)}

.bug-items{display:flex;flex-direction:column;gap:7px}
/* THE CARD IS THE POINT OF THE WHOLE WINDOW: «вот такая команда, вот такой результат». A count
   says there is something to look at; only the pair says what. */
.bug-item{border:1px solid var(--bd);border-left:3px solid var(--warn,#c9793a);border-radius:7px;
  padding:6px 9px;background:var(--bg-2)}
.bug-item.guard{border-left-color:var(--tx-3);opacity:.62}
.bug-line{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.bug-tool{font:600 12px var(--mono);color:var(--tx)}
.bug-tag{font:11px var(--mono);color:var(--tx-3);background:var(--bg-3);border-radius:5px;padding:0 5px}
.bug-tag.sub{color:var(--acc)}
.bug-when{margin-left:auto;font:11px var(--sans);color:var(--tx-3)}
.bug-cmd,.bug-err{margin:4px 0 0;font:11px/1.42 var(--mono);white-space:pre-wrap;word-break:break-word;
  max-height:8.6em;overflow:auto}
.bug-cmd{color:var(--tx-2)}
.bug-err{color:var(--warn,#c9793a);opacity:.92}

/* THE REFUSAL IS SHOWN WHERE THE HAND IS. Pressing 🔁 with an empty field and no standing text
   changes nothing, and it used to say so ONLY as a line in the feed — which on a long
   conversation is hundreds of pixels above the button, in grey. Measured 03.09 on a fresh row:
   the notice landed at the TOP of the feed, ~800px from the key that was pressed, and the press
   read as «nothing happens». The field itself now answers: it takes the caret and flashes once,
   so the eye lands on the thing that has to be filled in. */
.input.want{border-color:color-mix(in srgb, var(--acc) 70%, var(--line));
  animation:input-want .9s ease-out 1}
@keyframes input-want{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--acc) 45%, transparent)}
  100%{box-shadow:0 0 0 8px transparent}}
@media (prefers-reduced-motion: reduce){.input.want{animation:none}}

/* ─────────────── the sign-in recovery card (web/app.js → signinCard) ───────────────
   Drawn in the feed, above the row whose work is held, and read on a phone: the link must wrap
   rather than push the card sideways, and everything tappable is a thumb wide. */
.wait-card.signin { border-color: var(--accent); }
.signin-link { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
.signin-url {
  flex: 1 1 220px; min-width: 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--accent); font-size: 12px; line-height: 1.35;
  word-break: break-all; overflow-wrap: anywhere; text-decoration: none;
}
.signin-url:hover { text-decoration: underline; }
.signin-code { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.signin-code input {
  flex: 1 1 200px; min-width: 0; padding: 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--fg); font-size: 14px;
}
/* EVERY BUTTON ON THIS CARD IS A THUMB TALL. The ordinary wait-card buttons are 30px, which is
   fine for a mouse and wrong for the one card that is only ever read on a phone — the browser
   half of `npm run relogin` measures it and went red at 30. */
.signin-code button, .signin-link button, .wait-card.signin .wait-btns button {
  min-height: 38px; padding: 0 14px;
}
@media (max-width: 600px) {
  .signin-code input, .signin-url { flex-basis: 100%; }
  .signin-code button { flex: 1 1 100%; }
}

/* ─────────── the sign-in box in the SETTINGS (web/keys.js → paintSignIn) ───────────
   ⛔ A LINK IS COPIED FAR MORE OFTEN THAN IT IS CLICKED (owner, 28.09): it is forwarded to a phone,
   or to the person whose subscription it is. So the URL is a field-shaped box with a button beside
   it, exactly like the feed's recovery card above — an `<a>` whose text is the address can be
   dragged over with a mouse and cannot be selected with a thumb at all. */
.ksign .sign-urlrow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
.ksign .sign-urlrow > a, .ksign .sign-urlrow > span {
  flex: 1 1 220px; min-width: 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--acc); font-size: 12px; line-height: 1.35;
  word-break: break-all; overflow-wrap: anywhere; text-decoration: none;
}
.ksign .sign-urlrow.dim > span { color: var(--dim, var(--fg)); opacity: .75; }
.ksign .sign-copy { min-height: 38px; padding: 0 14px; white-space: nowrap; }
.ksign.dead > b { color: var(--warn); }
@media (max-width: 600px) {
  .ksign .sign-urlrow > a, .ksign .sign-urlrow > span { flex-basis: 100%; }
  .ksign .sign-copy { flex: 1 1 100%; }
}


/* ═══ 🧠 WHERE THE MEMORY GOES — the window behind the strip's two memory numbers ═══
   One scale for everything: every bar on this screen is a share of the same machine, so a
   length here always means the same thing. The four colours are the four groups of
   server/resources.js and they are the SAME four wherever they appear on this screen. */
/* A CLOSED WINDOW MUST NOT STAND OVER THE PAGE — the third veil in this file to need the line
   said out loud (#code 05.09, #bugs 07.09, this one 17.09, caught by `npm run machine` within the
   minute). `.modal-back` is `display:grid` in the stylesheet, so a veil with no rule of its own
   covers the viewport at z-index 150 and swallows every click; `close()` taking `.open` off would
   change nothing visible. Every modal hides by ITS OWN id. */
#memmap{display:none}
#memmap.open{display:grid}
.mem-modal{width:min(900px,calc(96vw / var(--ui-scale,1)))}
.mem-acts{display:flex;align-items:center;gap:4px}
.mem-top{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:12px}
.mem-big{flex:1 1 240px;background:var(--bg-3);border:1px solid var(--line);border-radius:10px;padding:10px 12px}
/* The half that was pressed is the one answered — it is marked, not merely present. */
.mem-big.on{border-color:var(--acc);background:color-mix(in srgb,var(--acc) 9%,var(--bg-3))}
.mem-big-k{font:12px var(--sans);color:var(--tx-3)}
.mem-big-v{font:700 25px/1.15 var(--mono);font-variant-numeric:tabular-nums;color:var(--tx);margin:3px 0 4px}
.mem-big-n{font:12px/1.5 var(--sans);color:var(--tx-3)}

.mem-bar{display:flex;height:16px;border-radius:8px;overflow:hidden;background:var(--bg);border:1px solid var(--line)}
.mem-seg{display:block;height:100%}
.mem-seg.g-server,.mem-dot.g-server,.mem-gh-dot.g-server,.mem-track i.f-server{background:var(--acc)}
.mem-seg.g-agents,.mem-dot.g-agents,.mem-gh-dot.g-agents,.mem-track i.f-agents{background:var(--blue)}
.mem-seg.g-mcp,.mem-dot.g-mcp,.mem-gh-dot.g-mcp,.mem-track i.f-mcp{background:var(--think)}
.mem-seg.g-engines,.mem-dot.g-engines,.mem-gh-dot.g-engines,.mem-track i.f-engines{background:var(--ok)}
.mem-seg.g-rest,.mem-dot.g-rest,.mem-track i.f-rest{background:var(--tx-3)}
.mem-seg.g-free,.mem-dot.g-free{background:var(--line-2)}

.mem-legend{display:flex;flex-wrap:wrap;gap:4px 14px;margin:8px 0 14px}
.mem-chip{display:flex;align-items:baseline;gap:5px;font:12px var(--sans);color:var(--tx-3)}
.mem-dot{width:9px;height:9px;border-radius:3px;display:inline-block;align-self:center}
.mem-chip-n{color:var(--tx-2)}
.mem-chip-v{font:12px var(--mono);font-variant-numeric:tabular-nums;color:var(--tx-3)}

.mem-h{font:600 13px var(--sans);color:var(--tx-2);margin:16px 0 6px}
.mem-gh{display:flex;align-items:baseline;gap:8px;padding:5px 2px 3px;border-top:1px solid var(--line)}
.mem-gh-dot{width:9px;height:9px;border-radius:3px;align-self:center}
.mem-gh-n{font:600 12.5px var(--sans);color:var(--tx)}
.mem-gh-v{font:600 12.5px var(--mono);font-variant-numeric:tabular-nums;color:var(--tx)}
.mem-gh-s{font:11.5px var(--sans);color:var(--tx-3);margin-left:auto}
/* An empty group is still drawn — dimmed, so «nothing is running there» and «we forgot to count
   it» can never look the same. */
.mem-gh.mem-quiet{opacity:.55}

.mem-list{display:flex;flex-direction:column;gap:2px;margin-bottom:6px}
.mem-row{display:grid;grid-template-columns:minmax(120px,1.5fr) minmax(60px,1fr) 74px 66px;gap:9px;
  align-items:center;padding:3px 4px;border-radius:6px}
.mem-row:hover{background:var(--bg-3)}
.mem-name{font:12.5px var(--sans);color:var(--tx-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mem-track{height:7px;background:var(--bg);border-radius:4px;overflow:hidden}
.mem-track i{display:block;height:100%;background:var(--tx-3);border-radius:4px}
.mem-val{font:12.5px var(--mono);font-variant-numeric:tabular-nums;color:var(--tx);text-align:right}
.mem-sub{font:11.5px var(--mono);color:var(--tx-3);text-align:right}
/* ── THE COLUMN HEADS, AND THE PROCESSOR ──────────────────────────────────────────────────────
   «You even put three crosses on the right. What those three crosses are is not clear, the column
   above is not signed» (the owner, 22.09). The heads share the row grid on purpose — a head list
   with a grid of its own drifts out of line the first time a column is resized. */
.mem-cols{display:grid;grid-template-columns:minmax(120px,1.5fr) minmax(60px,1fr) 74px 66px;gap:9px;
  align-items:end;padding:2px 4px 3px;font:10.5px var(--sans);letter-spacing:.06em;
  text-transform:uppercase;color:var(--tx-3)}
.mem-cols .r{text-align:right}
.mem-cpu-now{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;padding:4px 4px 8px}
.mem-cpu-big{font:600 20px var(--mono);font-variant-numeric:tabular-nums;color:var(--tx)}
.mem-cpu-n{font:12px var(--sans);color:var(--tx-2)}
.mem-cpu-bar{flex:0 0 100%;display:flex;height:9px;background:var(--bg);border-radius:5px;
  overflow:hidden;margin-top:2px}
.mem-cpu-bar .mem-seg{height:100%}
.mem-seg.mem-unnamed{background:var(--tx-3);opacity:.45}
.mem-name.mem-2{white-space:normal}
.mem-name.mem-2 .mem-sub2{display:block;margin-top:1px;font:11px var(--sans);color:var(--tx-3);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mem-row.mem-hot .mem-val{color:var(--tx);font-weight:600}
.mem-row.mem-click{cursor:pointer}
.mem-row.mem-click:hover{background:var(--bg-3)}
.mem-row.mem-click:hover .mem-name,.mem-row.mem-click:focus-visible .mem-name{text-decoration:underline}
@media (max-width:560px){
  .mem-cols{grid-template-columns:1fr 62px 54px;gap:7px}
  .mem-cols .c-track{display:none}
}
@media (max-width:560px){
  .mem-row{grid-template-columns:1fr 62px 54px;gap:7px}
  .mem-track{display:none}
}

/* ═══════════ THE PROJECT'S OWN VOICE — the assistant over a TASK (owner, 24.09) ═══════════
   «Вот тут должна появиться голосовая кнопочка, уже над самой задачей, а не над панелью.»
   The button lives among the meters of every window header (`headBits` in web/workspace.js and
   the main topbar), so it borrows `.icon-btn` and adds only what makes it a MICROPHONE: it
   lights in the clay accent while a conversation is open, because a hidden sheet and a
   switched-off assistant look identical, and talking into a switched-off microphone is the one
   failure this panel has already paid for twice. */
.cmp-btn{font-size:14px;line-height:1;opacity:.72}
.cmp-btn:hover{opacity:1}
.cmp-btn.on{opacity:1;color:var(--acc);box-shadow:0 0 0 1px var(--acc) inset}

.cmp-back{position:fixed;inset:0;background:var(--veil);z-index:70;display:flex;
  align-items:center;justify-content:center;padding:16px}
/* ⛔ THE DESKTOP WINDOW IS RESIZABLE (owner, 26.09: «в десктопной версии сделать, чтобы можно
   было менять размер окна, чтобы было больше видно диалога»). `resize` is inert unless `overflow`
   is anything but `visible` — hence `hidden` here, which costs nothing because the log scrolls
   inside its own box. The floors stop him dragging the sheet into something unusable; the ceiling
   is the viewport. The phone overrides all of it below. */
.cmp-box{width:min(560px,100%);max-height:min(92vh,1400px);display:flex;flex-direction:column;
  gap:10px;background:var(--bg-2);border:1px solid var(--line-2);border-radius:var(--r);
  box-shadow:0 18px 60px var(--shadow);padding:14px;
  resize:both;overflow:hidden;min-width:420px;min-height:340px;max-width:96vw}
.cmp-head{display:flex;align-items:flex-start;gap:10px}
.cmp-headwrap{flex:1;min-width:0}
.cmp-title{font:600 15px/1.25 var(--sans);color:var(--tx)}
.cmp-proj{font:500 12px/1.4 var(--figures);color:var(--acc);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.cmp-x{flex:0 0 auto}

.cmp-engines{display:flex;gap:8px}
.cmp-engine{flex:1;display:flex;flex-direction:column;gap:2px;align-items:flex-start;
  padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--bg-3);
  color:var(--tx-2);cursor:pointer;text-align:left}
.cmp-engine b{font:600 13px/1.2 var(--sans);color:var(--tx)}
.cmp-engine small{font:400 11px/1.3 var(--figures);color:var(--tx-3)}
/* The price lives on the face of the button, because the sentence under the switch speaks
   about the CHOSEN road alone — two of the three faces never quoted a figure at all. */
.cmp-engine i.cmp-cost{font:600 11px/1.3 var(--figures);color:var(--tx-2);font-style:normal}
.cmp-engine.on i.cmp-cost{color:var(--acc)}
.cmp-engine.off i.cmp-cost{color:var(--warn)}
.cmp-engine:hover{border-color:var(--line-2)}
.cmp-engine.on{border-color:var(--acc);background:var(--bg-4)}
/* ⛔ THE FACE AND THE BUBBLE WEAR THE SAME COLOUR, or the colour explains nothing: a tint on an
   answer only says «which road» if the road's own button is marked with it too. A stripe rather
   than a fill, because `.on` already owns the background. */
.cmp-engine[data-engine="live"]{box-shadow:inset 3px 0 0 rgba(56,150,255,.8)}
.cmp-engine[data-engine="gem"]{box-shadow:inset 3px 0 0 rgba(150,110,245,.85)}
.cmp-engine[data-engine="claude"]{box-shadow:inset 3px 0 0 rgba(240,186,64,.85)}
.cmp-engine[data-engine="local"]{box-shadow:inset 3px 0 0 rgba(66,190,120,.85)}
.cmp-engine[data-engine="cloud"]{box-shadow:inset 3px 0 0 rgba(236,110,172,.85)}
.cmp-engine.on b{color:var(--acc)}

/* The money he asked to see before the first word, and the reason it is not a chip: it is a
   SENTENCE — what the minute buys and what ten of them cost — and a figure with no sentence
   round it is the figure nobody believes the moment it looks large. */
.cmp-price{font:400 11.5px/1.45 var(--figures);color:var(--tx-2);background:var(--bg-3);
  border:1px solid var(--line);border-radius:8px;padding:8px 10px}
.cmp-price.warn{color:var(--warn);border-color:var(--warn);background:var(--warn-bg)}

/* ═══ THE WARM-UP, NARRATED — «должны крутиться большие часики» (owner, 25.09) ═══
   He must be able to tell, without talking first, whether anything is listening. The strip is
   one line of prose plus one row per leg of the road; the clock turns only while something is
   genuinely pending, because a spinner that always turns is decoration and stops being read. */
.cmp-strip{display:flex;align-items:flex-start;gap:12px;padding:9px 11px;border-radius:10px;
  border:1px solid var(--line);background:var(--bg-3)}
.cmp-spin{position:relative;width:34px;height:34px;flex:0 0 34px;display:grid;place-items:center}
.cmp-spin-ico{font-size:17px;line-height:1}
.cmp-spin-ring{position:absolute;inset:0;border-radius:50%;
  border:2px solid var(--line);border-top-color:var(--acc);opacity:0}
.cmp-strip.busy .cmp-spin-ring{opacity:1;animation:cmp-spin 1.1s linear infinite}
.cmp-strip.busy .cmp-spin-ico{animation:cmp-tick 2.2s steps(12) infinite}
@keyframes cmp-spin{to{transform:rotate(360deg)}}
@keyframes cmp-tick{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  .cmp-strip.busy .cmp-spin-ring,.cmp-strip.busy .cmp-spin-ico{animation:none}
  .cmp-strip.busy .cmp-spin-ring{opacity:.6}
}
.cmp-legs{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.cmp-stat{font:600 12px/1.4 var(--sans);color:var(--tx)}
.cmp-legrows{display:flex;flex-direction:column;gap:2px}
.cmp-leg{display:flex;align-items:baseline;gap:7px;font:400 11px/1.4 var(--figures);color:var(--tx-3)}
.cmp-leg.ok .cmp-leg-ico{color:var(--ok)}
.cmp-leg.bad .cmp-leg-ico{color:var(--warn)}
.cmp-leg-name{color:var(--tx-2);min-width:74px}
.cmp-leg-note{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ═══ «ЧТО-ТО ДОЛЖНО БОЛЕЕ ЯВНО ГОВОРИТЬ О ТОМ, ЧТО РАЗГОВОР ИДЁТ» (owner, 25.09) ═══
   The button's pulse says the dialog is alive; it cannot say «I can hear you THIS second». The
   bar is the analyser that decides the endpoint, drawn — so it moves with his voice and cannot
   lie about hearing him — and `.closing` with the countdown is the hush running down, which is
   the pause made watchable instead of ambushing. */
.cmp-rec{display:flex;align-items:center;gap:9px;padding:7px 10px;border-radius:10px;
  border:1px solid var(--line);background:var(--bg-3);opacity:.5}
.cmp-rec.rec-on{opacity:1}
.cmp-rec-dot{width:9px;height:9px;flex:0 0 9px;border-radius:50%;background:var(--tx-3)}
.cmp-rec.rec-on.rec-hear .cmp-rec-dot{background:var(--err);animation:cmp-beat 1.25s ease-in-out infinite}
.cmp-rec.rec-on.loud .cmp-rec-dot{animation-duration:.6s}
.cmp-rec.rec-on.rec-think .cmp-rec-dot{background:var(--warn);animation:cmp-beat 2s ease-in-out infinite}
.cmp-rec.rec-on.rec-talk .cmp-rec-dot{background:var(--acc);animation:cmp-beat 1.6s ease-in-out infinite}
@keyframes cmp-beat{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.55);opacity:.45}}
@media (prefers-reduced-motion:reduce){ .cmp-rec .cmp-rec-dot{animation:none} }
.cmp-rec-txt{flex:1;min-width:0;font:600 11.5px/1.4 var(--sans);color:var(--tx-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cmp-lvl{width:76px;height:6px;flex:0 0 76px;border-radius:3px;background:var(--bg-4);overflow:hidden}
.cmp-lvl-bar{display:block;width:2%;height:100%;border-radius:3px;background:var(--ok);
  transition:width .06s linear}
.cmp-rec.rec-hear.loud .cmp-lvl-bar{background:var(--err)}
.cmp-cd{font:400 10.5px/1 var(--figures);color:var(--warn);min-width:30px;text-align:right}
.cmp-rec.closing{border-color:var(--warn)}

.cmp-log{flex:1;min-height:120px;overflow:auto;display:flex;flex-direction:column;gap:8px;
  padding:4px 2px}
.cmp-row{display:flex;flex-direction:column;gap:3px;max-width:92%}
.cmp-me{align-self:flex-end;align-items:flex-end}
.cmp-ai,.cmp-err{align-self:flex-start}
.cmp-txt{font:400 13px/1.45 var(--sans);padding:7px 10px;border-radius:10px;white-space:pre-wrap}
.cmp-me .cmp-txt{background:var(--bg-4);color:var(--tx)}
/* His row, reserved the moment he stops speaking and waiting for the transcription that OpenAI
   finishes after the answer has started (web/companion.js -> mePending). Dimmed, never empty. */
.cmp-row.cmp-me.live .cmp-txt{opacity:.5;font-style:italic;animation:cmp-wait 1.4s ease-in-out infinite}
@keyframes cmp-wait{0%,100%{opacity:.32}50%{opacity:.62}}
@media (prefers-reduced-motion:reduce){ .cmp-row.cmp-me.live .cmp-txt{animation:none} }
.cmp-ai .cmp-txt{background:var(--bg-3);color:var(--tx);border:1px solid var(--line)}
.cmp-err .cmp-txt{background:var(--err-bg);color:var(--err);border:1px solid var(--err-bd)}
.cmp-meta{font:400 10.5px/1.3 var(--figures);color:var(--tx-3)}
/* ── which model said this, and in which road's colour ─────────────────────────
   Owner, 26.09: «ответы разных моделей желательно бы сделать разными цветами. С легонько
   жёлтым, легонько зелёным, легонько розовым там и легонько голубым… все четыре модели должны
   отличаться цветом и ответы, какой дала какая модель, тоже должны отличаться.»
   ⛔ THE TINT IS AN ALPHA OVERLAY ON `--bg-3`, NEVER A FIXED COLOUR. Eleven palettes live in this
   file, six of them light; a literal `#fff8e1` is legible in two of them and invisible in the
   rest. 13% of a hue over the theme's own surface is «легонько» in all eleven. */
.cmp-who{font:600 9.5px/1.2 var(--figures);letter-spacing:.06em;text-transform:uppercase;
  color:var(--tx-3);padding-left:2px}
.cmp-e-live   .cmp-txt{background:linear-gradient(rgba(56,150,255,.13),rgba(56,150,255,.13)),var(--bg-3);
  border:1px solid rgba(56,150,255,.42)}
.cmp-e-gem    .cmp-txt{background:linear-gradient(rgba(150,110,245,.14),rgba(150,110,245,.14)),var(--bg-3);
  border:1px solid rgba(150,110,245,.44)}
.cmp-e-claude .cmp-txt{background:linear-gradient(rgba(240,186,64,.15),rgba(240,186,64,.15)),var(--bg-3);
  border:1px solid rgba(240,186,64,.45)}
.cmp-e-local  .cmp-txt{background:linear-gradient(rgba(66,190,120,.14),rgba(66,190,120,.14)),var(--bg-3);
  border:1px solid rgba(66,190,120,.42)}
.cmp-e-cloud  .cmp-txt{background:linear-gradient(rgba(236,110,172,.14),rgba(236,110,172,.14)),var(--bg-3);
  border:1px solid rgba(236,110,172,.42)}
.cmp-e-live   .cmp-who{color:rgb(70,155,255)}
.cmp-e-gem    .cmp-who{color:rgb(138,102,238)}
.cmp-e-claude .cmp-who{color:rgb(196,145,30)}
.cmp-e-local  .cmp-who{color:rgb(48,158,96)}
.cmp-e-cloud  .cmp-who{color:rgb(214,88,150)}

/* ── the search over the conversation ─────────────────────────────────────────
   It FOLDS rows away and marks the ones it found; nothing is removed, so clearing the field
   gives the whole conversation back exactly as it was. */
.cmp-findbar{display:none;align-items:center;gap:6px}
.cmp-findbar.open{display:flex}
.cmp-findin{flex:1;min-width:0;padding:6px 9px;border:1px solid var(--line-2);border-radius:8px;
  background:var(--bg-3);color:var(--tx);font:400 12.5px/1.3 var(--sans)}
.cmp-findin:focus{outline:none;border-color:var(--acc)}
.cmp-findn{font:400 11px/1.3 var(--figures);color:var(--tx-3);min-width:52px;text-align:right}
.cmp-find-btn.on{color:var(--acc);box-shadow:0 0 0 1px var(--acc) inset}
.cmp-row.cmp-hid{display:none}
.cmp-row.cmp-hit .cmp-txt{box-shadow:0 0 0 2px var(--acc)}
.cmp-hint{font:400 11.5px/1.4 var(--figures);color:var(--tx-3);min-height:16px}

.cmp-foot{display:flex;align-items:center;gap:10px}
.cmp-talk{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;
  padding:11px 14px;border:1px solid var(--acc);border-radius:10px;background:var(--acc);
  color:var(--on-acc);font:600 14px/1 var(--sans);cursor:pointer}
.cmp-talk:hover{background:var(--acc-2)}
/* ⛔ `.on` IS THE DIALOG BEING ALIVE — it is put on when the conversation starts and taken off
   only by his press, the sheet closing, or the live road ending. It used to follow the RECORDER,
   so the one sign this button has went dark through thinking and through speaking (owner, 25.09:
   «раз и выключается»). What changes between turns is the phase below: a different colour and a
   different pulse period, so one glance answers «still on?» and «doing what?» at once. */
.cmp-talk.on{background:var(--ok);border-color:var(--ok);color:var(--on-ok);
  animation:cmp-pulse 1.15s ease-in-out infinite}
@keyframes cmp-pulse{0%,100%{opacity:1}50%{opacity:.62}}
/* listening — the fastest pulse, because it is the phase he is acting in */
.cmp-talk.on.ph-hear{background:var(--ok);border-color:var(--ok);color:var(--on-ok);
  animation-duration:1.15s}
/* thinking — amber, slower: it is working and he need not */
.cmp-talk.on.ph-think{background:var(--warn);border-color:var(--warn);color:var(--on-warn,#231a00);
  animation-duration:1.9s}
/* speaking — the accent, slowest: talk over it whenever you like */
.cmp-talk.on.ph-talk{background:var(--acc);border-color:var(--acc);color:var(--on-acc,#fff);
  animation-duration:2.6s}
/* recovering — the microphone refused, or the road is down; it is coming back by itself */
.cmp-talk.on.ph-wait{background:var(--bg-4);border-color:var(--warn);color:var(--tx);
  animation-duration:3.2s}
@media (prefers-reduced-motion:reduce){ .cmp-talk.on.ph-hear,.cmp-talk.on.ph-think,
  .cmp-talk.on.ph-talk,.cmp-talk.on.ph-wait{animation:none} }
.cmp-talk-ico{font-size:15px}
/* The meter is the same figure the price line quotes, only MEASURED — so it wears the figures
   face, not the prose one, and it never disappears once money has been spent. */
.cmp-meter{font:400 11.5px/1.3 var(--figures);color:var(--tx-2);white-space:nowrap}

@media (max-width:560px){
  .cmp-back{padding:0;align-items:stretch}
  /* ⛔ AND A DESKTOP SIZE HE DRAGGED MAY NEVER REACH THE PHONE («в мобильной версии всё
     правильно, на весь экран и вариантов никаких»). The native handle writes an INLINE width and
     height, and inline beats a media query — so these two are the one place in this file where
     `!important` is the mechanism rather than a shortcut. `web/companion.js → applySize` clears
     them as well, on open and on every viewport change; both halves are needed, because only the
     stylesheet can win against a size the browser itself wrote during a drag. */
  .cmp-box{resize:none;width:100%!important;height:auto!important;max-height:100%;
    min-width:0;min-height:0;border-radius:0;border:0}
  /* ⛔ ON A PHONE THE EXPLANATIONS ATE THE CONVERSATION (owner, 25.09: «все описания заняли
     весь экран, я даже почти не вижу чата… нужно оставить сверху просто краткое название
     модели»). Four subtitles and a five-line price paragraph is most of a 780px screen, above
     the very chat he opened the sheet to read. They are FOLDED, not deleted: a second tap on
     the road he is on, or a long press on any other, sets `.detail` / `.open` and gives back
     exactly the text that used to stand here — for that one road, on demand, once.
     Desktop is untouched: it has the room, and nothing there was ever in the way. */
  .cmp-engine small{display:none}
  .cmp-engine.detail small{display:block}
  .cmp-price{display:none}
  .cmp-price.open{display:block}
  /* Four faces across 390px: the name over its figure, and no padding spent on a subtitle
     that is no longer there. ⛔ Never `flex-direction:row` here — «Local» beside «$0.00 / min»
     is 150px of button and the fourth road falls off the screen. */
  .cmp-engine{padding:7px 8px}
  .cmp-engine b{font-size:12.5px}
  .cmp-engine i.cmp-cost{font-size:10.5px}
}
@media (prefers-reduced-motion:reduce){ .cmp-talk.on{animation:none} }

/* ── the companion's live road ─────────────────────────────────────────────── */
/* An engine this machine cannot reach is dimmed and says why in the line below, never hidden:
   a road that vanishes is a road he thinks he imagined. */
.cmp-engine.off{opacity:.45}
.cmp-engine.off b{color:var(--tx-3)}
/* Her line while it is still being spoken — the caret is the only thing that says «not yet». */
.cmp-row.cmp-ai.live .cmp-txt::after{content:'▌';color:var(--acc);animation:cmp-caret 1s steps(1) infinite;margin-left:1px}
@keyframes cmp-caret{50%{opacity:0}}
/* The knowledge base being consulted mid-sentence — quieter than either speaker. */
.cmp-row.cmp-tool .cmp-txt{font:400 11.5px/1.45 var(--figures);color:var(--tx-3);
  background:var(--bg-3);border:1px solid var(--line);border-radius:8px;padding:5px 8px}
/* The live line is open: the button is the way OUT of it, so it reads as one. */
.cmp-talk.live{background:var(--err);border-color:var(--err);color:var(--on-err,#fff)}
.cmp-talk.live:hover{filter:brightness(1.08)}
/* ...and it brightens while he is the one talking, so the barge-in is visible, not only heard. */
.cmp-talk.live.hearing{background:var(--ok);border-color:var(--ok);color:var(--on-ok)}
@media (prefers-reduced-motion:reduce){ .cmp-row.cmp-ai.live .cmp-txt::after{animation:none} }


/* ═══ the one microphone: the thinking line, the idle countdown, the dictation strip ═══
   Owner, 01.10.2026. The thinking row is deliberately the quietest thing in the log — it is
   temporary by construction and must not read like an answer. */
.cmp-think .cmp-txt { opacity: .66; font-style: italic; }
.cmp-idle { margin-left: auto; font-size: 11px; opacity: .72; white-space: nowrap; }
.cmp-dict { display: none; margin: 6px 0 2px; padding: 8px 10px; border-radius: 10px;
  border: 1px solid rgba(201, 111, 63, .45); background: rgba(201, 111, 63, .10); }
.cmp-dict.on { display: block; }
.cmp-dict-h { font-size: 11px; letter-spacing: .04em; opacity: .75; margin-bottom: 4px; }
.cmp-dict-txt { font-size: 13px; line-height: 1.35; max-height: 7.5em; overflow: auto; word-break: break-word; }
.cmp-dict-btns { display: flex; gap: 8px; margin-top: 8px; }
.cmp-dict-go, .cmp-dict-no { font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 8px; cursor: pointer; }
.cmp-dict-go { background: var(--accent, #c96f3f); color: #fff; border: 0; }
.cmp-dict-no { background: transparent; color: inherit; border: 1px solid rgba(128, 128, 128, .45); }


/* ══════════ THE PHONE'S HEADER HOLDS FIVE THINGS — NOTHING ELSE (owner, 19.10.2026) ══════════
   «Make it so that on the mobile version these back-forward arrows are gone, and that the version,
   the number of turns, MCP, tools and the versions are not printed. It does not fit anyway, so up
   there only the three stripes, the project's name and the task's name are left. Yes, and this
   count of red bugs — take that out for the mobile version only. And this number of minutes on the
   right — take that out too. For now only the microphone and the percentages stay.»

   He said it over a photograph of his own header: the grey footnote had wrapped onto FIVE lines and
   pushed the two things this bar exists for off the edge. What is left, left to right:
   ☰ · the task's name · the project's name · 🎤 · the key percentage and its bar.

   ⛔ EVERY RULE HERE IS A TWO-CLASS OR AN ID SELECTOR, AND THIS BLOCK IS LAST IN THE FILE. A flat
   `.nav-pair{display:none}` carries the same specificity as the `.nav-pair{display:flex}` further
   up and would be decided by source order alone — which is exactly how a healthy build chip
   refused to leave this same phone on 18.10 and how ☰ stood on the desktop for months.
   ⛔ AND NOTHING IS REMOVED FROM THE DOM, NOR HIDDEN WITH THE `hidden` ATTRIBUTE. `workspace.js →
   mirror` reads `#cache-left` and `#week-left` out of the live nodes to paint a window's head, and
   `renderStats` reads the hidden meters' `textContent`; a `display:none` node answers both, a
   removed one answers neither. The panes need no carve-out of their own either —
   `html.pane .topbar` is already `display:none!important`, so a narrow pane never saw this bar. */
@media(max-width:860px){
  /* The way out of a row on a phone is the drawer, not a 26px chevron: ☰ stands immediately to the
     left of these two and leads to the very list the arrows walked. The trail itself is untouched —
     still recorded, still replayed, and still two real buttons at the desk. */
  .topbar .nav-pair{display:none}
  /* The whole technical footnote under the task's name: ×N calls, N turns, N MCP, N tools, the
     model, the CLI version, the build id — and the 🐞 counter that rode with them. ⛔ The project is
     NOT inside `.sub-rest` (`setSub`/`paintSub` put it in `.sub-proj`), so it stays: it is the one
     word on this line that says WHERE the work is happening, and he asked for it by name. */
  .chat-sub .sub-rest{display:none}
  /* 🐞 and the minutes of warmth. The heat itself still speaks here and costs no width at all — it
     is the WASH across the whole bar (`html.heat-warm .topbar`), which is what he asked for on
     25.08 when the glyph was taken out of this corner. */
  .topbar #bug-meter{display:none}
  .topbar #cache-meter{display:none}
}

/* THE HEADER'S NAME CARD (owner, 31.10): a tap on the title area on a phone drops both names
   WHOLE from the top, with «Rename task» editing the task line in place (`titleCard`, web/app.js).
   Two classes on the backdrop so it outranks the flat `.modal-back` centring and the phone padding. */
.modal-back.title-back{place-items:start center;padding:calc(env(safe-area-inset-top,0px) + 8px) 8px 8px}
.title-card{padding:12px 14px 14px;animation:tc-drop .18s ease-out;display:flex;flex-direction:column;gap:10px}
@keyframes tc-drop{from{transform:translateY(-14px);opacity:0}to{transform:none;opacity:1}}
.tc-head{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--tx-2);font-size:12.5px}
.tc-x{min-width:38px;min-height:38px;border:none;background:none;color:var(--tx-2);font-size:17px;cursor:pointer;border-radius:8px}
.tc-lab{font:11px var(--mono);color:var(--tx-3);text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px}
.tc-val{font-size:15.5px;font-weight:600;line-height:1.3;overflow-wrap:anywhere;color:var(--tx)}
.tc-proj .tc-val{color:var(--acc)}
.tc-path{font:11px var(--mono);color:var(--tx-3);overflow-wrap:anywhere;margin-top:2px}
.tc-err .tc-in{border-color:var(--err)}
.tc-in{width:100%;box-sizing:border-box;font:inherit;font-size:15px;font-weight:500;padding:8px 10px;border-radius:8px;
  border:1px solid var(--acc);background:var(--bg-1);color:var(--tx);resize:none}
.tc-acts{display:flex;justify-content:flex-end;gap:8px}
.tc-acts .btn-soft,.tc-acts .btn-primary{min-height:40px;padding:8px 16px;font-size:14px}
.tc-rename:disabled{opacity:.5;cursor:default}
@media(max-width:860px){ .topbar .title-wrap{cursor:pointer} }
