
/* ---- dark Gruvbox (UI-10) -------------------------------------------------
   The official dark palette: bg0 282828, bg0_h 1d2021, bg1 3c3836, bg2 504945,
   bg3 665c54, bg4 7c6f64, fg0 fbf1c7, fg1 ebdbb2, fg4 a89984, red fb4934,
   green b8bb26, yellow fabd2f, blue 83a598, purple d3869b, aqua 8ec07c,
   orange fe8019 — and the neutral (grey) variants used for card fills.
   Card colours (UI-17) are the grey dim of the matching bright colour, so the
   hue reads at a glance but the text stays the same fg1 everywhere. */
:root{
  --bg:#282828; --bg0h:#1d2021; --bg1:#3c3836; --bg2:#504945; --bg3:#665c54;
  --bg4:#7c6f64; --line:#3c3836; --line2:#504945;
  /* WCAG 1.4.3: the section boxes and the dimmed trail. `--well` is the
     plan/work fill, darkened from --bg2 so the muted text in them reaches 4.5:1;
     `--stale` replaces the old `opacity` dimming, which dropped the trail below AA
     on every surface it sat on (2.19:1 on --bg2, 1.85:1 for a whole card). */
  --well:#443f3c; --stale:#b9b4ac;
  --text:#ebdbb2; --bright:#fbf1c7; --dim:#a89984; --dim2:#bdae93;
  /* WCAG 1.4.3: `--red` (#fb4934) is 3.4:1 on a card and 3.0:1 inside a plan/work box
     — right for a 1px edge, under the 4.5:1 floor for a word. Every red on this page
     that is read as text (an end badge, a stopped tool line, a rejected file, a form
     or terminal error) wears `--red-text` instead: 5.1:1 on a card, 4.6:1 in the
     deepest box it can sit on. Borders, dots and bars keep `--red`. */
  --red:#fb4934; --red-text:#f59184;
  --green:#b8bb26; --yellow:#fabd2f; --blue:#83a598;
  --purple:#d3869b; --aqua:#8ec07c; --orange:#fe8019;
  --accent:#fe8019;
  /* UI-17: one pair per card kind — fill (grey dim) and edge (bright). */
  --c-user:#403520;  --c-user-edge:#7c6426;
  --c-order:#403239; --c-order-edge:#7a4e5d;
  --c-final:#33351f; --c-final-edge:#6c6828;
  --c-agent:#333836; --c-agent-edge:#5c6f66;
  --read-size:14px; --read-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI","Noto Sans Thai","Leelawadee UI",Tahoma,sans-serif;
  /* UI-41: the tool trail's own size. Tool lines are metadata about the work, not
     the work itself — 11px suits them at default, but a reader who wants the calls
     bigger must not have to enlarge the answer to get them, so this is a third
     preference beside the reading size rather than a part of it. */
  --tool-size:11px;
  /* UI-25: what the Raw listing is drawn in. The family stays monospace until the
     bar's Mono/Sans button says otherwise — a listing with proportional characters
     loses its column alignment, which is the one thing code needs. */
  --code-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg0h);color:var(--text);
  font:14px/1.55 var(--read-family)}
/* #app is a grid, not a free-flowing page: the composer is a row of its own
   track, so it cannot be pushed below a long stream (UI-12). The row is
   `minmax(0,1fr)`, not the implicit `auto`, or a tall stream would stretch the
   row past the viewport and take the composer with it.
   The third column is the file panel (UI-19). A file no longer covers the window:
   it opens as a tab in the middle of the screen beside the session tabs (UI-27). */
#app{position:relative;display:grid;grid-template-columns:var(--side-w,290px) minmax(0,1fr) auto;
  grid-template-rows:minmax(0,1fr);height:100vh;height:100dvh;overflow:hidden}
/* UI-51: the session list is hideable now, and hiding it has to take its track with
   it. `hidden` alone cannot do that — a grid item removed from the flow still leaves
   its column standing, which is exactly why the list was never offered a hide button.
   The class collapses the track to nothing instead; the panel's own `display:none`
   below is what stops it painting inside the zero-width column. */
#app.sideHidden{grid-template-columns:0 minmax(0,1fr) auto}
/* The three panels are placed by number, not left to auto-placement. Auto-placement
   fills the tracks *in order*, so when the list is hidden the middle column is placed
   first and lands in the 0px track — the whole app measured 32px wide in Chrome with
   the list put away, because `#main` was in the collapsed track and the file panel
   got the flexible one. A layout that only works while a panel is visible is not a
   layout; the track each panel belongs in is a fact about the page, not about what
   happens to be on screen. */

/* UI-44: the sidebar is text the reader reads too — the session list, the agent
   list, the runtime lines — so it wears the same two preferences the header and
   the file panel wear. `body` pins a 14px ui-sans-serif, so without this the
   sidebar alone ignored every move of the controls sitting inside it. */
#side{grid-column:1;display:block;background:var(--bg0h);border-right:1px solid var(--line);padding:14px;
  overflow:auto;font-family:var(--read-family);font-size:var(--read-size)}
/* UI-51: the list is toggled with the `hidden` attribute like every other layer here,
   and `#side` sets `overflow`/`padding` at the same specificity as the UA's
   `[hidden]` — so the override is not optional (see `a_panel_the_script_hides_is_
   hidden_in_css_too`). */
#side[hidden]{display:none}
/* UI-52: the ✕ sits at the end of the name row — the panel's own head, which is where
   the file panel puts its controls. Scoped to `#side`: the login card and the admin bar
   wear `.brand` too, and a flex row there would move text nobody asked to move. The
   global `.spacer{flex:1}` does the pushing. */
.brand{font-weight:700;font-size:1.14em;letter-spacing:.3px;margin-bottom:12px;color:var(--bright)}
#side .brand{display:flex;align-items:baseline;gap:6px}
.brand span{color:var(--accent)}
/* HDR-1: `.bname` is the name's own box, not the accent half of it — the two-tone
   split lives on the span inside, so the wrapper must not inherit the accent rule
   above or the whole name would come out orange. */
.brand .bname{color:inherit}
/* The version of the program serving the page, next to the name it belongs to —
   the question "which build am I looking at?" is asked most often in the sidebar,
   and answered from /api/state so it is the running binary's own version, not a
   string typed into the markup that could go stale. */
/* HDR-1: the version is read, not whispered. It used to wear `--bg4` at weight 400
   — the same grey as the least important label on the page — while being the answer
   to the question people actually ask ("which build is this?"). `--bright` is the
   page's brightest text token (fg0: 14.5:1 on the sidebar fill, 10.2:1 on the login
   card, WCAG AAA on both) and 700 is the weight the name beside it already carries, so the pair reads as one line. */
.brand .ver{color:var(--bright);font-weight:700;font-size:.79em;letter-spacing:0;margin-left:7px;
  font-variant-numeric:tabular-nums}
.sectitle{font-size:.79em;text-transform:uppercase;letter-spacing:.09em;color:var(--dim);margin:18px 0 6px}
button{font:inherit;border-radius:6px;border:1px solid var(--line2);background:var(--bg1);
  color:var(--text);padding:7px 12px;cursor:pointer}
