/* GENERATED — do not edit.
   Verbatim mirror of packages/chrome/src/css/chrome.css, which is the
   implementation the product runs and therefore the truth. Change it
   there and re-run: npm run design:tokens
*/
/* _design chrome.css — the implementation of the _design skill's components.md.
   Consumes canon roles only (skills/shared/tokens.css, via the generated
   tokens.css beside this file); no literal colors. Every component here has a
   state in the skill's specimen.html — keep them in lockstep (ship gate). The
   skill's copy at skills/internal/design/assets/chrome.css mirrors this file. */

/* chrome-local tokens (not canon — see foundations.md) */
:root{
  --hairline:color-mix(in srgb, var(--ink) 10%, transparent);
  /* The step a quiet label takes when you point at it: toward `--ink` without
     arriving. Canon#7 gives three text steps, and a label going straight from
     `--ink-2` to full `--ink` on hover reads as promoted to content rather than
     acknowledged — the pointer is already saying which row it is. Derived from the
     two roles it sits between, so it re-steps with the theme like everything else
     (dark ≈ #d1d1d1 on #949494, light ≈ #3e3e3e on #6a6a6a). */
  --ink-lift:color-mix(in srgb, var(--ink) 65%, var(--ink-2));
  /* The measure, and there is only one: the thread column, the composer that
     writes into it, and the search pills on Artifacts and Skills. An input that
     is narrower than the text it produces reads as a different column, so these
     do not get to drift apart — move this and they all move together. */
  --thread-col:720px; /* max measure of the thread column (_design#16) */
  /* every pane header's height (_design#16). Named because a second place now
     measures from it — the gallery's sticky column head — and a bare `60px`
     written twice is two numbers that silently stop agreeing. */
  --pane-h:60px;
  /* the sign-in measure, and it is NOT the thread's: that screen asks for one
     short answer, so it is sized by the longest one it takes — the six code
     cases, 6×56 + 5×8. The case is a square and the column follows the box */
  --signin-col:376px;
  /* the inline-action hue (.act) — a step of the ONE blue, re-stepped for dark
     like every other role (canon#8). --c1 is the brand anchor, which is what an
     action offered by the platform should wear; dark takes --c2 because #3363FF
     on --paper #0f0f0f is too dim for 13.5px text. */
  --action:var(--c1);
  /* TWO modal measures, because a modal here holds one of two different things.
     A skill's doctrine is a FORM — labelled fields, short lines, read at the
     distance a form is read — and it keeps the 880px measure.
     A report is a DOCUMENT. It brings its own paper, its own 720px column and
     figures that deliberately overhang that column, and the frame is the
     figures' entire budget: the guest is a srcDoc iframe at width:100%, so the
     frame IS the report's viewport and `.wider`'s 92vw is measured against it.
     At 880 that made every figure 809.6px, so a diagram authored on the 860-unit
     canvas the skill specifies drew at 0.94 of its size and its 11px labels
     landed at 10.4px — the report's own 860px cap was unreachable, dead code
     behind the frame. 1200px gives the figure lane ~1104px and leaves the prose
     column untouched at --thread-col inside it.
     Reports were always meant to take the wider panel (see ArtifactModal); this
     is the CSS finally saying so. */
  --modal-w:880px;
  --modal-w-report:1200px;
  /* The scrim always DIMS, which means it has to follow the dark end of whichever
     theme is on: `--ink` is the dark role in light, `--paper` is the dark role in
     dark. A single paper-based scrim was white-on-white in light mode — a report is
     white paper, so the page it floated over and the page itself were one surface. */
  --scrim:color-mix(in srgb, var(--ink) 45%, transparent);
}
@media (prefers-color-scheme:dark){
  :root{--scrim:color-mix(in srgb, var(--paper) 82%, transparent);--action:var(--c2)}
}
:root[data-theme="light"]{--scrim:color-mix(in srgb, var(--ink) 45%, transparent);
  --action:var(--c1)}
:root[data-theme="dark"]{--scrim:color-mix(in srgb, var(--paper) 82%, transparent);
  --action:var(--c2)}

/* ---------- base ----------
   Two roots, not one: the modal is PORTALED to the body, so it is outside `.app`
   and inherits none of it. Everything below is declared for both — a rule that
   lived only on `.app` silently stopped applying the moment a surface moved out of
   that subtree, and the way it showed up was black text on a dark panel. */
.app *,.skill-modal *{box-sizing:border-box;margin:0}
/* every button in the chrome is custom-styled — kill UA chrome (iOS Safari
   otherwise draws its own boxes around bare buttons). Appearance ONLY:
   a font reset here would out-rank component font sizes by specificity. */
.app button,.skill-modal button{appearance:none;-webkit-appearance:none}
.app{
  display:grid;gap:0;
  grid-template-columns:var(--w-side,260px) 8px 1fr 8px var(--w-details,320px);
  /* pin the single row to the shell height — otherwise regions size to their
     content, overflow the shell, and the thread pane never scrolls */
  grid-template-rows:minmax(0,1fr);
  background:var(--paper);
}
/* the text grammar itself, shared by every root that renders chrome (canon#7) */
/* --text is the READING size, and every region reads at it: the trace, the
   thread cards, the details rows, a follow-up wherever it is standing. A
   sidebar set a step smaller than the thread said the threads in it mattered
   less than the one open, which is not true — it is the same record, listed.
   One declaration, two values (it steps up with the type on narrow viewports),
   and nothing to keep in sync. Junior tiers stay junior: labels and meta have
   their own sizes and are unaffected. */
.app,.skill-modal{color:var(--ink);--text:14px;
  font-family:var(--font-body);font-size:14px;letter-spacing:-0.01em;
  line-height:1.55}
.side,.details{overflow:hidden;min-width:0}
/* the positioning context for the pinned account row */
.side{position:relative}
/* the main region is a flex column — the thread pane scrolls INSIDE it
   (without this, .thread-scroll's flex:1 is inert and nothing scrolls) */
.main{padding:0 20px;min-width:0;display:flex;flex-direction:column}
/* inner wrappers hold the CONTENT width (--wc-*): dragging writes it, so
   resize reflows content live; collapse zeroes only the region width
   (--w-*), so content clips at its last width instead of squishing
   (_design#11, #15) */
/* Symmetric inset. Every pill in this pane — nav rows and thread cards alike — bleeds
   12px past the text column (`margin:0 -12px`), so the pane's own padding is what
   decides where those pills stop. At 20 left / 12 right they stopped 8px short on the
   left and ran flush into the right edge: one margin, not two. 20 both sides puts them
   8px off each edge. */
.side-in{width:var(--wc-side,260px);padding:0 20px;
  transition:opacity var(--t-fast) var(--ease)}
/* The list, and the only part of the pane that moves.
   Both edges are a MASK on the content itself — the same idiom as the thread pane
   (`.thread-scroll`), for the same reason: no backdrop and no paper strip, so titles
   stay continuously visible as they pass out rather than meeting an edge.
   BOTTOM is fixed: gone 12px above the account row's top (row 32px + 16px inset ⇒ top
   at 48px), starting 24px before that.
   TOP is driven by scroll position (`--fade-top`, set from the pane), so the first
   card is crisp until you actually scroll off it — a permanent top fade would dim a
   list that has nothing above it. Registered as a <length> at the `@property` block
   below, which is what lets it grow in rather than pop. */
/* Out to the pane's edges, then the inset re-applied INSIDE. A scroll container clips
   at its padding box, and a thread card's pill bleeds 12px past the text column on
   both sides (`.thread-card` margin:0 -12px) so titles align with the date labels
   while the fill runs wider. Left sitting inside the head's inset, the scrollport cut
   both rounded ends off square. */