button:hover:not(:disabled){border-color:var(--accent);color:var(--bright)}
button:disabled{opacity:.4;cursor:not-allowed}
button.primary{background:var(--accent);border-color:var(--accent);color:#1d2021;font-weight:700}
button.primary:hover:not(:disabled){background:#ffb066;border-color:#ffb066;color:#1d2021}
button.danger{border-color:#7a2e1f;color:var(--red-text)}
button.danger:hover:not(:disabled){border-color:var(--red);background:#3c2a24}
button.ghost{background:transparent}
button.iconBtn{background:transparent;border:none;padding:3px 7px;color:var(--dim);font-size:14px}
button.iconBtn:hover{color:var(--bright);border:none;background:var(--bg1)}
/* UI-45: a re-read of the file panel is shown on the button that started it. It
   used to be shown on the listing itself — the rows were replaced by a `reading…`
   line and came back a beat later, which is the flash this is about. A spinner on
   the ⟳ says the same thing without taking the panel away. */
button.iconBtn[aria-busy="true"]{opacity:.55;cursor:progress}
@media (prefers-reduced-motion: no-preference){
  button.iconBtn[aria-busy="true"]{animation:cmSpin 1.1s linear infinite}
}
@keyframes cmSpin{to{transform:rotate(360deg)}}
button.active{border-color:var(--accent);color:var(--bright)}
input,select,textarea{font:inherit;color:var(--text)}
select{width:100%;background:var(--bg1);border:1px solid var(--line2);border-radius:6px;padding:6px 8px}
select:focus,input:focus,textarea:focus{outline:none;border-color:var(--accent)}
/* The controls are chrome, not text: pinned to 12px so that enlarging the
   listing to 32px does not enlarge the picker and steppers that enlarge it
   (`button`/`select` are `font:inherit`, so an unpinned control grows with the
   very size it sets). `.sizeRead` shows the number, so it stays fixed too. */
.fontBox{display:flex;flex-direction:column;gap:7px;font-size:12px}
.sizeRow{display:flex;align-items:center;gap:8px}
.stepBtn{padding:4px 10px;min-width:44px}
.sizeRead{font-size:12px;color:var(--dim);flex:1;text-align:center}
.sessions,.agents,.files{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
.sessions li{padding:7px 9px;border-radius:6px;cursor:pointer;border:1px solid transparent;
  display:flex;align-items:flex-start;gap:6px}
.sessions li:hover{background:var(--bg1)}
.sessions li.active{background:var(--bg1);border-color:var(--line2)}
.sessions li .grow{flex:1;min-width:0}
/* CARD-1: the row's head band — the title, and the marks that belong to the session
   itself. The state chip used to sit beside the *whole* column, so a long one
   ("stopped by a restart", "needs a new session") took about 120px off a 290px
   sidebar and the msgs / prompts / round line under it wrapped to six lines: the
   figures the row exists to show were the thing that gave way. Measured in Chrome
   before this rule existed — a row 228px tall at the default 290px, and the text
   column collapsed to 0px and overflowed the row at the 200px minimum the grip
   allows. Now the title is what gives way: it ellipsises, and when a chip cannot fit
   beside it the head band wraps and the figures keep the row's full width. */
.sessions .head{display:flex;flex-wrap:wrap;align-items:center;gap:3px 5px;min-width:0}
.sessions .t{flex:1 1 8ch;min-width:6ch;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sessions .m{font-size:.79em;color:var(--dim)}
/* CARD-1: a figure and its word are one thing. Breaking "3" from "prompts" turns the
   line into a list of unrelated tokens; the break belongs at the separator. */
.sessions .m .f{white-space:nowrap}
/* UI-35: a session whose run is parked on an ask waits for the reader, and it
   will wait for ASK_TIMEOUT and then answer itself. The mark sits beside the row it
   belongs to (never on a single tab: the question exists whether or not one is open)
   and is yellow — the same warning colour the token bar uses as it nears the ceiling.
   The word is in the row's own status line (`waiting for you`); this is the mark that
   survives the row being scanned at a glance. */
/* CARD-1: the marks sit in the head band now, so none of them may be squeezed by a
   long title or a long chip. `.openMark` had no rule at all before — it was a bare
   glyph in a block, where nothing could squeeze it. */
.sessions .openMark{flex:0 0 auto}
.sessions .chainMark{flex:0 0 auto;min-width:16px;height:16px;margin-top:1px;border-radius:8px;
  border:1px solid var(--line2);color:var(--dim2);font-size:10px;line-height:15px;text-align:center}
/* The row's state word, as a chip rather than as text inside the meta line: a
   session that is paused for a new session, stopped by a restart or refused has to be
   tellable apart at a glance, from across the room. Only statuses the store really
   writes get a colour; anything else is shown in its own word, uncoloured. */
/* CARD-1: the chip may be the widest thing in the head band, so it wraps inside its
   own pill instead of setting the row's width. Ellipsising was the other option and
   it is the wrong one here: "stopped by a…" is not a state anyone can read at a
   glance, and the chip is the one part of the row that has to be readable from
   across the room. Only the two long words wrap, and only in a sidebar dragged
   narrow — at the default 290px every chip fits on one line. */
.sessions .runBadge{flex:0 1 auto;min-width:0;max-width:100%;font-size:10px;line-height:14px;
  padding:0 5px;border-radius:7px;border:1px solid var(--line2);color:var(--dim2)}
.sessions .runBadge.warn{border-color:var(--yellow);color:var(--yellow)}
.sessions .runBadge.bad{border-color:var(--red);color:var(--red-text)}
.sessions .runBadge.ok{border-color:var(--green);color:var(--green)}
.sessions .askMark{flex:0 0 auto;width:16px;height:16px;margin-top:1px;border-radius:50%;
  background:var(--yellow);color:#111;font-size:11px;font-weight:700;line-height:16px;
  text-align:center}
/* UI-21: the Group Chat size, the number that says how close a session is to
   compaction. The bar is the share of `context.max_size` — it turns yellow near
   the ceiling and red past it, where the next round will compact. */
.sessions .tok{display:flex;align-items:center;gap:6px;margin-top:2px;color:var(--dim2);
  font-variant-numeric:tabular-nums}
.sessions .tok .tokNum{color:var(--aqua);font-weight:600}
.sessions .tok.near .tokNum{color:var(--yellow)}
.sessions .tok.over .tokNum{color:var(--red-text)}
.sessions .tok.none{color:var(--bg4);font-style:italic}
.sessions .tok .bar{flex:1;height:3px;border-radius:2px;background:var(--bg2);overflow:hidden;min-width:26px}
.sessions .tok .bar i{display:block;height:100%;background:var(--aqua)}
.sessions .tok.near .bar i{background:var(--yellow)}
.sessions .tok.over .bar i{background:var(--red)}
.delBtn{border:none;background:transparent;color:var(--bg4);padding:0 4px;line-height:1.2;
  font-size:14px;flex:none}
.sessions li:hover .delBtn{color:var(--dim2)}
.delBtn:hover{color:var(--red-text)!important;border:none!important;background:transparent!important}
.agents li{display:flex;justify-content:space-between;font-size:.86em;color:var(--dim);padding:3px 2px}
.agents .v{color:var(--green)}
.runtime{margin:0;font-size:.79em;color:var(--dim);display:grid;grid-template-columns:auto 1fr;gap:2px 8px}
.runtime dd{margin:0;overflow-wrap:anywhere}
.files li{font-size:.79em;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.files .rej{color:var(--red-text)}
/* Five rows: the bar, the tab strip (UI-27), the pane area — the only row that
   gives — then the composer's grip and the composer itself. The strip must not be
   allowed to grow, and the pane area must be able to shrink to nothing: a long file
   name in a tab is what used to push the Download button out of the window. */
/* `grid-template-columns:minmax(0,1fr)` is not decoration (UI-27). Without it this
   grid gets one implicit `auto` track, and an auto track grows to its content's
   max-content width — a long file path and a toolbar row are wider than a phone,
   so #main came out 654px inside a 620px window and body{overflow:hidden} cut the
   viewer bar's Download button off, unreachable. minmax(0,1fr) caps the track to
   the window and lets the bar's own wrapping do its job; scripts/viewer-panels.mjs
   measures this at 620px and 420px. */
/* UI-51: three rows — the bar, the tab strip, the stream. The two the composer and
   its drag handle used to take are gone with them, which is the whole point of the
   move: the conversation gets the height they were spending. */
#main{grid-column:2;display:grid;grid-template-columns:minmax(0,1fr);
  grid-template-rows:auto auto auto minmax(0,1fr);min-width:0;height:100%;overflow:hidden}
/* WIP: the stream keeps its row whether or not the banner is up. Placement by hand
   is what makes that true — with the banner `hidden` the grid would otherwise slide
   `#panes` into the banner's `auto` row and take the flexible row away from it. */
#panes{grid-row:4}
/* Dark text on the page's own yellow measures about 11:1 — AA and AAA at this size,
   and the pulse is dropped for a reader who asked for no motion, where the dot stays
   solid and the words still carry the whole message on their own. */
.wipBanner{display:flex;align-items:center;gap:10px;flex:none;padding:7px 16px;
  background:var(--yellow);color:#1d2021;font-size:12.5px;font-weight:600;
  border-bottom:1px solid var(--bg0h)}
.wipBanner[hidden]{display:none}
.wipBanner .wipDot{width:9px;height:9px;border-radius:50%;background:#1d2021;flex:none;
  animation:wipPulse 1.1s ease-in-out infinite}
.wipBanner .wipCount{font-variant-numeric:tabular-nums;white-space:nowrap}
.wipBanner .wipName{font-weight:400;opacity:.8;min-width:0;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
@keyframes wipPulse{0%,100%{opacity:1}50%{opacity:.25}}
@media (prefers-reduced-motion:reduce){.wipBanner .wipDot{animation:none}}
#bar{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:11px 16px;
  border-bottom:1px solid var(--line);background:var(--bg0h)}
/* Hidden above 820px, where the sidebar owns the controls; shown below it, where
   `#side{display:none}` would otherwise leave no way to change the text at all.
   One visible pair at every width, never two. */
.barTextCtl{display:none;align-items:center;gap:6px;flex:1 1 100%;min-width:0;font-size:12px}
.barTextCtl select{width:auto;flex:1 1 auto;min-width:0;padding:0 6px;font-size:12px;height:28px}
.barTextCtl .stepBtn{padding:3px 8px;min-width:34px;font-size:12px;flex:none}
.barTextCtl .sizeRead{flex:none;text-align:right}
#sessionTitle{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40%;color:var(--bright)}
/* SID: the session's short code, the string the user types into ntfy to answer an
   ask_user from this session (`#cm a1b2 <your answer>`). It gets its own box rather
   than being a run inside #sessionTitle, because that element is ellipsised at 40%
   and would clip the one string on the bar worth copying. `user-select:all` takes the
   code alone in one press. It is text, never a link: the code is display and parsing
   only, and the uuid stays the session's address. --bright on --bg1 is 10.2:1. */
#sessionId{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:12px;font-weight:700;letter-spacing:.06em;color:var(--green);
  background:var(--bg1);border:1px solid var(--green);border-radius:5px;
  padding:2px 7px;user-select:all;-webkit-user-select:all;cursor:text}
#sessionId[hidden]{display:none}
/* SID, everywhere else: the same code in the same green on a session row and on a
   tab. One rule, two users, so the three places the code appears cannot drift apart
   in colour — the user asked for green at every one of them. --green on --bg1 is
   6.9:1, and the 1px edge is what keeps the badge readable when it sits on --bg1
   like the bar chip does. It is text, not a target: `cursor:text`, `user-select:all`
   takes the code in one press so it can be typed into ntfy. */
.sidBadge{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--green);
  background:var(--bg1);border:1px solid var(--green);border-radius:5px;
  padding:0 5px;line-height:14px;white-space:nowrap;
  user-select:all;-webkit-user-select:all;cursor:text}
.spacer{flex:1}
/* An announcement with no pixels. A status line a screen reader should read but
   that must not push a layout around: 1px, clipped, still in the a11y tree. */
.srOnly{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.dot{width:9px;height:9px;border-radius:50%;background:var(--dim)}
.dot.run{background:var(--green);animation:pulse 1.1s infinite}
.dot.err{background:var(--red)}
@keyframes pulse{50%{opacity:.35}}
#statusText{font-size:12px;color:var(--dim)}
/* The reading pane: the only place the user's font choice applies (UI-11). */
.stream{overflow:auto;padding:18px;display:flex;flex-direction:column;gap:12px;
  font-family:var(--read-family);font-size:var(--read-size)}
.card{border:1px solid var(--line);border-radius:8px;padding:12px 14px;min-width:0;background:var(--bg1);
  white-space:pre-wrap;overflow-wrap:anywhere;animation:in .18s ease-out}
@keyframes in{from{opacity:0;transform:translateY(5px)}}
.card .who{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);
  margin-bottom:7px;display:flex;align-items:center;gap:8px;font-family:inherit}
/* UI-32: the id beside the name is a reference, so it is dim, in the code face (ids
   are code), and it must never make a card wider or push the name out — a long id
   wraps below it instead. */
.card .who{flex-wrap:wrap;overflow-wrap:anywhere}
/* UI-31: the switch to the card's own text. Quiet until hovered: it belongs to the
   card, not to the reader's attention. */
.card .rawBtn{margin-left:auto;font-size:10px;padding:1px 7px;border-radius:20px;
  border:1px solid var(--line2);background:none;color:var(--dim);text-transform:none;
  letter-spacing:0;font-family:inherit}
.card .rawBtn:hover{color:var(--bright);border-color:var(--dim2)}
/* UI-28: the context figures at the right end of the name row, after the time.
   Smaller and dimmer than the time: an instrument reading, not a headline, and it
   must not compete with the agent's own words below. */
.card .who .tok2{font-size:9.5px;letter-spacing:.04em;color:var(--dim2);text-transform:none;
  margin-left:8px;white-space:nowrap;cursor:help}
.card .who .tok2 .tok{color:var(--dim)}
.card .who .badge{font-size:10px;padding:1px 6px;border-radius:20px;border:1px solid var(--line2);
  color:var(--dim2);text-transform:none;letter-spacing:0}
/* UI-24: the minute the entry was written, after the name. Dim and tabular: it is
   a reference, not the content, and it must not shove the name around as digits
   change. */
.card .who .when{margin-left:auto;color:var(--dim2);text-transform:none;letter-spacing:0;
  font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap;font-family:inherit}
/* UI-17: the colour IS the author. Yellow = the user's prompt, purple = the
   conductor ordering an agent, green = the conductor's summary of the work,
   blue = an agent's answer. */
.card.user{background:var(--c-user);border-color:var(--c-user-edge)}
.card.user .who{color:var(--yellow)}
.card.conductor{background:var(--c-order);border-color:var(--c-order-edge)}
.card.conductor .who{color:var(--purple)}
.card.agent{background:var(--c-agent);border-color:var(--c-agent-edge)}
.card.agent .who{color:var(--blue)}
.card.final{background:var(--c-final);border-color:var(--c-final-edge)}
.card.final .who{color:var(--green)}
.card.summary{border-color:#4a5338;background:var(--bg0h);color:var(--dim2)}
.card.err{border-color:#7a2e1f;background:#32201c;color:#fdd0c5}
/* UI-7: an `ask` exchange in the Group Chat. It borrows the user's yellow for
   its border because the half that matters — the answer — is the user's own words,
   but it stays visibly its own kind: an agent spoke first inside that card. */
.card.ask{background:var(--bg0h);border-color:var(--c-user-edge)}
.card.ask .who{color:var(--orange)}
.askQ{margin-bottom:8px}
.askA{display:flex;flex-wrap:wrap;gap:8px;padding:8px 10px;border-radius:6px;
  background:var(--c-user);border-left:3px solid var(--yellow)}
.askA .askTag{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--yellow);
  font-family:inherit;white-space:nowrap;align-self:flex-start;padding-top:2px}
/* UI-15: the folded tool trail inside an agent's card. Closed by default — the
   card shows the answer, the calls are a line away when the user wants them.
   UI-41: its size is its own preference — the summary line and every line under it
   are sized by `--tool-size`, so A−/A+ here moves the trail without touching the
   answer above it. The control is fixed at 11px so it does not grow itself. */
/* The 3-phase turn, phase 1: the agent's plan, the first section of its card.
   Muted like the tool trail under it — the plan is context for the answer, not
   the answer, so it must not out-shout the reply that follows it. */
.plan{margin:8px 0 0;padding:7px 9px;border:1px solid var(--line);border-radius:6px;
  background:var(--well);font-size:calc(var(--tool-size) - 1px);color:var(--dim2);min-width:0;
  overflow-wrap:anywhere}
.plan .planHead{display:flex;gap:8px;align-items:baseline;color:var(--fg);font-weight:600;
  font-size:var(--tool-size)}
.plan .planN{color:var(--dim2);font-weight:400}
.plan ol{margin:5px 0 0;padding-left:20px}
.plan li{margin:2px 0}
/* Phase 2's section: the tool calls, with the counter that says how far the turn
   has spent of the allowance it was given. The figure is muted until it reaches
   the ceiling, where it wears the same yellow as every other warning on the page —
   a reader scanning a finished run should see at a glance which agents stopped
   because they ran out of tool calls. */
.work{margin:8px 0 0;padding:7px 9px;border:1px solid var(--line);border-radius:6px;
  background:var(--well);font-size:var(--tool-size);color:var(--dim2);min-width:0;
  overflow-wrap:anywhere}
.work .workHead{display:flex;gap:8px;align-items:baseline;color:var(--fg);font-weight:600}
.work .workN{color:var(--dim2);font-weight:400}
.work .workN.over{color:var(--yellow)}
.work .workHead{border-bottom:1px solid var(--line2);padding-bottom:4px}
/* The divider the trail used to get from the border-top of the plain `.tools`
   rule is drawn under the heading instead: the line sits on the row that is
   always there, so folding the trail away cannot take it with it, and no
   element had to be added. (The literal selector is not spelled out here on
   purpose — a test finds the rule by searching APP_CSS for it, and a comment
   that quotes it steals the match.) */
.work .tools{margin-top:6px;border-top:0;padding-top:4px;min-width:0}
/* Phase 3's section: the answer. It carries no box of its own — the reply is the
   card's own body and must keep the weight of the card's main text, so a border
   around it would draw a frame inside a frame. */
.report{min-width:0}
.card .who .badge.end{color:var(--dim2);border-color:var(--line2)}
.card .who .badge.end.warn{color:var(--yellow);border-color:var(--yellow)}
.card .who .badge.end.bad{color:var(--red-text);border-color:var(--red)}
.tools{margin-top:10px;border-top:1px solid var(--line);padding-top:7px;font-family:inherit;
  min-width:0;overflow-wrap:anywhere}
.tools > summary{cursor:pointer;font-size:var(--tool-size);color:var(--dim);list-style:none;
  display:flex;align-items:center;flex-wrap:wrap}
.tools > summary::before{content:'▸ ';color:var(--dim2)}
.tools[open] > summary::before{content:'▾ '}
.tools > summary:hover{color:var(--dim2)}
.tools[open]{display:flex;flex-direction:column;gap:3px;margin-top:10px}
/* UI-41: the trail's own A−/A+. Fixed at 11px whatever it is set to — a control that
   resized itself would run out of the row it sits in, and the figure has to stay
   readable in order to say what it is set to. What keeps a press on them from also
   folding the trail up is the `preventDefault`/`stopPropagation` in the delegated
   handler in `initTextControls()` — a button inside a `<summary>` still receives its
   own click, and the summary would then toggle on the way out.

   The other way was tried and does not work: taking the click back with
   `.tools > summary{pointer-events:none}` and giving the buttons theirs again leaves
   the trail's own words unable to open it. `elementFromPoint` on the words answers
   DETAILS rather than SUMMARY, and `ui-41.mjs` fails at its first summary check with
   exactly that. The stepper needs no `pointer-events` of its own — nothing above it
   switches them off. */
/* Every one of these was `--bg4` (#7c6f64): 2.13:1 on the section fill `--well`
   and 2.45:1 on the agent card — under the 4.5:1 WCAG 1.4.3 floor for the 9.5–11px
   they are drawn at. `--dim2` (#bdae93) measures 4.77:1 on `--well`, 5.48:1 on
   `--c-agent` and 5.32:1 on `--bg1`, so the trail's control, its byte notes, the
   fold marker and the name row's figures all clear AA on every fill they sit on.
   The dimming that `--bg4` was doing by colour is `--stale`'s job, not this one's. */
.toolCtl{display:inline-flex;align-items:center;gap:4px;margin-left:8px;font-size:11px;
  color:var(--dim2);white-space:nowrap;flex:none}
/* The trail's own words get whatever the row has left, and give way before the
   control does (UI-41): the control is what the reader is aiming at, so it stays
   exactly where they left it. */
.tools > summary .toolWords{flex:1 1 auto;min-width:0;overflow-wrap:anywhere}
.tools > summary .toolCtl{margin-left:auto}
.toolCtl .toolStep{padding:0 6px;min-width:0;height:18px;font-size:11px;line-height:1;
  border-radius:4px}
.toolCtl .toolSizeRead{min-width:38px;text-align:center;font-variant-numeric:tabular-nums}
/* The bar above the calls while they are still arriving (UI-41): the same steppers,
   outside a <summary> so nothing has to be un-folded, and at its own fixed 11px so
   enlarging the lines does not enlarge the handle that enlarges them. */
.toolCtlBar{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim2)}
.toolCtlBar .toolCtl{margin-left:0}
.toolLine{font-size:var(--tool-size);color:var(--dim);display:flex;gap:6px;flex-wrap:wrap;
  overflow-wrap:anywhere;padding-left:8px}
.toolLine code{color:var(--blue);font-family:ui-monospace,monospace;flex:none}
.toolLine.note{color:var(--dim2)}
.toolLine.stale{color:var(--stale)}
/* UI-33: the tool that never came back — dimmer than a note, because it says nothing
   happened: the run was cut short while this was going. */
.toolLine.stopped{color:var(--red-text)}
.card.stale{color:var(--stale);border-color:var(--bg3)}
/* A dimmed card dims as a whole: the name row, the answer, its badges, its two
   context figures and its trail all wear `--stale` together, or a compacted card
   keeps bright furniture around a grey body and reads as a card that lost its
   text. `--stale` reaches AA on every card fill it can sit on (5.62:1 on --bg1,
   5.79:1 on the agent card, 5.04:1 inside the plan/work boxes). */
.card.stale .who,.card.stale .body,.card.stale .who .badge,.card.stale .who .tok2,
.card.stale .who .tok2 .tok,.card.stale .who .when,.card.stale .toolLine{color:var(--stale)}
/* The end badge and the section headings keep their own colour rules, which out-rank
   the four-class selector above, so a dimmed card could still wear a bright "tool
   limit" pill and bright section heads around a grey answer. `--stale` is neutral, so
   dimming them costs no meaning: the badge's *word* is what says how the turn ended,
   and it is still printed. `--stale` reaches AA on every fill these sit on (5.79:1 on
   --c-agent, 5.04:1 inside the plan/work boxes, 7.95:1 on a compacted card). */
.card.stale .plan .planHead,.card.stale .work .workHead,.card.stale .plan .planN,
.card.stale .work .workN,.card.stale .tools>summary,.card.stale .toolLine code,
.card.stale .toolLine .note,.card.stale .toolCtl,.card.stale .toolCtlBar,
.card.stale .who .badge.end,.card.stale .who .badge.end.warn,.card.stale .who .badge.end.bad
{color:var(--stale)}
.card.compact{border-color:var(--bg3);background:var(--bg0h);color:var(--dim2)}
/* D2: the handoff card. It is the one card in the stream the reader is meant to act
   on, so it is the loudest border on the page \u2014 and its prompt keeps the exact
   characters that will be pasted into the next session. */
.card.continuation{border-color:var(--yellow);background:var(--bg0h)}
.card.continuation .contWhy{color:var(--dim);margin-bottom:8px}
.card.continuation .contSize{color:var(--dim2);font-variant-numeric:tabular-nums;margin-bottom:8px}
/* "not measured" is a missing fact, not a small one — it is drawn as one. */
.card.continuation .contSize.unknown{font-variant-numeric:normal;font-style:italic}
.card.continuation pre{background:var(--bg0);border:1px solid var(--line2);border-radius:6px;
  padding:10px;max-height:340px;overflow:auto}
.card.continuation .contActions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
/* CONT (D29): the auto-continue notice is a record, not an action, so it is drawn
   like the handoff card it follows but with a dashed edge — nothing in it is pressable
   and it must not look like something that still needs a click. */
.card.continuation.autoCont{border-style:dashed}
#autoContCtl{display:none;align-items:center;gap:5px;font-size:12px;color:var(--dim);
  cursor:pointer;user-select:none}
#autoContCtl input{margin:0;accent-color:var(--accent)}
#autoContCtl.off{color:var(--dim2);cursor:not-allowed}
/* CLOSE (D5): a disabled close must look disabled — it is the difference between
   "this session is finished" and "the page is being slow", and only colour tells them. */
#endBtn:disabled{opacity:.45;cursor:not-allowed}
.card.closed{border-style:solid;border-color:var(--line2)}
.card .body{font-family:inherit}
.card pre{margin:0;font-family:ui-monospace,SFMono-Regular,monospace;font-size:.92em;white-space:pre-wrap}
.card .files{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px}
.chip{font-size:11px;border:1px solid var(--line2);border-radius:20px;padding:2px 9px;
  color:var(--dim);background:var(--bg0h)}
.chip img{max-height:34px;border-radius:4px;display:block}
/* An image attachment is a button, not a chip with an onclick: a chip you cannot
   reach with Tab is a thumbnail that does not exist for the keyboard. `font-family`
   only — the `font` shorthand would reset the 11px this chip's own rule sets. */
button.chip.thumb{font-family:inherit;font-size:11px;padding:2px 9px;cursor:pointer;
  display:inline-flex;align-items:center;gap:6px;max-width:100%}
button.chip.thumb:hover{border-color:var(--accent);color:var(--bright)}
button.chip.thumb:focus-visible{outline:2px solid var(--bright);outline-offset:2px}
.chip .cap{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px}
.chip .kind{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--purple)}
/* The sidebar's rows are links now. Dense padding, as the rest of the sidebar is
   dense (`.delBtn` is 14px tall); the focus ring is inset so the list's own edge
   cannot clip it. */
.files li a.dl{display:block;padding:4px 6px;border-radius:6px;color:var(--dim);
  text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.files li a.dl:hover{background:var(--bg0h);color:var(--bright)}
.files li a.dl:focus-visible{outline:2px solid var(--bright);outline-offset:-2px}
/* Above the ask popup: an image opened from a card stays readable while a
   question is waiting underneath it. */
.lightbox{z-index:60}
.lbFig{margin:0;display:flex;flex-direction:column;gap:8px;max-width:min(92vw,1100px)}
.lbImg{max-width:100%;max-height:78vh;object-fit:contain;border:1px solid var(--bg3);
  border-radius:6px;background:var(--bg0h)}
.lbCap{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim2);
  background:var(--bg1);border:1px solid var(--bg3);border-radius:6px;padding:6px 10px}
.lbCap #lightboxCap{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lbDl{color:var(--yellow);text-decoration:none;font-weight:700;padding:2px 6px;border-radius:6px}
.lbDl:hover{text-decoration:underline}
.lbDl:focus-visible,.lightbox .ghost:focus-visible{outline:2px solid var(--bright);outline-offset:2px}
/* TERM-2: the shell is a pane now, not a layer — the same area a session or a file
   occupies, because a terminal you have to close before you can read the run is a
   terminal that hides your work. The emulator fills whatever the pane gives it and
   reports its own rows and columns back to the PTY, so the pane must not size itself
   to the shell's output (that would resize the shell on every line it wrote): the
   host is `flex:1` inside a pane that is already `minmax(0,1fr)`. */
.termPane{background:var(--bg1)}
.termHead{display:flex;align-items:center;gap:10px;font-size:12px;color:var(--dim2);
  padding:6px 10px;border-bottom:1px solid var(--line);flex:none}
.termHead .spacer{flex:1}
.termHost{flex:1;min-height:0;min-width:0;background:#1d2021;
  border-radius:0;padding:4px 6px;overflow:hidden}
.termHost:focus-visible{outline:2px solid var(--bright);outline-offset:-2px}
/* The emulator's own scrollbars in the page's palette; xterm draws its buffer into
   a canvas and leaves the bar to us. */
.termHost ::-webkit-scrollbar{width:10px;height:10px}
.termHost ::-webkit-scrollbar-thumb{background:var(--bg3);border-radius:6px}
/* TERM-3: the shell's text controls. `select{width:100%}` is the sidebar's rule and
   would push the head's buttons out of the pane, so the picker is pinned to its
   content here the way `.barTextCtl select` is pinned in the header. */
.termHead select{width:auto;flex:none;max-width:190px;padding:2px 6px;font-size:12px;height:26px}
.termHead .termSizeStep{padding:2px 8px;font-size:12px;flex:none}
.termHead .termSizeRead{font-size:12px;color:var(--dim);flex:none;min-width:42px;text-align:center}
.termPane .ghost:focus-visible{outline:2px solid var(--bright);outline-offset:2px}
/* The status line is the one place that says *why* a shell is not there: a dead
   socket, a host without /dev/ptmx and a ceiling already reached all look like a
   black box otherwise. */
.termState{color:var(--dim);font-style:italic}
.termState.bad{color:var(--red-text);font-style:normal}
.termState.off{color:var(--yellow);font-style:normal}
.card .dots::after{content:'…';color:var(--dim)}
/* UI-14's drag handle is gone with the bottom composer (UI-51): a box in a dialog is
   resized by its own corner, which the browser draws and the rule below paints. The
   `.grip` class stays for the two column edges' shared look — they are `.colGrip`,
   and this is the shape they were modelled on. */
/* The two column edges, dragged to resize the side and file panels. They are
   children of #app rather than of the panels they belong to: both panels are
   `overflow:auto`, which is a clipping context, so a handle straddling their
   border would be cut away and be nothing to catch. `touch-action:none` keeps a
   drag on a phone a drag and not a page scroll, as on the composer's handle. */
.colGrip{position:absolute;top:0;bottom:0;width:10px;z-index:20;cursor:col-resize;
  display:flex;align-items:center;justify-content:center;touch-action:none}
.colGrip::before{content:'';width:3px;height:26px;border-radius:3px;background:var(--bg3)}
.colGrip:hover::before,.colGrip.dragging::before{background:var(--accent)}
.colGrip:focus-visible{outline:2px solid var(--bright);outline-offset:-2px}
#sideGrip{left:calc(var(--side-w,290px) - 5px)}
#treeGrip{right:calc(var(--tree-w,330px) - 5px)}
/* The handle belongs to the panel it resizes: a hidden panel has no edge to pull.
   The grips are later siblings of #tree, which is what lets this reach it. */
#tree[hidden] ~ #treeGrip{display:none}
#side[hidden] ~ #sideGrip{display:none}
/* While a drag is on, the pointer is over text that would otherwise be selected
   and dragged along with it. */
body.resizing{user-select:none;cursor:col-resize}
/* UI-51: the composer is the prompt dialog's body now, so it no longer owns a strip
   of the window. `.promptDialog` is wider than the ask popup's 560px — a prompt is
   usually a paragraph and the attachment row under it needs the room. */
/* UI-52: `fit-content` so the dialog follows the width the corner was dragged to — the
   box is the thing being resized and a fixed-width dialog would either clip it or leave
   the row under it standing in a column of its own. The shipped 720px look comes from
   the box's own default width plus the dialog's 20px padding, so nothing here has to
   repeat it. Two classes, not one: `.dialog` is declared below and would otherwise win
   the tie — a bug I shipped and a real browser caught. */
.dialog.promptDialog{width:fit-content;max-width:94vw}
.pending{display:flex;flex-direction:column;gap:4px}
.pending .p{font-size:12px;color:var(--yellow)}
/* UI-14 → UI-51: resizable by the user, and the height is *remembered* — the corner
   handle is the only control now, so it has to be visible on a dark theme (the
   browser's own is not) and the height it produces is written back to `cm_prompt_h`
   the moment the pointer is released, the same rule the old grip followed. `max-height`
   is the give: a box dragged past the window would push Send off the bottom, which is
   the failure the ask popup's dialog already documents. */
/* UI-52: `resize:both` — the corner grew the box downward only, because
   `resize:vertical` was the rule the old bottom strip needed (its width was the
   window's) and nobody had asked for a wider box since. The width is now the box's
   own, not a percentage: the dialog is `fit-content` and measures this box, so a
   percentage would give it nothing to measure and `max-width:100%` would be a
   percentage of a width derived from the box itself. The default is capped by the
   window so the shipped look survives a narrow screen before the script has run;
   `min-width` is the browser's own floor for the drag, and `MIN_W`/`promptMaxW()` in
   the script are the floor and ceiling of what gets remembered. */
#input{width:min(680px,calc(94vw - 40px));background:var(--bg0h);border:1px solid var(--line2);
  border-radius:8px;padding:10px 12px;resize:both;min-height:60px;max-height:50vh;
  min-width:220px;font:inherit;overflow:auto;
  font-family:var(--read-family);font-size:var(--read-size)}
#input::-webkit-resizer{background:var(--bg3);border-radius:0 0 7px 0}
.composerRow{display:flex;align-items:center;gap:10px;margin-top:9px}
.fileBtn{border:1px dashed var(--bg4);border-radius:6px;padding:6px 12px;cursor:pointer;
  color:var(--dim);font-size:12px}
.fileBtn:hover{border-color:var(--accent);color:var(--text)}
.hint{font-size:11px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hint .bad{color:var(--red-text)}
.kbd{font-size:11px;color:var(--dim);border:1px solid var(--line2);border-radius:4px;padding:2px 6px}
/* ---- ntfy (NT-2, NT-3) --------------------------------------------------
   The channel is a secret, so nothing here ever echoes the saved value back: the
   placeholder is the mask, and the input starts empty. */
.configDialog{width:min(620px,92vw)}
.configSection{display:flex;flex-direction:column;gap:8px;padding-top:10px;
  border-top:1px solid var(--line2)}
.configHead{display:flex;flex-direction:column;gap:2px}
.configName{font-size:.92em;font-weight:700;color:var(--bright)}
.configNote{font-size:11px;color:var(--dim);overflow-wrap:anywhere}
#sysPrompt{min-height:110px}
.configSummary{font-size:11px;color:var(--dim);overflow-wrap:anywhere}
.notifyBox{display:flex;flex-direction:column;gap:7px;font-size:12px}
.notifyInput{background:var(--bg1);border:1px solid var(--line2);border-radius:6px;
  padding:6px 9px;color:var(--text);font:inherit;font-size:12px;width:100%}
.notifyInput:focus{outline:none;border-color:var(--accent)}
.notifyRow{display:flex;gap:7px}
.notifySend{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dim);cursor:pointer}
.notifySend input{accent-color:var(--accent)}
/* HIDDEN-BUTTON: `hidden` is a UA rule of the same specificity as a class, so any
   `display:` on a class the script toggles with `.hidden` wins and the element stays on
   screen. `.notifySend` is set `hidden` until the account has an ntfy channel, so without
   this the "notify when done" box was offered to accounts with nothing to notify. */
.notifySend[hidden]{display:none}
#notifyHint.bad{color:var(--red-text)}
#notifyHint.good{color:var(--green)}
/* ---- the file panel (UI-19) ----------------------------------------------
   A tree, not a list: folders expand in place, files open in the viewer. It is
   the working directory the agents' `run` tool works in, so it is a way to watch
   a run produce its output. */
/* UI-44: the panel reads in the reader's font and at the reader's size, moved by
   the page's one pair of controls. Every size below is an `em` of that one size,
   at the ratio the fixed pixels used to paint — the tree keeps its hierarchy
   while the whole panel scales together. */
#tree{grid-column:3;background:var(--bg0h);border-left:1px solid var(--line);width:var(--tree-w,330px);min-width:240px;
  max-width:50vw;display:flex;flex-direction:column;overflow:hidden;
  font-family:var(--read-family);font-size:var(--read-size)}
#tree[hidden]{display:none}
/* The head wraps: at the panel's 240px minimum a long folder name beside the
   three icon buttons is wider than the panel, and the way out is a second line —
   never a button that sits where nothing can reach it. */
.treeHead{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;padding:10px 12px;
  border-bottom:1px solid var(--line)}
.treeHead #treeTitle{font-size:.85em;text-transform:uppercase;letter-spacing:.09em;color:var(--dim)}
/* UI-47: `.treePath` used to sit between the head and the listing showing the
   folder a listing belongs to — `.` at the root, and the same string the foot
   already prints in full. It cost a bordered band and told nobody anything. */
.treeNote{font-size:.78em;color:var(--dim2);padding:5px 12px;border-bottom:1px solid var(--line);
  white-space:pre-wrap;word-break:break-word;font-family:var(--read-family)}
.treeNote.bad{color:var(--red-text)}
.treeNote[hidden]{display:none}
/* UI-48: the file panel's right-click menu. Fixed, not absolute: #tree clips its
   children and the menu is opened at the pointer, which is sometimes near the
   panel's bottom edge — `placeTreeMenu` flips it up when it would run off screen.
   The font is the panel's own, so the menu scales with the reading size the way
   the rest of the panel does (the rule ui_43 guards). */
.treeMenu{position:fixed;z-index:60;min-width:170px;padding:4px;
  background:var(--bg);border:1px solid var(--line2);border-radius:6px;
  box-shadow:0 6px 20px rgba(0,0,0,.45);
  font-family:var(--read-family);font-size:var(--read-size)}