.side-threads{flex:1;min-height:0;overflow-y:auto;overflow-anchor:none;
  margin:0 -20px;padding:0 20px 60px;--fade-top:0px;
  mask-image:linear-gradient(to bottom,transparent 0,#000 var(--fade-top),
    #000 calc(100% - 84px),transparent calc(100% - 60px));
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 var(--fade-top),
    #000 calc(100% - 84px),transparent calc(100% - 60px));
  transition:--fade-top var(--t-med) var(--ease)}
.details-in{width:var(--wc-details,320px);padding:0 12px 18px;transition:opacity var(--t-fast) var(--ease)}
.app{transition:grid-template-columns var(--t-med) var(--ease)}
.app.no-side{--w-side:0px}
.app.no-details{--w-details:0px}
.app.no-side .side-in,.app.no-details .details-in{opacity:0}
.app.dragging{transition:none}
/* region boundaries are live (_design#15): a REALLY slim quiet line, always
   on; hovering doesn't widen it — the color steps to full ink (white/black
   per theme) to mark the col-resize handle */
.resizer{cursor:col-resize;display:flex;align-items:stretch;justify-content:center;
  touch-action:none;user-select:none}
/* square ends: a rounded cap's anti-aliasing fades the tips of a hairline,
   so the line would look like it stops short of the page edges */
.resizer::after{content:"";width:0.5px;border-radius:0;
  background:var(--hairline);
  transition:background var(--t-fast) var(--ease)}
.resizer:hover::after,.resizer.active::after{background:var(--ink)}
.app.no-side .rz-side,.app.no-details .rz-details{pointer-events:none}
/* toggle icons live in their region while it is open; the thread-header
   copy appears only when that region is closed (_design#15) */
.app:not(.no-side) .main .tgl-side{display:none}
.app:not(.no-details) .main .tgl-details{display:none}

/* ---------- text grammar (_design#3,4 · canon#7) ---------- */
.title{font-family:var(--font-title);font-weight:600;font-size:18px;letter-spacing:-0.02em;text-wrap:wrap}
.wordmark{font-family:var(--font-emph);font-weight:500;font-size:18px;color:var(--ink);text-transform:lowercase}
.label{font-weight:400;font-size:13px;color:var(--ink-2);display:flex;align-items:center;justify-content:space-between;gap:8px}
/* narration is junior by color alone — never italic (canon#7) */
.narr{color:var(--ink-2)}
.agent{font-family:var(--font-emph);font-size:13px;color:var(--ink)}
.attr{color:var(--ink-2);font-size:13px;margin-bottom:4px}
.meta,.tally{font-size:12.5px;color:var(--ink-2);font-variant-numeric:tabular-nums}
.tally{display:inline-flex;align-items:center;gap:3px;white-space:nowrap}

/* ---------- layout utilities — the inline styles that kept repeating ---------- */
.grow{flex:1;min-width:0}
.mt6{margin-top:6px}
.mt8{margin-top:8px}
.mt10{margin-top:10px}
/* the icon-button cluster at a pane header's end */
.pane-actions{display:flex;gap:2px}

/* ---------- state glyphs (_design#5) — small, 13px system ---------- */
.g-done{width:13px;height:13px;border-radius:50%;background:var(--ink);color:var(--paper);
  display:inline-flex;align-items:center;justify-content:center;flex:none;
  font-size:7px;font-weight:700;line-height:1}
/* the done check uses Lucide's check geometry, masked in --paper —
   crisp and font-independent (state glyphs stay CSS constructions) */
.g-done::after{content:"";width:7px;height:7px;background:var(--paper);
  -webkit-mask:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center/contain no-repeat;
  mask:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center/contain no-repeat}
/* the fourth state glyph (_design#5): a run that ENDED WITHOUT ITS ANSWER —
   refused, failed, or interrupted. Same 13px disk and the same masked-Lucide
   construction as .g-done, carrying `minus` instead of `check`: the check says
   the row got what it was for, the bar says it stopped. Monochrome like every
   other state mark — a failure is not a colour in this chrome (canon#2), and
   --crit stays a data role.
   The alternative considered and rejected: no glyph at all (.g-none), letting
   the words carry it. A trace is scanned down its glyph column, and a run that
   died is the one row that must not be scanned past.

   WHERE THE DISK SURVIVES, now that a trace row carries a kind mark instead
   (.g-kind below): everywhere the summary is "it stopped" and there is no room
   to say which way — the thread card in the sidebar, the details panel's status
   line. A card answers "is this thread still going"; only the trace has space
   for the answer to "and how did it end". */
.g-stop{width:13px;height:13px;border-radius:50%;background:var(--ink);color:var(--paper);
  display:inline-flex;align-items:center;justify-content:center;flex:none}
.g-stop::after{content:"";width:7px;height:7px;background:var(--paper);
  -webkit-mask:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>') center/contain no-repeat;
  mask:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>') center/contain no-repeat}

/* ---------- the kind mark — .g-kind ----------
   The six ways a run ends without its answer, carried by the GLYPH COLUMN
   instead of by a clause: `ban` refused, `arrow-right-left` switched,
   `cloud-off` the service, `gauge` the context window (the composer's own arc,
   again), `hourglass` the turn budget, `log-in` not signed in. Lucide, 13px to
   match the disk it replaces, --ink-2 like every other stroke icon, one per row
   and no disk beside it.

   IT COSTS TWO RULES, and both were taken knowingly. (a) the filled disk is
   reserved for state and stroke for everything else, and this is a stroke mark
   in the state column; (b) the column stops meaning one thing — it used to be
   scanned for "a run died here" and is now read for which way. What buys them
   back is the row: an outcome that was 98px of block at the thread's own
   measure is 24px of line, because the mark is doing the classifying the
   sentence used to do.

   WEIGHT: 2.77 user units is a true 1.5px at 13/24 — the same arithmetic that
   gives .ic.lucide its 2.57 at 14/24. Anything less and the marks read lighter
   than every other icon on screen.

   ALIGNMENT is to the TEXT, not to the row. Pinned to the row's top the way the
   disk is, the mark drifted line to line — a row's top is set by whatever else
   it carries (a 12.5px tally, a 14px action, a fold head) while the sentence's
   first line is not. An SVG baselines on its bottom edge, so bare baseline
   alignment hangs the whole box above the baseline and reads high; the negative
   bottom margin drops it until the box's centre lands on the text's cap centre,
   which is where every .g-done/.g-stop disk in the chrome already sits.

   FULL INK, unlike every other stroke icon in the chrome. The mark stands in
   the state column, where the disks it replaces are solid --ink, and it is
   doing their job: a reader scans that column to find where a run stopped. At
   --ink-2 it matched the narration beside it and stopped being a mark at all —
   the row read as one more gray line rather than as the one to stop on. */
.g-kind{width:13px;height:13px;flex:none;stroke:currentColor;stroke-width:2.77;
  fill:none;stroke-linecap:round;stroke-linejoin:round;color:var(--ink)}
.trace-row>.g-kind{align-self:baseline;margin-bottom:-1.5px}

/* the bare check — a stroke glyph with NO disk, and the only state glyph that
   is not small: .g-done says "this row finished" inside a trace, this says "the
   screen's question is answered" and stands alone where a field was. The disk
   is what separates a row's state from a screen's answer. */
.g-check{width:24px;height:24px;stroke:currentColor;stroke-width:1.5;fill:none;
  stroke-linecap:round;stroke-linejoin:round;color:var(--ink);flex:none}
/* exactly 13px wide (3+2+3+2+3) — the same slice as the disk and the spacer,
   so the run → settled swap never moves the text beside it */
.g-run{display:inline-flex;gap:2px;align-items:center;flex:none;height:13px;width:13px}
.g-run i{width:3px;height:3px;border-radius:50%;background:var(--ink);
  animation:pulse 1.2s var(--ease) infinite}
.g-run i:nth-child(2){animation-delay:.2s}
.g-run i:nth-child(3){animation-delay:.4s}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
@media (prefers-reduced-motion:reduce){
  .g-run i{animation:none;background:var(--ink-2)}
  .app *{transition:none!important}
}

/* ---------- thread card (_design#9) — stadium, tight, title-only ----------
   the title owns the full width; a live run's pulse dots sit IN FLOW at the
   end, taking their slice of the row only while the run exists */
/* titles align with the Threads/Today labels; the hover/active pill BLEEDS
   outward instead (negative margin mirrors the padding) */
.thread-card{display:flex;align-items:center;gap:8px;
  padding:7px 12px;margin:0 -12px;border-radius:var(--r-full);cursor:pointer;
  transition:background var(--t-fast) var(--ease)}
.thread-card:hover{background:var(--accent-soft)}
.thread-card.active{background:var(--panel)}
/* a two-line card is a taller stadium, whose curve bites deeper into its first
   pixels — its text steps 3px in so it does not sit against the bend */
.thread-card.has-meta{padding-left:15px}
/* the text column; the run glyph sits beside it, centered on the card
   whatever its height */
.thread-card .tc-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.thread-card .t{min-width:0;font-weight:500;font-size:var(--text);line-height:1.4;
  color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* who is working it, under the title: mark + model (DM Mono, _design#4) + the
   thread's age ("7h ago", tabular, canon#12) — the whole line junior by color
   alone, so an open card says more without shouting (_design#3) */
.thread-card .tc-meta{display:flex;align-items:center;gap:6px;min-width:0;
  color:var(--ink-2)}
.thread-card .tc-meta .h-mark{width:12px;height:12px}
.thread-card .tc-meta .agent{font-size:12px;color:inherit;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.thread-card .tc-meta .meta{font-size:12px;white-space:nowrap}

/* pane headers share ONE height (60px) so sidebar, thread, and details
   headers sit on the same line across regions */
.pane-h,.thread-h,.details-h{height:var(--pane-h);min-height:var(--pane-h);padding:0;align-items:center}
/* the sidebar wordmark sits at header scale (was an inline style) */
.pane-h .wordmark{font-size:15px}
/* The collapse control bleeds out with the pills below it — same -12px the nav rows
   and thread cards use — so one margin runs down the pane's right edge instead of the
   header's box sitting 12px inside everything else. The wordmark stays on the text
   column: this moves the BOX, not the text, which is the same distinction that lets a
   thread title line up with its date label while its pill runs wider. */
.side-in .pane-h{margin-right:-12px}

/* ---------- thread header (_design#16) — sticky, content fades under it ---------- */
.thread-h{position:sticky;top:0;z-index:2;
  background:linear-gradient(var(--paper) 62%,transparent);
  display:flex;align-items:center;gap:8px}
/* the thread column has a max measure: header, trace, and composer share
   --thread-col and center when the pane is wider (collapsing side panels
   widens the margins, never the text) */
.thread-scroll>:not(.thread-h){max-width:var(--thread-col);margin-inline:auto}
/* title at body size, ALWAYS left-aligned; flex:1 so it owns the middle and
   the toggle icons pin to the ends — it can never be squeezed by them */
.thread-h .t{font-family:var(--font-title);font-weight:600;font-size:14px;
  letter-spacing:-0.01em;flex:1;min-width:0;text-align:left;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* every pane scrolls its own wrapper; scrollbars are not part of the language.
   The end padding lets the last content rise clear of the floating composer.
   The bottom fade is a MASK on the content itself (no backdrop, no paper
   strip): text starts fading ~24px above the pill top and is gone just
   before it — pill geometry: 52px tall + 16px inset = top at 68px. */
.thread-scroll{container-type:inline-size; /* the figure lane's measure (embed-fig) */
  flex:1;min-height:0;overflow-y:auto;padding-bottom:84px;
  /* the pane owns its pinning — browser scroll anchoring would fight it */
  overflow-anchor:none;
  mask-image:linear-gradient(to bottom,#000 calc(100% - 92px),transparent calc(100% - 56px));
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 92px),transparent calc(100% - 56px))}
/* …and when the composer carries a line about where the message will run, the
   end padding grows by that line's height plus its gap. The fade stays where it
   is — the line is an opaque box, so what passes behind it is covered rather
   than dissolved — but content that stopped at the pill's edge would come to
   rest UNDER that box with no way to bring it out. Stated as a fact about the
   markup (`:has`) rather than measured: the line is one or two lines of 13px,
   and a number the stylesheet can reason about beats a number JavaScript has to
   keep telling it. */
.thread-slot:has(.run-where) .thread-scroll{padding-bottom:148px}
/* ---------- keep-alive thread slots ----------
   every visited thread's pane stays mounted in an absolute slot; switching
   threads is a visibility flip — DOM, scroll position, and draft survive.
   Slots carry the region's 12px side inset (the host spans the full main
   region), so in-slot chrome anchors at 0. The .off guard matters:
   visibility:visible on a child ESCAPES a hidden parent, so live slots are
   only shown while the host itself is on. */
.thread-host{position:absolute;inset:0}
.thread-host.off{visibility:hidden}
/* home/artifacts keep-alive pages: same posture — absolute layers over the
   main region, visibility-flipped; each carries the region's side inset */
.page{position:absolute;inset:0;padding:0 12px;display:flex;flex-direction:column;
  min-width:0}
.page.off{visibility:hidden}
.thread-slot{position:absolute;inset:0 12px;display:flex;flex-direction:column;
  visibility:hidden}
.thread-host:not(.off) .thread-slot.live{visibility:visible}

/* the composer FLOATS over the thread pane (_design#16): no backdrop, no
   fade, no margins — content stays continuously visible as it passes
   beneath the input pill; the pill's own fill is the only thing that
   covers it. Anchored to the SLOT (which already carries the side inset). */
.main{position:relative}
.composer{position:absolute;left:0;right:0;bottom:16px;margin:0 auto;padding:0;
  max-width:var(--thread-col)}
/* jump-to-latest: a small disk floating above the composer (tracks its
   height); debounced show, immediate hide on arriving back at the end */
.jump-pill{position:absolute;left:50%;bottom:calc(100% + 12px);z-index:3;
  transform:translateX(-50%) translateY(6px);
  width:34px;height:34px;border:0;border-radius:var(--r-full);
  background:var(--panel-2);color:var(--ink);cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
  opacity:0;pointer-events:none;
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
.jump-pill.on{opacity:1;pointer-events:auto;transform:translateX(-50%) translateY(0)}
.jump-pill:hover{background:var(--ink);color:var(--paper)}
/* The sidebar does NOT scroll — its list does. The wordmark and the three pages are
   the pane's fixed head, the same way `.thread-h` is the thread pane's: where you can
   go should not be somewhere you have to scroll back up to find. */
.side-in{height:100%;overflow:hidden;display:flex;flex-direction:column}
.details-in{height:100%;overflow-y:auto;display:flex;flex-direction:column}
.thread-scroll,.side-threads,.details-in{scrollbar-width:none}
.thread-scroll::-webkit-scrollbar,.side-threads::-webkit-scrollbar,
.details-in::-webkit-scrollbar{display:none}

/* ---------- trace rows (_design#8) ---------- */
/* Reading text sits airier than the chrome's 1.55: a thread is where the product
   is READ at length, and the sidebar's density is not its measure. 24px at the
   resting 14px body — written as an offset of --text rather than a pinned pixel,
   so the phone step (16px) carries the same air instead of tightening. This is
   the ONE declaration that sets thread line-height: raising any other
   line-height (.app's, a token, the derived skill-asset copy) moves nothing
   here, which is exactly the trap to not fall into when retuning it. */
.trace{display:flex;flex-direction:column;gap:14px;font-size:var(--text);
  line-height:calc(var(--text) + 10px)}
/* the user's turn: a RIGHT-aligned --panel box (sent-message convention;
   tint, not border — canon#3), no attribution line — the box itself marks
   the speaker. --r-lg clamps to a stadium on one-liners, echoing the
   composer pill the text came from */
.msg-user{background:var(--panel);border-radius:var(--r-lg);padding:10px 16px;
  width:fit-content;max-width:78%;margin-left:auto}
/* hover actions under the user's turn: copy + resend, right-aligned,
   revealed on hover (--t-fast fade) */
.msg-wrap{display:flex;flex-direction:column;align-items:flex-end;min-width:0}
/* Turn actions (copy, retry) cost NO layout until you reach for them: the
   row is a 0fr grid track that opens to 1fr on hover, so the gap under a
   turn belongs to the turn itself rather than being reserved for a button
   nobody is looking at. Height and fade run together on the same ease. */
.msg-actions,.row-actions{display:grid;grid-template-rows:0fr;
  opacity:0;margin-top:0;
  transition:grid-template-rows var(--t-med) var(--ease),
             margin-top var(--t-med) var(--ease),
             opacity var(--t-fast) var(--ease)}
.msg-actions>*,.row-actions>*{overflow:hidden;display:flex;gap:2px;min-height:0}
.msg-actions{justify-items:end}
.msg-wrap:hover .msg-actions,.msg-actions:focus-within,
.content-wrap:hover .row-actions,.row-actions:focus-within{
  grid-template-rows:1fr;opacity:1;margin-top:4px}
/* keyboard and touch never hover: reveal for anyone who cannot */
@media (hover:none){
  .msg-actions,.row-actions{grid-template-rows:1fr;opacity:1;margin-top:4px}
}
@media (prefers-reduced-motion:reduce){
  .msg-actions,.row-actions{transition:opacity var(--t-fast) var(--ease)}
}
/* THE GLYPH COLUMN IS A FIXED GUTTER. Every row spends it, glyph or not, so
   the text edge is one vertical line that never moves: a row that streams
   under the run dots and settles to no mark keeps its exact geometry — the
   gutter simply goes quiet. The alternative was tried (text flush at the
   column edge, only glyph rows indented) and taken back: a settling answer
   collapsed its gutter and the whole paragraph reflowed 23px left at the
   exact moment the reader was on it. Only the LEFT gutter is reserved — the
   mirrored right padding of the oldest layout stays gone. */
.trace-row{--glyph:13px;--glyph-gap:10px;
  display:flex;gap:var(--glyph-gap);align-items:baseline;
  /* browser-native virtualization: off-screen rows skip layout and paint;
     the second value is the pre-render estimate, `auto` keeps the last
     measured size so scrollHeight stays honest for long threads */
  content-visibility:auto;contain-intrinsic-size:auto 44px}
/* A row hosting a figure leaves the virtualization: content-visibility implies
   PAINT containment, which clips descendants to the row's column-capped box —
   the figure lane laid out at 880px and painted at 720, the overhang simply not
   shown. The report's sections make the exact same exclusion for its .wider/
   .widest lanes (components.css § lanes: an overhang "must join this selector").
   The cost is real but small: threads hold a handful of figure rows, not the
   hundreds the virtualization exists for. */
/* `:has(.tbl)` and not `:has(.tbl.wide)`, which is where this started: a row that
   holds a table is a row that MAY widen, and matching only the widened state made
   the containment flip on every toggle — the row left the virtualization set on
   expand and rejoined it on shrink, swapping its real height for the intrinsic
   estimate mid-gesture and taking the scroll position with it. A row that can
   widen stays out of the set whether it is widened or not. */
.trace-row:has(.embed-fig),.trace-row:has(.lane){content-visibility:visible;contain-intrinsic-size:none}
.trace-row>.g-done,.trace-row>.g-stop{align-self:flex-start;margin-top:3px}
/* the run dots RIDE THE PEN: bottom-aligned, they sit beside the line being
   written rather than a first line long since scrolled by — the liveness
   mark stays where the reader is looking. On settle the state mark takes
   the row's top, as every settled mark does. */
.trace-row>.g-run{align-self:flex-end;margin-bottom:4px}
/* the landing beat for a reader sent here from the gallery: the row rises on
   the panel tint and settles — the quiet "you are here", said once */
.trace-row.row-landed{border-radius:var(--r-lg);animation:row-landed 1.6s var(--ease)}
@keyframes row-landed{from{background:var(--panel)}to{background:transparent}}
@media (prefers-reduced-motion:reduce){.trace-row.row-landed{animation:none}}
/* the spacer HOLDS ITS SLICE: a row with nothing to say in the gutter still
   spends it, which is what keeps every body on the same text edge through
   the run → settled swap (the fixed gutter above) */
.g-none{width:var(--glyph,13px);flex:none}
.trace-row .body{min-width:0}
/* …and the same for the answer's wrapper, which had never been given it. A flex
   item's `min-width` is `auto`, so it refuses to shrink below its CONTENT's
   min-content width — and a `pre` does not wrap, so its min-content width is its
   longest line. An answer holding a 120-character line therefore laid its content
   column out at that line's width (1097px measured, inside a 720px row): the row
   overflowed the measure, and `.md pre`'s own `overflow-x:auto` never engaged,
   because the block was never asked to be narrower than its content.
   The visible symptom was a code block that pushed the text column sideways past
   every paragraph around it, rather than scrolling inside itself the way canon#10
   says wide content does. */
.trace-row>.content-wrap{min-width:0}
.trace-row .body p{color:var(--ink)}

/* ---------- the thought process ----------
   live: label + the provider's thinking text streaming in, junior gray.
   settled: collapses to "Thought for …" — a quiet clickable label with a
   chevron; expansion reveals the full process in place. */
.thought-head{border:0;background:none;padding:0;margin:0;font:inherit;
  color:var(--ink-2);cursor:pointer;display:inline-flex;align-items:center;gap:6px;
  transition:color var(--t-fast) var(--ease)}
.thought-head:hover{color:var(--ink)}
.thought-head .chev{width:13px;height:13px;flex:none;
  transition:transform var(--t-fast) var(--ease)}
.thought.open .thought-head .chev{transform:rotate(90deg)}
/* the thinking window: ten lines deep, scrollable inside, and clickable to
   open to full height. The CSS bound is em-based so it stays ten LINES at
   any type size (mobile bumps this to 16px) until the measured px bound
   takes over — the expanded value must be a length, `none` cannot animate.

   The edges fade only toward unread text: --fade-top/--fade-bottom are set
   from the scroll position, so the top is clean until you scroll off it and
   the bottom is clean once you reach the end. Registering them as <length>
   is what lets the fade grow in rather than pop. */
@property --fade-top{syntax:'<length>';inherits:false;initial-value:0px}
@property --fade-bottom{syntax:'<length>';inherits:false;initial-value:0px}
.thought-body{color:var(--ink-2);font-size:13px;line-height:1.6;margin-top:8px;
  padding-left:14px;white-space:pre-wrap;overflow-wrap:break-word;
  max-height:calc(10 * 1.6em);overflow-y:auto;overscroll-behavior:contain;
  /* no scrollbar: the edge fades already say there is more, and a track
     inside a trace row is one machine part too many */
  scrollbar-width:none;-ms-overflow-style:none;
  cursor:pointer;
  --fade-top:0px;--fade-bottom:0px;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)),transparent 100%);
  mask-image:linear-gradient(to bottom,transparent 0,#000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)),transparent 100%);
  transition:max-height var(--t-med) var(--ease),
             --fade-top var(--t-med) var(--ease),
             --fade-bottom var(--t-med) var(--ease)}
.thought-body::-webkit-scrollbar{width:0;height:0}
.thought-body:focus-visible{outline:1px solid var(--ink-3);outline-offset:4px;border-radius:var(--r-xs)}
@media (prefers-reduced-motion:reduce){.thought-body{transition:none}}

/* ---------- exchanges — prompt-to-answer segments ----------
   an exchange groups a prompt with everything until the next one; the
   toggle collapses the PROCESS (thoughts, tools, notes) while artifacts
   and the final answer stay. The group keeps the trace's own row gap. */
.exchange{display:flex;flex-direction:column;gap:14px;min-width:0}
/* the thread pane's own trace opens 6px under the header it slides beneath —
   the one thing that differed between it and the follow-up pane's trace once
   both were drawn by the same component (Trace) */
.trace.pane-trace{padding-top:6px}
/* 14px, the narration size: this line sits in the same column as "Working" and
   "Thought it through" and says the same kind of thing about the run, so reading
   smaller made it look like a lesser class of note rather than a peer. 12.5px was
   also a one-off off the 4px grid (foundations.md § Spacing). */
.seg-toggle{border:0;background:none;padding:0;margin:0;font:inherit;
  font-size:14px;color:var(--ink-2);cursor:pointer;
  display:inline-flex;align-items:center;gap:6px;
  transition:color var(--t-fast) var(--ease)}
.seg-toggle:hover{color:var(--ink)}
.seg-toggle .chev{width:13px;height:13px;flex:none;
  transition:transform var(--t-fast) var(--ease)}
.seg-toggle.open .chev{transform:rotate(90deg)}

/* ---------- the working line — .steps (patterns.md § Streaming) ----------
   Narration succession, collapsed to the step that is happening NOW. A run
   working through many steps used to grow the trace a row per step, which
   made the process the biggest thing on screen while it was the least
   settled: the stretch renders as ONE line carrying the newest narration,
   and the arrival of the next step is the list scrolling by a row — the
   new line rises in from below as the old leaves above, log direction
   made motion. Nothing is rewritten to buy this: the steps are ordinary
   rows in an append-only log and the line is a one-row window onto them,
   never a replacement — the full sequence is one press away, forever.

   THE CHEVRON TRAILS, against the fold heads' leading one, deliberately.
   A fold's chevron labels content hidden below it; this line's words ARE
   the newest of the content — live narration first, affordance second —
   and they must sit on the trace's one text edge like every narration row
   they stand for. A leading chevron would indent the one line in the
   chrome that changes most often. It rests --ink-3, the quietest thing on
   the row: the words are already saying everything else.

   THE LINE CARRIES NO MARK, LIVE OR SETTLED. The gutter is spent empty
   (.g-none — the fixed gutter, so the text edge never moves). Live, the
   swap itself is the liveness signal, and a second animated mark beside a
   line that is already moving was one too many — the dots also promised
   "this row is streaming" when what is live is the RUN, not the sentence.
   Settled, the words are the settlement: the head swaps to the resting
   label the way it swapped to every step before it. No done disk either —
   the disk marks a step that ran and finished, and the working line is
   not a step, it is a window onto steps that each carry their own disk
   inside; a check on the head would count the same work twice. Only the
   opened list's current step wears the dots, because a static row needs
   the mark the moving line does not.

   OPEN, THE HEAD STOPS REPEATING THE LIST. The current step is visible as
   the list's last row, so the head morphs to the roll-up — `Working` plus
   the step tally, ticking in tabular figures (_design#7) — the same fact
   never twice on screen. SETTLED, the exchange's own fold takes over
   (`Worked for 58 seconds, 9 steps` — the .seg-toggle above): the working
   line is the LIVE presentation of the process, and the fold is its
   resting one. Only the system's own step narration may join a group;
   content — attribution blocks, embeds, the user's turn, outcomes —
   lands in the trace as itself and CLOSES the group. Further steps start
   the next one. */
.steps{display:flex;flex-direction:column;min-width:0}
.steps-head{border:0;background:none;padding:0;margin:0;font:inherit;
  color:var(--ink-2);cursor:pointer;text-align:left;
  display:flex;align-items:baseline;gap:8px;min-width:0;
  transition:color var(--t-fast) var(--ease)}
.steps-head:hover{color:var(--ink)}
.steps-head .chev{width:13px;height:13px;flex:none;align-self:center;
  color:var(--ink-3);
  transition:transform var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
.steps-head:hover .chev{color:var(--ink)}
.steps.open .steps-head .chev{transform:rotate(90deg)}
/* the one-row window: successor and predecessor share the cell during the
   swap; each is one line, cut by ellipsis, so the row's height never moves */
.steps-now{display:grid;min-width:0;flex:0 1 auto}
.steps-now>span{grid-area:1/1;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.steps-now>span.in{animation:step-in var(--t-med) var(--ease)}
.steps-now>span.out{animation:step-out var(--t-med) var(--ease) forwards;
  pointer-events:none}
@keyframes step-in{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
@keyframes step-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-7px)}}
/* expansion is the house 0fr→1fr track (the embed's, the turn actions'):
   the sequence opens in place, rows on the trace's own gap and text edge */
.steps-list{display:grid;grid-template-rows:0fr;opacity:0;margin-top:0;
  transition:grid-template-rows var(--t-med) var(--ease),
             margin-top var(--t-med) var(--ease),
             opacity var(--t-fast) var(--ease)}
.steps-list>div{overflow:hidden;min-height:0;
  display:flex;flex-direction:column;gap:14px}
.steps.open .steps-list{grid-template-rows:1fr;opacity:1;margin-top:14px}

/* ---------- the lull sheen ----------
   A long step starves the working line of its own signal: the swap is the
   liveness mark, and a model that thinks silently for three minutes
   (observed: 168s) leaves the head motionless — a live run wearing a dead
   line. The dots are not brought back for it (a second animated mark was
   rejected once, and stillness does not change the argument); instead the
   words take over the dots' own pulse: a soft band, a breath wide, drifts
   through the line — the letters dim a step and return as it passes.
   Monochrome by construction (the same letters at a lower alpha, via mask —
   never a painted gradient, which would tear on theme change), and small:
   the line stays legible at every phase of the wave.

   IT APPEARS ONLY AFTER A BEAT OF STILLNESS — the waiting placeholder's own
   rule: while steps are landing, the swap is the signal and the sheen would
   be a second one. The class is the surface's to add when nothing has
   landed for a few seconds, and to clear on the next swap. It also lives
   only where the dots do not: an OPEN group's current step already pulses
   in the list, so .lull is ignored while .open. Under reduced motion there
   is no sheen — a still line is an acceptable rendering (canon#9), and the
   run's liveness is still said by the thread card's dots. */
/* the band is ~40% of the line and dips to quarter strength: measured
   wider and shallower first (90% × .45) and invisible in practice — a
   whole-line fade reads as nothing at 14px. Narrow and deep is what makes
   it a SHEEN: letters visibly dip and recover as it passes, and the line
   around the band stays at full strength the whole time. */
.steps.lull:not(.open) .steps-now>span:not(.out){
  -webkit-mask-image:linear-gradient(100deg,#000 43%,rgba(0,0,0,.25) 50%,#000 57%);
  mask-image:linear-gradient(100deg,#000 43%,rgba(0,0,0,.25) 50%,#000 57%);
  -webkit-mask-size:300% 100%;mask-size:300% 100%;
  animation:lull-sheen 2.8s linear infinite;
}
/* 100%→0% is a LEFT-TO-RIGHT sweep (the mask is 3× the line, so position
   walks it backwards); reading direction, one breath, then a quiet gap
   while the band is off the edge — a pulse, not a marquee */
@keyframes lull-sheen{
  from{-webkit-mask-position:100% 0;mask-position:100% 0}
  to{-webkit-mask-position:0% 0;mask-position:0% 0}
}
@media (prefers-reduced-motion:reduce){
  .steps-now>span.in,.steps-now>span.out{animation:none}
  .steps-now>span.out{opacity:0}
  .steps-list{transition:opacity var(--t-fast) var(--ease)}
  .steps.lull:not(.open) .steps-now>span:not(.out){animation:none;
    -webkit-mask-image:none;mask-image:none}
}

/* the waiting placeholder (client-side, never in the log): appears only
   after a beat of real dead air, so it never flickers between rows */
/* the fade the placeholder rides in on. It MUST exist: the rule below
   parks the row at opacity 0 and relies on the animation to bring it back,
   so a missing keyframe leaves "Waiting on …" in the DOM and invisible. */
@keyframes chunkin{from{opacity:0}to{opacity:1}}
.trace-row.waiting{opacity:0;animation:chunkin var(--t-fast) var(--ease) .5s forwards}
@media (prefers-reduced-motion:reduce){.trace-row.waiting{animation:none;opacity:1}}
/* a refused write, stated in place: gray narration with the server's own
   sentence, and the dismiss beside it rather than a banner over anything */
.trace-row.refusal .body{display:flex;align-items:baseline;gap:10px}
.trace-row.refusal .narr{flex:1;min-width:0}
.trace-row.refusal .btn{flex:none}

/* ---------- run outcomes (patterns.md § Waiting & empty states) ----------
   A run that ended without its answer says so where the answer would have been:
   what happened, then the one thing that changes it. Never a banner, never a
   toast, never a modal, and never red — an error is not a status hue in this
   chrome (canon#2) and the weight lives in the words.

   The voice is NARRATION (--ink-2), not content, and that is the load-bearing
   decision: a sentence ABOUT the run set in --ink is indistinguishable from a
   sentence the model wrote, which is the exact failure the Claude adapter's
   attribution rule exists to prevent (a harness's "Invalid API key · Fix
   external API key" published as prose). The mark is what makes the row
   unskimmable, not the ink.

   THE ROW IS A LINE, NOT A BLOCK. It was a column — the account, then a rail of
   numbers, then the fold, each on its own line — and at three lines an outcome
   was the biggest thing in the trace, which inverts the point: the run FAILED,
   it did not become the most important event in the thread. Everything shares
   one baseline now and the sentence is cut until it fits, which it can be
   because .g-kind above is carrying the classification. Measured at the thread's
   760px measure: a refusal went from 98px to 24px, the weight of "Thought for 5
   seconds", which is the weight it should always have had. */
.trace-row.outcome .body{display:flex;flex-wrap:wrap;align-items:baseline;
  gap:4px 12px;min-width:0}
/* What it spent before it stopped — a value never ships bare (canon#12).
   THE TOKEN RAIL IS NOT HERE: what a failed run cost is a COST, and costs live
   in the details panel, which already carries them. Printing them under the
   sentence made the second line of every outcome a number nobody was going to
   act on. Two things survive, because neither is a price: the held-against-fits
   pair, which IS the failure rather than the cost of it, and the elapsed time
   where the duration is part of what happened. A row carrying two actions
   carries no tally at all — measured, it wraps at 760px and at 700px with both.
   The one job the rail did do ("zero in and zero out, so the request never
   reached a model") is said in the sentence instead: `nothing ran`. */
.outcome-cost{display:flex;flex-wrap:wrap;gap:10px}
/* The fold's HEAD joins the line and its BODY takes the next one. display:contents
   is what allows both at once: head and body become flex items of the row itself,
   so the head sits inline where a reader meets it and the body claims a full
   basis underneath. The fold is the thought's, because opening a machine's own
   words in place is the same act. */
.outcome .thought{display:contents}
.outcome .thought-body{flex-basis:100%;padding-left:0;margin-top:0}
/* the command is never inline — it is a block a reader copies */
.outcome .picker-cmd{flex-basis:100%;max-width:280px}

/* ---------- editing what a branch carried ----------
   A branch opens on what it was given: system narration naming the parent, with
   the assembled text behind the ordinary thought fold. That row REPORTS and does
   not edit in place — it is a thing that happened, sitting in an append-only log
   among other things that happened, and no other row in a trace carries a control
   that rewrites its own content. Its Edit and the details panel's both open the
   one editor.

   The panel's `Carried` section is where a thread's standing facts are kept and
   changed (Goal above it, `Branched from` beside it) — what is carried is one of
   those, not an event.

   THE PANEL SHOWS THE TEXT, not a description of it. A one-line summary of a
   summary ("the goal, 2 reports, and the last 6 exchanges") tells a reader
   nothing they can check — the whole reason to look is to find the sentence that
   is wrong, and that sentence is in the text. So the section carries the opening
   of the real thing, cut off by a fade rather than a rule: six lines at the
   panel's own measure, going pale toward the bottom, which says "there is more"
   in the same breath as it stops. The same fade the thought window already uses
   for unread text — a mask, never an ellipsis and never a gradient painted in
   the paper's colour, which would break the moment a theme changed.

   EDITING IS A MODAL, the skills editor's (`.skill-modal`). Four paragraphs is a
   document, and a document is edited at a document's measure: in a 300px column
   the field was taller than the panel and the reader scrolled a narrow strip to
   read a sentence. The modal is also what makes one editor serve two entry
   points — the carried row in the trace and this section both open it, and
   neither becomes a second place for the text to be true. The field inside it is
   the panel's own bare textarea (no ground, no inset): the read view with a
   caret in it. */
/* THE FADE IS CONDITIONAL, and set by the component the way .thought-body's is.
   Applied unconditionally it goes pale at the bottom of a summary that fits,
   which says "there is more" about text that has already ended — the one thing a
   fade must never do. Twelve lines is the bound; `.clipped` is the fact.

   THE CUT IS MOSTLY FADE. At six lines with the mask starting at 45% the section
   showed two solid lines and stopped almost at once — the reader saw the shape
   of a summary rather than any of it, and the fade read as a lid. Twelve lines
   opening at 30% gives roughly four solid and eight dissolving: enough real text
   under the gradient to judge whether it is worth opening, which is the only
   question a preview has to answer. */
/* FULL INK, because this is the text itself and not a note about it. Set in
   narration gray it read as the section's caption — a line explaining what was
   carried, rather than the thing that was — and the reader came here to find the
   sentence that is wrong IN it. Same rule the `Goal` section above it follows. */
/* MARKDOWN, not source: the section renders the carried text through the same
   `.md` every content row uses, so the block sets the MEASURE and the ink and
   lets `.md` set the structure. No `white-space:pre-wrap` — markdown has already
   decided where the paragraphs are, and pre-wrap on top of it turns every source
   newline inside a paragraph into a break the author never wrote. */
.carried-preview{font-size:13px;line-height:1.6;color:var(--ink);
  overflow-wrap:break-word;
  max-height:calc(12 * 1.6em);overflow:hidden}
/* `.md` is sized for the thread's measure; the panel is a 300px column, so the
   headings come down to the step above body and the rhythm tightens with them.
   Nothing else is restated — colour, code, tables and math are the one type
   system, and a second set of rules here would be a second answer to them. */
.carried-preview .md h1,.carried-preview .md h2{font-size:14px;margin:12px 0 6px}
.carried-preview .md h3,.carried-preview .md h4,
.carried-preview .md h5,.carried-preview .md h6{font-size:13px;margin:10px 0 4px}
.carried-preview .md p,.carried-preview .md ul,.carried-preview .md ol{margin:0 0 8px}
.carried-preview .md ul,.carried-preview .md ol{padding-left:18px}
.carried-preview.clipped{
  -webkit-mask-image:linear-gradient(to bottom,#000 30%,transparent 100%);
  mask-image:linear-gradient(to bottom,#000 30%,transparent 100%)}
/* Opened, the height bound and the fade both leave: the section scrolls with
   the panel, which is the surface's own answer to a long document and not a
   second scroller inside it. Reading the whole summary is what `See all` was
   pressed for; the modal is still where it is CHANGED. */
.carried-preview.open{max-height:none;overflow:visible}
/* The way in sits CENTRED, under the fade it lifts — the fade runs the width of
   the column and dissolves toward its own bottom edge, so the one place a
   control can stand without landing on half-legible words is the middle of that
   edge. Pulled up into the gradient's tail rather than parked below it: the
   button belongs to the text it opens, and a gap between them would read as the
   next section starting. Ghost, because it is the quiet register — what is
   carried is the section, and this is only the way to see the rest of it. */
.carried-more{display:flex;justify-content:center;margin-top:-10px}
/* a document's height in the modal, so the reader sees paragraphs and not a slot */
.carried-input{min-height:320px}

/* Text a model began and the harness took back when it handed the turn to
   another model. It stays exactly where it landed — the log is append-only and a
   reader watched it arrive — and goes vestigial, which is what --ink-3 already
   means in the text grammar (foundations § Text-color grammar). Nothing else
   about the row changes: no strike-through (that is an edit), no fold (the words
   were on screen; hiding them now would be the second surprise). */
.trace-row.withdrawn .body,
.trace-row.withdrawn .body p,
.trace-row.withdrawn .md{color:var(--ink-3)}

/* ---------- embed (embedding#5) — the summary row that OPENS an artifact ----------
   No unfold: the row is a door, and the artifact is shown in the modal. */
.embed{background:var(--panel);border-radius:var(--r-lg)}
/* An embed introduced by prose in the SAME row is one delivery, and the two parts
   still need air between them: flush against the last line, the panel reads as
   though the sentence ran into it. 16px — the grid step above the 14px that
   separates whole trace rows, so the pair stays closer to each other than to
   anything else (foundations.md § Spacing). Keyed to the structure rather than set
   on one component: any row that grows a preamble gets it, and an embed that IS the
   row keeps its flush top. */
.trace-row .body > .embed:not(:first-child){margin-top:16px}
.embed-head{display:flex;align-items:center;gap:12px;padding:16px 18px;width:100%;
  background:none;border:0;font:inherit;text-align:left;cursor:pointer;color:var(--ink)}
.embed-head .t{font-weight:600;font-size:14px;color:var(--ink);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.embed-head .count{margin-left:auto;flex:none}
/* link-out variant: no chevron, no expansion — the whole row is an anchor
   to the standalone page; the trailing arrow marks the navigation */
a.embed-head{text-decoration:none;transition:background var(--t-fast) var(--ease);
  border-radius:var(--r-lg)}
a.embed-head:hover{background:var(--panel-2)}
.embed-link .go{color:var(--ink-2);flex:none}
.pill{background:var(--ink);color:var(--paper);border-radius:var(--r-full);
  font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
  padding:4px 10px;flex:none;line-height:1.2}
.count{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2)}

/* figure embed (embedding#12) — the one artifact that ships EXPANDED, in the flow
   of the answer; its head row folds it in place instead of opening a modal. The
   chevron turns to say which way the fold goes — the fold is the embed's own
   motion, nothing above it moves (canon#9). */
.embed-fig .embed-head .go{color:var(--ink-2);flex:none;
  transition:transform var(--t-fast) var(--ease)}
.embed-fig .embed-head .go.open{transform:rotate(90deg)}
.embed-fig .fig-body{padding:0 10px 10px}
/* The guest frame opens small and grows to the height the figure announces —
   the modal's 100vh opening size is for a document, and inside a thread it would
   shove the whole conversation down for one diagram. */
.embed-fig .rep-frame{height:220px;border-radius:var(--r-md)}
/* The figure LANE: the report's .wider overhang, spoken in the thread's own
   units. When the region is wider than the column, a figure takes up to 880px,
   centred on the prose it sits in; when it is not, max() collapses the lane to
   the column and the margin to zero — the same convergence the report's lanes
   have on a narrow frame. Sized by 100cqw (the .thread-scroll container, which
   already sits inside the pane's 20px insets) minus a 24px breath, never by
   vw — a pane is not the viewport. Scoped under .thread-scroll on purpose: the
   details region also renders traces, at widths no lane should be spent on.
   The lane is only spent on an OPEN figure: folded, the row is prose-adjacent
   furniture and sits on the text measure like every other embed head. */
.thread-scroll .embed-fig:has(> .embed-head[aria-expanded="true"]){
  --fig-lane:min(880px,100cqw - 24px);
  width:max(100%,var(--fig-lane));
  margin-left:calc((100% - max(100%,var(--fig-lane)))/2)}

/* ---------- details panel (_design#2) — header sticky, never scrolls away ---------- */
.details-h{position:sticky;top:0;z-index:2;
  background:linear-gradient(var(--paper) 62%,transparent);
  display:flex;align-items:center;justify-content:space-between;
  margin-bottom:4px}
/* pane header titles share one spec — same as .thread-h .t (_design#16):
   left-aligned, taking the row's remaining width so the actions pin to the end
   and can never squeeze it. Without the flex, a header with a control on BOTH
   sides (a follow-up's back arrow and its actions) centres its title on the
   `space-between` — the one header in the chrome that would read differently
   from the rest. */
.details-h .t{font-family:var(--font-title);font-weight:600;font-size:14px;
  letter-spacing:-0.01em;flex:1;min-width:0;text-align:left;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* a header's EDGE buttons hang their glyph on the pane's text edge — the
   stadium-bleed idiom the rows below already follow. The 7px between a 28px
   button's box and its 14px glyph is hover halo, not inset, and with the
   panes' content now flush that halo read as a leftover pad pushing the back
   arrow (and the trailing action) in from the edge everything else sits on. */
.details-h>.iconbtn:first-child,.doc-h-rail>.iconbtn:first-child{margin-left:-7px}
.details-h .pane-actions>.iconbtn:last-child{margin-right:-7px}
.hairline{border:0;border-top:1px solid var(--hairline);margin:16px 0}
.details .trace{gap:10px;font-size:var(--text)}
/* details body text — one class so the mobile overlay can rescale it */
.dp-text{font-size:var(--text)}
/* ---------- the goal pair ----------
   A GOAL IS NOT A PROPERTY OF A THREAD, it is the agent's reading of one ask.
   The model declares it in the opening line of its answer, so the unit is the
   pair — what was asked, and what it set out to do about it — and the panel
   shows both halves. Alone, the goals read as an unsourced list: "Name the
   important players adjacent to…" at 9 of 19 tells a reader nothing about which
   question produced it.

   It also settles what was wrong before. `run.started` carries the raw prompt in
   the goal field as a stand-in, so a chain built from goals alone filled with
   prompts wearing the wrong name. Here the prompt is present AS the prompt and
   the goal is the agent's, each labelled by its own position and ink — nothing
   is discarded and nothing is misnamed, and a run where the model never declared
   one shows an ask with no goal under it rather than a prompt pretending to be
   one.

   THE ASK IS QUIET AND CUT AT TWO LINES; the goal is ink and whole. Two lines
   is enough to recognise a question you wrote — the reader is placing it, not
   re-reading it — while the goal is the thing the section is named for and is
   never cut.

   BUT THE CUT IS A DEFAULT, NOT A CEILING. A prompt is the one text on this
   surface the reader is entitled to in full, because they wrote it: a long ask
   is often long for a reason (constraints, an example, what to avoid) and the
   goal beneath it is the model's compression of exactly that. So the ask opens
   on demand, in the chrome's one expand idiom (`.seg-toggle`). Offered ONLY
   when there is something under the cut: a toggle that expands nothing is a
   control that lies.

   NO CHEVRON, the same call `See all` takes over the carried summary. A fold's
   chevron marks a heading that stays put while something opens beneath it; this
   is not a heading and does not stay put — it rides the bottom of the ask and
   moves with it. The word already says which way it goes. */
.goal-pair{display:flex;flex-direction:column;align-items:flex-start;gap:5px}
.goal-ask{font-size:13px;line-height:1.55;color:var(--ink-2)}
.goal-ask.clamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden}
/* Sized to the ASK it opens rather than to `.seg-toggle`'s own 14px: it belongs
   to that paragraph, and a control louder than the text it acts on reads as the
   more important of the two. */
.goal-more{font-size:12.5px}
/* A run nobody prompted — a checkpoint's own resume — has no ask to show, and
   the slot names the event instead of standing empty. The lead-in is narration
   in the machine's register (`.meta`) and the checkpoint's title is the
   reader's own words, so the two do not merge into one sentence. */
.goal-ask .meta{font-size:inherit}
/* details SECTION titles are uppercase micro-labels */
.details-in .label{text-transform:uppercase;letter-spacing:.07em;font-size:12px}
/* the status line: state at left, elapsed at right — content, not a title */
.dp-live{display:flex;align-items:center;justify-content:space-between;gap:8px;
  color:var(--ink-2)}
/* one value per line under a section label */
.dp-stack{display:flex;flex-direction:column;gap:6px;margin-top:8px}
/* The way into the rest of a capped list. Quieter than the rows it follows —
   it is about the list, not in it — and left-aligned under them rather than
   centred, because there is no fade spanning the column here for it to sit at
   the middle of: the rows have a left edge and this belongs on it. */
.dp-more{font-size:13px;margin-top:2px}
/* The goal chain's pager, in the slot `.label`'s own space-between opens on the
   right — where `Status` used to put its Close, and for the same reason: it acts
   on the section its heading names. The buttons are the chrome's `.iconbtn` at a
   step down, because a pager is not a destination: it moves you inside one
   thing. The count is `.meta` and tabular, so `1 / 5` and `10 / 12` do not shift
   the arrows apart as the reader walks. */
.goal-nav{display:inline-flex;align-items:center;gap:2px;flex:none}
/* The readout leads and the arrows follow, so the gap that separates them is
   its own: tabular figures keep `1 / 5` and `10 / 12` the same width, and the
   right margin holds the arrows off the number rather than letting them read as
   a third and fourth digit. */
.goal-nav .meta{font-variant-numeric:tabular-nums;margin-right:4px}
.goal-nav .iconbtn{width:22px;height:22px}
.goal-nav .ic{width:13px;height:13px}
/* An end of the chain is a fact, not a broken button: the arrow stays in place
   so the pair never reflows, and steps back to the disabled ink the chrome
   already uses for a control that has nothing to do. */
.goal-nav .iconbtn:disabled{color:var(--ink-3);cursor:default;background:none}
.dp-tallies{display:flex;gap:10px;margin-top:8px}
/* flags/roles beside values (skills "active", model roles, elapsed) read at
   the same size as the values they annotate */
.details-in .dp-text .meta{font-size:inherit}
/* details as a hover card — surfaces where the panel is absent (never the
   thread page); anchored to a thread card, --t-fast fade after a short delay */

/* ---------- chips, buttons, inputs ---------- */
.chip{background:var(--panel);border-radius:var(--r-full);padding:5px 12px;
  font-size:12.5px;color:var(--ink);display:inline-flex;align-items:center;gap:6px}
button.chip{border:0;cursor:pointer;font:inherit;font-size:12.5px;
  transition:background var(--t-fast) var(--ease)}
button.chip:hover{background:var(--panel-2)}
.btn{border:0;background:none;color:var(--ink);font:inherit;font-size:13.5px;font-weight:600;
  padding:8px 14px;border-radius:var(--r-md);cursor:pointer;
  transition:background var(--t-fast) var(--ease)}
.btn:hover{background:var(--panel)}
.btn.solid{background:var(--ink);color:var(--paper)}
.btn.solid:hover{background:var(--ink)}
/* ghost: the quiet register — narration-gray until reached for, and no border,
   because a surface here is separated by tint and never by a line (canon#1).
   The one that was referenced before this existed rendered as a bare UA button. */
.btn.ghost{color:var(--ink-2);font-weight:500;font-size:13px;padding:6px 12px;
  display:inline-flex;align-items:center;gap:6px}
.btn.ghost:hover:not(:disabled){background:var(--panel-2);color:var(--ink)}
/* Size and stroke come from `.ic.lucide` — the 14px box and its 2.57 stroke are
   calibrated against the bespoke 16-grid pair (foundations.md § Icons), so a local
   override here would be a second, uncalibrated definition. The glyph takes the
   label's colour by currentColor: part of the word, never a decoration beside it. */
.btn:disabled{opacity:.45;cursor:default}
/* hover backing is a rounded square (--r-xs), never a disk (_design#5) */
.iconbtn{border:0;background:none;color:var(--ink-2);width:28px;height:28px;
  border-radius:var(--r-xs);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;
  transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
.iconbtn:hover{background:var(--panel);color:var(--ink)}
.input{background:var(--panel);border:0;border-radius:var(--r-full);
  font:inherit;font-size:14px;color:var(--ink);padding:10px 16px;width:100%}
textarea.input{border-radius:var(--r-lg)}
/* placeholder is muted TEXT (--ink-2), not the hairline tier — --ink-3 is a
   border value and reads as invisible against --panel */
.input::placeholder{color:var(--ink-2)}
/* focus is the tint step, never a ring — canon#3 extends to focus states */
.input:focus{background:var(--panel-2);outline:none}

/* ---------- icons (stroke, _design#5) ---------- */
.ic{width:16px;height:16px;stroke:currentColor;stroke-width:1.5;fill:none;
  stroke-linecap:round;stroke-linejoin:round;flex:none}
/* Lucide draws on a 24-grid and its glyphs run optically larger than the
   bespoke 16-grid pair — render Lucide at a 14px box to match them, and
   compensate the stroke: 2.57 user units at 14px/24 = a true 1.5px
   (_design#5) */
.ic.lucide{width:14px;height:14px;stroke-width:2.57}

/* ---------- the one input pill (PillInput) ----------
   the PILL is the container: fill and radius live here so adornments
   (logo, model chip, stop) sit inside it; the field itself is bare.
   The field is a textarea that grows from ONE line (the pill keeps its
   52px stadium at rest) and scrolls inside past ~5 lines */
/* multiline: the field grows UPWARD and adornments stay pinned to the LAST
   line (align-items:flex-end), optically centered on it via fixed 22px
   line-boxes — vertical padding (15px) centers a single line at 52px */
.pill-input{display:flex;align-items:flex-end;gap:10px;width:100%;
  padding:15px 18px;background:var(--panel);border-radius:var(--r-lg);
  transition:background var(--t-fast) var(--ease)}
.pill-input:focus-within{background:var(--panel-2)}
/* base height = EXACTLY one line box (22px), so the caret is line-sized and
   sits on the text line — auto height let it grow tall and sink below.
   The JS sizer overrides with a px height once there is content. */
.pill-input .input{background:none;flex:1;min-width:0;padding:0;
  border-radius:0;outline:none;resize:none;height:22px;max-height:148px;
  overflow-y:auto;line-height:22px;scrollbar-width:none}
.pill-input .input::-webkit-scrollbar{display:none}
.pill-input .input:focus{background:none;outline:none}
/* adornments ride the last line: a 22px box matching the field's line
   height, contents centered inside it */
.pill-input .pill-lead,.pill-input .model-chip-wrap,.pill-input .ctx-wrap{height:22px;
  display:inline-flex;align-items:center}
/* the stop disk is taller than the line — nudge so its center matches */
.pill-input>.iconbtn{margin-bottom:-3px}
.pill-lead{display:inline-flex;flex:none;color:var(--ink-2)}

/* ---------- the context ring — .ctx-ring ----------
   How much room the conversation has left, riding the pill after the model
   chip. The ARC IS THE REMAINING FRACTION — full ring, empty thread — in
   --ink on an --ink-3 track, 1.5px stroke (_design#5), monochrome always
   (canon#2): running out of room is carried by the number in the card, never
   by a hue. Three honesty states, because three is what the harnesses give:
   an exact reading (solid --ink arc), an estimated one (the arc steps to
   narration gray — a reading the platform inferred, not one the harness
   reported), and no reading at all (no arc, dashed track: "not reported",
   which is a different statement from "empty"). The card it opens says which,
   in words. */
.ctx-wrap{position:relative;flex:none}
.ctx-ring{width:22px;height:22px;border:0;background:none;padding:0;
  display:inline-grid;place-items:center;cursor:pointer}
/* no hover backing: the ring is a reading first and a control second, and a
   square lighting up behind it made it shout. Reaching for it steps the
   TRACK --ink-3 → --ink-2 — the same quiet step every stroke icon takes —
   and that step is also the visible focus state (canon#3: never a ring). */
.ctx-ring:focus-visible{outline:none}
.ctx-ring:hover .track,.ctx-ring:focus-visible .track{stroke:var(--ink-2)}
.ctx-ring svg{width:16px;height:16px;transform:rotate(-90deg)}
.ctx-ring .track{fill:none;stroke:var(--ink-3);stroke-width:1.5;
  transition:stroke var(--t-fast) var(--ease)}
/* r=6.25 → circumference 39.27; --p is the remaining fraction */
.ctx-ring .arc{fill:none;stroke:var(--ink);stroke-width:1.5;stroke-linecap:round;
  stroke-dasharray:calc(var(--p) * 39.27) 39.27;
  transition:stroke-dasharray var(--t-med) var(--ease)}
.ctx-ring.est .arc{stroke:var(--ink-2)}
.ctx-ring.unknown .arc{display:none}
.ctx-ring.unknown .track{stroke-dasharray:2 2.9}

/* the card the ring opens: readouts and narration in the menu framework.
   A fact row is row-shaped but inert — the LABEL narrates in gray and only
   the value carries ink, tabular (_design#7), every value with its unit
   (canon#12). Narration wraps, the one row that may. */
.menu-fact{display:flex;align-items:center;gap:10px;font-size:13px;
  color:var(--ink-2);padding:5px 12px}
.menu-fact .menu-hint{color:var(--ink)}
.menu-say{font-size:12.5px;color:var(--ink-2);padding:4px 12px 6px;
  line-height:1.45;white-space:normal;max-width:280px}
/* the in-use gauge: a 4px track in --panel-2 with an --ink fill, stadium
   ends; an estimated fill steps to --ink-2, matching its ring */
.menu .gauge{height:4px;border-radius:var(--r-full);background:var(--panel-2);
  margin:4px 12px 6px;overflow:hidden}
.menu .gauge i{display:block;height:100%;width:var(--w);background:var(--ink);
  border-radius:var(--r-full)}
.menu .gauge.est i{background:var(--ink-2)}
/* ---------- the model picker — .picker ----------
   The card the model chip opens: a filter, then every model this account can
   run, folded into the harness that serves it. A `.menu` with a head and a
   scrolling body — the head is the frame and does not scroll away with the list.

   A harness that serves nothing KEEPS its row: it used to vanish, which made
   "Cursor is waiting for a login" and "this machine never heard of Cursor" the
   same empty list, and the fold then holds what would connect it instead of
   models.

   NO machine here, deliberately. It named the source of a list rather than a
   destination — runners race for the lease and the winner runs it — so it was a
   row that could not answer the question a reader would ask of it, and there is
   one machine to answer for anyway. `.run-where` is what speaks to where a run
   lands. When routing becomes explicit, the machine comes back as a thing that
   CHOOSES rather than a thing that labels. */
.picker{display:flex;flex-direction:column;gap:0;
  /* a folded row starts under its harness NAME, and the number is its parts, so
     the two can never drift: 12px row padding + 14px chevron slot + the card's
     own 10px row gap */
  --fold-indent:36px}
.picker-head{display:flex;flex-direction:column;gap:6px;padding-bottom:6px;flex:none}
/* the filter: the glyph makes it a search, and the glyph is also its focus —
   the field is already --panel-2 on a --panel card and stepping DOWN on focus
   would dissolve it into the card (canon#3 still forbids the ring) */
.picker-search{position:relative;display:flex;align-items:center;flex:none}
.picker-search .ic{position:absolute;left:12px;width:14px;height:14px;color:var(--ink-3);
  pointer-events:none;transition:color var(--t-fast) var(--ease)}
.picker-search:focus-within .ic{color:var(--ink)}
.picker-search input{width:100%;height:30px;padding:0 12px 0 34px;border:0;
  border-radius:var(--r-full);background:var(--panel-2);font:inherit;font-size:13px;
  color:var(--ink)}
.picker-search input:focus{outline:none}
.picker-search input::placeholder{color:var(--ink-3)}
.picker-list{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  scrollbar-width:none;display:flex;flex-direction:column;gap:2px}
.picker-list::-webkit-scrollbar{display:none}
.picker-group+.picker-group{margin-top:2px}
/* the harness is a LABEL: gray until reached for, never as loud as a model */
.menu .picker-harness{display:flex;align-items:center;width:100%;padding:5px 12px;
  border:0;border-radius:var(--r-full);background:none;font:inherit;font-size:13px;
  color:var(--ink-2);text-align:left;cursor:pointer;
  transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
.menu .picker-harness:hover,.menu .picker-harness:focus-visible{
  background:var(--panel-2);color:var(--ink);outline:none}
.menu .picker-harness .agent{color:inherit;font-size:13px}
.picker-harness .chev{width:14px;height:14px;flex:none;
  transition:transform var(--t-fast) var(--ease)}
.picker-harness[aria-expanded="true"] .chev{transform:rotate(90deg)}
/* the NAME takes the flexible margin, so the chevron is PINNED to the row's
   end and the count or state can come and go without anything moving. The
   count used to carry the margin, and opening a fold — which hides the
   count — let the chevron slide in against the name. */
.menu .picker-harness .agent{margin-right:auto}
/* open, the rows ARE the count. The fold-check that once replaced a folded
   count is RETIRED: the shelf shows the current model at all times, and the
   check belongs to the model, never to the harness that serves it. */
.picker-harness .n{color:var(--ink-2)}
.picker-harness[aria-expanded="true"] .n{display:none}
/* the fold is the embed's own motion, so nothing above it moves (canon#9) */
.picker-body{display:grid;grid-template-rows:0fr;
  transition:grid-template-rows var(--t-med) var(--ease)}
.picker-harness[aria-expanded="true"]+.picker-body{grid-template-rows:1fr}
/* hidden, not merely zero-height: the roving arrow keys must not walk into rows
   nobody can see */
.picker-body>div{overflow:hidden;min-height:0;display:flex;flex-direction:column;gap:2px;
  visibility:hidden;transition:visibility 0s linear var(--t-med)}
.picker-harness[aria-expanded="true"]+.picker-body>div{visibility:visible;transition-delay:0s}
/* qualified: `.menu button` is the same weight and lands later, so a bare
   class here would lose its indent to the framework's own row padding */
.menu .picker-body button{padding-left:var(--fold-indent)}
/* density: a picker row is a line of text with 5px of air, not a button
   pad — the framework's 300px cap answers with half again as many rows */
.picker .menu-group button,.menu .picker-body button{padding-top:5px;padding-bottom:5px}
@media (prefers-reduced-motion:reduce){.picker-body{transition:none}}

/* ---------- the recently-used shelf — .picker-shelf ----------
   Under the search, before the catalog: at most five models, most recent
   first, the CURRENT one leading — the question the picker is usually opened
   with ("the one from before"), answered without a fold touched. The rows
   are the menu's own; the vendor's mark leads each in the same 14px slot a
   harness row uses, so every model name in the card lands on --fold-indent.
   The current model carries the bare .menu-mark check at its row's end (the
   skills menu's convention) and stays at normal weight — the check carries
   the state, bold would say it twice. The shelf is the list's first group,
   not part of the pinned head: it scrolls away with the catalog. It LEAVES
   while filtering (recency is not the filter's answer) and is ABSENT on an
   account with no history — never a labelled zero (_design#13). */
/* the label is capped, so it drops to 11px with tracking: a section name,
   quieter than any row it heads */
.picker-shelf .menu-label{font-size:11px;letter-spacing:.06em;color:var(--ink-3);
  padding:4px 12px 2px}
/* a remembered model whose source cannot serve right now keeps its place in
   the order, dimmed through — mark included — and not pressable: remembered,
   not offered. It relights when the sign-in does. */
.menu button[disabled] .h-mark{color:var(--ink-3)}
/* a shelf row's mark steps with its row, as a harness row's does */
.menu button:hover .h-mark,.menu button:focus-visible .h-mark{color:var(--ink)}
/* author display outranks the UA's [hidden] — the same trap as .menu[hidden] */
.menu-group[hidden]{display:none}
/* the seam under the shelf: a hairline inset to the rows' own 12px edge — a
   shelf edge, not a cut through the card. A dense-stack use of --hairline
   (_design#2); a label here instead ("models") named the obvious and read
   as a second shelf. */
.picker-sep{border:0;border-top:1px solid var(--hairline);margin:4px 12px 2px;flex:none}
.picker-sep[hidden]{display:none}

/* the vendor mark takes the SAME 14px slot the chevron had, so --fold-indent is
   unchanged and model names still land under their harness name. One ink, never
   brand colour (canon#2): it takes the row's ink and dims with it, which is also
   why the set is single-path — a multi-colour logo could not do it. */
.h-mark{width:14px;height:14px;flex:none;color:var(--ink-2);
  transition:color var(--t-fast) var(--ease)}
.menu .picker-harness:hover .h-mark{color:var(--ink)}
/* a harness that cannot serve: gray through, and its state where its count was */
.menu .picker-harness.off,.menu .picker-harness.off .agent{color:var(--ink-3)}
.menu .picker-harness.off .h-mark{color:var(--ink-3)}
.menu .picker-harness.off:hover{color:var(--ink-2)}
.menu .picker-harness.off:hover .h-mark{color:var(--ink-2)}
.picker-state{font-size:12.5px;color:var(--ink-3);white-space:nowrap}
/* displaced by the mark, the fold's chevron sits pinned at the row's end
   (the name's margin does the pinning; the row's own gap spaces it) */
.picker-fold{width:14px;height:14px;flex:none;color:var(--ink-3);
  transition:transform var(--t-fast) var(--ease)}
.picker-harness[aria-expanded="true"] .picker-fold{transform:rotate(90deg)}
/* what would connect it, in the fold where its models would have been */
/* The connect journey (§ Connecting a machine): the same picker card walking
   a reader from no machine to a model. Content sits on the card directly —
   no fold indent; this is not a fold body. The step map speaks in one glyph:
   a not-yet step is the done check dimmed (.g-todo — same geometry, tint disk,
   faint check), waiting-on-the-machine is the pulsing glyph, done is .g-done.
   The current step's row takes .now and opens its body in place. */
.connect{padding:16px 16px 14px;display:flex;flex-direction:column}
.connect p{font-size:12.5px;color:var(--ink-2);margin:0;line-height:1.45}
.connect .t{font-family:var(--font-title);font-weight:500;font-size:15px;margin:0 0 10px;
  display:flex;align-items:center;gap:8px}
.connect .t .ic{width:18px;height:18px;color:var(--ink)}
.connect .hairline{margin:12px 0 10px}
/* A finished ✓ row is a door back to its step: the ROW is a button, quiet —
   no pad, no pill fill (the .menu row look would say "choose me"); the
   narration lifting to ink is the whole hover, the same step as everything
   else quiet in the chrome. */
.connect button.trace-row{padding:0;border-radius:0;align-items:baseline}
.connect button.trace-row:hover,.connect button.trace-row:focus-visible{background:none}
.connect button.trace-row:hover .narr,
.connect button.trace-row:focus-visible .narr{color:var(--ink)}
.connect .menu-label{font-size:11px;letter-spacing:.06em;color:var(--ink-3);padding:0;margin:0}
.connect .step-t{font-weight:600}
/* The map's glyphs run a step larger than trace glyphs: in a card this sparse
   they are the structure, not a whisper in the margin. */
.connect .g-todo,.connect .g-done{width:16px;height:16px}
.connect .g-todo::after,.connect .g-done::after{width:8px;height:8px}
/* Proceed: a bare text control. The card is one tinted surface, and every
   filled pill tried on it disappeared into its own background — so the door
   is weight plus a chevron, and hover answers in ink. */
.connect .start{border:0;background:none;cursor:pointer;font:inherit;font-size:13px;
  font-weight:600;color:var(--ink-2);display:inline-flex;align-items:center;gap:2px;
  padding:0;transition:color var(--t-fast) var(--ease)}
/* background restated for the hover STATE: the menu's generic row hover
   (`.menu button:hover`) would otherwise paint its panel-2 box here. */
.connect .start:hover,.connect .start:focus-visible{color:var(--ink);background:none}
.connect .start .ic{width:14px;height:14px}
/* The one retreat, at the card's foot — the same bare grammar as Proceed,
   junior in weight because going back is never the card's ask. */
.connect .back{border:0;background:none;cursor:pointer;font:inherit;font-size:13px;
  font-weight:500;color:var(--ink-2);display:inline-flex;align-items:center;gap:2px;
  padding:0;margin-top:12px;transition:color var(--t-fast) var(--ease)}
.connect .back:hover,.connect .back:focus-visible{color:var(--ink);background:none}
.connect .back .ic{width:14px;height:14px}
/* The command sits on paper: --code on the card's tint was a whisper,
   invisible in light. A cut-out reads as "this is the thing you take". */
.connect .picker-cmd{background:var(--paper)}
/* Copy is quiet: no backing on hover — the command's own box is backing
   enough. */
.picker-cmd .iconbtn:hover,.picker-cmd .iconbtn:focus-visible{background:none;color:var(--ink)}
/* A COPY BUTTON'S YES IS THE BARE CHECK, everywhere one is offered — the
   command, the prompt, the answer. The filled disk is the trace's state
   vocabulary: it says how a ROW ended, and wearing it here made a button's
   acknowledgement look like an event in the log that produced it. Full ink
   because it is the answer to something the reader just did. */
.iconbtn .ok{color:var(--ink)}
.g-todo{width:13px;height:13px;border-radius:50%;background:var(--ink-3);
  display:inline-flex;align-items:center;justify-content:center;flex:none}
.g-todo::after{content:"";width:7px;height:7px;background:var(--panel-2);
  -webkit-mask:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center/contain no-repeat;
  mask:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center/contain no-repeat}

.picker-connect{padding:2px 12px 8px var(--fold-indent);display:flex;
  flex-direction:column;gap:6px}
.picker-connect p{font-size:12.5px;color:var(--ink-2);margin:0;line-height:1.45}
.picker-cmd{display:flex;align-items:center;gap:8px;background:var(--code);
  border-radius:var(--r-sm);padding:5px 6px 5px 10px}
.picker-cmd code{font-family:var(--font-mono);font-size:12px;color:var(--ink);
  white-space:nowrap;overflow-x:auto;scrollbar-width:none}
.picker-cmd code::-webkit-scrollbar{display:none}
.picker-cmd .iconbtn{width:24px;height:24px;margin-left:auto;flex:none}
/* the door to a SECOND machine: a ghost row at the list's end — present,
   never loud; the catalog is the card's job and this is an aside to it */
.picker-add{margin-top:6px}

/* ---------- active skills — .skill-label ----------
   What the next message runs WITH, stated at the pill's leading edge beside
   the logo that opens the kit. It is a LABEL, not a control: a span,
   no hover step, no pointer, nothing to press — the logo is the control, and
   a second target for the same card would be two ways to do one thing.
   The glyph is the Skills icon (the sidebar's own for that page) and it is
   the LABEL of the number, not decoration beside it: that is what keeps a
   bare count inside canon#12 and makes this a tally rather than a badge.
   Colour is --c4, the tag palette's "what is yours" — the third and last
   enumerated chromatic thing in the chrome (with .tag and .act), earning the
   hue the same way: it separates a stated FACT from the two quiet controls
   at the pill's two ends. Absent, never a zero, when no skill is on
   (_design#13) — a line that always says everything is fine stops being read. */
/* absent, never a zero: the label leaves at 0 — its own display outranks
   the UA's [hidden] rule, the same trap as .menu[hidden] */
.skill-label[hidden]{display:none}
.skill-label{display:inline-flex;align-items:center;gap:4px;flex:none;
  margin-left:6px;font-size:13px;color:var(--c4);font-variant-numeric:tabular-nums}
.skill-label .ic{width:14px;height:14px;color:inherit}

/* model chip: narration-gray inside the pill; hidden while typing */
.model-chip-wrap{position:relative;flex:none}
.model-chip{border:0;background:none;font:inherit;font-size:13px;
  color:var(--ink-2);cursor:pointer;white-space:nowrap;padding:4px 2px;
  display:inline-flex;align-items:center;gap:6px;
  transition:color var(--t-fast) var(--ease)}
.model-chip:hover{color:var(--ink)}
.model-chip .h-mark{width:13px;height:13px;color:inherit}
/* A NARROW pill keeps the mark and drops the name. The pill is the container
   asked, not the window: the same composer is the width of a pane at the
   bottom of a thread and 440px floating beside a selection, and it is the
   second one that has no room — a truncated model name reads as a different
   model. The mark is the same control, saying which harness answers and
   opening the same chooser. */
.pill-input{container-type:inline-size}
@container (max-width:460px){
  .model-chip .name{display:none}
  .model-chip{padding-left:0}
}
/* the model menu is an instance of the popover framework below —
   `.menu.up.right` (it opens over the composer, aligned to the pill's end) */

/* ---------- popover menus — .pop + .menu ----------
   ONE floating-menu framework for the language: model chooser, skills
   toggle, row actions, right-click context menus. A --panel card floating
   over its anchor — tint and radius, never a shadow or a border (canon#3);
   placement is a class, not a per-component rule. */
/* the anchor: any trigger becomes one by wrapping it (the menu is
   positioned against this box, so it must be the trigger's own box —
   never a whole toolbar) */
.pop{position:relative;display:inline-flex}
/* …unless the trigger lives INSIDE a control — the model chip in the
   composer pill, the logo beside it. Then the anchor is the CONTROL:
   `.pop-host` on the pill takes the positioning context and the wrapper
   goes static. A menu measured from the 22px chip clears the chip and
   lands on the field it sits in — on top of the text being typed — and
   aligns to an adornment's edge instead of the edge the reader sees.
   Two triggers can share one host: each keeps its own alignment class. */
.pop-host{position:relative}
.pop-host .pop{position:static}
.menu{position:absolute;z-index:20;
  /* default placement: BELOW the anchor, leading edges aligned — the
     ordinary case; `.up` / `.right` move it, nothing else does */
  top:calc(100% + 8px);left:0;
  background:var(--panel);border-radius:var(--r-md);padding:6px;
  min-width:200px;max-width:min(320px,90vw);
  display:flex;flex-direction:column;gap:2px;
  /* a real catalog runs to hundreds of rows — the menu scrolls inside
     itself rather than growing off the viewport; the scrollbar is not part
     of the language (canon#10's containment discipline). Two bounds, and
     the tighter wins: `--pop-max` is the room the opener actually found on
     the side it chose (50vh is the no-JS fallback), and 300px is the cap —
     about eight rows. A menu that fills the pane is a pane. */
  max-height:min(var(--pop-max,50vh),300px);
  overflow-y:auto;overscroll-behavior:contain;
  scrollbar-width:none;
  animation:menu-in var(--t-fast) var(--ease)}
/* The connect journey is a plan, not a menu: clipping its Start behind a
   scroll hides the one thing the card asks for. When the card hosts it, the
   cap yields to the content (still bounded by the room the opener found). */
.menu:has(.connect){max-height:min(var(--pop-max,80vh),640px)}
.menu::-webkit-scrollbar{display:none}
.menu.up{top:auto;bottom:calc(100% + 8px);animation-name:menu-in-up}
.menu.right{left:auto;right:0}
/* pointer-anchored (right-click): fixed to the viewport, coordinates
   written inline by the opener — `position:fixed` needs a body portal for
   the same reason a modal does (components.md § Modal) */
.menu.free{position:fixed;top:auto;bottom:auto;left:auto;right:auto}
/* the menu arrives from the side it belongs to — 3px, a --t-fast fade;
   just enough to say where it came from (canon#9) */
@keyframes menu-in{from{opacity:0;transform:translateY(-3px)}}
@keyframes menu-in-up{from{opacity:0;transform:translateY(3px)}}
@media (prefers-reduced-motion:reduce){.menu{animation:none}}
/* rows: one line each, glyph slot · label · optional trailing hint */
.menu button{border:0;background:none;font:inherit;font-size:13px;
  color:var(--ink);text-align:left;padding:8px 12px;border-radius:var(--r-full);
  cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:10px;
  transition:background var(--t-fast) var(--ease)}
/* keyboard focus is the same tint step as hover — visible, never a ring
   (canon#3 extends to focus states) */
.menu button:hover,.menu button:focus-visible{background:var(--panel-2);outline:none}
/* a row's own `display` outranks the UA's [hidden] rule — without this a
   filtered-out row is a visible one (the same trap as `.menu[hidden]`) */
.menu button[hidden]{display:none}
.menu button.on{font-weight:600}
.menu button[disabled]{color:var(--ink-3);pointer-events:none}
.menu button .ic{color:var(--ink-2);transition:color var(--t-fast) var(--ease)}
.menu button:hover .ic,.menu button:focus-visible .ic{color:var(--ink)}
.menu:focus{outline:none}
/* trailing value or shortcut — meta, so it takes the meta spec */
.menu-hint{margin-left:auto;padding-left:16px;font-size:12.5px;color:var(--ink-2);
  font-variant-numeric:tabular-nums}
/* groups separate by SPACE and a label, never a rule — a menu is not the
   details panel, the one place a hairline lives (_design#2) */
.menu-group{display:flex;flex-direction:column;gap:2px}
.menu-group+.menu-group{margin-top:6px}
.menu-label{font-size:13px;color:var(--ink-2);padding:6px 12px 2px}
/* empty is one narration line, not a blank card (_design#13) */
.menu-empty{font-size:13px;color:var(--ink-2);padding:8px 12px;white-space:nowrap}
/* selection in a MULTI-select is a bare check at the END of the row (no disk,
   no leading slot): every name starts on the same edge whether or not it is
   on, an unchecked row has nothing standing in front of it, and the mark is
   read where the eye lands after the word. A leading circle asked to be
   filled in — a form's question — where this states a fact about the row.
   The name itself stays at normal weight: the check carries the state, and
   bolding it as well says the same thing twice. */
.menu-mark{margin-left:auto;width:14px;height:14px;flex:none}
.menu button .menu-mark{color:var(--ink)}
/* a hint and a mark can share a row (the default effort, chosen): the hint's
   auto margin does the pushing and the mark rides at the very end */
.menu-hint+.menu-mark{margin-left:0}
/* hosted on --panel (inside a modal, a card, a panel pane) the card steps
   up and its hover steps back down — the menu keeps one step of separation
   from whatever ground it floats over, the same rule as an icon button's
   backing (components.md § Modal) */
.menu.on-panel{background:var(--panel-2)}
.menu.on-panel button:hover,.menu.on-panel button:focus-visible{background:var(--panel)}
/* …and the trigger that opens it, sitting on that same ground */
.iconbtn.on-panel:hover,.iconbtn.on-panel[aria-expanded="true"]{background:var(--panel-2)}
/* a trigger stays lit while its menu is open — the control that was
   pressed must not look untouched */
.iconbtn[aria-expanded="true"]{background:var(--panel);color:var(--ink)}
button.chip[aria-expanded="true"]{background:var(--panel-2)}
.model-chip[aria-expanded="true"]{color:var(--ink)}

/* ---------- the composer kit — .kit (on the composer logo) ----------
   The card the composer LOGO opens: what the next send goes out WITH — files
   it carries, skills it runs with. One card because it is one question, and
   one entrance because a second icon at the pill's leading edge would split
   that question across two doors.

   Structure, top to bottom: the picker's own filter head (one search
   component in two cards — present only when the catalog overflows the
   framework's cap, a filter over four rows is furniture), an ATTACH group of
   action rows (leading icons, the action-menu grammar), then the SKILLS as a
   labelled multi-select (bare mono names, end checks). Two grammars in one
   card is deliberate: the groups are different parts of speech, each keeps
   its own row anatomy, and the label between them is the seam.

   An attach row CLOSES the card — it hands off to the OS picker; a skill row
   toggles and STAYS (§ Popover menu's row-level rule: the row's role
   decides, never the card). While filtering, the attach group leaves —
   verbs are not the filter's answer — and returns when the field clears.
   When the backend cannot take files yet, the attach group is ABSENT, never
   a teaser (_design#13); the card is then exactly the skills card it grew
   from. */
.skills-wrap{position:relative;display:inline-flex}
.logo-btn{border:0;background:none;padding:0;margin:0;display:inline-flex;
  color:inherit;cursor:pointer;border-radius:var(--r-xs);
  transition:color var(--t-fast) var(--ease)}
.logo-btn:hover,.logo-btn[aria-expanded="true"]{color:var(--ink)}
/* the kit is a .menu with the picker's bones: head pinned, list scrolling,
   picker density (a row is a line of text with 5px of air) */
.kit{display:flex;flex-direction:column;gap:0;min-width:240px}
.kit .menu-group button,.kit .picker-list>.menu-group button{
  padding-top:5px;padding-bottom:5px}
/* the group label is capped (SKILLS), so it takes the shelf's own
   treatment: 11px with tracking — a section name, quieter than any row
   it heads (§ Model picker's RECENTLY USED) */
.kit .menu-label{display:flex;align-items:center;font-size:11px;
  letter-spacing:.06em;color:var(--ink-3);padding:4px 12px 2px}
/* …and the label's trailing SEARCH — a quiet control at the section name's
   end (the details panel's label-action, one scale down). Search here is ON
   DEMAND: pressed, the card swaps into its searching state — the bar tops
   the card, the attach verbs leave — so the resting card stays minimal at
   any catalog size, and the always-on filter head is retired. While the
   search is open the glyph is gone: the bar's ✕ is the way back. */
.kit .label-search{margin-left:auto;border:0;background:none;padding:0;
  display:inline-flex;color:var(--ink-3);cursor:pointer;
  transition:color var(--t-fast) var(--ease)}
.kit .label-search:hover,.kit .label-search:focus-visible{
  color:var(--ink);outline:none}
.kit .label-search .ic{width:14px;height:14px}
.kit .label-search[hidden]{display:none}
/* the searching state arrives and leaves whole — its head's own display
   outranks the UA's [hidden] rule, the same trap as .menu[hidden] */
.kit .picker-head[hidden]{display:none}
/* dismissing the search: a bare ✕ inside the field's end, stepping
   --ink-3 → --ink like the magnifier it answers */
.picker-search .x{position:absolute;right:10px;border:0;background:none;
  padding:0;display:inline-flex;color:var(--ink-3);cursor:pointer;
  transition:color var(--t-fast) var(--ease)}
.picker-search .x:hover,.picker-search .x:focus-visible{
  color:var(--ink);outline:none}
/* back in flow: the field's own `.ic` rule pins ANY icon to the magnifier's
   spot at the leading edge — the ✕ is the button's content, not the field's
   adornment */
.picker-search .x .ic{position:static;left:auto;width:14px;height:14px;
  color:inherit;pointer-events:auto}
.kit .picker-search input{padding-right:32px}

/* ---------- attachments on the composer — .attach-strip ----------
   What the kit's verbs — or a drop on the pane, or a paste into the pill —
   leave behind: a strip of chips above the pill, floating with the composer
   at the thread measure. The chips ARE the statement, so attachments get no
   count label beside the logo (skills get one because an active skill is
   otherwise invisible; a chip is not).

   One chip anatomy for every kind: a leading 18px slot (the kind's icon —
   file-text, image, film — or the image's own THUMBNAIL: content, not
   chrome, so it may carry color, canon#2), the name in body 13px, and a
   bare ✕ that removes — no backing disk inside a stadium. The slot is also
   where state lives (_design#5): the running dots while the upload is in
   flight, the stopped disc when it ended without landing — pressing a
   failed chip retries, and its label says so. */
.attach-strip{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.attach-chip{display:inline-flex;align-items:center;gap:7px;height:30px;
  padding:0 9px 0 7px;background:var(--panel);border-radius:var(--r-full);
  font-size:13px;color:var(--ink);max-width:220px}
.attach-chip .kind{width:18px;height:18px;flex:none;display:inline-flex;
  align-items:center;justify-content:center;color:var(--ink-2)}
.attach-chip .kind .ic{width:14px;height:14px}
/* the thumbnail fills its slot; the radius is the scale's smallest step */
.attach-chip .thumb{width:18px;height:18px;flex:none;border-radius:var(--r-xs);
  background:var(--panel-2);overflow:hidden;display:inline-flex;
  align-items:center;justify-content:center}
.attach-chip .thumb img,.attach-chip .thumb svg{width:100%;height:100%;
  object-fit:cover;display:block}
.attach-chip .name{min-width:0;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
/* remove: a bare glyph stepping ink-2 → ink, the model chip's own move */
.attach-chip .x{border:0;background:none;padding:0;margin:0;display:inline-flex;
  color:var(--ink-2);cursor:pointer;flex:none;
  transition:color var(--t-fast) var(--ease)}
.attach-chip .x:hover,.attach-chip .x:focus-visible{color:var(--ink);outline:none}
.attach-chip .x .ic{width:14px;height:14px}
/* in flight: the slot runs; failed: the stopped disc, and the chip's body
   is the retry (the one thing on screen about that file) */
.attach-chip .kind .g-run,.attach-chip .kind .g-stop{transform:scale(.85)}
.attach-chip .body{display:inline-flex;align-items:center;gap:7px;min-width:0;
  border:0;background:none;padding:0;margin:0;font:inherit;color:inherit;
  text-align:left}
.attach-chip.failed .body{cursor:pointer}
/* a drag over the pane is answered by the pill's own focus step — the strip
   is where the drop lands, the tint says it is welcome. Both composers: the
   thread's and home's. */
.composer.dropping .pill-input,
.home-composer.dropping .pill-input{background:var(--panel-2)}

/* ---------- attachments on a SENT turn — .msg-attach ----------
   The strip's chips, landed in the log: they ride ABOVE the user's box,
   right-aligned to its edge — the sent turn is the composed moment made
   fact, and the composer showed exactly this picture (the strip above the
   pill). One chip anatomy across both moments; the sent chip drops its ✕
   (a log is a fact, nothing to remove) and carries no upload state (nothing
   lands in the log that did not finish landing in the store). It becomes
   PRESSABLE whole — pressing opens the file, which the content hash keeps
   fetchable for as long as the log points at it. Below the box was never a
   candidate: that ground belongs to the turn's hover actions. The 8px to
   the box is the composer's own strip-to-pill gap. */
.msg-attach{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end;
  width:fit-content;max-width:78%;margin:0 0 8px auto}
/* a chip that is a BUTTON owes the tint step every pressable thing owes */
button.attach-chip{border:0;font:inherit;text-align:left;cursor:pointer;
  transition:background var(--t-fast) var(--ease)}
button.attach-chip:hover,button.attach-chip:focus-visible{
  background:var(--panel-2);outline:none}

/* ---------- top nav (home & artifacts views) ----------
   TRUE centering: absolutely anchored to the header's middle, so the
   history toggle at the edge never pushes it off-center */
.home-h{justify-content:space-between}

/* ---------- sidebar pages (above Threads) ----------
   Where you can GO, over what you have. Same stadium and the same active fill as a
   thread card, because they are the same kind of row in the same column — a second
   shape here would say these are a different sort of thing to click. The glyph sits
   in a fixed 14px slot so the three names start on one edge. */
/* 16px under the pages, a grid step (foundations.md § Spacing) and the same gap the
   retired "Threads" label used to contribute. Two kinds of thing share this column —
   where you can go, and what you have — and without a deliberate step between them
   the first date bucket reads as a fourth page. */
.sidenav{display:flex;flex-direction:column;gap:2px;margin-bottom:16px}
/* No explicit width: the row is a block-level flex box, so it already fills the
   column, and the negative margins then bleed it 12px past each edge exactly the way
   a thread card does. `width:100%` pinned it to the container instead, so the right
   bleed had nothing to bleed into and the fill stopped short of the edge. */
/* ---------- a date bucket's heading, which is its disclosure ----------
   The same control as every other fold in the chrome (`.seg-toggle`, `.thought-head`):
   the words, then a chevron that turns 90°. It reads at `.label` weight and colour
   because it IS the section label — folding is something the label gained, not a new
   kind of row. Left-aligned as an inline-flex so the chevron sits beside the word
   rather than being pushed to the pane's far edge. */
.grp{border:0;background:none;padding:0;margin:6px 0 4px;font:inherit;
  font-weight:400;font-size:13px;color:var(--ink-2);cursor:pointer;
  display:inline-flex;align-items:center;gap:6px;
  transition:color var(--t-fast) var(--ease)}
.grp:hover{color:var(--ink-lift)}
.grp .chev{width:13px;height:13px;flex:none;
  transition:transform var(--t-fast) var(--ease)}
.grp.open .chev{transform:rotate(90deg)}
@media (prefers-reduced-motion:reduce){.grp .chev{transition:none}}

.sidenav-item{display:flex;align-items:center;gap:9px;
  padding:7px 12px;margin:0 -12px;border:0;background:none;cursor:pointer;
  border-radius:var(--r-full);color:var(--ink);font:inherit;font-size:var(--text);
  font-weight:500;text-align:left;
  transition:background var(--t-fast) var(--ease)}
.sidenav-item:hover{background:var(--accent-soft)}
.sidenav-item.on{background:var(--panel)}
.sidenav-item svg{flex:none;width:14px;height:14px;color:var(--ink-2)}
.sidenav-item.on svg{color:var(--ink)}

/* ---------- home — the new-conversation state ----------
   the COMPOSER is the centered element (true vertical center of the pane);
   the wordmark floats above it and never shifts the centering */
/* the pill sits IN FLOW at the pane's vertical center (the -60px cancels
   the pane header, keeping the same height as the old fixed version); the
   wordmark hangs above it and never shifts the centering */
.home{position:relative;
  flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
  margin-top:-60px}
.home .wordmark{position:absolute;bottom:calc(100% + 10px);left:50%;
  transform:translateX(-50%);font-size:26px}
.home-composer{position:relative;width:min(100%,var(--thread-col))}

/* ---------- sign-in ---------- */
/* The opening composition at its OWN measure (--signin-col), centered, with what
   the system is doing said directly under it. It is NOT the thread composer's
   width: an email address and a six-digit code are short answers, and a 720px
   pill around six digits reads as a place to write a message. The say-line
   reserves its height so answering does not move the field the cursor is in. */
.signin-col{position:relative;width:var(--signin-col);max-width:100%;margin:0 auto}
/* the wordmark steps down with the column — this rule follows `.home .wordmark`
   in source order, which is what makes it win on the sign-in screen (both are
   two-class selectors and the root carries `home signin`) */
.signin-col .wordmark{position:absolute;bottom:calc(100% + 20px);left:50%;
  transform:translateX(-50%);font-size:20px}
/* ONE slot, ONE height (56px, the case) for every state the screen passes
   through: asking (the field), waiting (the dots), answered (the check). The
   field is REMOVED while the system works — there is nothing to type into, and
   a live-looking field that ignores you is a lie — and because the slot keeps
   its height, nothing under it moves as they swap. */
.signin-field{min-height:56px;display:flex;align-items:center;justify-content:center}
/* everything the screen says is CENTERED under the field: one centered
   composition, not a form with a label rail */
.signin .signin-say{margin-top:14px;min-height:44px;padding:0;
  display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center}
.signin .signin-say p{margin:0;font-size:13.5px;line-height:1.5}
/* the two ways out live INSIDE the sentence (.act), not as a rail of buttons
   under it — see the .act rule below */

/* ---------- the code input — .code-in ----------
   Six cases, one per digit: the field states the shape of the answer, so a
   six-digit code is six boxes and not a sentence-shaped pill. Same fill, same
   tint-step focus and same absence of a border as .input (canon#3) — only the
   geometry differs, because what it takes is one character. The case is a
   SQUARE (one character has no reading direction) and --signin-col is derived
   from it, never the reverse. */
.code-in{display:flex;gap:8px}
.code-in input{width:56px;height:56px;flex:none;border:0;padding:0;
  background:var(--panel);border-radius:var(--r-md);text-align:center;
  /* the digits are the machine's string, relayed by hand (canon#6) — mono and
     tabular, so six cases hold six identically-shaped characters */
  font-family:var(--font-emph);font-size:20px;color:var(--ink);
  font-variant-numeric:tabular-nums;caret-color:var(--ink);
  transition:background var(--t-fast) var(--ease)}
.code-in input:focus{background:var(--panel-2);outline:none}
/* a refused code CLEARS the cases and the message does the explaining; they
   never go red (canon#2: errors are not a hue). There is no disabled state to
   style — while the code is checked the row is gone from the slot entirely. */

/* ---------- inline action — .act ----------
   The two things a person can do next said INSIDE the sentence that explains
   the situation ("Send again or change email"), not as a rail of buttons under
   it: they are alternatives to reading the line, so they belong in it. A step
   of the one blue (--action) marks them as reachable where nothing else in the
   chrome is colored.

   This is the SECOND chromatic thing in the chrome, after .tag, and it sits
   within canon#2 for the same reason: the hue only reinforces text that already
   says what it does. Remove the words and there is nothing left to click, not
   merely something quieter. For actions inside a sentence — never a standalone
   control, which is .btn, and never anything destructive. */
/* `text-align:inherit` because a <button> centres its own text and this one is
   a phrase inside a sentence: a thread title long enough to wrap — which is
   most of them in the details panel's column — came out centred under a
   left-aligned label, reading as a caption rather than as the line it is in. */
.act{background:none;border:0;padding:0;font:inherit;color:var(--action);
  cursor:pointer;text-decoration:none;text-align:inherit}
.act:hover,.act:focus-visible{text-decoration:underline;outline:none}
.act[disabled]{color:var(--ink-3);pointer-events:none}

/* the signed-out pane: the main region's geometry, without the regions —
   there is nothing to put in them until someone is signed in */
.signed-out{height:100%;display:flex;flex-direction:column;padding:0 20px;
  background:var(--paper);color:var(--ink);font-family:var(--font-body);
  font-size:14px;letter-spacing:-0.01em;line-height:1.55}

/* ---------- sidebar account row ---------- */
/* The row FLOATS over the list, and carries no backdrop of its own (_design#16): the
   list is masked out before it gets here, so there is nothing left to cover. An
   earlier version filled a full-width strip with `--paper` to hide what scrolled
   behind it — correct, and much louder than a line naming who you are needs to be.
   Pinned to the foot of the REGION, not the end of the list: the account is where you
   left it whether the thread list is two rows or two hundred. Absolute against
   `.side`, and a SIBLING of `.side-in` — the scroller carries a mask, which would
   otherwise make it the containing block and take the row down with the threads. */
.side-foot{position:absolute;left:0;right:0;bottom:0;padding:0 8px 16px}
/* No fill. The list fades out before it reaches here, so there is nothing to sit on
   top of — and a standing pill was a permanently-lit version of the shape a thread
   card only wears while you point at it, which read as a selected row that could not
   be deselected.
   Horizontal padding stays — it puts the name on the same left edge as every thread
   title above it. A little vertical padding is back: the bare 20px line box sat too
   tight against the window edge, so the row is 32px and rides a 16px foot inset. */
.side-account{display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding:6px 12px}
.side-account .who{color:var(--ink);font-size:var(--text);font-weight:500;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The glyph IS the control — no box of its own, and no second backing inside a row
   that already carries one. It brightens instead. */
.side-account .signout{border:0;background:none;padding:0;flex:none;cursor:pointer;
  color:var(--ink-2);display:inline-flex;align-items:center;
  transition:color var(--t-fast) var(--ease)}
.side-account .signout:hover{color:var(--ink)}

/* where the next message will run, when that needs saying (_design#13).
   It carries its own GROUND — the pill's, one step up from paper, at the pill's
   own radius and side padding, so the two read as one stack. This is the
   exception _design#16's "no backdrop" exists for rather than a break from it:
   the composer needs no ground because the pill's own fill already covers what
   passes beneath it, and this line is the one thing in the pane that would
   otherwise be read THROUGH the trace scrolling under it.
   It hugs its sentence rather than spanning the measure — a line that is
   sometimes there must not read as a region that is sometimes empty.
   Tint and radius alone, no shadow (canon#3): the fill is opaque, so what
   passes beneath is covered rather than dimmed, and being in front is a thing
   the reader can see without being told. */
.run-where{margin:8px 0 0;padding:9px 18px;width:fit-content;max-width:100%;
  background:var(--panel);border-radius:var(--r-lg);
  font-size:13px;line-height:1.45}
/* the composer floats over the pane, so its line goes ABOVE the pill rather than
   under it — below is off the bottom of the window */
.composer{display:flex;flex-direction:column-reverse;gap:0}
.composer .run-where{margin:0 0 8px}
/* ON THE OPENING PAGE it goes to the FOOT, centred on the composer's own axis.
   That page is a composition centred in the window — wordmark, then the pill —
   and a line hung under the pill pushes the composition off its centre to say
   something that is not about what you are writing. At the foot it is present
   without being in the way, and the axis is what keeps it attached to the thing
   it is about. Its own margin is cleared: the offset is the page's, not the
   line's. */
.home>.run-where{position:absolute;left:50%;transform:translateX(-50%);
  bottom:28px;margin:0;text-align:center}

/* ---------- artifacts page ---------- */
.artifacts{flex:1;min-height:0;overflow-y:auto;scrollbar-width:none;padding-bottom:40px}
.artifacts::-webkit-scrollbar{display:none}
/* the gallery's own dissolving header — the same spec as the thread pane's,
   living INSIDE the scroller, so cards fade under it as they approach instead
   of clipping below a dead strip. Its title speaks only once the in-flow page
   title has scrolled away: one name, said by whichever place is on screen. */
.gal-t{opacity:0;transition:opacity var(--t-fast) var(--ease)}
.gal-t.on{opacity:1}

/* ---------- the docked control line — .gal-dock ----------
   The controls obey the same rule the page's name does: the header re-states
   each of them once its own place in the composition has left the screen. The
   name goes first (its slot is 60px), then the search, tally, facets and tools
   together when the facet row goes — one mark under that row is what says so
   (Gallery.tsx), never a measured guess at how the chips wrapped.
   The order is the composition's own, read down the page and laid flat: search,
   tally, facets, then the pinned tools. Docking is then a COLLAPSE — nothing
   crosses anything else on its way into the line.
   `visibility`, not an aria flag: the parked copy leaves the tab order and the
   a11y tree with it, so the page never offers the same control twice.
   The live state is `inherit`, NEVER `visible`. A gallery is kept alive when the
   reader leaves it (`.page.off{visibility:hidden}`), and `visible` on a
   descendant ESCAPES a hidden ancestor — the docked line went on standing over
   home and compute, a header for a page nobody was on. `inherit` says the one
   thing meant here: visible unless something above me is hidden. */
/* no `position` here: `.thread-h` is already sticky, and sticky is positioned —
   it is the containing block the fade strip below hangs from. A `relative` of
   its own, later in the cascade at equal weight, would quietly unstick it. */
.gal-dock{flex:1;min-width:0;display:flex;align-items:center;gap:8px;
  opacity:0;visibility:hidden;
  transition:opacity var(--t-fast) var(--ease),visibility 0s linear var(--t-fast)}
.gal-dock.on{opacity:1;visibility:inherit;
  transition:opacity var(--t-fast) var(--ease),visibility 0s}
/* the page's name holds only its own width here: it is a short name that never
   needs the middle of the line, and the controls need every pixel of it */
.gal-h .t{flex:none;max-width:38%}
/* the pill at header scale — one line, so a stadium (_design#6), and short: it
   is the way back to the search, not the search composition itself. The glyph
   carries the meaning the long placeholder carries below, which at this width
   would only clip. */
.dock-search{flex:0 1 260px;min-width:110px}
.dock-search .pill-input{padding:7px 14px;border-radius:var(--r-full)}
.dock-search .input{font-size:13.5px}
/* the cluster stays pinned right, exactly as it is in the facet row */
.gal-dock .gal-tools{margin-left:auto}
/* one line and one line only: the row below may wrap, the header may not */
.gal-dock .facets{flex-wrap:nowrap;overflow:hidden}
.gal-dock .gal-tally{margin-right:2px;white-space:nowrap}
/* A line carrying filled controls cannot sit on a gradient that is transparent
   at its own foot — cards would show through the gaps between the chips. The
   header goes solid and the dissolve moves just BELOW it, which is the same
   move the rows head makes (see `.gal-rowh`): still a paper fade, still no
   edge, only measured from the bottom of what is opaque. In rows view the
   column head already provides that fade, so the strip would double it. */
.gal-h.on{background:var(--paper)}
.artifacts:not(:has(.gal-rowh)) .gal-h.on::after{content:"";position:absolute;
  left:0;right:0;top:100%;height:18px;pointer-events:none;
  background:linear-gradient(var(--paper),transparent)}
/* the mark the dock watches — the foot of the composition, nothing to look at */
.gal-mark{height:1px}
/* Narrow panes cannot hold the whole line, and what leaves is what stays
   cheapest to reach: the facets and the tally are two flicks of the wheel away
   in their own row; the search is the reason the line exists. */
@media (max-width:1180px){.gal-dock .facets{display:none}}
@media (max-width:900px){.gal-dock .gal-tally{display:none}}
/* reports own the WHOLE pane width — the region's padding is the only inset */
.artifacts-in{max-width:none;margin:0}
/* the search pill keeps the standard input measure (the composer's column).
   No top margin of its own: the page title above sets the one gap every
   destination's first element shares (Compute's pills sit at the same height). */
.asearch{max-width:var(--thread-col);margin:0 auto 48px}
/* as many cards as fit at a readable width — the 240px floor is the rule,
   the column count is whatever the pane affords, so cards never squish.
   auto-FIT, not auto-fill: leftover space goes to the cards, not to empty
   tracks, so a short shelf still fills its row like the fixed grid did */
.acards{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px;margin-top:10px}
/* column flex, NOT block: a stretched <button> centers its content box
   vertically by UA rule, so button-backed cards would float mid-card while
   anchor-backed ones sat at the top. flex + flex-start pins both to the top. */
.acard{display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;
  background:var(--panel);border-radius:var(--r-lg);padding:18px 20px;cursor:pointer;
  text-align:left;border:0;font:inherit;color:var(--ink)}
/* cards that open a standalone page are anchors — same card, no underline */
a.acard{text-decoration:none}
.acard .kind{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2)}
.acard h3{font-family:var(--font-title);font-weight:600;font-size:14px;letter-spacing:-0.01em;
  margin:8px 0 12px;text-wrap:wrap}
/* goal text caps at three lines, ellipsized */
.acard p{font-size:13.5px;margin:0;display:-webkit-box;-webkit-line-clamp:3;
  -webkit-box-orient:vertical;overflow:hidden}
/* provenance footer, pinned to the card's bottom edge: the model in the
   machine's voice, then the thread it came from — the one interactive part
   of a card that is otherwise one big button */
.acard-meta{display:flex;align-items:baseline;gap:10px;margin-top:auto;
  padding-top:12px;min-width:0}
.acard-meta .agent{font-size:11.5px;color:var(--ink-2);flex-shrink:0}
.acard-go{font-size:12px;color:var(--ink-2);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.acard-go:hover,.acard-go:focus-visible{color:var(--ink)}
/* small screens: cards stack one per row and get SHORTER — tighter inset,
   goal clamped to two lines (AFTER the base rules so the cascade wins) */
@media (max-width:640px){
  /* minmax(0,…): a plain 1fr mins at the nowrap title's content width and
     overflows the viewport */
  .acards{grid-template-columns:minmax(0,1fr)}
  .acard{padding:12px 16px}
  .acard h3{margin:6px 0 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .acard p{-webkit-line-clamp:2}
  .gal-facets{gap:6px}
  .gal-tally{width:100%;margin:0 0 2px}
}

/* ---------- the facet row — .gal-facets ----------
   One line under the search pill, at the SHELF's width rather than the pill's:
   it belongs to what is below it, not to what is above. Left, how much of the
   catalog is in view and whatever the page narrows by; right, the pinned tools.
   The right cluster is pinned — it holds its place whether the shelf is full,
   filtered to two, or empty, which is exactly what a create CARD could not do. */
.gal-facets{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 24px}
/* how much of the catalog you are looking at; tabular, so it does not jitter as
   the filter narrows (_design#7) */
.gal-tally{font-size:12.5px;color:var(--ink-2);font-variant-numeric:tabular-nums;
  flex:none;margin-right:6px}
.facets{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
.gal-tools{margin-left:auto;display:flex;align-items:center;gap:4px;flex:none}

/* A facet chip is `.chip` in the MULTI-SELECT grammar the composer's skills menu
   already speaks (§ Popover menu): the word never changes weight, and being on
   is a bare check at the END. Off is narration gray on --panel; on is ink on
   --panel-2 with the mark. Three monochrome signals and no inversion — an
   ink-filled chip is the primary-action register, and a filter is not an action.
   Every facet stands ON at rest and the reader turns them OFF. An empty row
   meaning "all of it" is a control whose rest state contradicts its own reading:
   it says nothing is selected while the shelf shows everything, and its first
   click narrows by switching something on. */
button.chip.facet{color:var(--ink-2);gap:7px}
button.chip.facet:hover{color:var(--ink)}
button.chip.facet.on{background:var(--panel-2);color:var(--ink)}
.chip .count{font-variant-numeric:tabular-nums;color:var(--ink-3)}
.chip.on .count{color:var(--ink-2)}
.chip .facet-mark{width:13px;height:13px;flex:none;margin-right:-2px}
.facet-clear{padding-left:4px}
/* the standing view, marked the way every selection is: tint (_design#9) */
.gal-tools .iconbtn.on{background:var(--panel);color:var(--ink)}

/* ---------- a group of the shelf — .gal-group ----------
   The heading is a section label (_design#3): sentence case, regular weight,
   --ink-2, with its tally beside it. No divider — groups are separated by space,
   like every other region in this language (canon#3). The tally sits WITH the
   name: `.label`'s space-between is for a panel section head, where the right
   edge is inches away; across a shelf it strands the number. */
.gal-group{margin:0 0 28px}
.gal-group:last-child{margin-bottom:0}
.gal-group>.label{margin:0 0 10px;padding:0 20px;justify-content:flex-start;gap:8px}
.gal-group>.label .count{color:var(--ink-3)}

/* ---------- rows view — .gal-rows / .gal-row / .gal-rowh ----------
   The catalog as a file list. Columns exist so the eye can run DOWN one fact
   instead of across every card: kind under kind, date under date. A grid, not a
   flex row — columns that agree only by accident are not columns.
   Every track is a fixed width or a fraction, never `auto`: the head and the
   rows are separate grid containers, so an auto track sizes to ITS OWN content
   and the two disagree by however wide the widest model name happens to be.

   The column set is DATA, not a page's stylesheet: `--gcols` is written on
   `.shelf` by the component from the columns each gallery declares (shelf.tsx),
   because a column's width and its label belong in one declaration — and because
   the reader can move it, which no stylesheet can answer for. Artifacts run a
   title, kind, thread, model and time; skills a name, the trigger contract, the
   owner and when it changed. Same row, same head, different facts. */
.gal-rows{display:flex;flex-direction:column;gap:4px}

/* The column head is NOT a row: no stadium, no fill, no rule under it — a filled
   head is one more item at the top of the list. It STAYS: scroll into the fourth
   group and the columns still say what they are, because a table whose head has
   left is five columns of unlabelled facts. Sticky under the pane header, at its
   height, with the same --paper fade (_design#16) — rows dissolve as they reach
   it, never a hard edge, a hairline or a shadow. */
.gal-rowh{position:sticky;top:var(--pane-h);z-index:1;
  display:grid;align-items:center;gap:14px;padding:8px 20px 18px;
  grid-template-columns:var(--gcols);
  /* The stop is in PIXELS, not a percentage: the labels sit in the first 26px
     and must be on solid paper. A percentage runs the fade through the middle of
     the words the moment the head's height changes, and washes them. */
  background:linear-gradient(var(--paper) 28px,transparent)}
/* Two sticky bands stack in this view, and they must read as ONE paper region
   with ONE fade at its bottom. Left alone the pane header reaches transparent a
   third of the way down, so a row shows through it and then vanishes again
   behind the opaque head below — a ghost slice sliding under the title rather
   than a dissolve. The header goes solid; the fade moves to the bottom of the
   pair. Scoped to the presence of a head, so the cards view keeps its own. */
.artifacts:has(.gal-rowh) .thread-h{background:var(--paper)}
/* A head CELL holds two things: the column's label, and the boundary between it
   and the column after it. The cell is the grid item, so the label can be a
   button (sortable) or a bare span (prose) without either becoming the track. */
.gal-colh{position:relative;display:flex;align-items:center;min-width:0}
.gal-rowh .colh{background:none;border:0;padding:0;font:inherit;font-size:13px;
  color:var(--ink-2);display:inline-flex;align-items:center;gap:3px;min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gal-rowh button.colh{cursor:pointer}
/* keyboard focus is the same step the pointer gets, never a browser outline —
   the language marks state by color and tint (_design#5, #9) */
.gal-rowh button.colh:hover,.gal-rowh button.colh:focus-visible{color:var(--ink);outline:none}
.gal-rowh .colh.on{color:var(--ink)}
/* The right-aligned column puts its sort mark BEFORE the label. Left after it,
   the mark's box — 12px plus its gap, held even at opacity 0 so the head does not
   reflow when the sort moves — sits between the word and the column's right edge,
   and the label lands 15px inboard of every date under it. */
.gal-rowh .last{justify-content:flex-end}
.gal-rowh .last .colh{flex-direction:row-reverse}
/* the mark rides the label it sorts, and is invisible until that column is the one */
.gal-rowh .sortmark{width:12px;height:12px;opacity:0;flex:none;
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease)}
.gal-rowh .colh.on .sortmark{opacity:1}
.gal-rowh .colh.on.asc .sortmark{transform:rotate(180deg)}

/* ---------- a column boundary — .colgrip ----------
   Every boundary between two columns is live, exactly the way a boundary between
   two REGIONS is (_design#15): invisible at rest, a mark under the pointer, held
   and slid, the columns reflowing with the drag rather than at the end of it.
   And it is the SAME mark — a hairline that goes to full ink under the pointer
   (`.resizer::after`, above). It was a glyph for a while, on the argument that a
   column gap has no gutter for a line to appear in; but a boundary in this
   language is a line, and two columns of dots between every pair of labels read
   as decoration on a head that is meant to carry none.
   The line is INVISIBLE at rest, where a region's is always faintly drawn: a
   region has one boundary and a table has one per column, and drawing them all
   at rest turns a no-rule table into a ruled one.
   The target is the whole 14px gap and a little above and below it — wider than
   the line it shows, because a hairline is a thing you hunt for. It sits in the
   gap and never over a label. No grip after the last column: a boundary moves
   the two columns it is between, and past the last one there is only the edge. */
.colgrip{position:absolute;top:-4px;bottom:-4px;right:-15px;width:16px;
  display:flex;align-items:center;justify-content:center;
  background:none;border:0;padding:0;cursor:col-resize;
  touch-action:none;user-select:none}
/* The MARK is short and the TARGET is not. The line runs to the label's own
   height and no further — a rule the full height of the head reads as a table
   border, which is the thing this head does not have; the box around it stays
   the whole gap and a little past the row, because a hairline is hard to hit.
   Square ends: a rounded cap's anti-aliasing fades the tips of a hairline, so
   the line would look like it stops short (the region resizer's own note). */
.colgrip::after{content:"";width:1px;height:13px;border-radius:0;background:var(--ink);
  opacity:0;transition:opacity var(--t-fast) var(--ease)}
.colgrip:hover::after,.colgrip:focus-visible::after,.colgrip.active::after{opacity:1}
/* An element that held the pointer stays in the `:hover` chain after the capture
   is released, and the browser does not recompute until the pointer leaves the
   DOCUMENT — so a dragged boundary stayed lit while the reader moved away from
   it. `cold` outranks the hover, and the drag drops it again on the first
   pointer move over the grip (columns.ts § cool). */
.colgrip.cold::after{opacity:0}
.colgrip:focus-visible{outline:none}
/* while a boundary is held, the pointer belongs to it wherever it wanders, and
   nothing on the shelf may animate its way to the width the drag is writing */
.shelf.dragging{cursor:col-resize;user-select:none}

/* the row: a stadium, because list items are stadiums (_design#6) */
.gal-row{display:grid;align-items:center;gap:14px;grid-template-columns:var(--gcols);
  background:var(--panel);border-radius:var(--r-full);padding:11px 20px;width:100%;
  border:0;font:inherit;color:var(--ink);text-align:left;cursor:pointer;
  transition:background var(--t-fast) var(--ease)}
.gal-row:hover,.gal-row:focus-visible{background:var(--panel-2);outline:none}
/* the open one, marked the way a selected thread card is (_design#9) */
.gal-row.on{background:var(--panel-2)}
.gal-row>*{min-width:0}
.gal-row .nm{font-family:var(--font-title);font-weight:600;font-size:13.5px;
  letter-spacing:-0.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* a skill's name is machine vocabulary where an artifact's title is prose — the
   same split the card already makes (.acard h3.agent) */
.gal-row .nm.agent{font-family:var(--font-emph);font-weight:400;font-size:13px}
.gal-row .kind{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gal-row .from{font-size:12.5px;color:var(--ink-2);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
.gal-row .from:hover,.gal-row .from:focus-visible{color:var(--ink)}
.gal-row .agent.mdl{font-size:11.5px;color:var(--ink-2);white-space:nowrap}
.gal-row .when{font-size:12.5px;color:var(--ink-2);font-variant-numeric:tabular-nums;
  text-align:right;white-space:nowrap}
/* the cards keep their shelf, now inside the same groups */
.gal-group .acards{margin-top:0}

/* ---------- a table in the shelf's shapes — .tbl ----------
   The model writes tables, and a table asks the shelf's question: one fact per
   column, aligned down the page so the eye can run DOWN one of them. So it takes
   the shelf's shapes rather than a second table design of its own — the head
   with no fill, rows separated by space, and every boundary between two columns
   live (.colgrip, above).

   A shelf DECLARES its columns; a table's are content-derived, which is what
   kept the two apart. One measure pass bridges it: the browser lays the table
   out natively once, and those widths become the declared set (`.on`, added by
   the script that measures). That pass buys the gesture as well as the look —
   the table now fits its measure instead of scrolling past it, so a boundary can
   EXCHANGE width between its two columns the way a shelf's does, rather than
   growing the table off the side of the page.

   EVERY table, whatever its shape. There was a gate once — three to six columns
   — and what it bought was a page where two tables a paragraph apart were two
   different components, and the reader had to work out why. A design that only
   applies to the tables that suit it is two designs.

   CELLS WRAP. They were one line with an ellipsis, on the argument that a cell
   which wraps has shown everything it has and widening its column gains nothing.
   That argument was answered by the first table that clipped a sentence: what
   the reader lost was the sentence, and no gesture gives it back — a boundary
   moves width between columns, it does not conjure any. So a cell shows all of
   itself, the row grows to hold it, and dragging a boundary is what it always
   was on a table that fits: choosing which column does the wrapping.

   The rows are --r-md and not --r-full for the same reason. A stadium is the
   shape of a one-line list item (_design#6); on a row three lines tall it is a
   lozenge. One radius for every row beats a radius that changes under the
   reader as a column narrows.

   Rows do NOT hover. A `.gal-row` tints because it opens something, and tint is
   SELECTION in this language (_design#9); a table row in prose opens nothing, so
   a row that lit under the pointer would promise a click that never lands. */
.tbl{--tbl-row:var(--panel);margin:0 0 14px}
/* TYPE first, and unconditionally — the measure pass reads the browser's own
   column widths off the table before `.on` exists, so every rule that changes a
   cell's TEXT METRICS has to be in force by then. Uppercase letterspacing on a
   kind is a fifth of that column's width; measured without it, the column
   arrives narrower than its own content. Only LAYOUT waits for `.on`. */
.tbl th{font:inherit;font-size:13px;font-weight:400;color:var(--ink-2);text-align:left}
.tbl td{font-size:12.5px;color:var(--ink-2)}
/* the first column names the record, exactly as `.gal-row .nm` does */
.tbl td:first-child{font-family:var(--font-title);font-weight:600;font-size:13.5px;
  letter-spacing:-0.01em;color:var(--ink)}
/* the two other cell registers the shelf already speaks: a kind, and a number */
.tbl .kind{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
.tbl .num{text-align:right;font-variant-numeric:tabular-nums}

/* then the shapes, once the columns have been measured */
.tbl.on table{display:block;width:100%;overflow:visible;
  border-collapse:separate;border-spacing:0;margin:0;font-size:13px}
/* the surface may inset its own table cells; a grid row supplies the inset */
.tbl.on th,.tbl.on td{padding:0}
.tbl.on thead,.tbl.on tbody{display:block}
.tbl.on tr{display:grid;grid-template-columns:var(--gcols);gap:14px;align-items:start}
/* the head is not a row (§ The shelf): no fill, no rule, no stadium. Not sticky
   either — a shelf runs past the fold and a table in prose does not, and a
   heading that detaches from four rows is furniture chasing the reader.
   `stretch`, because a head cell holds a BOUNDARY as well as a label, and an
   empty label — a table whose first column heads nothing, which is a shape the
   model writes often — collapses to no height at all. The boundary beside it
   then had none either: a 14px sliver of target that could not be found, on the
   one column that most needed widening. */
.tbl.on thead tr{padding:2px 20px 9px;align-items:stretch}
.tbl.on th{position:relative;display:flex;align-items:center;min-width:0;
  overflow-wrap:break-word}
.tbl.on th.num{justify-content:flex-end}
.tbl.on tbody tr{background:var(--tbl-row);border-radius:var(--r-md);
  padding:11px 20px;margin-bottom:4px}
.tbl.on tbody tr:last-child{margin-bottom:0}
.tbl.on td{min-width:0;overflow-wrap:break-word}
/* the head's own boundaries are the shelf's, unchanged — § .colgrip */

/* ---------- a block the reader can widen — .lane.wide / .lane-fit ----------
   TWO blocks in prose genuinely want more width than the measure, and they want it
   for different reasons — which is why the lane is a role (`.lane`) rather than a
   feature of either one.

   A TABLE too narrow wraps: everything is still on the page and the cost is
   height. The reader can rebalance that between columns but never add to the
   total, so the total is theirs too.

   A LISTING does not wrap — `.md pre` scrolls (canon#10) — so a line past the
   measure is a line that is not on the page at all, and the scroll gesture only
   ever shows one screenful of it. Widening is the only move that puts more of the
   program in front of the reader at once.

   Both step into the SAME lane an open figure takes (`--fig-lane`, above), spoken
   in the same construction, so a widened table, a widened listing and an open
   figure line up down the page rather than nearly doing so.

   It is a control and not a default because the lane costs something: a block on
   the measure reads as part of the paragraph around it, and one in the lane reads
   as a figure. Which of those a given block is, only the reader knows.

   Scoped under `.thread-scroll`, exactly as the figure lane is: the details region
   renders traces too, at widths no lane should be spent on, and a block in a modal
   is already at the modal's width. The scripts probe for that rather than test for
   it (lane.ts) — switch it on, measure, switch it back. */
/* The transition is on the WRAPPER and not on `.wide`, which is where it was and
   why the two directions did not match: a class carrying its own transition
   animates only on the way IN. Coming out, the rule stops applying and the
   transition goes with it, so the width it was supposed to animate has already
   been dropped by the time anything could ease — smooth to expand, a snap to
   shrink. A property that animates in both directions has to be declared where
   it is true in both states. */
.lane{transition:width var(--t-med) var(--ease),margin-left var(--t-med) var(--ease)}
.thread-scroll .lane.wide{width:max(100%,var(--fig-lane,min(880px,100cqw - 24px)));
  margin-left:calc((100% - max(100%,var(--fig-lane,min(880px,100cqw - 24px))))/2)}
.lane-fit{margin-top:8px;font-size:13px}
.lane-fit .ic{width:13px;height:13px;flex:none}

/* small screens: the row drops columns rather than wrapping into a block, and
   provenance goes first — the thread and the model are what the cards view still
   carries at this width. The name then takes a line of its own instead of a
   hundred pixels of ellipsis, and kind and time share the second: two lines
   inside ONE stadium is still a list item, where five columns fighting over
   375px is a table nobody can read. The column head goes with them — labels over
   one sortable column are a control, not a header — and its boundaries with it,
   since there is nothing left to hold apart.
   AFTER the rows section, not with the cards': these override rules written
   further up the file, and a media query wins nothing on specificity alone. */
@media (max-width:640px){
  .gal-row{grid-template-columns:minmax(0,1fr) auto;
    grid-template-areas:"nm nm" "kind when";row-gap:4px;padding:12px 20px;gap:10px}
  .gal-row .nm{grid-area:nm;white-space:normal}
  .gal-row .kind{grid-area:kind}
  .gal-row .when{grid-area:when}
  .gal-row .from,.gal-row .agent.mdl,.gal-rowh{display:none}
}

/* ---------- compute page (specimen § Compute, hand-designed) ----------
   A ledger of machine pills: one line each — laptop glyph, mono name, the
   --c1 dot on the machine the reader is sitting at — then the right cluster:
   models tally, tool marks (dimmed when they cannot serve), and unpair,
   ALWAYS in view. Connecting is not this page's job: one ghost line hands
   the reader to the composer's picker card, and only when the machine under
   the browser is not yet on the account. */
/* the details content compacted into a --panel card (components.md § Details
   hover card): --r-lg, tint alone floats it — canon#3, no shadow. */
.hovercard{background:var(--panel);border-radius:var(--r-lg);padding:14px 16px}
/* One column, one measure: title, pills, and the connect line share it — a
   ledger whose pieces disagree on width reads as three surfaces. */
.compute-in{max-width:760px;margin:0 auto}
.page-title{font-family:var(--font-title);font-weight:500;font-size:28px;
  text-align:center;margin:10px 0 28px}
.compute-rows{display:flex;flex-direction:column;gap:12px}
.machine-row{display:flex;align-items:center;gap:10px;padding:15px 22px;
  background:var(--panel);border-radius:var(--r-full)}
.machine-row>.ic{width:18px;height:18px;color:var(--ink);flex:none}
.machine-row .name{font-size:14px;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The one deliberate chromatic mark in the chrome: the machine the reader is
   ON. Presence-of-self is data (the brand anchor, canon#2's --c1), and the
   name beside it carries the meaning colour alone must not. */
.here-dot{width:6px;height:6px;border-radius:50%;background:var(--c1);flex:none}
/* every piece of the cluster centers on the row's one line — tallies, marks,
   the verb, and the mid-confirm swap alike; nothing sits on its baseline */
.machine-row .meta-cluster{margin-left:auto;display:flex;align-items:center;
  gap:14px;flex:none;white-space:nowrap}
.machine-row .meta-cluster .btn{padding-top:0;padding-bottom:0}
/* bare on the pill, hover included: a box inside a pill is a pill inside a
   pill. The verb answers in ink, same grammar as the journey's doors. */
.machine-row .btn.ghost:hover:not(:disabled),
.machine-row .btn.ghost:focus-visible{background:none;color:var(--ink)}
.machine-row .marks{display:inline-flex;align-items:center;gap:10px;color:var(--ink-2)}
.machine-row .marks>span{display:inline-flex;align-items:center}
.machine-row .marks .dim{color:var(--ink-3)}
.machine-row.off .name,.machine-row.off>.ic{color:var(--ink-3)}
.compute-connect{margin-top:16px;padding-left:22px}
/* a thread's presence in one dot: lit is live, dimmed is settled — the
   trace's own ink step, never a colour (canon#2) */
.st-live{width:8px;height:8px;border-radius:50%;background:var(--ink);flex:none}
.st-done{width:8px;height:8px;border-radius:50%;background:var(--ink-3);flex:none}
/* an in-place confirm replaces the control it answers (canon#9): only the
   line that changed fades in — a modal would move the question elsewhere */
.swap-in{animation:chunkin var(--t-fast) var(--ease)}
@media (prefers-reduced-motion:reduce){.swap-in{animation:none}}

/* ---------- in-place confirm — .sure (specimen § The in-place confirm) ----------
   The destructive verb answered where it was pressed: the control swaps
   (.swap-in, canon#9) for one line — the act restated as a narration question,
   then the verb and its refusal, always Keep, as ghost buttons. Never a modal
   (_design#13): the question stands where the finger already is and the rest
   of the screen stays live. While it stands it owns its cluster — sibling
   ACTIONS step back (none of them answer it) and return with the answer;
   passive facts (title, tallies, marks) stay where the line holds them and
   yield where it cannot. Focus lands on Keep, so a doubled Enter cannot
   destroy anything; Escape, a pointer-down outside, or focus leaving answers
   Keep. No timeout — a question that flees on a timer punishes reading it.
   The verb is never red: choosing an action is not a status (canon#2). */
.sure{display:inline-flex;align-items:center;gap:8px;min-width:0}
.sure .q{font-size:13px;color:var(--ink-2);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.sure .btn{flex:none}
/* a pane header's action cluster runs at a 2px gap — the question is a
   sentence, not an icon, and needs a word's distance from the title */
.pane-actions>.sure{margin-left:8px}
/* a stepped-back sibling must actually leave: their own display outranks the
   UA's [hidden] — the .menu trap */
.btn[hidden],.iconbtn[hidden],.note-meta .meta[hidden]{display:none}

/* ---------- assembled report frame — the sandboxed guest (arch#10): the
   document brings its own styles/runtime; the host gives it the container
   and a rounded window; height is driven by the guest's content ---------- */
/* --panel, matching the guest document's own page ground: the frame is what shows
   while the report loads, and a paper backing under a panel page flashed the app's
   colour and then changed. Opens OVER-tall rather than collapsed, and over-tall
   rather than nearly-tall: the modal centers its frame and clamps it to the
   viewport, so a placeholder that lands short of the clamp (85vh did) leaves the
   modal floating at 85% height until the guest's first measurement arrives — at
   which point the whole modal visibly grows and re-centers, a resize the reader
   sees on every open. 100vh always exceeds the clamp (the scrim has padding), so
   the modal opens already at its final size and the measurement lands below the
   fold, where nothing the reader is looking at moves. */
.rep-frame{display:block;width:100%;border:0;border-radius:var(--r-md);
  background:var(--panel);height:100vh;min-height:200px}

/* ---------- report guest content (.rep) — the embed-contract shapes the
   declare_report HTML uses: lead paragraph, kicker eyebrows, pull quotes.
   Styled here because the HOST owns the container (embedding#1). ---------- */
.rep{font-size:14px;line-height:1.6}
.rep p{margin:0 0 10px}
.rep>*:last-child{margin-bottom:0}
.rep .lead{font-size:15px;color:var(--ink)}
.rep .kick{display:block;margin:18px 0 4px}
.rep h3{font-family:var(--font-title);font-weight:600;font-size:15px;
  letter-spacing:-0.01em;margin:0 0 8px;text-wrap:wrap}
.rep .pq{font-family:var(--font-mono);font-size:13px;color:var(--ink-2);
  margin:12px 0;padding-left:14px}
.rep ul,.rep ol{margin:0 0 10px;padding-left:22px}
/* `:not(.shelf)` — a table the shelf pass has taken over (tables.ts) is no
   longer this surface's plain table, and every rule here would fight the shapes
   it was given. Excluded by name rather than out-specified: an armed table is a
   different component, not a heavier version of this one. */
.rep table:not(.shelf){border-collapse:collapse;font-size:13px;margin:0 0 10px;display:block;
  overflow-x:auto;max-width:100%}
.rep table:not(.shelf) th,.rep table:not(.shelf) td{padding:6px 12px;text-align:left}
.rep table:not(.shelf) thead th{color:var(--ink-2);font-weight:500}
.rep .katex-display{overflow-x:auto;overflow-y:hidden;max-width:100%;padding:2px 0}
.rep img,.rep svg{max-width:100%}

/* ---------- markdown content (.md) — agent prose in the chrome type system.
   Pure-text content rows render as markdown; code blocks scroll inside
   their own container (canon#10), fills not borders (canon#3) ---------- */
.md p{margin:0 0 10px}
.md>*:last-child{margin-bottom:0}
.md h1,.md h2{font-family:var(--font-title);font-weight:600;font-size:16px;
  letter-spacing:-0.01em;margin:16px 0 8px;text-wrap:wrap}
.md h3,.md h4,.md h5,.md h6{font-family:var(--font-title);font-weight:600;
  font-size:14.5px;margin:14px 0 6px;text-wrap:wrap}
.md>*:first-child{margin-top:0}
.md ul,.md ol{margin:0 0 10px;padding-left:22px}
.md li{margin:4px 0}
.md li>p{margin:0}
.md strong{font-weight:600}
.md a{color:var(--c5);text-decoration:underline}
.md a:hover{color:var(--c4)}
.md code{font-family:var(--font-mono);font-size:12.5px;background:var(--code);
  padding:1px 6px;border-radius:6px}
.md pre{background:var(--code);border-radius:var(--r-md);padding:12px 14px;
  overflow-x:auto;margin:0 0 10px}
.md pre code{background:none;padding:0;font-size:12.5px}
/* The wrapper the widen pass puts around a listing (code.ts). It is what the lane
   resizes, and it takes over the block's bottom margin — the `pre` gives its own
   up, so a listing that got a wrapper and one that did not (nothing to gain, or
   still streaming) sit the same distance off the paragraph beneath them. */
.md .code{margin:0 0 10px}
.md .code pre{margin:0}
/* Syntax spans (highlight.js, registered in md.tsx) collapsed onto FOUR named
   roles plus the comment step — the same five the report substrate lights a block
   with, so a fence reads the same in a thread as it does in a published document.
   highlight.js emits ~40 classes; a theme that honoured all of them would be the
   rainbow canon#2 exists to refuse. The rank is what carries the meaning — key
   strongest, fn next, str third — and everything with no rank of its own
   (identifiers, operators, punctuation, params) stays at `--ink`, which is most of
   the block. Colour marks the exceptions; it is not the ground.
   `--syntax-*` re-step per theme (tokens.local.css) because the steps that clear
   4.5:1 on the light `--code` fill are the deep end of the one blue and the ones
   that clear it on the dark fill are the pale end.
   Comments are the one class carrying no hue at all: `--ink-2`, one step under the
   code, with italic as the channel that separates them. A diff's deletions take the
   same quiet step — the `-` glyph is already the signal, and `--crit` is reserved
   for a failure the reader has to act on (canon#2). */
.md pre code .hljs-keyword,.md pre code .hljs-built_in,.md pre code .hljs-type,
.md pre code .hljs-literal,.md pre code .hljs-selector-tag,.md pre code .hljs-section,
.md pre code .hljs-name,.md pre code .hljs-meta,.md pre code .hljs-doctag,
.md pre code .hljs-symbol,.md pre code .hljs-bullet,
.md pre code .hljs-template-tag{color:var(--syntax-key)}
.md pre code .hljs-title,.md pre code .hljs-attr,.md pre code .hljs-attribute,
.md pre code .hljs-property,.md pre code .hljs-variable.language_,
.md pre code .hljs-selector-id,.md pre code .hljs-selector-class,
.md pre code .hljs-selector-attr,
.md pre code .hljs-selector-pseudo{color:var(--syntax-fn)}
.md pre code .hljs-string,.md pre code .hljs-regexp,.md pre code .hljs-char.escape_,
.md pre code .hljs-template-variable,.md pre code .hljs-link,.md pre code .hljs-quote,
.md pre code .hljs-formula,.md pre code .hljs-addition{color:var(--syntax-str)}
.md pre code .hljs-number{color:var(--syntax-num)}
.md pre code .hljs-comment{color:var(--ink-2);font-style:italic}
.md pre code .hljs-deletion{color:var(--ink-2)}
.md pre code .hljs-emphasis{font-style:italic}
.md pre code .hljs-strong{font-weight:600}
.md blockquote{margin:0 0 10px;padding-left:14px;color:var(--ink-2)}
.md hr{border:0;border-top:1px solid var(--hairline);margin:14px 0}
.md table:not(.shelf){border-collapse:collapse;font-size:13px;margin:0 0 10px;display:block;
  overflow-x:auto;max-width:100%}
/* wide content never widens the pane (canon#10): display math scrolls
   inside its own container, exactly like code blocks and tables */
.md{overflow-wrap:break-word}
.md .katex-display{overflow-x:auto;overflow-y:hidden;max-width:100%;
  padding:2px 0;margin:0 0 10px}
.md .katex-display::-webkit-scrollbar{height:4px}
.md .katex{max-width:100%}
.md table:not(.shelf) th,.md table:not(.shelf) td{padding:6px 12px;text-align:left}
.md table:not(.shelf) thead th{color:var(--ink-2);font-weight:500}
.md table:not(.shelf) tbody tr:nth-child(odd){background:var(--panel)}

/* ---------- responsive-icon utilities ---------- */
.only-mobile{display:none}
.only-desktop{display:inline-flex}

/* ---------- mobile overlay (≤768px) ----------
   platforms.md posture: one chrome, thin overlay. Panels become full PAGES
   (history top-left, details top-right in threads only), type steps up,
   insets tighten, the composer goes fluid. Keep the breakpoint in lockstep
   with Shell.tsx isSmallScreen(). */
@media (max-width:768px){
  /* icon swap: the sidebar toggle reads as HISTORY on mobile */
  .only-mobile{display:inline-flex}
  .only-desktop{display:none}

  /* 1 · type steps up; 16px inputs also stop iOS focus-zoom.
     Cards, history, and details read at the SAME 16px as the composer
     (the thread header title keeps its own scale) — one step, on --text,
     and every region that reads at it follows. */
  /* The MODAL steps with the app, and must: a report is read at `--text` like
     everything else, and the doc modal is portalled to the body — outside `.app`
     — so left off this line it kept the desktop step while the thread behind it
     grew, and a report read a size smaller than the trace it came from. The
     guest follows this value through the frame (artifacts-runtime/guest.ts). */
  .app,.skill-modal{font-size:15px;--text:16px}
  .label{font-size:14px}
  .meta,.tally{font-size:13px}
  .pill-input .input{font-size:16px}
  .acard h3{font-size:16px}
  .acard p{font-size:15px}
  .pane-h .wordmark{font-size:22px}
  .thought-body{font-size:16px}
  .agent{font-size:15px}
  .attr{font-size:15px}
  /* pane titles match: thread title and "Details" share one size */
  .thread-h .t,.details-h .t{font-size:16px}
  .dp-live,.dp-live .meta{font-size:16px}
  .details-in .tally{font-size:16px}
  .details-in .agent{font-size:16px}

  /* 2 · icons step up with the type */
  .ic{width:20px;height:20px}
  .ic.lucide{width:18px;height:18px;stroke-width:2}
  .iconbtn{width:34px;height:34px}

  /* 2 · panels are full pages, exclusive with the main region */
  .app{grid-template-columns:0 0 1fr 0 0}
  .app.no-side:not(.no-details){grid-template-columns:0 0 0 0 1fr}
  .app:not(.no-side){grid-template-columns:1fr 0 0 0 0}
  /* resizers must STAY grid items (display:none would shift auto-placement
     and strand .main in a 0px track) — just inert and lineless */
  .resizer{pointer-events:none}
  .resizer::after{display:none}
  /* visibility (not display) keeps keep-alive scroll positions alive.
     The live thread slot sets visibility:visible itself, which would ESCAPE
     the hidden main — pin the slots hidden with higher specificity */
  .app:not(.no-side) .main,
  .app.no-side:not(.no-details) .main{visibility:hidden;padding:0}
  .app:not(.no-side) .main .thread-slot,
  .app.no-side:not(.no-details) .main .thread-slot{visibility:hidden}
  .side-in,.details-in{width:100%;padding:0 14px 20px}

  /* 3 · sent prompts span nearly the whole column */
  .msg-user{max-width:92%}

  /* 4 · tighter insets everywhere — but rows keep clear air between them */
  .main{padding:0 12px}
  .thread-slot{inset:0 12px}
  .page{padding:0 12px}
  .details-in .label{font-size:13px}
  .composer{bottom:10px}
  .trace{gap:14px}
  .exchange{gap:14px}
  .home{margin-top:-40px}
  .asearch{margin:0 auto 24px}
  .artifacts{padding-bottom:24px}
  .thread-scroll{padding-bottom:76px}

  /* 5 · the input pill is fluid and tighter */
  .pill-input{padding:12px 14px;gap:8px}
  .jump-pill{bottom:calc(100% + 10px)}

  /* the back chevron hugs the thread title — title starts further left */
  .thread-h{gap:4px}
  .thread-h .tgl-side{margin-left:-8px}

  /* the docked line, at the step: the pill keeps the 16px field (anything
     smaller makes the phone zoom on focus) but loses the padding it does not
     need, so it still rides inside a 60px header. The name gives way first —
     the big title it repeats is one flick of the wheel up. */
  .gal-h .t{max-width:30%}
  .dock-search{flex:1 1 140px}
  .dock-search .pill-input{padding:8px 14px}
}

/* ---- skills: catalog and editor, two panes ---- */
/* the write-one card: the same card, its title carrying the plus inline */
.acard-new h3{display:flex;align-items:center;gap:7px}

/* ---------- skill modal ----------
   A tint, not a black wash: the scrim separates layers the way every other surface
   here separates them (canon#3), and a heavy overlay would be the one place the
   product goes dark to say "stop". Backdrop blur is deliberate — it says the page is
   still there and still yours, only parked. */
.skill-modal{position:fixed;inset:0;z-index:40;display:flex;align-items:center;
  justify-content:center;padding:32px;
  background:var(--scrim);
  backdrop-filter:blur(6px);
  animation:scrimin var(--t-fast) var(--ease)}
@keyframes scrimin{from{opacity:0}to{opacity:1}}
/* Big radius, no shadow — the panel is separated by tint and elevation of layer,
   never by a drop shadow (canon#3). Scrolls inside itself so the doctrine can be
   long without the scrim scrolling with it. */
/* The frame carries the modal's measure and DOES NOT SCROLL. Everything pinned to
   the modal rather than to the document — the corner control, the scroll rail —
   hangs off this, which is the only reason it exists: both used to sit inside the
   scroller, where "pinned" lasts exactly until the reader scrolls. */
.modal-frame{position:relative;display:flex;min-height:0;max-height:100%;
  width:min(var(--modal-w),100%)}
/* A document's measure, not a form's — and because the frame is the guest
   iframe's viewport, this number is what every `92vw` inside the report resolves
   against. It is the figure lane's budget; the prose column inside it does not
   move. */
.modal-frame.doc{width:min(var(--modal-w-report),100%)}
/* THE FRAME NEVER MOVES. The rail opens INSIDE it — a region at the side of the
   page, the way every region in this product opens, and the way the specimen
   shows it. It used to take new width instead, on the reasoning that a squeezed
   column re-scales its figures; but a modal that grows when you open a panel
   inside it is a window resizing itself under the reader, and at any width where
   `100%` was already the answer it could not grow anyway — so the rail took its
   width from the page in the narrow case and from the screen in the wide one,
   which is two behaviours for one control. One behaviour: the frame is the
   frame, and the rail is inside it. */
.doc-anno{width:100%}
.doc-anno>.skill-modal-panel{min-width:0}
/* An artifact brings its own paper: no panel behind it, no inset around it. The
   modal is the frame, and a frame inside a frame is the box-in-a-box again.
   `display:block` matters: as a flex column the report's iframe was a flex ITEM and
   shrank to the panel's height, so the guest document scrolled inside itself and
   drew its OWN scrollbar — the browser's bar, in the guest's colours, over a page
   that already has a frame. As a block the frame keeps the height its content asked
   for and the panel is what scrolls. */
.skill-modal-panel.bare{background:none;padding:0;display:block;overflow-x:hidden}
/* The controls a bare panel still owes the reader — a way out that does not
   depend on knowing about Escape, and the toggle for the rail beside it — are
   the report header's (`.doc-h`, below): they sit in a row over the page rather
   than floating one to a corner, because by the time there were two of them a
   corner was a pile. */
/* scoped past `.skill-modal-head h2`, which is class+element and would otherwise
   out-specify a bare class and keep every title in the machine's voice */
.skill-modal-head h2.artifact-title{font-family:var(--font-title);font-size:17px;font-weight:600;
  letter-spacing:-0.01em;margin:0;line-height:1.4}
.skill-modal-panel{background:var(--panel);border-radius:var(--r-lg);
  padding:24px 26px;flex:1;min-width:0;overflow-y:auto;
  scrollbar-width:none;
  display:flex;flex-direction:column;gap:14px;
  animation:panelin var(--t-med) var(--ease)}
.skill-modal-panel::-webkit-scrollbar{display:none}

/* ---------- the scroll rail (ScrollRail) ----------
   The native bar's replacement: a short mark in the GUTTER beside the panel, held
   at the vertical middle, never a full-height bar down the edge of the document.
   It sits in the scrim's own inset, so it costs the page no width and never covers
   a line of what it measures. Sized in CSS and read by the component — the rail's
   height is a layout decision, not a constant in a script. */
.srail{position:absolute;right:-18px;top:50%;transform:translateY(-50%);
  height:40%;min-height:64px;max-height:132px;width:14px;
  cursor:pointer;touch-action:none;
  /* off is transparent, not `display:none`: a rail that is not laid out has no
     height, and its height is the thing the component measures to decide whether
     to show it at all. */
  opacity:0;pointer-events:none;
  transition:opacity var(--t-fast) var(--ease)}
.srail.on{opacity:1;pointer-events:auto}
/* the track is a hairline, the thumb is the only thing with weight (canon#2: this
   is chrome, so it is ink and paper — nothing here is coloured) */
.srail::before{content:"";position:absolute;left:50%;top:0;bottom:0;
  width:2px;margin-left:-1px;border-radius:var(--r-full);background:var(--hairline)}
.srail-thumb{position:absolute;left:50%;top:0;width:4px;margin-left:-2px;
  border-radius:var(--r-full);background:var(--ink-2);
  transition:background var(--t-fast) var(--ease),width var(--t-fast) var(--ease),
    margin-left var(--t-fast) var(--ease)}
.srail:hover .srail-thumb,.srail.drag .srail-thumb{background:var(--ink);
  width:6px;margin-left:-3px}
@media (prefers-reduced-motion:reduce){
  .srail,.srail-thumb{transition:none}
}
@keyframes panelin{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .skill-modal,.skill-modal-panel{animation:none}
}

/* head: what you opened, and what you can do to it. The name carries the machine's
   voice because it IS one — `_report`, not "Report". */
.skill-modal-head{display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px}
.skill-ident{display:flex;flex-direction:column;gap:6px;min-width:0}
/* ONE title, whether or not it is editable. Declared together so they cannot drift:
   a heading that gains a box, an inset and a hover tint the moment the record is
   yours is a different component wearing the same words — the two views stop being
   one object in two states, which is the whole point of the layout. The caret is
   the affordance; nothing else needs to move. */
.skill-modal-head h2,.skill-title-input{font-family:var(--font-emph);font-size:17px;
  font-weight:500;letter-spacing:-0.01em;line-height:1.4;margin:0;padding:0}
.skill-title-input{background:none;border:0;color:var(--ink);width:100%}
.skill-title-input:focus{outline:none}
.skill-title-input::placeholder{color:var(--ink-3)}
.skill-modal-actions{display:flex;align-items:center;gap:6px;flex:none}
/* An icon button's hover backing is `--panel`, which is what this panel already is —
   so on the modal the × had no visible hover at all. One step down, the same
   relationship to its ground that the backing has everywhere else. */
.skill-modal-panel .iconbtn:hover{background:var(--panel-2);color:var(--ink)}

/* ---------- tags ----------
   Canon#2 keeps the interface grayscale and reserves chromatic colour for data.
   A tag is the case that rule leaves open: it states a fact ABOUT the record, and
   it is never colour alone — the word is always there, so the hue reinforces a
   label rather than carrying meaning on its own.
   Hues come from the named roles only: the brand anchor for what the platform owns,
   `--good` for what is yours to change, `--warn` for a constraint worth noticing
   before you try. Tint at low alpha for the ground, the hue itself for the word, so
   a tag stays a quiet surface like every other. */
.tags{display:inline-flex;flex-wrap:wrap;gap:6px}
.tag{font-family:var(--font-emph);font-size:10.5px;font-weight:500;letter-spacing:.08em;
  text-transform:uppercase;padding:3px 8px;border-radius:var(--r-full);
  white-space:nowrap;line-height:1.4;
  color:var(--ink-2);background:var(--panel-2)}
/* The GROUND is always `--panel-2`, a named surface role — the hue is carried by the
   word alone. Tinting the background instead meant mixing new colours out of the
   palette, and canon#1 is that every colour on screen is one of the named roles,
   not a value derived from one.
   ONE hue, in distant steps (canon#2). `--good`/`--warn` are the status hues and a
   skill's ownership is not a status — nothing here is going wrong. `--c5` re-steps
   per theme, so the quiet pair stays legible on `--panel-2` in both; `--c1` is the
   brand anchor and does not, which is exactly why the platform's own doctrine wears
   it. */
.tag-platform{color:var(--c1)}
.tag-locked{color:var(--c5)}
.tag-mine{color:var(--c4)}
/* a skill that does not exist yet has nothing to report — neutral on purpose */
.tag-new{color:var(--ink-2)}

/* Read and edit are ONE layout in two states: same label, same rhythm, same
   measure. A field is a label above its content whether that content is an input
   or the prose it holds. */
.skill-field{display:flex;flex-direction:column;gap:6px}
.field-label,.skill-modal-panel label>span{font-family:var(--font-emph);font-size:11px;
  letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2)}
.skill-trigger{color:var(--ink-2);font-size:14px;line-height:1.6;margin:0}
.skill-trigger.clamp{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;
  overflow:hidden}
/* the fold control sits under its own field, aligned with it */
.skill-field .seg-toggle{align-self:flex-start}
/* No panel behind the doctrine. The modal IS the surface it sits on; boxing it
   inside its own panel makes the content read as a quotation of itself, and costs
   an inset on every side of the longest text here. */
.skill-doctrine{font-size:13.5px;line-height:1.65}
.skill-doctrine>:first-child{margin-top:0}
.skill-doctrine>:last-child{margin-bottom:0}
.skill-doctrine h1,.skill-doctrine h2,.skill-doctrine h3{font-family:var(--font-title);
  font-size:15px;font-weight:600;margin:18px 0 6px}
.skill-doctrine p,.skill-doctrine ul,.skill-doctrine ol{margin:0 0 10px}
.skill-doctrine code{font-family:var(--font-emph);font-size:12.5px}
/* a doctrine table is a real table, not the pipes it was written with */
.skill-doctrine table{border-collapse:collapse;width:100%;font-size:12.5px;
  margin:0 0 12px;display:block;overflow-x:auto}
.skill-doctrine th{text-align:left;font-weight:600;color:var(--ink-2);
  padding:6px 12px 6px 0;white-space:nowrap}
.skill-doctrine td{padding:6px 12px 6px 0;vertical-align:top}

/* the open one, marked the way a selected thread card is */
.acard.on{background:var(--panel-2)}
/* a skill's name is machine vocabulary — `_report`, `lecture-notes` — so it keeps
   DM Mono here, where an artifact's title is prose and does not */
.acard h3.agent{font-family:var(--font-emph);font-size:13.5px}
.skill-row{display:flex;align-items:center;gap:8px;border-radius:var(--r-sm);
  padding:2px 6px 2px 0}
.skill-row.on{background:var(--panel-2)}
.skill-open{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;
  background:none;border:0;text-align:left;cursor:pointer;padding:10px 12px;
  border-radius:var(--r-sm);color:inherit;font:inherit}
.skill-open:hover{background:var(--panel)}
.skill-desc{color:var(--ink-2);font-size:12.5px;line-height:1.45;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* The editor's fields are the read view with a caret in them.
   No inset and no padding: a field padded away from its own label indents the text
   relative to everything else in the panel, so the same sentence sat in a different
   place depending on whether the skill was yours. The title is excluded by name —
   it is a heading, and this rule would out-specify its own (class+element beats
   class).
   No resize grip either: the two marks in the corner are a control the reader did
   not ask for, on a panel that already scrolls. */
.skill-modal-panel label{display:flex;flex-direction:column;gap:6px}
.skill-modal-panel input:not(.skill-title-input),.skill-modal-panel textarea{
  background:none;border:0;padding:0;color:var(--ink);font:inherit;
  font-size:14px;line-height:1.6;resize:none;width:100%;box-sizing:border-box}
.skill-modal-panel input:not(.skill-title-input):focus,.skill-modal-panel textarea:focus{outline:none}
.skill-desc-input{min-height:76px}
.skill-body-input{min-height:340px;font-family:var(--font-emph);font-size:12.5px;
  line-height:1.6}
.skills-actions{display:flex;gap:8px}
.skill-refusal{color:var(--ink);background:var(--panel-2);border-radius:var(--r-sm);
  padding:10px 14px;font-size:13.5px;margin:0}

/* ---------- annotation & follow-up — .hl, .anno-mark, .note-card,
   .anno-float, .fu-pane, .fu-list, .anno-row, .doc-rail ----------
   The reader's own marks on the record: select text anywhere in a thread or
   an artifact, then leave a note on it or open a follow-up thread from it.
   The selection bar is the popover framework at a selection anchor
   (.menu.free.bar — one line, two actions) — never a new control language. */

/* the persistent highlight — the reader's ink on the page. A chromatic use
   (a step of the one blue: --c4, the light step — the "yours" hue, which is
   what an annotation is), the fourth enumerated one beside .tag/.act/
   .skill-label; the hue never stands alone (canon#2): every highlight ends in
   its marker glyph, which names the kind and opens it.
   Low strength keeps ink at full contrast on either theme. The shape is a
   STADIUM (--r-full, the pill idiom), cloned per line across breaks — and
   the side padding is what buys the curve its room: with less, the corner arc
   cuts into the first and last glyphs. Caps used to appear only at a mark's
   two ends, where a space or a comma sat beside them; they now appear at the
   start and end of every line it wraps to, hard against whatever letter the
   line begins with, so that room has to be right at every size.
   The padding is in em, and that is the point. A stadium's radius is half its
   height, its height comes from the type, and the type steps up on narrow
   viewports (`.trace` reads at 16px under 768px) — so a padding in px is room
   enough on one screen and a curve through the first letter on another.
   AND THERE IS NO NEGATIVE MARGIN, which there was for a long time: it pulled
   the pill back over its own padding so surrounding text barely moved. That
   works until the pill WRAPS. A fragment at the start of a line cannot bleed
   left of its line — WebKit clamps it there (Chrome does not, which is why
   this survived so long) — so the margin lands on the first fragment only and
   every wrapped line is left with padding-minus-margin: a 12px cap radius
   squeezed into 2px, and a flat edge where a cap belongs.
   The cost is that a mark now displaces its neighbours by its padding instead
   of by 2px. That is the honest price of a shape that is the same on every
   line, and it is why the padding is as small as it is.
   The VERTICAL padding is bounded by the line box and stated in em so it
   tracks the type: padding on an inline box does not grow the line, so
   anything the pill sticks out by is outside every line-height-tall band
   drawn over it — including the browser's own selection. Inside a passage
   that overflow is hidden by the neighbouring line's band; on the FIRST line
   of a selection there is no band above, and the pill's cap stands proud of
   it, reading as a highlight the selection was cut off at the top of. */
.hl{background:color-mix(in srgb,var(--hl,var(--c4)) 30%,transparent);
  border-radius:var(--r-full);padding:0.085em 0.4em;color:inherit;
  -webkit-box-decoration-break:clone;box-decoration-break:clone}
/* the FOCUSED highlight: while its follow-up pane is open, the mark steps
   to the brand anchor (--c1 — the vivid, theme-invariant step). Opening a
   follow-up scrolls its source text into view and lights it; closing the
   pane hands the color back. Only ever one at a time.
   And while one IS open the page steps back around it (.anno-focus on the
   scope): every other mark drops to a third of its strength, so the lit one
   is the only bright thing on the page. Two follow-ups on the same passage
   are otherwise a step apart in one hue — close by construction, since the
   hierarchy exists to separate NEIGHBORS, not to name them — and "which of
   these am I reading about" is the one question the reader has. Contrast,
   not hue: the answer stays legible under either theme (canon#2).
   Pointing at a mark says the same thing while the pointer is on it (.hot,
   .anno-hot): the reader asking "which words are this one" gets the answer
   from the page rather than by clicking to find out. An open follow-up
   outranks the pointer — the two never light at once. */
.hl.live,.hl.hot{background:color-mix(in srgb,var(--c1) 32%,transparent)}
.anno-focus .hl:not(.live),
.anno-hot .hl:not(.hot){
  background:color-mix(in srgb,var(--hl,var(--c4)) 10%,transparent)}

/* OVERLAPS — the color hierarchy and the chain geometry. Every annotation
   takes its color from CREATION ORDER — --c4, then --c3, then --c6, cycling.
   DISTANT steps, not neighbors: adjacent marks must separate by lightness
   (canon#2's series rule) or the reader cannot tell where one ends. Where
   ranges overlap THE LAST ONE ADDED WINS the paint. Two shapes:
   · SAME RANGE, SAME KIND — one highlight (the last one's color), ONE marker
     wearing the shared kind's glyph and a count badge (.anno-mark .n).
     Clicking never navigates blind: it opens the ordinary menu (.anno-pick)
     listing each mark with its color dot beside its kind and label, and the
     reader chooses — a chosen note is then read INSIDE the same card
     (.anno-nav below). Mixed kinds keep one marker PER KIND (each badged
     past one), stacked in creation order.
   · PARTIAL — the run splits at every boundary, and THE EDGE RULE IS THE
     ANNOTATION'S OWN: an edge is ROUNDED wherever an annotation STARTS or
     ENDS there — a later mark opens with its own cap mid-run — and STRAIGHT
     wherever the paint merely continues or hands over (.sq-l/.sq-r strip
     the cap and its padding so segments butt). A finished annotation leaves
     its marker at its stop before the survivor resumes, square on the
     resume edge. */
.hl.h2{--hl:var(--c3)}
.hl.h3{--hl:var(--c6)}
.hl.sq-l{border-top-left-radius:0;border-bottom-left-radius:0;padding-left:0}
.hl.sq-r{border-top-right-radius:0;border-bottom-right-radius:0;padding-right:0}
/* the count badge: a state-mark construction (_design#5 — ink disk, paper
   numeral), riding the marker's top-right corner */
.anno-mark{position:relative}
.anno-mark .n{position:absolute;top:-6px;right:-7px;min-width:12px;height:12px;
  padding:0 3px;border-radius:var(--r-full);background:var(--ink);
  color:var(--paper);font-size:9px;font-weight:600;line-height:12px;
  text-align:center;font-variant-numeric:tabular-nums}
/* choosing among stacked marks: the ordinary menu; the dot says WHICH
   highlight (the same mix the mark wears, stronger so it reads at 9px), the
   glyph and label say which record — color never stands alone (canon#2) */
.menu.anno-pick{min-width:240px}
.menu.anno-pick .dot{width:9px;height:9px;border-radius:50%;flex:none;
  background:color-mix(in srgb,var(--hl,var(--c4)) 45%,transparent)}
/* the hierarchy is declared once, on .hl; a dot names the same step so the
   swatch and the highlight it stands for cannot drift apart */
.menu.anno-pick .dot.h2{--hl:var(--c3)}
.menu.anno-pick .dot.h3{--hl:var(--c6)}
.menu.anno-pick .q{min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
/* …and stacked NOTES are read INSIDE it. Choosing a note used to raise a
   second floating card over the menu — two layers answering one question,
   each with its own dismiss. Now the card the reader opened is the only
   card there is: choosing a note swaps its content to that note in place
   (navigation is a swap, _design#11 — the move the rail makes for a
   follow-up), the back arrow returns to the list, and the dot rides into
   the meta line so the record stays named (canon#2). Escape closes the
   card from either view — back is the in-card return, not a second Escape
   stop. Only notes ever read in here: a follow-up navigates to its pane,
   and a mixed stack keeps one marker per kind. The width is fixed so
   nothing reshapes when the content swaps — and the rows truncating where
   the note view does not is the point: a full reading without a second
   layer. */
.menu.anno-nav{width:280px}
.anno-nav .anno-view{display:flex;flex-direction:column;gap:2px;min-width:0}
/* its own display outranks the UA's [hidden] — the .menu trap again */
.anno-nav .anno-view[hidden]{display:none}
.anno-nav .nv-head{display:flex;align-items:center;gap:2px}
.anno-nav .nv-head .dot{margin-left:6px}
.anno-nav .nv-head .meta{margin:0 auto 0 8px}
/* the menu's row spec is for rows; the view's quiet actions are icon
   buttons and keep their own geometry (rounded-square backing, _design#5) */
.anno-nav .iconbtn{width:28px;height:28px;padding:0;
  border-radius:var(--r-xs);justify-content:center;gap:0}
/* a ghost pair inside an on-panel card steps its hover back down, like
   every other backing one step from its ground */
.menu.on-panel .btn.ghost:hover:not(:disabled){background:var(--panel)}
.anno-nav .nv-text{font-size:13px;line-height:1.5;color:var(--ink);
  margin:0;padding:2px 12px 8px}

/* the selection bar — the menu framework laid in ONE LINE: .bar on a .menu
   puts its two actions side by side (a selection sits inside a line of
   text; a vertical card under the cursor covers the very words being
   annotated). Same card, same rows, same physics — direction is the only
   new fact. */
.menu.bar{flex-direction:row;align-items:center;min-width:0;max-width:none}

/* the marker — a real button set right after its highlight, in the text,
   at the whisper size (_design#5): a note keeps StickyNote; a follow-up
   carries the product's own logo (the composer pill's lead glyph — a
   follow-up IS a grothendieck thread). Both stroke, 13px, --ink-2 at rest,
   ink on hover/focus. A range can carry BOTH: the markers stack after it
   in creation order, each opening its own thing. */
.anno-mark{display:inline-flex;align-items:center;vertical-align:-2px;
  margin-left:4px;padding:0;border:0;background:none;cursor:pointer;
  color:var(--ink-2);transition:color var(--t-fast) var(--ease)}
.anno-mark:hover,.anno-mark:focus-visible{color:var(--ink);outline:none}
.anno-mark .ic{width:13px;height:13px;stroke-width:2.77}

/* the product logo (packages/chrome/src/logo.tsx — vendored in the
   specimen as <symbol id="gmark">): a FILL glyph on a 100-grid, taking its
   color from the text via currentColor — never the stroke .ic treatment.
   14px beside 14px icons. It says "grothendieck", so it appears where the
   product speaks as itself — the sidebar's brand, the skills chip — and not
   as the glyph for any one KIND of thing: a follow-up is named by the reply
   mark wherever it is named, marker and sidebar row and details row alike. */
.g-logo{width:14px;height:14px;flex:none}

/* the note card — the hovercard idiom at a marker (components.md § Details
   hover card): --panel, --r-lg, no shadow (canon#3). Appears on hover/focus
   of the marker with a --t-fast fade; leaves on pointer-out. The meta line
   names the kind and age (canon#12); the quiet actions sit at its end —
   edit reopens the input pre-filled, delete removes note AND highlight. */
.note-card{position:absolute;z-index:30;width:280px;
  background:var(--panel);border-radius:var(--r-lg);padding:12px 14px;
  animation:menu-in var(--t-fast) var(--ease)}
.note-card p{font-size:13px;line-height:1.5;color:var(--ink);margin:0}
/* …and when the room below the glyph ran out it opens above instead, so it
   arrives from up there — `.up` is the popover framework's own mark for the
   flipped side, written by the same placer (Popover § the fit decides). */
.note-card.up,.anno-float.up{animation-name:menu-in-up}
.note-card .note-meta{display:flex;align-items:center;gap:2px;margin-top:8px}
.note-card .note-meta .meta{margin-right:auto}
.note-card .iconbtn:hover,.note-card .iconbtn:focus-visible{background:var(--panel-2)}
@media (prefers-reduced-motion:reduce){.note-card{animation:none}}

/* FLOATING PIECES OVER A PANEL GROUND — the .on-panel rule generalized: a
   floating surface keeps ONE STEP from whatever it floats over, and the
   OPENER stamps .on-panel when the selection or marker it anchors to sits
   on --panel (an open report) rather than --paper (a thread). Fill steps to
   --panel-2; anything that stepped up inside the card steps back down. */
.note-card.on-panel{background:var(--panel-2)}
.note-card.on-panel .iconbtn:hover,
.note-card.on-panel .iconbtn:focus-visible{background:var(--panel)}
.anno-float.on-panel .pill-input{background:var(--panel-2)}
.anno-float.on-panel .pill-input:focus-within{background:var(--panel-2)}
.anno-float.on-panel .menu{background:var(--panel-2)}

/* the annotation input — THE composer pill (components.md § Inputs: there
   is one input component), floating at the selection inside .anno-float:
   Enter commits, Escape cancels, the leading glyph names the kind. A note
   is not a run, so its pill carries no model chip. A follow-up IS a run,
   so its pill carries the standard model chip under the standard rules —
   visible while the draft is empty, gone the moment typing starts, opening
   the ordinary .menu.up.right; the choice rides the follow-up thread (its
   narration and its card name the model) and persists per user, exactly as
   on the thread composer. */
.anno-float{position:absolute;z-index:30;width:440px;max-width:92vw;
  animation:menu-in var(--t-fast) var(--ease)}
/* their own display outranks the UA's [hidden] — the .menu trap; without
   this a dismissed float or pane is a visible one */
.anno-float[hidden],.fu-pane[hidden]{display:none}
@media (prefers-reduced-motion:reduce){.anno-float{animation:none}}

/* …AND THE PIECES FLOAT OVER WHATEVER RAISED THEM. Each one is opened over the
   thing being read, and that thing can itself be a raised layer: a report is
   read in the doc modal (z 40), so a bar at the menu layer and a pill at the
   card layer both open BEHIND the very page they are annotating. Stated once,
   here, after all three have taken their resting layer. */
.menu.free.bar,.menu.anno-pick,.anno-float,.note-card{z-index:50}

/* the follow-up pane — a linked thread IN THE DETAILS REGION: choosing
   Follow up swaps the details content for a compact thread view (navigation
   is a swap, _design#11): header (back to Details · title · open full),
   then an ordinary trace at panel density opening with THE READER'S OWN
   PROMPT as a sent message, and the composer. There is NO quote inset:
   opening the pane focuses the source passage and lights it --c1
   (.hl.live) — the document itself shows what was quoted, and restating
   it in the pane said the same thing twice. The thread is
   REAL: it nests under its parent's card in the sidebar (.fu-list) and
   opens full-width like any other. */
/* the same three parts as the thread pane, in the same relationship
   (_design#16): ONE scroller holding a sticky dissolving header, and a
   composer FLOATING over it with the content masked out as it approaches.
   The pane used to be three stacked boxes with the trace scrolling between
   them — which clips at hard edges instead of dissolving, and gives the
   header and the composer a visible container the thread pane does not have.
   Pill geometry: 52px tall at the pane's foot, so the fade starts 24px above
   its top edge and is gone 12px inside it, and the scroller's end padding
   lets the last row rise clear. */
.fu-pane{position:relative;display:flex;flex-direction:column;min-height:0;height:100%}
.fu-scroll{flex:1;min-height:0;overflow-y:auto;scrollbar-width:none;
  padding-bottom:84px;
  mask-image:linear-gradient(to bottom,#000 calc(100% - 76px),transparent calc(100% - 40px));
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 76px),transparent calc(100% - 40px))}
.fu-scroll::-webkit-scrollbar{display:none}
/* (the glyph gutter and its mirrored padding are gone from `.trace-row`
   itself — the base rule — so the pane needs no override of its own) */
/* THE SCROLLER TAKES THE PANE'S INSET OVER, where the pane hosts the header
   INSIDE it (the details panel; the modal rail says its header in `.doc-h`).
   A scroll container clips at its own box, so the back arrow's -7px stadium
   bleed — fine everywhere else — came up against the scroller's edge and lost
   the left of its hover halo. Trading the host's 12px padding for the
   scroller's own gives the bleed room without moving any content: same text
   edge, one box wider to clip against. */
.details-in .fu-scroll{margin-inline:-12px;padding-inline:12px}
/* the pane's composer is THE PillInput — one input component for every
   thread, follow-ups included (components.md § Inputs): the pane passes a
   placeholder and handlers, never a restyle */
.fu-composer{position:absolute;left:0;right:0;bottom:0}

/* follow-ups in the sidebar: nested under their parent's card, shown ONLY
   while that thread is the active one — every other card keeps its single
   line. Row anatomy, in order: the reply glyph (says "follow-up"), the
   ADAPTER's mark (what runs it — the same mark the parent card wears in
   its meta line), then the question (one line, ellipsis). The indent is
   shallow — the reply glyph sits close to the parent's edge. THE SELECTED
   FOLLOW-UP IS WEIGHT, NOT TINT: 600 + ink, no fill (the row is inside the
   parent's territory; a second tinted stadium there reads as a second
   selection). Hover keeps the thread-card's half-step. The first three
   always show; the rest fold behind a See more row (the embed's motion) —
   a sidebar list never scrolls on its own. */
/* 3px: the parent is always a has-meta card (an active thread shows its
   adapter), whose 15px pad over a -12px bleed puts its text at column+3 —
   the list pads to the same edge */
.fu-list{display:flex;flex-direction:column;gap:2px;margin:2px 0 6px;
  padding-left:3px}
.fu-item[hidden]{display:none} /* its display outranks the UA's [hidden] */
/* the reply glyph aligns with the PARENT CARD'S TEXT START: the row's 8px
   pad is cancelled by a matching negative margin — the thread-card idiom
   (its 12px pad bleeds -12px), so both hang from the same edge */
.fu-item{display:flex;align-items:center;gap:7px;min-width:0;
  border:0;background:none;cursor:pointer;font:inherit;font-size:var(--text);
  color:var(--ink-2);text-align:left;padding:4px 8px;margin:0 -8px;
  border-radius:var(--r-full);
  transition:background var(--t-fast) var(--ease),color var(--t-fast) var(--ease)}
/* a WORKING follow-up carries the running dots at its end, exactly as its
   parent card does; they leave when the run settles and the model's title
   lands (until then the row wears the question it was asked) */
.fu-item .g-run{margin-left:auto}
/* the two leading glyphs take the ROW'S OWN colour at rest — inherited, not
   restated, so a row that is quiet, selected or working carries them at its
   own weight and they can never drift a step from the words they introduce.
   A glyph paler than its label reads as a disabled row rather than a quiet
   one. Hover is the one place they part: the label steps to --ink and the
   glyphs hold at --ink-2, because what the pointer is asking about is the
   NAME. */
.fu-item>.ic{width:12px;height:12px;flex:none;color:inherit}
.fu-item .h-mark{width:11px;height:11px;flex:none;color:inherit}
.fu-item>span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fu-item:hover,.fu-item:focus-visible{background:var(--accent-soft);
  color:var(--ink);outline:none}
.fu-item:hover>.ic,.fu-item:hover .h-mark{color:var(--ink-2)}
.fu-item.on{color:var(--ink);font-weight:600}
/* The OPEN row's marks hold full ink, hover included — now that this list mixes
   two kinds, the mark is what a reader scans to find which one they are in, and
   the hover rule above would have dimmed the very row they are standing on. */
.fu-item.on>.ic,.fu-item.on .h-mark,
.fu-item.on:hover>.ic,.fu-item.on:hover .h-mark{color:var(--ink)}
/* A follow-up standing in the main pane wears a heading in two parts: what it
   reads, quiet and small above, and what it asks below beside the reply glyph.
   Both lines ellipsize on their own — a long parent title must not push the
   question the reader is actually in out of view. The pair still fits the
   header's fixed height, so nothing about the pane's geometry moves. */
.thread-h .t.fu-title{display:flex;flex-direction:column;gap:1px;line-height:1.3;
  min-width:0}
.thread-h .fu-parent{font-family:var(--font-body);font-weight:400;font-size:11.5px;
  letter-spacing:-0.01em;color:var(--ink-2);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.thread-h .fu-self{display:flex;align-items:center;gap:6px;min-width:0;
  white-space:nowrap;overflow:hidden}
.thread-h .fu-self>.ic{flex:none;color:var(--ink-2)}

/* the disclosures under a list: no glyphs, and side by side — they answer
   different questions (how much of this list, versus whether this list) and a
   reader reaching for one must not have to read the other first. They are
   quiet by the same step every other row here is (--ink-2 → --ink): junior to
   the follow-ups above them by ORDER and by carrying no glyph, which is enough.
   A third step down reads as unavailable, and these are the two controls the
   reader came to this list to find. */
.fu-more-row{display:flex;align-items:center;gap:12px}
/* the disclosures read at the META size, not the list's: they are facts about
   the list rather than entries in it, and a row the same size as the threads
   above it reads as one more thread */
.fu-item.fu-more{font-size:12.5px}
.fu-item.fu-more:hover{background:none}
/* what a collapsed list leaves on the card: the count, on the meta line with
   the other facts about the thread, and reachable — a number you cannot open
   is a number about nothing */
/* ONE control carrying up to two tallies — branches and follow-ups keep their
   own marks and numbers, because they are different things, but they open the
   same list and so they are one button: they light together because they act
   together. The outer gap separates the PAIRS; the inner one holds each mark to
   its own number, so `⑂2  ↳3` never reads as one run of four glyphs. */
.tc-fu-count{display:inline-flex;align-items:center;gap:8px;border:0;padding:0;
  background:none;cursor:pointer;font:inherit;font-size:12px;color:var(--ink-2);
  font-variant-numeric:tabular-nums;
  transition:color var(--t-fast) var(--ease)}
.tc-fu-count>span{display:inline-flex;align-items:center;gap:3px}
.tc-fu-count .ic{width:12px;height:12px}
.tc-fu-count:hover,.tc-fu-count:focus-visible{color:var(--ink);outline:none}
.fu-fold{display:grid;grid-template-rows:0fr;
  transition:grid-template-rows var(--t-med) var(--ease)}
.fu-fold.open{grid-template-rows:1fr}
.fu-fold>div{overflow:hidden;display:flex;flex-direction:column;gap:2px;
  min-height:0}

/* the details panel: Notes and Follow-ups are SEPARATE sections — a note
   and a follow-up are different entities (one is the reader's text, the
   other is a thread) and never share a list. One row per mark: the kind's
   glyph, what was written (one line, ellipsis), the age. A row is a
   button; a note row scrolls to its highlight, a follow-up row opens its
   pane. Stadium hover, the sidebar-row idiom (_design#6). */
.anno-row{display:flex;align-items:center;gap:8px;width:calc(100% + 20px);
  margin:0 -10px;padding:5px 10px;border:0;background:none;cursor:pointer;
  border-radius:var(--r-full);font:inherit;text-align:left;
  transition:background var(--t-fast) var(--ease)}
.anno-row:hover,.anno-row:focus-visible{background:var(--panel);outline:none}
.anno-row>.ic{width:13px;height:13px;flex:none;color:var(--ink-2);stroke-width:2.77}
.anno-row .q{flex:1;min-width:0;font-size:var(--text);color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A row whose subject is another THREAD — a follow-up, a branch either way —
   carries the action colour on its name, because the name IS the way there.
   A note's row keeps ink: its label is the reader's own words, and the row goes
   to a highlight on this page rather than to somewhere else.
   No underline: that is what an `.act` does inside a sentence, where there is
   nothing else to show a word is live. A row has itself — it answers by
   lighting up, which is the hover this rule leaves untouched. */
.anno-row.thread .q{color:var(--action)}
.anno-row .meta{flex:none}
/* a mark whose passage the page can no longer find — the anchor's honest third
   answer: kept and listed, saying "passage no longer found" where its age would
   be, never silently dropped. The KIND'S GLYPH steps back, because what is
   missing is the mark on the page and not the row here. A row that has also
   stopped being a control — a note's, whose whole act was to go to a highlight
   that is gone; a follow-up's before its thread exists — takes no hover and its
   text steps to narration gray; the reader's own words never go further than
   that, being still theirs. */
.anno-row.lost>.ic{color:var(--ink-3)}
.anno-row[disabled]{cursor:default}
.anno-row[disabled] .q{color:var(--ink-2)}
.anno-row[disabled]:hover,.anno-row[disabled]:focus-visible{background:none}

/* Every menu's rows — vertical cards and the one-line .bar alike — rest in
   narration gray and answer in ink, text and icon together, with NO hover
   backing; weight and the end-mark still say what is chosen. The card takes
   the multiline input's radius. Landed with annotations and applied to the
   framework rather than to this one card: a second menu look is exactly what
   the popover framework exists to prevent. */
.menu{border-radius:var(--r-lg)}
.menu button:not([disabled]){color:var(--ink-2)}
.menu button:not([disabled]):hover,
.menu button:not([disabled]):focus-visible{
  background:none;color:var(--ink)}
.menu button:not([disabled]):hover .ic,
.menu button:not([disabled]):focus-visible .ic{color:var(--ink)}
.menu button.on,
.menu button[aria-checked="true"]{color:var(--ink)}

/* the ARTIFACT surface — the doc modal's annotation rail (.doc-anno /
   .doc-rail): the same mechanism on an open report, composed as the product
   ships it. Beside the modal's × sits the SIDEBAR-RIGHT REGION TOGGLE (the
   same bespoke icon the details panel uses — the rail is that region at a
   new anchor). It opens the rail with region physics: --t-med on the grid
   column, the rail's content at FIXED width fading in — clip on collapse,
   exactly the app shell's idiom. The separator is a vertical --hairline on
   the rail's inner edge: the details-panel boundary transplanted into the
   modal — a dense-stack hairline habitat (draft: foundations' enumeration
   gains it when this lands). The rail holds the details idiom (same .label
   sections, same .anno-row rows, same .fu-pane swap); creating a follow-up
   opens the rail on its own. Notes and follow-ups made on a report are ONE
   set of records anchored to the artifact — the thread's details pane and
   this rail list the same marks, from the embed and the full view alike.
   The report is a sandboxed iframe guest: the guest reports selection
   rects and wraps the ranges (.hl and markers render guest-side, themed
   per embedding#3); the host draws the floating pieces and owns the rail. */
/* THE REPORT HEADER — over the scrolling page, pinned to the frame and
   outside the scroller (a way out that scrolls away is not a way out). The
   title at the left FADES IN once the document's own masthead has gone past:
   before that the title is on the page, in the report's own type, and saying
   it again in 14px above itself says the same thing twice. Then the rail
   toggle and the way out, in the order the reader meets them.
   The ground-fade is the thread header's (_design#16) over the REPORT's own
   ground: a report is --panel, so the gradient is --panel, and passing text
   dissolves into it instead of being cut at a hard edge. It reaches across
   the rail as well — one header over the whole frame, which is why the rail's
   own inset clears it. */
/* A GRID ON THE FRAME'S OWN COLUMNS, not a flex row: the header spans both
   regions, and what it says about each has to sit OVER each — the document's
   title over the document, the rail's head over the rail. Sharing the column
   variable is what keeps the two aligned through every drag of the boundary.
   The paddings live on the cells rather than the row for the same reason: a
   row inset would shift the rail cell 10px off the rail column it names. */
.doc-h{position:absolute;top:0;left:0;right:0;z-index:6;
  display:grid;grid-template-columns:minmax(0,1fr) 0 auto;align-items:center;
  height:44px;padding:0 0 4px;
  border-radius:var(--r-lg) var(--r-lg) 0 0;
  background:linear-gradient(var(--panel) 62%,transparent)}
.doc-h.rail-open{grid-template-columns:minmax(0,1fr) 8px var(--w-doc-rail,300px)}
/* The rail toggle, open, wore the app-wide expanded treatment — a --panel fill
   that is invisible on this --panel ground, leaving only its ink shift: one
   dark glyph in a row of quiet ones, reading as a stray rather than a state.
   The open rail says "open" itself, so the button stays in the row's register
   and keeps only its hover step. */
.doc-h .iconbtn[aria-expanded="true"]{background:none;color:var(--ink-2)}
.doc-h .iconbtn[aria-expanded="true"]:hover,
.doc-h .iconbtn[aria-expanded="true"]:focus-visible{background:var(--panel-2);color:var(--ink)}
/* the DOCUMENT title — a direct child, so the rail's own .t (below) is not
   caught by the masthead fade */
.doc-h>.t{min-width:0;padding-left:22px;font-family:var(--font-title);font-weight:600;
  font-size:14px;letter-spacing:-0.01em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  opacity:0;transition:opacity var(--t-fast) var(--ease)}
.doc-h.scrolled>.t{opacity:1}
.doc-h .iconbtn:hover,.doc-h .iconbtn:focus-visible{background:var(--panel-2)}
/* the rail's cell: what the rail is showing, then the controls that were
   always here. 12px left inset is the rail's own, so "Annotations" stands
   over the rows it heads; the right inset is the row's old 10px. */
.doc-h-side{display:flex;align-items:center;gap:2px;min-width:0;
  padding-right:10px;justify-content:flex-end}
.doc-h.rail-open .doc-h-side{padding-left:12px}
/* fades with the rail (the .doc-rail-in motion): the cell snaps to the rail's
   width while the column below eases, and the fade is what covers the seam */
.doc-h-rail{flex:1;min-width:0;display:flex;align-items:center;gap:2px;
  opacity:0;transition:opacity var(--t-fast) var(--ease)}
.doc-h.rail-open .doc-h-rail{opacity:1}
.doc-h-rail .t{flex:1;min-width:0;text-align:left;
  font-family:var(--font-title);font-weight:600;font-size:14px;
  letter-spacing:-0.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Both columns take the frame's height, and that is load-bearing rather than
   cosmetic: the panel is what SCROLLS the document, and a column left at its
   own content height is a panel as tall as the report — nothing to scroll, and
   a modal overflowing a fixed layer that cannot scroll either. `min-height:0`
   for the same reason one line down: without it the grid refuses to be shorter
   than its content and the cap never bites. */
/* ONE SHEET, TWO REGIONS. The ground and the corners belong to the pair, not to
   the page and the rail separately: a rounded document meeting a rounded rail
   shows the seam between them — two blocks that happen to touch — when what the
   rail IS is the side of the page it opens from. So the sheet is here, the
   report's own frame goes square inside it, and the only thing between the two
   regions is the hairline. Clipped, so the sheet's corners are the corners
   whatever scrolls past them. */
.doc-anno{display:grid;grid-template-columns:minmax(0,1fr) 0 0;
  min-height:0;
  background:var(--panel);border-radius:var(--r-lg);overflow:hidden;
  transition:grid-template-columns var(--t-med) var(--ease)}
/* the middle track is the BOUNDARY — 8px of grab, the shell's own figure */
.doc-anno.rail-open{grid-template-columns:minmax(0,1fr) 8px var(--w-doc-rail,300px)}
.doc-anno .rep-frame{border-radius:0}
/* while the pointer is down the edge must track it, not ease after it */
.modal-frame.dragging .doc-anno{transition:none}
/* shut, the boundary is not there to be grabbed */
.doc-anno:not(.rail-open) .rz-doc{pointer-events:none}
/* …and OPEN, the boundary runs the frame's whole height — through the header
   band, now that the header carries the rail's own title: the line is what
   says where the document's header ends and the rail's begins. Under the
   header (which paints last at z-index 6) its top 44px vanished, and the
   two regions read as separated everywhere except the one row that names
   them both. Raised only while open, so the closed frame does not wear a
   stray hairline at its edge. */
.doc-anno.rail-open .rz-doc{z-index:7}
.doc-rail{min-width:0;height:100%;overflow:hidden}
/* 12px each side — the details panel's own inset; the .anno-row stadiums
   bleed 10px of it and text lands at 12, never at the clipped edge */
/* No ground, no corners and no boundary of its own — it stands on the sheet
   above, and the hairline beside it belongs to the RESIZER, which is what makes
   that edge a thing you can take hold of rather than a line drawn on a panel.
   All the rail owns is its inset, whose top clears the header reaching across
   both regions. */
.doc-rail-in{width:var(--w-doc-rail,300px);height:100%;
  padding:44px 12px 8px;opacity:0;
  transition:opacity var(--t-fast) var(--ease)}
.doc-anno.rail-open .doc-rail-in{opacity:1}
/* ON A --panel GROUND EVERY QUIET SURFACE STEPS ONE TINT (the modal's rule):
   a pane-header fade fades from ITS ground — --paper in the app's details
   panel, --panel here, and a paper fade over a panel paints a stray lighter
   band — and every hover, fill and card inside steps to --panel-2. */
/* The rail's follow-up pane is headerless (its head is in `.doc-h`), so its
   rows leave at the top the way the DOCUMENT leaves — under the header, through
   the same gradient. The pane takes the rail's top inset back and carries it as
   scroller padding instead: at rest the first row still starts below the
   header, and a scrolled row passes beneath it and dissolves where the page
   does, not at a second fade line of the rail's own. (A mask on the scroller
   was the first cut at this, and it faded rows out a band EARLIER than the
   main part — the seam the reader noticed.) */
.doc-rail-in:has(.fu-pane){padding-top:0}
.doc-rail .fu-scroll{padding-top:44px}
.doc-rail .anno-row:hover,.doc-rail .anno-row:focus-visible,
.doc-rail .iconbtn:hover,.doc-rail .iconbtn:focus-visible,
.doc-rail .msg-user,.doc-rail .pill-input,.doc-rail .menu{background:var(--panel-2)}