.treeMenu[hidden]{display:none}
.treeMenu .menuRow{display:block;width:100%;text-align:left;background:transparent;border:none;
  padding:6px 10px;color:var(--text);font:inherit;border-radius:4px;cursor:pointer}
.treeMenu .menuRow:hover,.treeMenu .menuRow:focus{background:var(--bg1);color:var(--bright);
  border:none;outline:none}
.treeList{overflow:auto;flex:1;padding:6px 0;font-size:.85em;user-select:none}
.treeList .row{display:flex;align-items:center;gap:6px;padding:3px 10px;cursor:pointer;
  white-space:nowrap;overflow:hidden}
.treeList .row:hover{background:var(--bg1)}
.treeList .row.sel{background:var(--bg1);color:var(--bright)}
.treeList .row .tw{width:12px;flex:none;color:var(--bg4);font-size:.75em}
.treeList .row .nm{overflow:hidden;text-overflow:ellipsis}
.treeList .row .sz{margin-left:auto;color:var(--bg4);font-size:.84em;flex:none;padding-left:8px}
.treeList .row.dir .nm{color:var(--dim2)}
.treeList .row.blocked{opacity:.5}
.treeList .msg{padding:6px 12px;color:var(--red-text);font-size:.92em}
.treeList .empty{padding:6px 12px;color:var(--bg4);font-size:.92em}
.treeList .ld{padding:4px 12px;color:var(--bg4);font-size:.92em}
.treeFoot{border-top:1px solid var(--line);padding:6px 12px;font-size:.72em;color:var(--bg4);
  display:flex;align-items:center;gap:8px}
.treeFoot .spacer{flex:1}
/* ---- the tab strip and its panes (UI-20, UI-27) --------------------------
   Sessions and files share one strip, because both are "something I opened and
   left open". A file no longer covers the chat: it sits in the same area, so you
   can read what the run wrote and watch the run in the same window. */
/* TERM-2: the strip is wrapped so the two scroll arrows can sit beside it without
   being scrolled away with it. The strip keeps its own overflow — the arrows are a
   keyboard/mouse-reachable way to the tabs at either end, not a replacement for the
   scroll the trackpad gives you. */
.tabsWrap{display:flex;align-items:stretch;flex:none;background:var(--bg0h);
  border-bottom:1px solid var(--line);min-height:38px;padding:0 4px}
.tabsWrap[hidden]{display:none}
.tabScroll{flex:none;align-self:center;width:26px;height:26px;padding:0;line-height:1;
  border:1px solid var(--bg3);border-radius:6px;background:var(--bg1);color:var(--dim);
  font-size:17px;cursor:pointer}
.tabScroll:hover{background:var(--bg2);color:var(--bright)}
.tabScroll:disabled{opacity:.35;cursor:default}
.tabScroll[hidden]{display:none}
.tabs{display:flex;align-items:stretch;gap:2px;background:var(--bg0h);
  padding:0 4px;overflow-x:auto;overflow-y:hidden;scroll-behavior:smooth;
  flex:1 1 auto;min-width:0;min-height:38px}
/* TERM-2: a tab must not shrink. It used to be `flex:0 1 auto`, which let the strip
   squeeze every tab to fit the window — so `scrollWidth` never exceeded `clientWidth`,
   the bar never had anything to scroll, and the arrows the user asked for could not
   appear no matter how many tabs were open. `flex:0 0 auto` makes the strip overflow
   like the strip it is; the name still ellipsises at `max-width` (`.tname`), so a long
   title costs a tab's width and not the whole bar's. */
.tab{display:flex;align-items:center;gap:8px;padding:0 10px;border:none;background:transparent;
  color:var(--dim);font-size:12px;border-radius:0;border-bottom:2px solid transparent;max-width:260px;
  min-width:0;cursor:pointer;flex:0 0 auto}
.tab:hover{background:var(--bg1);color:var(--text);border-color:var(--bg3)}
.tab.active{background:var(--bg);color:var(--bright);border-bottom-color:var(--accent)}
.tab .tname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.tab .x{color:var(--bg4);padding:0 2px;font-size:13px}
.tab .x:hover{color:var(--red-text)}
/* The kind is marked before the name, so a session and a file of the same name —
   `notes.md` and a session titled `notes.md` — are not the same-looking tab. */
.tab .kind{opacity:.6;font-size:11px;flex:none}
.tab.session .kind{color:var(--blue)}
.tab.file .kind{color:var(--green)}
.tab.term .kind{color:var(--yellow)}
/* Something arrived while this tab was not on screen. */
.tab .badge{color:var(--yellow);font-weight:700;flex:none}
.tab.file.active{border-bottom-color:var(--green)}
.tab.term.active{border-bottom-color:var(--yellow)}

/* One pane is on screen at a time; the others keep their DOM (and their scroll
   position, and their running EventSource) hidden behind it. */
/* One pane is on screen at a time; the others keep their DOM (and their scroll
   position, and their running EventSource) hidden behind it.

   `min-width:0` on all three is what keeps the window's edges honest (checked by
   scripts/viewer-panels.mjs at 620px and 420px). A flex or grid item's default is
   `min-width:auto` — never smaller than its content's minimum width — and a file
   path and a row of toolbar buttons have a wide minimum. While the viewer was a
   `position:fixed` overlay it contributed nothing to the page's widths; as an
   in-flow pane it does, so without these the whole middle column grew past the
   window and body{overflow:hidden} cut the Download button off — the same bug
   this bar had once before, arriving by a different route. */
.panes{min-height:0;min-width:0;overflow:hidden;position:relative;display:flex;flex-direction:column}
.pane{min-height:0;min-width:0;display:flex;flex-direction:column;overflow:hidden;flex:1}
.pane[hidden]{display:none}
.sessionPane .stream{flex:1}
/* UI-34: the way back to the newest card. A stream that stopped following has
   unread work below its bottom edge, and the reader has to be able to see that
   and end it with one press — so the mark sits inside the pane, over the stream,
   not in the bar above where it reads as a property of the session. */
.sessionPane{position:relative}
.tailJump{position:absolute;left:50%;transform:translateX(-50%);bottom:14px;z-index:3;
  display:none;align-items:center;gap:7px;padding:5px 13px;border-radius:999px;
  border:1px solid var(--line2);background:var(--bg0h);color:var(--bright);
  font:inherit;font-size:12px;box-shadow:0 3px 10px rgba(0,0,0,.45);cursor:pointer}
.tailJump.show{display:inline-flex}
.tailJump .n{color:var(--accent);font-weight:600}
/* The bar is two groups that wrap onto a second line when the window is narrow.
   It has to wrap: adding the text controls (UI-23) made the row wider than a
   small window, and what spilled off the edge was the Download button — the one
   thing in the bar you cannot do without. `min-width:0` on the file group is what
   lets the path give way to its ellipsis instead of pushing everything right. */
.viewerBar{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;padding:8px 14px;min-width:0;
  border-bottom:1px solid var(--line);background:var(--bg0h);flex:none;font-size:12px;color:var(--dim)}
.viewerBar .fileBits{display:flex;align-items:center;gap:10px;min-width:0;flex:1 1 auto}
/* The controls wrap among themselves as a last resort: at a very narrow window
   their own minimum width is wider than the pane, and the alternative is the
   Download button sitting where nothing can reach it. */
.viewerBar .barCtl{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;
  gap:6px 8px;flex:0 1 auto;min-width:0;margin-left:auto}
.viewerBar .path{font-family:ui-monospace,monospace;color:var(--dim2);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;min-width:60px;flex:1 1 auto}
.viewerBar .warn{color:var(--yellow)}
/* UI-20: Download is a button, not a link. It used to be an <a class="primary">,
   which no CSS rule matched — `button.primary` only styles buttons — so it
   rendered as plain underlined text in the corner of the bar and nobody saw it.
   Solid Gruvbox yellow with the dark background as its text colour: the one
   element in that bar that is unmistakably pressable. */
/* One height for every control in this bar. Left to their own rules the plain
   buttons and the Download button came out at different heights — 53px against
   35px — which reads as a broken toolbar the moment the row wraps. */
.viewerBar .barCtl > button,.viewerBar .dlBtn{height:30px;padding:0 12px;font-size:12px;
  display:inline-flex;align-items:center;gap:6px;flex:none;white-space:nowrap}
.viewerBar .dlBtn{text-decoration:none;font:inherit;font-size:12px;font-weight:700;padding:0 14px;
  border-radius:6px;border:1px solid var(--yellow);background:var(--yellow);color:#1d2021;cursor:pointer}
.viewerBar .dlBtn:hover{background:#ffd257;border-color:#ffd257;color:#1d2021}
.viewerBar .dlBtn:focus-visible{outline:2px solid var(--bright);outline-offset:2px}
/* UI-23 → UI-44: the viewer wears the same reading font and the same reading size
   as the Group Chat, the file panel and the composer — one pair of custom
   properties, so the controls in the file panel move the text everywhere at once.
   The viewer's own size used to be a third preference beside these; it duplicated
   the page-wide one and made two bars disagree, so it is gone. */
.viewerBody{flex:1;overflow:auto;background:var(--bg);
  font-family:var(--read-family);font-size:var(--read-size)}
.viewerBody img{max-width:100%;display:block;margin:16px auto;border:1px solid var(--line);border-radius:6px}
.viewerBody .notice{padding:24px;color:var(--dim);font-size:13px;text-align:center}
/* UI-44: the text controls left this bar for the file panel, so the only controls
   left here are the family toggle and the Raw/Formatted switch — both sized by
   the `.barCtl > button` rule above, one height for the row. */
/* UI-25: which of the two family buttons is in force. The plain one is the default
   (mono), so only the switched-on state is marked — same idea as the Raw/Formatted
   button: mark the state that changes what you are looking at. */
.viewerBar .famToggle.on{background:var(--bg3);border-color:var(--bg4);color:var(--bright);font-weight:700}
/* UI-23: raw/rendered switch. Yellow while the formatted view is on, because
   that is the state you can be caught out by — what is on screen is no longer
   the file's own characters. */
.viewerBar .fmtBtn.on{background:var(--yellow);border-color:var(--yellow);color:#1d2021;font-weight:700}
.viewerBar .fmtBtn.on:hover{background:#ffd257;border-color:#ffd257}
/* Rendered markdown (UI-23). Prose, not code: no line numbers, and the measure
   is capped so a long document stays readable while the pane is full width. */
.md{padding:22px 28px 60px;max-width:820px;line-height:1.7;color:var(--text)}
.md > :first-child{margin-top:0}
.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{color:var(--bright);line-height:1.3;margin:1.5em 0 .5em;font-weight:700}
.md h1{font-size:1.7em;border-bottom:1px solid var(--line);padding-bottom:.3em}
.md h2{font-size:1.35em;border-bottom:1px solid var(--line);padding-bottom:.25em}
.md h3{font-size:1.15em}
.md h4,.md h5,.md h6{font-size:1em}
/* UI-39: in the file viewer, headings at every level are bright green and bold text
   is bright yellow, so the shape of a markdown file reads at a glance instead of
   needing to be parsed line by line. Both use the palette's own tokens (`--green`
   #b8bb26, `--yellow` #fabd2f) — bright against `--bg` and already the colours of
   "structure" and "emphasis" elsewhere in the page (a file tab's kind, a user's name).

   Scoped to `.viewerBody`, which is only ever the file pane's body (UI-14): the chat
   cards render markdown through the same `md` class and keep the cream headings and
   bold they have always had. Ask was for the file content viewer, and a chat card
   recoloured by it would be a change to a surface nobody asked about.

   A heading's own bold words stay the heading's colour — `## **Note**` renders as
   `<h2><strong>Note</strong></h2>`, so an unscoped bold rule would split one heading
   into two colours. `inherit` on the nested case also catches bold deeper inside. */
.viewerBody .md :is(h1,h2,h3,h4,h5,h6){color:var(--green)}
.viewerBody .md :is(strong,b){color:var(--yellow)}
.viewerBody .md :is(h1,h2,h3,h4,h5,h6) :is(strong,b){color:inherit}
.md p,.md ul,.md ol,.md blockquote,.md table,.md pre{margin:0 0 1em}
.md ul,.md ol{padding-left:1.6em}
.md li{margin:.2em 0}
.md li.task{list-style:none;margin-left:-1.3em}
.md li.task input{margin-right:.5em}
.md a{color:var(--blue);text-underline-offset:2px}
.md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.92em;
  background:var(--bg0h);border:1px solid var(--line);border-radius:4px;padding:1px 5px}
.md pre{background:var(--bg0h);border:1px solid var(--line);border-radius:8px;padding:12px 14px;
  overflow:auto;font-size:.92em}
.md pre code{background:none;border:none;padding:0;font-size:1em}
.md blockquote{border-left:3px solid var(--bg3);padding:2px 0 2px 14px;color:var(--dim2)}
.md hr{border:none;border-top:1px solid var(--line2);margin:1.6em 0}
.md table{border-collapse:collapse;display:block;overflow:auto;max-width:100%}
.md th,.md td{border:1px solid var(--line);padding:6px 11px;text-align:left}
.md th{background:var(--bg1);color:var(--bright);font-weight:600}
.md img{max-width:100%;border-radius:6px}
.md .mdErr{color:var(--yellow)}
/* UI-23: rendered is not raw — say so, and keep the raw text one click away. */
.md .mdNote{font-size:11px;color:var(--bg4);margin-bottom:16px;font-family:inherit}
/* UI-48: a diagram. It wears a fenced block's frame, because in one document a
   picture of a process and the text of a process are the same kind of thing — and
   because what is on screen *before* the renderer draws, and for ever after if it
   cannot, is the fence's own source: it is laid out as code so a diagram that
   cannot be drawn is still readable rather than a blank box. */
.md .mermaidBlock{background:var(--bg0h);border:1px solid var(--line);border-radius:8px;
  padding:12px 14px;margin:0 0 1em;overflow:auto;white-space:pre;
  font-family:var(--code-family);font-size:.92em;color:var(--text)}
.md .mermaidBlock.mermaidDrawn{white-space:normal;text-align:center}
.md .mermaidBlock.mermaidDrawn svg{max-width:100%;height:auto}
/* The one state worth a colour: the file asked for a picture and the bundle said
   no. The source is still there — this only says why it is still there. */
.md .mermaidBlock.mermaidFailed{border-color:var(--c-order-edge);color:var(--dim2)}
/* Code with line numbers. The gutter is sticky so it survives horizontal scroll.
   UI-25: this is the Raw view, and it used to wear a fixed 12.5px monospace no
   matter what the Text controls said — the picker and A−/A+ moved the formatted
   document and left the file's own characters untouched. It now wears exactly what
   the controls say: one size, one reading, no hidden offset. (A `calc(-1.5px)` with
   a `max()` floor was tried first and failed in a real browser: at the small end of
   the slider three settings in a row painted the same size, so A+ looked broken.)
   The family follows `--code-family`, which stays monospace until the bar's
   Mono/Sans button says otherwise — proportional characters lose the column
   alignment a listing depends on. */
.code{display:block;font-size:var(--read-size);line-height:1.5;
  font-family:var(--code-family);padding:10px 0;min-height:100%}
.ln{display:flex;padding:0 14px 0 0}
.ln:hover{background:#32302f}
.ln .num{flex:none;min-width:52px;width:auto;padding-right:14px;text-align:right;color:#665c54;user-select:none;
  position:sticky;left:0;background:var(--bg)}
.ln .src{white-space:pre;tab-size:4}
.t-comment{color:#928374;font-style:italic}
.t-str{color:#b8bb26}
.t-chr{color:#b8bb26}
.t-num{color:#d3869b}
.t-kw{color:#fb4934}
.t-type{color:#fabd2f}
.t-fn{color:#83a598}
.t-macro{color:#8ec07c}
.t-prep{color:#d3869b}
.t-deco{color:#8ec07c}
.t-tag{color:#83a598}
.t-attr{color:#fabd2f}
.t-punct{color:#a89984}
.t-key{color:#83a598}
.t-op{color:#a89984}
.t-ident{color:#ebdbb2}
.overlay{position:fixed;inset:0;background:rgba(29,32,33,.72);display:flex;
  align-items:center;justify-content:center;z-index:50}
.overlay[hidden]{display:none}
/* The dialog is capped and the question gives way first: the four-line answer box
   made the dialog taller than a short window, and the overlay centres without
   scrolling, so the Answer button sat below the edge of the screen where nothing
   could reach it (measured in a real browser at a 300px-tall window: the dialog
   302px tall, the button at 242..280 with the window ending at 300 — and past that
   the button left the window entirely). The question scrolls in its own box; the
   dialog scrolling is the last resort. */
.dialog{background:var(--bg1);border:1px solid var(--bg3);border-radius:10px;padding:20px;
  width:min(560px,92vw);max-height:94vh;overflow:auto;display:flex;flex-direction:column;gap:12px}
.popupAgent{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--purple);flex:none}
.popupQuestion{white-space:pre-wrap;overflow-wrap:anywhere;font-family:var(--read-family);
  font-size:var(--read-size);flex:none;max-height:28vh;overflow:auto}
/* UI-7: the popup's answer box. A textarea, not an input: an answer is often
   several lines (a list of values, a paragraph, a command) and a single-line field
   forces it all onto one. Four lines are shown from the start (`rows="4"`), it reads
   at the reader's own font, and the corner handle is painted (the browser's own is
   invisible on a dark theme — the same reason the composer got a grip, UI-14).
   `min-height` is the give: in a window too short for four lines plus a long
   question, the box shortens rather than pushing the Answer button off the screen. */
.dialog input,.dialog textarea{background:var(--bg0h);border:1px solid var(--line2);border-radius:6px;
  padding:9px 11px;font:inherit;width:100%}
.dialog textarea{font-family:var(--read-family);font-size:var(--read-size);line-height:1.5;
  resize:vertical;min-height:2.5em;max-height:60vh;overflow:auto}
.dialog textarea::-webkit-resizer{background:var(--bg3);border-radius:0 0 5px 0}
.dialogRow{display:flex;align-items:center;gap:8px}
body.login{display:flex;align-items:center;justify-content:center;height:100vh;background:var(--bg0h)}
.loginCard{background:var(--bg1);border:1px solid var(--line2);border-radius:10px;padding:26px;
  width:320px;display:flex;flex-direction:column;gap:12px}
.loginCard .msg{color:var(--dim);margin:0;font-size:12px;min-height:16px}
.loginCard input{background:var(--bg0h);border:1px solid var(--line2);border-radius:6px;padding:9px 11px;font:inherit}
@media (max-width:1100px){
  #tree{position:fixed;right:0;top:0;bottom:0;z-index:30;box-shadow:-6px 0 18px rgba(0,0,0,.45)}
  /* The drawer is a floating overlay at its own width; a handle on the window's
     edge would resize nothing. */
  #treeGrip{display:none}
}
@media (max-width:820px){
  /* #side and #tree are position:fixed/none here, so #main is the only in-flow
     item left and takes this track — one shrunken row keeps the composer pinned
     (UI-12). */
  #app{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}
  #side{display:none}
  #sideGrip{display:none}
  /* One track, so the numbered placement has to come back to it — `grid-column:2`
     here would open an implicit column and put the app off the screen. */
  #main{grid-column:1}
  /* UI-51: the list is already out of the layout here, so a button to hide it would
     be a control that hides nothing — the same reason `.barTextCtl` only appears at
     this width instead of duplicating the sidebar's controls. */
  #sessionsBtn{display:none}
  /* WIP: the banner keeps its message at this width but drops the session name —
     the count is the point, and the title is already in the bar above it. */
  .wipBanner{padding:6px 10px;gap:8px;font-size:12px}
  .wipBanner .wipName{display:none}
  /* The header takes over the text controls the hidden sidebar used to own. */
  .barTextCtl{display:flex}
  #tree{width:min(330px,86vw)}
  #main{height:100dvh}
}

/* UI-30: maths. The viewer draws TeX with the palette's own colours rather than a
   library's defaults, so a formula inside a paragraph reads as the prose around it.
   Fractions stack, scripts sit smaller and lower, and a run keeps the text's line
   height so one formula cannot stretch the paragraph holding it (UI-10 rule). */
.tex{font-family:"Latin Modern Math","STIX Two Math",Cambria Math,"Times New Roman",serif;
  white-space:normal;line-height:1;font-size:1.03em}
.tex sup,.tex sub{font-size:.74em;line-height:0}
.tex sup{vertical-align:.52em}.tex sub{vertical-align:-.28em}
.tex-frac{display:inline-flex;flex-direction:column;text-align:center;
  vertical-align:-.45em;margin:0 .14em}
.tex-num{border-bottom:1px solid currentColor;padding:0 .2em .1em}
.tex-den{padding:.1em .2em 0}
.tex-sqrt{display:inline-flex;align-items:flex-start}
.tex-rad{font-size:1.08em}
.tex-under{border-top:1px solid currentColor;padding:.08em .12em 0;margin-left:-.06em}
.tex-index{font-size:.64em;align-self:flex-start;transform:translate(.12em,.5em)}
.tex-acc{position:relative;display:inline-block}
.tex-accmark{position:absolute;left:0;right:0;top:-.58em;text-align:center;font-size:.86em}
/* The environment markup the renderer actually emits is .tex-env > .tex-delim +
   .tex-grid > .tex-row > .tex-cell. Rows are flex rows and columns line up because
   every cell reports the same flex basis, so `&` alignment holds without a table. */
.tex-env{display:inline-flex;align-items:center;vertical-align:middle;margin:0 .1em}
.tex-delim{font-size:1.24em;line-height:1;font-style:normal}
/* Two or more rows: stretch the bracket rather than leave a small glyph beside a
   tall grid. A scale on the glyph is an approximation of TeX's stretch — the
   curvature is slightly off, the height is not. */
.tex-env-tall{align-items:stretch}
.tex-env-tall .tex-delim{display:flex;align-items:center;transform:scaleY(1.75)}
.tex-env-tall .tex-grid{justify-content:center}
.tex-grid{display:inline-flex;flex-direction:column;gap:.2em;padding:.14em .42em}
.tex-row{display:flex;gap:.62em;align-items:baseline}
.tex-cell{display:block;flex:0 1 auto;text-align:center;min-width:2.2em}
.tex-cellsep{display:inline-block;width:1.1em}
.tex-sp{display:inline-block;width:.28em}
.tex-thin{display:inline-block;width:.17em}
.tex-quad{display:inline-block;width:1em}
.tex-bounded{display:inline-block;width:.22em}
.tex-block{display:block;margin:.7em 0;text-align:center;overflow-x:auto;overflow-y:hidden}
.tex-block .tex{display:inline-block;font-size:1.08em}
.tex-op{font-style:normal;padding:0 .06em}
.tex-sym{font-style:normal}
.tex-var{font-style:italic}
.tex-unk{font-style:normal;text-decoration:underline dotted;text-underline-offset:.22em}
.tex-katex .katex{font-size:1em}
/* ---- ADMIN-1: the admin console -----------------------------------------
   Same Gruvbox, same buttons, same text as the app — the console is the same
   program, so it must not look like a different product bolted onto a port. The
   layout is a single centred column rather than the app's three-pane grid: there is
   nothing to keep open beside a settings form, and a sidebar here would only be a
   place to hide the one button that deletes somebody's work.

   Contrast: every sentence on this page is `--dim2` (#bdae93) or brighter on its own
   background — 5.3:1 on the panel and 5.9:1 on a form card, both over the 4.5:1 AA
   floor for text this size. `--dim` measures 4.2:1 on the panel and `--bg4` 2.4:1, so
   neither carries words here; the hierarchy between a label, a hint and a value is
   carried by size, case and letter-spacing instead of by colour, which is the part a
   low-vision reader cannot recover from a colour alone. */
body.admin{background:var(--bg0h);color:var(--text);min-height:100vh;
  /* The palette's --red is 3.4:1 on a panel — fine for a border, under the 4.5:1 floor
     for a sentence, and every red string on this page is a sentence. The console keeps
     --red for edges and uses this lighter one for the words. */
  --err-text:#ff8073}
#adminApp{max-width:960px;margin:0 auto;padding:18px 18px 64px;display:flex;
  flex-direction:column;gap:13px}
#adminBar{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.adminWho{font-size:12px;color:var(--dim2)}
.adminPath{margin:0;font-size:12px;color:var(--dim2);overflow-wrap:anywhere}
.adminPath code,.hint code{font-family:var(--code-family);font-size:.95em;color:var(--dim2)}
.adminMsg{margin:0;font-size:12.5px;min-height:18px;color:var(--dim2);
  overflow-wrap:anywhere}
.adminMsg.ok{color:var(--green)}
.adminMsg.err{color:var(--err-text)}
.adminH{font-size:.79em;text-transform:uppercase;letter-spacing:.09em;color:var(--dim2);
  margin:8px 0 0;font-weight:600}
.adminCount{color:var(--dim);letter-spacing:0;font-variant-numeric:tabular-nums}
.adminCard{background:var(--bg0h);border:1px solid var(--line2);border-radius:10px;
  padding:14px 16px;display:flex;flex-direction:column;gap:9px}
.createForm{display:flex;gap:8px;flex-wrap:wrap}
.createForm input{flex:1 1 210px;background:var(--bg1);border:1px solid var(--line2);
  border-radius:6px;padding:9px 11px;font:inherit;color:var(--text)}
/* The tab bar. Underline-style rather than pill-style: with nine tabs a row of pills
   reads as nine buttons and gives no hint of which one you are looking at, while a bar
   under the active name ties the panel below it to that name. It wraps rather than
   scrolls at desktop widths — a tab you cannot see is a tab that does not exist. */
.adminTabs{display:flex;flex-wrap:wrap;gap:2px;border-bottom:1px solid var(--line2);padding:0 2px}
.adminTab{background:none;border:1px solid transparent;border-bottom:none;
  border-radius:8px 8px 0 0;padding:8px 13px;font-size:12.5px;color:var(--dim2);
  cursor:pointer;position:relative;top:1px}
.adminTab:hover{color:var(--bright);background:var(--bg1)}
.adminTab[aria-selected="true"]{background:var(--bg1);border-color:var(--line2);
  color:var(--bright);font-weight:600}
.adminTab:focus-visible{outline:2px solid var(--blue);outline-offset:1px}
/* The panel is the card, so a tab and the thing it reveals share a background.
   `tabindex=0` on the panel makes it a stop for a keyboard user who wants to scroll a
   long form without tabbing through every field first. */
.adminPanel{background:var(--bg1);border:1px solid var(--line2);border-radius:0 10px
  10px 10px;padding:15px 16px;display:flex;flex-direction:column;gap:11px;outline:none}
.adminPanel:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
/* HIDDEN-BUTTON: the bug the operator saw as "every tab shows its settings at once".
   `.adminPanel` sets `display:flex` and `showTab` hides a panel with `p.hidden = true`,
   but `[hidden]` is a UA rule of the *same* specificity as a class and an author rule
   wins a tie — so all nine panels stayed painted and the tab bar only moved the page.
   Every other toggled element here already carried its own override (`.pane[hidden]`,
   `.overlay[hidden]`, `#tree[hidden]`); the console's panels were the ones that did not. */
.adminPanel[hidden]{display:none}
/* The standing line (what this tab is) and the body the script fills. Splitting them
   means a load that fails leaves the description on screen with the error in the
   status line, rather than wiping the tab back to a blank card. */
.panelBody{display:flex;flex-direction:column;gap:11px;min-width:0}
.panelLoad,.panelNote,.panelHint{margin:0;font-size:12px;color:var(--dim2);overflow-wrap:anywhere}
.panelHint{font-size:11.5px}
.usedBy{font-size:11.5px;color:var(--dim2)}
/* ADMIN-EXPORT: exporting the team is a download, and a download is a link — the
   browser's Save dialog needs an `href` and the server's own `Content-Disposition`,
   which a button that fetches a blob cannot hand over. UI-20 is the lesson here: an
   `<a class="primary">` got no styling at all, because `button.primary` matches
   buttons only. So the link is given the button's own box rather than a class it does
   not own — same border, padding, hover and focus ring as `button` above. */
a.dlLink{display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:12.5px;
  padding:7px 12px;border-radius:6px;border:1px solid var(--line2);background:var(--bg1);
  color:var(--text);text-decoration:none;cursor:pointer;white-space:nowrap;
  overflow-wrap:anywhere}
a.dlLink:hover{border-color:var(--accent);color:var(--bright)}
a.dlLink:focus-visible{outline:2px solid var(--blue);outline-offset:1px}
/* The two controls that act on the team as one thing, in one row above the list they
   act on. Wraps rather than squeezes: at 360px the note is the thing that takes the
   next line, not the buttons. */
.teamBar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.teamBar .formNote{flex:1 1 220px;min-width:0}
/* One form per row (a provider, an agent) and one grid of fields inside it. The grid
   is auto-fit rather than a fixed column count so the same markup gives three fields
   across on a monitor and one on a phone, without a media query per breakpoint. */
.adminForm{display:flex;flex-direction:column;gap:10px}
.rowForm{border:1px solid var(--line2);border-radius:10px;padding:13px 14px;background:var(--bg0h)}
.fieldGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));gap:11px 16px}
.field{display:flex;flex-direction:column;gap:4px;min-width:0}
.field.wide{grid-column:1/-1}
.field label,.fieldLabel{font-size:11px;text-transform:uppercase;letter-spacing:.07em;
  color:var(--dim2)}
.field input,.field select,.field textarea{background:var(--bg0h);
  border:1px solid var(--line2);border-radius:6px;padding:8px 10px;font:inherit;
  color:var(--text);min-width:0}
.rowForm .field input,.rowForm .field select,.rowForm .field textarea{background:var(--bg1)}
.field textarea{font-family:var(--code-family);font-size:12.5px;resize:vertical}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  outline:2px solid var(--blue);outline-offset:1px}
.field input[readonly]{color:var(--dim2);border-style:dashed;cursor:default}
.fieldHint,.fieldDefault,.formNote{font-size:11px;color:var(--dim2);overflow-wrap:anywhere}
.formNote{font-size:11.5px}
.fieldDefault code,.fieldLocked{font-family:var(--code-family);color:var(--dim2)}
.fieldLocked{margin:0;font-size:12px}
.formFoot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.switch{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--text)}
.switch input{width:auto}
/* A refused batch marks the box that caused it. The red border is never the whole
   message — the sentence goes in `.fieldErr` under the field, because a colour cannot
   say which of two similar keys was rejected or what value would be accepted. */
.field.bad > input,.field.bad > select,.field.bad > textarea{border-color:var(--red)}
.fieldErr{margin:0;font-size:11.5px;color:var(--err-text);overflow-wrap:anywhere}
/* A refused save is shown where the button is, not only in the status line at the top
   of the page: the Security tab is long, so a one-line message above the panels is off
   screen by the time the operator reads the box they just changed — and the sandbox
   note under that form is a permanent paragraph that looks exactly like an answer.
   This is a box, it appears only when the server refused, and it says so in words. */
.saveErr{margin:0;display:flex;flex-direction:column;gap:5px;
  border:1px solid #7a2e1f;background:#32201c;color:var(--err-text);border-radius:8px;
  padding:10px 12px;font-size:12.5px;overflow-wrap:anywhere}
.saveErr b{color:#fdd0c5;font-weight:600}
.saveErr code{font-family:var(--code-family);font-size:.95em;color:var(--dim2)}
.saveErr:focus-visible{outline:2px solid var(--red);outline-offset:1px}
.derived{border-top:1px solid var(--line2);padding-top:11px;display:flex;
  flex-direction:column;gap:7px}
.kv{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:7px 14px;
  margin:0;font-size:12.5px}
.kv > div{min-width:0}
.kv dt{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:var(--dim2)}
.kv dd{margin:1px 0 0;color:var(--dim2);overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
.userList{display:flex;flex-direction:column;gap:10px}
/* One card per account. A table is the natural shape for this much metadata and it is
   also the shape that breaks first on a phone: eight columns of counts do not fit
   360px. A card wraps its own grid, so one markup serves both widths. */
.userCard{background:var(--bg0h);border:1px solid var(--line2);border-radius:10px;
  padding:12px 14px;display:flex;flex-direction:column;gap:9px}
.userTop{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.userTop .name{font-weight:700;color:var(--bright);font-size:1.06em;overflow-wrap:anywhere}
.badge{font-size:10px;text-transform:uppercase;letter-spacing:.06em;border:1px solid var(--line2);
  border-radius:999px;padding:2px 9px;color:var(--dim2);white-space:nowrap}
.badge.on{border-color:var(--c-final-edge);color:var(--green)}
/* The warn badge keeps the plum edge and takes its text colour from the yellow: the
   purple is 4.2:1 at 10px, and a badge that says "home outside workspace" is a warning
   a person has to be able to read. */
.badge.warn{border-color:var(--c-order-edge);color:var(--yellow)}
.userMeta{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:7px 14px;
  margin:0;font-size:12px}
.userMeta > div{min-width:0}
.userMeta dt{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:var(--dim2)}
.userMeta dd{margin:1px 0 0;color:var(--dim2);overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
.userFoot{font-size:11.5px;color:var(--err-text)}
.userActions{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto}
.dlgTitle{margin:0;font-size:1.02em;color:var(--bright);font-weight:700;overflow-wrap:anywhere}
.dlgNote{margin:0;font-size:12.5px;color:var(--dim2)}
.dlgLabel{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--dim2)}
/* The two gates of the confirm dialog: a label and its field, kept together so the
   dialog's own column gap falls between the gates rather than between a label and the
   field it belongs to. `[hidden]` is overridden because `.dlgGate` sets `display` at
   the same specificity as the UA rule behind `[hidden]` and an author rule wins the tie
   — the exact bug HIDDEN-BUTTON pinned for the panels. */
.dlgGate{display:flex;flex-direction:column;gap:5px}
.dlgGate[hidden]{display:none}
.dangerTag{color:var(--err-text)}
.importBox{border:1px dashed var(--line2);border-radius:10px;padding:13px 14px;
  background:var(--bg0h);display:flex;flex-direction:column;gap:9px}
.importRow{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.importName{font-size:12px;color:var(--dim2);overflow-wrap:anywhere;min-width:0}
/* The native chooser, kept native on purpose (a hidden input behind a styled label is
   not in the tab order), with its button pulled into the palette. */
.importFile{flex:1 1 240px;min-width:0;font:inherit;font-size:12px;color:var(--dim2);
  background:var(--bg1);border:1px solid var(--line2);border-radius:6px;padding:6px 8px}
.importFile::file-selector-button{font:inherit;font-size:12px;cursor:pointer;
  background:var(--bg2);border:1px solid var(--line2);border-radius:6px;
  padding:5px 10px;margin-right:9px;color:var(--text)}
.importFile:focus-visible{outline:2px solid var(--blue);outline-offset:1px}
.delList{margin:0;padding-left:18px;font-size:12.5px;display:flex;flex-direction:column;gap:5px}
.delList li{overflow-wrap:anywhere}
.delList .gone{color:var(--dim2)}
.delList .gone b{color:var(--err-text)}
.delList .keep{color:var(--green)}
/* A refusal is not a "will be kept" row from the warning list the dialog opened with —
   it is the answer to the press. It takes the page's own error colour, the one
   `.adminMsg.err` and `.gone b` already use on this background. */
.delList .refused{color:var(--err-text)}
/* The app's `.hint` is 11px, `--dim` and `white-space:nowrap` with an ellipsis — right
   for a one-line file row, wrong here: "no users yet — the first one has to be created
   above" is a sentence, and on this background `--dim` is 4.2:1. The console takes its
   own copy of that class back to a readable size, colour and wrapping. */
.adminPanel .hint{font-size:12px;color:var(--dim2);white-space:normal;overflow:visible;
  text-overflow:clip;margin:0}
.loginHint{margin:0;font-size:11.5px;color:var(--dim)}
@media (max-width:640px){
  #adminApp{padding:12px 12px 48px}
  .userActions{margin-left:0;width:100%}
  .userActions button{flex:1}
  /* The chooser and its name share a row; at 360px the name is the thing that must
     wrap, not the buttons, or the Import button is pushed off the card. */
  .importRow{gap:8px}
  .importName{flex:1 1 100%;order:3}
  /* Nine tabs at 360px: two rows of five is unreadable, so the bar becomes a
     horizontally scrollable strip and the browser keeps the focused tab in view. One
     axis of scrolling inside a scrolling page is acceptable here because the strip has
     a visible edge and holds a single row of content. */
  .adminTabs{flex-wrap:nowrap;overflow-x:auto;padding-bottom:2px;-webkit-overflow-scrolling:touch}
  .adminTab{white-space:nowrap}
  .adminPanel{border-radius:0 8px 8px 8px;padding:13px 12px}
  .formFoot button{flex:1}
}
@media (prefers-reduced-motion:reduce){.tex *{transition:none}}
