/* The WinIQ visual language.
 *
 * Implemented from the Figma frames and the specimen sheet in them, rather than invented here
 * — which matters because every earlier attempt at this file was invented here, and each one
 * was worse than the last. Where a comment gives a reason, it is the design's reason; where a
 * value departs from the frames, the comment says so and why.
 *
 * The principle everything follows from, in the specimen sheet's own words: **the white paper
 * register, leveraging generous whitespace and hairline rules rather than filled panels.**
 * Grouping is space and a line. Nothing is a card, nothing is tinted, nothing is elevated —
 * with exactly one exception, the data-fitness caution, which is tinted in the frames because
 * it is the one thing on the page that must interrupt.
 */

/* ------------------------------------------------------------------ typefaces */
/* Two files for the serif, not four. Literata ships from Google only as a variable font — no
 * static instances are offered — so each style is one file carrying a weight axis, and the
 * `font-weight` descriptor is the range that file actually holds rather than a wish. Declare a
 * single weight and the browser synthesises the rest; declare a range wider than the file and
 * the browser believes you, silently rendering the missing weights at the nearest real one.
 *
 * **400 900, measured — not the `200 900` Google's CSS API prints.** That declaration belongs to
 * the full-axis request; these files came from a request pinning discrete weights, and Google
 * clips the axis to what was asked for. Rendered at 40px, `Handgloves` measures 674.00px at
 * wght 200, 300 and 400 alike, then 687.06 at 500 and 715.20 at 700 — so the floor is 400 and
 * anything below it is not lighter, merely a lie. PROVENANCE.md records the full measurement.
 *
 * The axis that is *not* in these files is `opsz`. Literata's optical-size axis costs 71 KB per
 * style — 223,260 bytes for the pair against 79,232 — which is the same measurement
 * PROVENANCE.md made for Newsreader, landing the other way because here there is no static
 * alternative to weigh it against. What is lost is real: `font-optical-sizing` is `auto` by
 * default, so a design running 12px to 33px would otherwise get a sturdier cut at the caption
 * and a finer one at the hero for free. Three times the bytes for a refinement at two sizes is
 * not the trade this page makes. */
@font-face {
  font-family: "Literata";
  src: url("/ui/fonts/literata-variable.woff2") format("woff2");
  font-weight: 400 900; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Literata";
  src: url("/ui/fonts/literata-variable-italic.woff2") format("woff2");
  font-weight: 400 900; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "Instrument Sans";
  src: url("/ui/fonts/instrument-sans-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Instrument Sans";
  src: url("/ui/fonts/instrument-sans-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/ui/fonts/jetbrains-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}

/* ------------------------------------------------------------------ palette
 * Four colours, from the specimen sheet. Not four families of colour — four values. The
 * 60/30/10 the ratio was asked for lives in ink and rule rather than in areas of fill, which
 * is the correction to an earlier attempt that read it as tinted surfaces and produced a grey
 * dashboard. */
:root {
  --ground: #fbfbfa;        /* Canvas Ground — warm newsprint bone, and the only background */
  --ink: #1c1c1e;           /* Primary Ink — carbon black, every word that carries meaning */
  --slate: #2f5b88;         /* Telemetry Slate Blue — labels, the mark, the live state */

  /* Caution Terracotta, two hundredths darker than the specified `#a96246`. That value
   * measures 4.48:1 on the ground and 4.34:1 on its own tinted panel, and it is set in 13px
   * italic — so it fails WCAG 1.4.3 in the one place the design most needs to be read. The
   * shift is imperceptible; a caution some readers cannot read is not a caution. */
  --caution: #a45d41;       /* 4.81:1 on ground, 4.65:1 on the tint */
  --caution-tint: #fdf6f0;  /* the one filled panel in the whole design */

  /* Two steps of quieter ink. `--ink-2` is the frames' own `#48484a`, which measures 8.81:1
   * and needed nothing. `--ink-3` is the frames' `#8e8e93` darkened: at the specified value it
   * measures **3.15:1**, and it carries the table headers, the provenance line, the stage
   * labels and the footer — the quiet text that a reader is most likely to be squinting at.
   * Same hue, enough of it to read. `test_every_ink_step_can_be_read` recomputes both. */
  --ink-2: #48484a;         /* 8.81:1 */
  --ink-3: #6e6e73;         /* 4.90:1 */

  /* Hairline rule standards, named by the specimen sheet: 1.5px for primary section dividers,
   * 1.0px for table and grid sub-dividers. There is no third weight, because a third weight
   * would be a third grouping language. */
  --rule-strong: 1.5px solid var(--ink);
  --rule: 1px solid #e5e5ea;
  --rule-colour: #e5e5ea;

  /* The composer's own border, and the one place a rule is not grey. Two-thirds of the way from
   * the ordinary hairline to the slate — computed once and written as a literal, like the ink
   * ramp, so what it actually is can be read rather than resolved in the head.
   *
   * It was `#93a7be`, a little under half, and that value measured **2.41:1** on the ground.
   * WCAG 1.4.11 sets 3:1 for the boundary of a control, and this is the boundary of the one
   * control the whole product turns on — the shape that tells a reader where to type. At two
   * thirds it measures 3.50:1 and still reads as the quiet blue it was chosen to be.
   *
   * There is no second, darker value for focus. There used to be: the border went to the full
   * slate the moment anything inside the pill had focus, which is 6.83:1 — and a 6.83:1 hairline
   * on newsprint is not a blue frame, it is a black one. That is what it looked like, and on the
   * composing screen, where the pane and the standing work recede to 7% and the box is the only
   * thing left at full strength, it looked like it twice as much. Focus is already said twice
   * over: the ring on the pill, and the magnifier turning slate. A third signal that changes the
   * colour of the box's outline was the one that had to be a different colour to be noticed, and
   * the only colour left in that direction is ink. */
  --rule-blue: #6c89a9;

  /* The one surface a hair off the ground: the pill, the thread entries, the unfurling panel.
   * At 1.001:1 against the ground it is not a contrast device and is not doing the grouping —
   * the border is. It only stops those shapes reading as holes cut in the page. */
  --raised: #fcfbf9;

  /* The pane's surface, and the line where it ends.
   *
   * The pane used to be the ground with things on it, told apart from the canvas only by the
   * fact that nothing was written in the gap between them — which works until the gap is the
   * widest thing on the screen, and then it reads as a margin that happens to have a list in it
   * rather than as a region. A fill says where the furniture lives; the line says where the
   * work begins.
   *
   * Warm rather than neutral, both of them, because the ground is warm: `#f5f5f3` is the same
   * newsprint a step down, and the pane reads as more of the page — the same paper in shadow.
   * The cool `#f3f4f6` was tried against it and put back: it reads as chrome the page sits in
   * rather than paper the page is printed on, and a second material is a bigger claim than a
   * column holding a mark and a list has any need to make.
   *
   * The edge is the darkest of the three and the fill sits between it and the ground, which is
   * the order that makes a surface recede instead of float. It is also the reason the pane's own
   * hairlines are `--pane-edge` and not the ordinary `--rule`: `#e5e5ea` is a cool line drawn
   * against the ground, and on this fill it is both too cool and too faint to be a boundary. */
  --pane-ground: #f5f5f3;
  --pane-edge: #e6e2de;

  --serif: "Literata", Georgia, "Times New Roman", serif;
  --sans: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace;

  /* The spacing contrast scale, all four steps, straight from the specimen sheet. Anything
   * that is not one of these is a mistake. */
  --s-hero: 64px;      /* hero gaps and main canvas breathing room */
  --s-section: 48px;   /* section breaks and query-response grouping */
  --s-prose: 24px;     /* card internal margins and prose spacing */
  --s-tight: 12px;     /* data table row packing and compact details */

  /* A fifth step, and an addition to the sheet rather than a reading of it — said plainly
   * because the sheet's own note is that anything outside its four steps is a mistake.
   *
   * Those four were written against a static comp, where every gap is a gap *within* a page.
   * A live screen also has a viewport, and the home screen's largest division — between what
   * you are being asked to do and the work already standing there — is a different order of
   * thing from a section break inside either of them. Measured before it was chosen: the home
   * screen was carrying 172px of unspent room at the bottom while the top was packed. */

  /* The band both columns start on, under the mark's slot — the invitation on one side and
   * "Recent questions" on the other, set on one line across the page.
   *
   * This was one token with `--s-page`, which was the division between the ask and the standing
   * work below it — the largest gap on the page, and 96. The *start* of the page inheriting
   * that number pushed everything down by the same amount and left the surplus at the bottom:
   * 237px of it on a 1080 window, with the content pinned to the top of a screen it did not
   * fill. They were split; the standing work has since gone, and `--s-page` with it.
   *
   * `--s-section` at the cap, so at any comfortable height the band is exactly one section
   * break below the mark, which is a number this design already uses rather than one invented
   * for the occasion. It still gives way on a short window, for the same reason the other did. */
  --s-start: clamp(24px, 5vh, var(--s-section));

  --canvas: 1120px;    /* the content column, from the frames' 1440 less 160 either side */
  --pill: 840px;       /* the composer, from the frames */
  --measure: 46rem;    /* body prose, at the width a reader finds the next line reliably */

  /* The pane.
   *
   * 300px was not chosen, it was measured: on the 1440 the design is drawn against, the composer
   * is 840px wide and centred, so it started at exactly 300 and there were 300px of unused page
   * to the left of it. The pane was that space, and nothing had to move to make room for it.
   *
   * It is 360 now, and that derivation is spent — said plainly rather than left as a comment
   * describing a number that is no longer there. Two things paid for the extra sixty. The
   * content column is centred in the track the pane leaves rather than on the window, so a
   * wider pane moves the box right instead of squeezing it, and at 1440 the gap between the
   * thread list and the ink had opened to about 200px — a margin that wide stops reading as a
   * margin. And the list itself was living in 204px with every title but the shortest ellipsed,
   * which is a column that can tell you a thread exists and not which one it is. 360 leaves 263
   * after the two `--s-section` insets and the edge, and the titles mostly fit.
   *
   * It briefly had a mirror — the same width given back as padding on the right, so the content
   * column stayed centred on the *window* and every band was where it was before the pane
   * existed. That preserved the old positions and it left the page visibly lopsided: the ink
   * ended a third of the way across a wide window with a quarter of it empty on the right, which
   * is what happens when a column is centred on a frame it no longer occupies. The content is
   * centred in the track it actually has now, and the gutter either side is `--s-section` like
   * every other inset on the page. */
  --pane: 360px;

  /* Six thread rows and the five gaps between them, which is what the list is capped at however
   * many have been loaded into it.
   *
   * A row measures 67.6px — 10px of padding either side of two line boxes, in two faces at two
   * sizes, plus a hairline — and that is written down rather than assembled in a `calc()` out of
   * the four values that produce it. A calc would look derived and would not be: it would still
   * be guessing at two line heights the browser resolves from font metrics, and it would be
   * wrong in a way nobody could see. This was measured in the browser and it is checkable the
   * same way.
   *
   * Rounded up rather than down, deliberately. At 466 the sixth row ends a fraction of a pixel
   * inside the box; at 465 it is clipped, and a hairline of a seventh card is a rendering
   * artefact rather than a signal. The signal that there is more is the scrollbar. */
  --threads-cap: 466px;

  /* The mark's slot, and the band the two columns start below.
   *
   * `--mark-line` is the mark's line box, frozen — 2.4rem is what `line-height: normal` resolved
   * to at the size it was, so it changed nothing when it arrived. What it changes is that the
   * mark's type size and the position of everything under it stopped being the same number.
   * `--masthead` is that box plus its padding, and it is the canvas's top padding as well, so
   * the invitation begins on the line the mark's slot ends on rather than at the top of the
   * window. Both columns start from the same band because they are given the same number. */
  --mark-line: 2.4rem;
  --masthead: calc(var(--mark-line) + 2 * var(--s-prose));

  /* One curve and one duration for every transition on the page. Two of either would read as
   * two separate events happening at once, which is what makes an interface feel assembled. */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --shift: 260ms;
  /* Shorter than `--shift`, and deliberately so: the composer growing a line is a response to
   * a keystroke, and a response that takes a quarter of a second reads as lag rather than as
   * grace. Long enough to be a movement, short enough to keep up with typing. */
  --grow: 130ms;
  /* One hint giving way to the next. Slower again than the growth, because this movement is
   * unprompted — nobody asked for it — and unprompted motion that happens quickly reads as
   * something going wrong rather than as something being offered. */
  --turn: 420ms;

  /* Light, and said out loud. Without this the browser renders scrollbars, the caret and the
   * selection highlight for whatever the OS prefers, so a reader on a dark system gets a dark
   * scrollbar down the side of a bone-white page. There is no dark palette here on purpose:
   * the design is specified light-only, and a dark counterpart nobody has designed is a second
   * identity invented in a stylesheet. */
  color-scheme: light;
}

* { box-sizing: border-box; }

/* `[hidden]` must actually hide: the attribute works through a UA rule that any author
 * `display` outranks, and several things here set one. Without this, `element.hidden` changes
 * nothing and the failure reads as a JavaScript bug. */
[hidden] { display: none !important; }

/* Present to a screen reader, absent to everything else. `display: none` would remove it from
 * both, which defeats the purpose — this is how the status region and the composer's label
 * exist at all. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

:focus-visible { outline: 2px solid var(--slate); outline-offset: 3px; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.6;
  /* Lining figures, all one width. A column of numbers that does not line up is not a column,
   * and proportional digits make `1,024` narrower than `2,410` in the same cell. */
  font-variant-numeric: lining-nums tabular-nums;
  -webkit-font-smoothing: antialiased;
}

button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
button:disabled { color: var(--ink-3); cursor: default; }
button.link { color: var(--slate); }
button.link:hover { text-decoration: underline; }

/* A section heading: "Prepared for you", "Recent threads".
 *
 * Instrument Sans rather than the mono this used to be, which is what the frames specify —
 * `reports-title` and `threads-title` are both "Instrument Sans 600 12.0px UPPER #2f5b86"
 * (spec.txt:25, :50). Mono belongs to metadata: elapsed times, row counts, stage labels,
 * provenance. A heading is not metadata, and setting one in the measuring face made the two
 * columns of standing work read as instrument readings rather than as titles.
 *
 * 700 rather than the frames' 600: only 400 and 700 are vendored, and the difference at 12px
 * uppercase is slighter than the 17KB a third weight would cost. See fonts/PROVENANCE.md. */
/* The two section rubrics, in the serif — sentence case at 600, which is the combination that
 * works where two earlier attempts did not.
 *
 * Both failures were the same mistake. Set in Literata at 13px/700 in letterspaced caps it went
 * dense and, from a step back, looked like the sans it replaced; dropped to 400 to fix that, it
 * became *lighter than the 14px/400 detail text inside the cards below it*, so it stopped
 * outranking the thing it was supposed to name and read as a caption. Caps had been carrying the
 * label-ness, and removing the weight without replacing the signal left nothing.
 *
 * So: keep a real weight and drop the caps instead. 600 is a drawn instance rather than a
 * synthesised one — see PROVENANCE.md, which also records that this file's axis starts at 400
 * and not the 200 Google's CSS API prints, so nothing below 400 may be asked for here.
 * Sentence case is what lets Literata contribute anything at all; all-caps discards the
 * ascenders and pen-formed terminals that are the reason to reach for it.
 *
 * Same slate-free, sentence-case setting at 18px. At 13px it was still wearing the eyebrow
 * label's proportions — small, coloured, subordinate to the cards it names — which is app
 * furniture rather than document structure. A section heading in a report is *larger* than the
 * items under it, so at 18px it now outranks the 16px card titles, and the colour moves off
 * slate to `--ink-2`: slate was doing double duty as both "this is a label" and "this is live"
 * (the mark, the focus ring, the active stage), and the quiet grey lets it mean only the second.
 * 8.81:1, so nothing is spent on legibility to get there. */
.label {
  font-family: var(--serif);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-2);
  margin: 0;
}

/* The wordmark. Literata 600 italic where the frames set Lora 700 italic — see the fonts'
 * PROVENANCE.md for why a fourth family was not worth one word. */
.wordmark {
  font-family: var(--serif); font-style: italic; font-weight: 600;
  font-size: 22px; line-height: 1.2; color: var(--slate);
  letter-spacing: -0.01em; margin: 0;
}

/* ------------------------------------------------------------------ signed out */

.gate { display: grid; place-items: center; min-height: 100dvh; padding: var(--s-prose); }
.gate-card { text-align: center; max-width: 28rem; }
.gate-card .wordmark { font-size: 34px; margin-bottom: var(--s-tight); }
.gate-line {
  font-family: var(--serif); font-style: italic; font-size: 18px;
  color: var(--ink-2); margin: 0 0 var(--s-section);
}
.gate-card button:not(.link) {
  font-family: var(--mono); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--slate); border-bottom: 1px solid var(--slate); padding-bottom: 3px;
}
.error { color: var(--caution); font-size: 13px; word-break: break-word; margin-top: var(--s-prose); }

/* ------------------------------------------------------------------ the frame
 * A pane, one content column, the docked composer — and still no masthead: there is no brand
 * bar and no live status, because the stages in the run panel already say a turn is running and
 * a second indicator saying the same thing in the corner is furniture.
 *
 * Two columns and two rows. The pane spans both rows down the left; the canvas is the `1fr`
 * row of the right column and the only thing that scrolls. `min-height: 0` on the scrollers
 * because a grid item will not shrink below its content's intrinsic height unless told to —
 * without it the canvas sizes itself to the whole transcript, the page scrolls instead of the
 * canvas, and the dock goes off the bottom of the window with everything else.
 *
 * The pane's width is a fixed track rather than a fraction. A pane that grows with the window
 * is a pane whose contents reflow at every size for no reason: there is one list in it, and a
 * list of ellipsised titles wants a width, not a share. */
.app {
  display: grid;
  grid-template-columns: var(--pane) 1fr;
  grid-template-rows: 1fr auto;
  /* `dvh`, not `vh`, and the difference is not academic. `100vh` is the viewport with every
   * dynamic browser chrome *expanded* — a mobile URL bar, a tablet toolbar — so a shell that
   * claims to be exactly one screen tall is taller than the screen, and the composer sits below
   * the fold on the one device where that is hardest to recover from. It bit us on the desktop
   * too: `100vh` also counts the space a horizontal scrollbar takes, so a single overhanging
   * element made the app 15px too tall, which grew a vertical scrollbar, which took 15px off
   * every measurement on the page. Nothing here page-scrolls, so the dynamic value cannot
   * oscillate; it just tells the truth. */
  height: 100dvh;
}

/* The pane. The mark at the head, the threads already run in the middle, who you are at the
 * foot — the page's furniture, in one column that is identical on every screen.
 *
 * It does not scroll and it never has to: the only thing in it that can grow without bound is
 * the thread list, and that list carries its own scroller. So the mark is on screen at every
 * scroll position by construction rather than by being sticky, which is a stronger guarantee
 * than the one it replaces and needs no opaque background to make good on.
 *
 * The left inset is `--s-section`, the same as the canvas's own gutter beside it, which leaves
 * 263px for the list — a fill and a hairline having replaced the empty gap that used to be the
 * only thing saying where this column ended. The fill is the reason the pane no longer needs the
 * canvas to be a different colour to be a different place. */
.pane {
  grid-row: 1 / -1;
  display: flex; flex-direction: column; min-height: 0;
  padding: 0 var(--s-section) var(--s-prose);
  background: var(--pane-ground);
  border-right: 1px solid var(--pane-edge);
}

.canvas {
  display: flex; flex-direction: column; align-items: center;
  overflow-y: auto; min-height: 0;
  /* The same inset either side, so the content is centred in the track the pane left it — not
   * on the window. The pane is the page's left margin and the gutter is the canvas's own, which
   * is why the two are different lengths and neither is a mirror of the other.
   *
   * The top padding is the mark's slot, which used to be a real element here — the mark was the
   * canvas's first child, and the invitation was 96px below *it*. With the mark in the pane the
   * space has to be owned by something, and the honest owner is the band both columns start
   * below. It is safe to pad the top now for the same reason: the sticky element that could not
   * rise above the canvas's content box has gone with it.
   *
   * A quarter of the old bottom padding. It is space below the last thing on the page, which
   * shows only once there is something to scroll to — and buying it cost this page a scrollbar
   * on a 900px window. */
  padding: var(--masthead) var(--s-section) var(--s-prose);
}
/* `flex: none` on every child, and it is load-bearing rather than tidiness.
 *
 * The canvas is a flex column that scrolls, so the moment its content exceeds the viewport the
 * flex algorithm starts shrinking items to make them fit — which is precisely what must not
 * happen when the answer to overflow is a scrollbar. An item is normally protected by its
 * automatic minimum size, but `overflow: hidden` sets that minimum to zero, and both the hero
 * and the starter questions carry `overflow: hidden` so their heights can be collapsed and
 * animated. The result was a section reporting `max-height: 320px`, `visibility: visible`,
 * `opacity: 1` and a measured height of **0**: fully expanded, fully visible, and squeezed flat
 * by nine pixels of overflow elsewhere on the page.
 *
 * `:not(.visually-hidden)` is the other half, and it is not tidiness either. This rule and
 * `.visually-hidden` have the same specificity and this one is written later, so `width: 100%;
 * max-width: var(--canvas)` was quietly winning against `width: 1px` on `#status` — the live
 * region, which is a canvas child. An absolutely-positioned 1120px box at the static position
 * of a centred flex child hangs off *both* edges of a track narrower than that, and it counts
 * toward the document's scroll width even though nothing in it can be seen.
 *
 * At a 300px pane its right edge landed at 1430 on the 1440 the design is drawn against — ten
 * pixels inside the window, which is why nobody found it. Widening the pane to 360 moved it to
 * 1453 and the page grew a horizontal scrollbar; that cost 15px of height, which pushed
 * `height: 100vh` past what was left, which grew a *vertical* scrollbar, which took 15px of
 * width off every measurement on the page. A hidden element must cost nothing, including
 * overflow.
 *
 * `:where()` around the exclusion, and it is the whole point rather than a flourish. A bare
 * `:not(.visually-hidden)` takes its argument's specificity, so this rule would go from (0,1,0)
 * to (0,2,0) and start outranking every later `.hero` rule that sets a narrower
 * `max-width` — measured, not guessed: the invitation went from 768 to 984 and the standing work
 * from the composer's 840 to the same 984, which is the one alignment on the home screen. A
 * `:where()` contributes nothing, so the exclusion changes what matches and not what wins. */
.canvas > *:not(:where(.visually-hidden)) { width: 100%; max-width: var(--canvas); flex: none; }

/* Who you are and the way out, at the foot of the pane.
 *
 * It used to be a band across the whole bottom of the page — a horizontal rule and two strings
 * a reader's eye had to cross on the way from the answer to the box, for information that is
 * never the reason anybody came. In a 204px column it stacks instead of spreading, which is
 * also the only arrangement an email address and a control both fit in. `margin-top: auto`
 * puts it on the floor of the pane however short the thread list is. */
.colophon {
  margin-top: auto; padding-top: var(--s-prose); border-top: 1px solid var(--pane-edge);
  display: grid; gap: var(--s-tight); justify-items: start;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-3);
}
.who-block { display: grid; gap: 4px; justify-items: start; margin: 0; min-width: 0; max-width: 100%; }
.who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; text-transform: none; letter-spacing: 0; }
.colophon .link { font-size: 11px; letter-spacing: 0.04em; }
.confidential { margin: 0; }

/* ------------------------------------------------------------------ home
 * The hero and the composer are separated in the DOM by the transcript, which is empty here —
 * so the three read as one centred block on the home screen and fall into document order the
 * moment there is an answer. Nothing moves in the DOM to make that happen. */

/* The mark, at the head of the pane, in the one position it holds on every screen.
 *
 * Not sticky, and it does not need to be: the pane does not scroll, so a way back that is one
 * click from anywhere is a property of where it lives rather than of a rule that pins it. That
 * also takes the opaque background with it — there was nothing to paint over. There is no bar,
 * no border and nothing beside it: the masthead was removed for being a brand strip with a live
 * status in it, and this is one line of text.
 *
 * `#brand` and `#home-link` share it and are never both shown — see index.html. One size for
 * both, which is new: the wordmark used to be 34px on the home screen and the back link 24px,
 * because they sat inline with the content and one was greeting somebody while the other was a
 * control. In the pane they are the same fixed thing in the same fixed place, and a mark that
 * changes size between screens is a mark that moved. 34px is the size the signed-out gate
 * already sets it at, so the product now introduces itself identically everywhere. */
.brand-mark {
  /* `margin: 0` because one half of this slot is a `<p>` and the other is a `<button>`: the
   * paragraph brought 22px of user-agent margin top and bottom that the button does not have,
   * so the two halves of the same position sat 22px apart. */
  margin: 0; text-align: left; padding: var(--s-prose) 0;
  /* A lockup on the answer screen — an arrow, the square mark, the word — and the word alone on
   * the home screen. `baseline` would be wrong for the lockup: the glyph is a square with no
   * baseline of its own, so the browser would use its bottom edge and hang the whole mark off
   * the wordmark's descenders. Centred on the line box is the alignment a square and a word
   * actually share. `relative` is what the cube is positioned against. */
  display: flex; align-items: center; gap: 12px; position: relative;
  /* The slot's height, frozen, and the number the canvas's top padding is built from — see
   * `--masthead`. Without it the mark's type size and the position of the invitation beside it
   * would be the same number, and enlarging one would move the other. The glyph is sized under
   * that ceiling for the same reason: a mark taller than the line box would push the slot open
   * and take the whole invitation down the page with it. */
  line-height: var(--mark-line);
  font-family: var(--serif); font-style: italic; font-weight: 600;
  font-size: 34px; letter-spacing: -0.01em; color: var(--slate);
}
/* 30px against the wordmark's 34, and `currentColor` so the mark is whatever the lockup is —
 * slate here, ink on hover, without a second copy of the geometry existing. An `<svg>` with no
 * fill declared is black, which is not a colour in this palette and was visibly not the
 * wordmark beside it.
 *
 * 30 rather than the 26 that cap height suggests. 26 is the arithmetic answer — Literata's caps
 * are about 0.7em — and it is the wrong one, because the monogram is knocked *out* of the
 * square: at 26 the letters are seven pixels tall, the counters close, and the mark reads as a
 * plain four-square grid at the one size it is actually shown. `flex: none` because the glyph is
 * the one thing in this row that must not be negotiated with: a squeezed logo is a different
 * logo. */
.brand-glyph { width: 30px; height: 30px; flex: none; fill: currentColor; }

/* The mark's own square, holding two drawings of the same thing in the same place: the static
 * glyph, and the cube that replaces it on hover. Stacked rather than swapped in the DOM, so the
 * handoff is two opacities and nothing reflows — a logo that changes the width of its own box
 * mid-hover takes the wordmark with it. */
.brand-slot { position: relative; width: 30px; height: 30px; flex: none; }
.brand-slot .brand-glyph { position: absolute; inset: 0; }
.brand-cube {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* The cube is drawn well outside the window this element frames — the window is the square-on
   * face, and the cube stands taller and wider than the square it folds into. Visible overflow
   * is what lets it do that from a 30px box without occupying more than 30px. */
  overflow: visible;
  opacity: 0; pointer-events: none; fill: currentColor;
}
/* The handoff, and it is a cut rather than a fade on purpose. The cube's first frame and its
 * last are the *same drawing* as the static glyph, in the same box — so there is nothing to
 * dissolve between, and a fade would cross-dissolve a thing with itself and read as a flicker.
 * What moves is the geometry underneath: the face unfolds out of that square and folds back
 * into it. */
.brand-mark.is-spinning .brand-glyph { opacity: 0; }
.brand-mark.is-spinning .brand-cube { opacity: 1; }
/* The one difference between the two halves of the slot. `#brand` is a paragraph and `#home-link`
 * is a button, and with the arrow gone they draw identically — which is the point: the mark is
 * one fixed thing in one fixed place, and it now genuinely is, to the pixel. What separates them
 * is that one responds. Darkening to ink on hover, plus the pointer cursor every button here
 * gets, is the whole affordance; the title says where it goes. */
.brand-mark.is-link:hover { color: var(--ink); }

/* Where the mark's geometry is defined and never drawn. Out of flow and zero-sized, because a
 * `<symbol>` host that takes part in layout is the `#status` bug again — an invisible element
 * with a box, pushing the page around and adding to its scroll width. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* The invitation, on its own now that the mark has moved out of it. No height cap: this used
 * to collapse to nothing on the way into the composing state, and it does not any more — it
 * travels with the box instead. */
/* 48rem, not the 44 this held under Newsreader. Literata sets the invitation 4% wider — 731.4px
 * against 704 of room at 33px, measured, which is why the line broke in two the moment the
 * serif changed. Widening rather than dropping to 31px: the title is the one thing on the home
 * screen the type scale was just deliberately grown for, and a two-line hero also cost ~43px of
 * height, which is what put the page back into scrolling at 150% zoom.
 *
 * 768px leaves 36px of headroom, so a font revision or a slightly longer invitation does not
 * silently re-break it. Below about a 930px window the canvas padding binds first and the line
 * wraps anyway — which is correct, and is the only place it should. */
.hero { text-align: center; max-width: 48rem; margin: var(--s-start) auto var(--s-section); }
.hero-title {
  font-family: var(--serif); font-weight: 400; font-size: 33px; line-height: 1.3;
  letter-spacing: -0.01em; color: var(--ink); margin: var(--s-tight) 0 0;
}

/* The threads, in the middle of the pane. `flex: 1` takes whatever the mark above and the
 * account below have not, and the list inside carries its own scroller — so thirty threads
 * bound at the pane's height rather than at their own.
 *
 * `--s-start` below the mark rather than a section break: it is the same gap the invitation
 * takes from the top of the canvas, which puts "Recent questions" and "Ask a question about
 * Windows business metrics" on one line across the two columns. Both moved up together when
 * that band was separated from the page division; the alignment is the reason they share a token at
 * all, and it would be the first thing to break if only one of them were tuned. */
.recent {
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0; flex: 1;
  margin-top: var(--s-start);
}

/* --------------------------------------------------------------- asking, then composing
 * Two states, not one, and the split is the point.
 *
 * **`.asking` — the cursor is in the box.** The starter questions open underneath it, and
 * nothing else changes: the hero stays, the standing work stays. Clicking into a field is not
 * a commitment, and clearing the page around somebody who has only just clicked takes away the
 * thing they were about to read the answer against.
 *
 * **`.composing` — the question has outgrown one line.** Now they are writing rather than
 * considering, so the page recedes to almost nothing, the hero gives back its space, the box
 * settles toward the middle, and the starter questions go — anyone two lines into their own
 * question has stopped shopping for one. It arrives from wherever the first state left off,
 * because it is the same elements moving and not a different screen.
 *
 * Two rules learned by getting this wrong:
 *
 *  1. **Every property that changes between the states must be animatable, and must be in the
 *     transition list.** The first version moved the columns with `margin-top: auto`, and
 *     `auto` is not an interpolable value — so the largest movement on the screen happened in
 *     a single frame and made the whole thing read as a cut, however carefully the rest of it
 *     eased. `test_the_two_states_differ_only_in_things_that_can_animate` checks this now.
 *  2. **`visibility` preserves layout.** Fading the hero left its box behind, so the composer
 *     had nothing to rise into and only ever moved down. It collapses its height as well.
 *
 * Nothing here uses `hidden`: an element removed from the box tree cannot transition into or
 * out of anything. `visibility` does the hiding once the fade has run, and takes the content
 * out of the accessibility tree with it.
 *
 * One easing curve and one duration for all of it. Two would read as two events. */

.hero, .home-slot .composer, .pane, .home-extras, .report, .threads button {
  transition:
    opacity var(--shift) var(--ease),
    transform var(--shift) var(--ease),
    filter var(--shift) var(--ease),
    max-height var(--shift) var(--ease),
    margin var(--shift) var(--ease),
    padding var(--shift) var(--ease),
    border-color var(--shift) var(--ease),
    background-color var(--shift) var(--ease),
    visibility 0s linear var(--shift);
}

/* The title comes with it. It used to collapse, on the reading that "question only" meant the
 * question and nothing else — but the title is what says *what* can be asked, and it is the
 * one line on the page a first-time reader needs most while they are writing. The two move as
 * a pair, which is why the margin goes on the hero rather than on the composer: put it on the
 * box and the title stays at the top of the page while the box travels away from it. */
.app.composing .hero { margin-top: 16vh; }

/* Where the way out is written down. A state you can get into and not out of is a trap, and
 * the frames have no affordance for leaving this one — so it needs a key, and a key nobody can
 * see needs a line saying so. */
.escape-hint {
  /* No margin while collapsed. `max-height: 0` zeroes the box and not the margin, so 12px of a
   * hidden element used to leak into the largest gap on the page — measured at 108px against a
   * declared 96. The margin arrives with the hint. */
  margin: 0; text-align: center;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);
  max-height: 0; opacity: 0; overflow: hidden; visibility: hidden;
  transition:
    opacity var(--shift) var(--ease),
    max-height var(--shift) var(--ease),
    margin var(--shift) var(--ease),
    visibility 0s linear var(--shift);
}
.app.composing .escape-hint {
  max-height: 2rem; margin-top: var(--s-tight);
  opacity: 1; visibility: visible; transition-delay: 0s;
}
.escape-hint kbd {
  font-family: inherit; font-size: inherit; color: var(--ink-2);
  border: var(--rule); border-radius: 4px; padding: 1px 5px; margin-right: 4px;
}

/* Almost gone. Not gone: a reader who clicked the box by accident, or who is deciding between
 * typing and picking something they already ran, needs to see the page is still underneath —
 * and it coming back is what tells them the state is reversible. Blurred as well as faded,
 * because opacity alone at a level this low still leaves legible edges, and half-read text
 * beside a list of suggestions is a second thing to read. `margin` is untouched: this is a
 * change of presence, not of layout, so nothing moves and nothing can jump.
 *
 * The pane recedes with the rest of it, mark and all. It was the last thing left at full
 * strength once the columns had gone, and a wordmark burning at the edge of a screen that has
 * otherwise cleared itself is the loudest thing on it. */
.app.composing .pane {
  opacity: 0.07; filter: blur(3px); transform: scale(0.99); pointer-events: none;
}
/* The card chrome dissolves with everything else. Written as `background-color` rather than
 * the `background` shorthand: the shorthand resets eight properties at once, only one of which
 * is being transitioned, and a reader of this file should not have to work out which. */
.app.composing .threads button { border-color: transparent; background-color: transparent; }

/* `prefers-reduced-motion: reduce` means reduce **motion** — travel across the screen, the
 * thing that triggers vestibular symptoms — not "remove every transition". This block used to
 * read `transition: none`, which is the over-reading, and it is not a hypothetical one: on
 * Windows the preference is set by *Settings → Accessibility → Visual effects → Animation
 * effects*, a switch plenty of people turn off for reasons that have nothing to do with
 * vestibular disorders, and plenty of managed machines ship off by default. Everyone in that
 * group got a hard cut between the two states and no way to know why.
 *
 * So: nothing travels, and nothing scales. The cross-fade stays, because a fade has no
 * direction and no velocity — there is nothing in it to be sensitive to — and it is what keeps
 * the change legible as one thing becoming another rather than a page replacing itself.
 *
 * `max-height` is left out too: collapsing the hero is small, but it moves everything below it
 * up the screen, and that is motion by any reading. Under this preference the layout settles
 * at once and only the ink fades. */
@media (prefers-reduced-motion: reduce) {
  .hero, .home-slot .composer, .pane, .home-extras, .report, .threads button {
    transition:
      opacity var(--shift) var(--ease),
      filter var(--shift) var(--ease),
      border-color var(--shift) var(--ease),
      background-color var(--shift) var(--ease),
      visibility 0s linear var(--shift);
  }
  .app.composing .hero, .app.composing .pane { transform: none; }
  /* The hint still changes — it is the only thing telling a reader what can be asked — but it
   * cross-fades in place instead of travelling. Rotating text that slides is precisely the
   * kind of unrequested movement this preference is about. */
  .hint-line { transition: opacity var(--turn) var(--ease); }
  .hint-line.leaving, .hint-line.arriving { transform: none; }
  /* The composer still resizes — a box that will not fit the question is not an option — it
   * just arrives at the new height instead of travelling to it. */
  .composer textarea { transition: none; }
  /* The tick arrives drawn rather than drawing itself. What it says — that the copy
   * landed — is carried by the mark being there at all, so the stroke is not animated on.
   * Kept in this block with everything else answering the preference: two blocks can
   * disagree, and a reader who asked for less motion cannot see which one won. */
  .tick path { animation: none; stroke-dashoffset: 0; }
  .stage.active .state::before { animation: none; }
  /* The hop goes; the solve stays. They are not the same decision and it is worth saying why.
   * `mark.js` documents at length that the cube's rotation ignores this preference, because the
   * reduced variant that was built jumped between states with no tweening — the same distance
   * travelled in one frame, which is not less motion. The hop has no such problem: it carries no
   * information at all, so removing it removes travel and costs nothing. */
  .wait-cube.is-solving svg { animation: none; }
}


/* Its own scroller. Thirty threads should not make the home screen thirty screens tall, and
 * they certainly should not push the standing reports beside them out of reach — the page and
 * the list are two different things to move through, and conflating them means you cannot get
 * back to the top of one without losing your place in the other.
 *
 * `overscroll-behavior: contain` stops the page taking over once the list hits its end, which
 * is the detail that makes an inner scroller feel deliberate rather than like a trap. */
/* The list, capped at six rows however many have been loaded into it.
 *
 * `flex: 1` before this, which is why Load more sat marooned at the foot of the pane: the list
 * grew to fill every pixel the section had, so the button that belongs directly under the last
 * card was pushed to the bottom of a mostly empty box. It does not grow now — it is as tall as
 * what is in it, up to `--threads-cap`, and past that it scrolls.
 *
 * Both bounds, and both are load-bearing. `max-height` is the cap a reader asked for. `flex: 0 1
 * auto` with `min-height: 0` is what happens on a window too short to honour it: the list gives
 * way rather than pushing the account and the sign-out link off the bottom of a pane that is
 * forbidden to scroll. Cap without shrink breaks at 700px of viewport; shrink without cap is
 * where this started. */
.threads {
  list-style: none; margin: var(--s-tight) 0 0; padding: 0 4px 0 0;
  display: grid; gap: var(--s-tight); grid-auto-rows: max-content;
  flex: 0 1 auto; min-height: 0; max-height: var(--threads-cap);
  overflow-y: auto; overscroll-behavior: contain;
}

/* The list's own scrollbar, drawn rather than inherited.
 *
 * A platform scrollbar is 15px of chrome in a 263px column — six percent of the pane, in a grey
 * nobody chose, sitting against the one edge where this design has been careful about what a
 * line means. At 5px it is a rule like every other rule here: the pane's own edge colour at
 * rest, the composer's blue while the pointer is in the list, so it says "this scrolls" when
 * you are in a position to scroll it and nothing the rest of the time.
 *
 * The webkit pseudo-elements are the only way to set a width, and Chromium ignores them the
 * moment `scrollbar-width` is declared — so the standard properties go inside an `@supports`
 * that fires only where the pseudo-elements do not exist. Firefox gets `thin` and the same two
 * colours; Chromium and WebKit get the 5px bar. Neither browser sees both. */
.threads::-webkit-scrollbar { width: 5px; }
.threads::-webkit-scrollbar-track { background: transparent; }
.threads::-webkit-scrollbar-thumb { background: var(--pane-edge); border-radius: 999px; }
.threads:hover::-webkit-scrollbar-thumb { background: var(--rule-blue); }
@supports not selector(::-webkit-scrollbar) {
  .threads { scrollbar-width: thin; scrollbar-color: var(--pane-edge) transparent; }
}

/* The rest of them, six at a time.
 *
 * Shaped like a thread entry without being one — same width, same radius, same hover fill, no
 * border, because it is not a thing you can open. Two words and no number: a count of what is
 * behind it is a fact about the store rather than about the reader's work, and it is stale the
 * moment anything is asked anywhere else. The button being there at all is the whole message. */
.load-more {
  flex: none; width: 100%; margin-top: var(--s-tight);
  padding: 10px var(--s-tight); border-radius: 12px;
  font-family: var(--sans); font-size: 13px; color: var(--slate);
}
.load-more:hover { background: var(--raised); }

/* What the column says before anybody has asked anything. Shaped like the entries that will
 * replace it — same hairline, same radius — so it reads as the place they go rather than as a
 * notice about their absence, and it says what fills it, because "empty" on its own tells a
 * first-time reader nothing they can act on.
 *
 * Shaped like *one* entry, not like the whole column. It used to be `flex: 1`, which was
 * unremarkable while this sat beside the standing reports at 336px tall and became a 555px
 * empty rectangle down the pane — a box that size is not a placeholder, it is a hole, and it
 * announced the absence far louder than the sentence inside it. It sits under the label where
 * the first thread will, and the rest of the column is space. */
.empty-note {
  flex: none; margin: var(--s-tight) 0 0;
  display: flex; align-items: center; justify-content: center; text-align: center;
  padding: var(--s-prose); border: 1px solid var(--pane-edge); border-radius: 12px;
  font-family: var(--sans); font-size: 14px; line-height: 1.5; color: var(--ink-3);
}
/* A thread entry. Bordered and softly rounded, which is the one place the design allows a
 * shape around content — these are targets to be picked from a list, not prose to be read. */
.threads button {
  display: flex; align-items: center; gap: var(--s-tight); width: 100%;
  padding: 10px var(--s-tight); border: 1px solid var(--pane-edge); border-radius: 12px;
  background: var(--raised); text-align: left;
}
.threads button:hover { border-color: var(--ink-3); }
/* The thread you are reading. The border it already has, in the accent.
 *
 * Hover firms the same border to `--ink-3`; being the current thread takes it a step further
 * to the page's own accent. One property, on an element that was already drawing it — nothing
 * added to the card and nothing new to look at. */
.threads button.is-current { border-color: var(--slate); }
.threads button.is-current .chevron {
  border-top-color: var(--slate); border-right-color: var(--slate);
}
.threads .thread-text { display: grid; gap: 4px; min-width: 0; flex: 1; }
.threads .thread-title {
  font-family: var(--sans); font-weight: 700; font-size: 14px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.threads .thread-when { font-family: var(--mono); font-size: 12px; color: var(--ink-3); }
/* The chevron in the frames, drawn in CSS rather than shipped as an asset — two borders and a
 * rotation, which is less than the bytes of the `<svg>` that would replace it. */
.threads .chevron {
  width: 7px; height: 7px; flex: none; transform: rotate(45deg);
  border-top: 1.5px solid var(--ink-3); border-right: 1.5px solid var(--ink-3);
}

/* ------------------------------------------------------------------ the composer
 * The pill. Rounded because it is the one thing on the page you type into — a control rather
 * than a container, which is the distinction that keeps "no filled boxes" honest. */

/* Pinned below the canvas once there is anything in it. This is the row that makes the answer
 * screen usable: the transcript can be any length and the box is still there, at the same
 * place, without scrolling to the end of everything already asked. The same horizontal inset as
 * the canvas above it, so the box keeps the column's centre line whether it is in the page or in
 * the dock — and moving between the two moves it vertically and not by a pixel sideways. */
.dock { padding: var(--s-prose) var(--s-section) var(--s-tight); }
.home-slot:empty { display: none; }

.composer {
  display: flex; align-items: center; gap: 14px;
  width: 100%; max-width: var(--pill); margin: 0 auto;
  /* Asymmetric, and it is the button that makes it so. 32 either side was right when both ends
   * held a mark the same weight — a 20px magnifier on the left, a line of small type on the
   * right. The right end holds a 60px disc of solid colour now, and a shape that size wants to
   * sit *in* the corner of the pill rather than floating 32px inside it: the eye reads the gap
   * as a second margin and the button as a thing that has drifted off the end.
   *
   * 16 all the way round it instead. The vertical is the same 16, so the clearance above, below
   * and to the right of the disc is one number and the button nests in the end of the pill the
   * way the pill nests in the page. The left stays 32, because that is where the sentence starts
   * and the sentence is not the button. The pill's height is unchanged: the sixteen the padding
   * gave up is the eight the button took at each end. */
  /* 94, and it is the send button's height plus the padding and the two hairlines rather than a
   * round number. The button is `display: none` until something is typed, so with 92 here the
   * pill stood at 92 empty and 94 the instant a character arrived — a two-pixel jolt under the
   * cursor at the exact moment somebody starts typing. Measured, not guessed: 60 + 2×16 + 2. */
  padding: 16px 16px 16px 32px; min-height: 94px;
  background: var(--raised); border: 1px solid var(--rule-blue); border-radius: 32px;
  /* The whole pill is the field, so the whole pill has to look like one. The text cursor used
   * to appear over the 22px strip the textarea actually occupies and nowhere else — an arrow
   * over four-fifths of a control that accepts a click anywhere, which reads as a decorated
   * box with a small input hidden in it. */
  cursor: text;
}
.composer button { cursor: pointer; }

/* The focus ring, on the pill rather than on the field inside it.
 *
 * This shipped broken and an audit measured it. `.composer textarea` carried `outline: none`
 * in its *base* rule, at specificity (0,1,1) against the global `:focus-visible` at (0,1,0) —
 * so the one control the whole product turns on painted no ring at all, for any input
 * modality. After Tab the field matched `:focus-visible` and still computed
 * `outline-style: none`, while Sign out and Return both drew 2px. All that was left was the
 * border swap, at 2.86:1, under the 3:1 floor WCAG 2.4.11 sets for a state change.
 *
 * The ring belongs to the pill because the field is a 22px rectangle inside a 32px-radius
 * shape: drawn on the textarea it is a hard-edged box floating inside a rounded one. `:has()`
 * hangs it on the parent while `:focus-visible` keeps the modality rule the rest of the page
 * uses. Nothing suppresses it anywhere, which is the entire point. */
.composer:has(textarea:focus-visible) {
  outline: 2px solid var(--slate);
  outline-offset: 3px;
}
/* And the field does not draw a second one inside the first. This is the only suppression the
 * page permits, and it is only safe because the rule directly above it moves the same ring to
 * the parent — measured before and after: with both, a hard-edged rectangle sat around the
 * placeholder inside a 32px-radius pill. Delegation, not removal. */
.composer textarea:focus-visible { outline: none; }
/* And the border does not change at all. `.composer:focus-within { border-color: var(--slate) }`
 * used to live here — see `--rule-blue` for why it is gone. Focus is the ring above and the
 * magnifier below; the frame is the frame. */

/* The magnifier. Slate once the field has focus, so the pill has a single quiet state change
 * rather than three competing ones. */
.glass {
  width: 20px; height: 20px; flex: none;
  fill: none; stroke: var(--ink-3); stroke-width: 2; stroke-linecap: round;
}
.composer:focus-within .glass { stroke: var(--slate); }

/* The field and the hint occupy the same space, so the hint has to be positioned against
 * something. This wrapper is that something and does nothing else. */
.field { position: relative; flex: 1; min-width: 0; }

.composer textarea {
  display: block; width: 100%; resize: none; border: 0; background: none;
  font-family: var(--sans); font-size: 18px; line-height: 1.35; color: var(--ink);
  /* Zero padding, because the mirror that measures this has none and `box-sizing:
   * border-box` takes any padding out of the height `fitComposer` computes. Two pixels of
   * user-agent padding was enough to make a two-line question scroll inside a box built
   * to hold it. */
  padding: 0;
  /* Grows a line at a time as the question outruns the width, and stops. Beyond the ceiling it
   * scrolls: a box that keeps growing eventually pushes the answer it belongs to off the
   * screen, and nobody writes six lines here. The height itself is written by `fitComposer`,
   * measured on the mirror below — this rule only says how it travels. */
  height: 25px; max-height: 7.5rem; overflow-y: hidden;
  transition: height var(--grow) var(--ease);
}

/* The rotating hint, where a `placeholder` attribute used to be.
 *
 * A placeholder is one immutable string that the browser paints for us; these are four
 * questions shown in turn, so they have to be real text in the document. Behind the field and
 * `pointer-events: none`, so it never intercepts a click and never competes with the caret.
 * `overflow: hidden` is what turns the vertical travel into a reveal rather than a slide over
 * the surrounding chrome. */
.hint {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  font-family: var(--sans); font-size: 18px; line-height: 1.35; color: var(--ink-3);
}
.hint-line {
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: transform var(--turn) var(--ease), opacity var(--turn) var(--ease);
}
/* Leaving upward, arriving from below — one line replacing another, which is the only motion
 * that reads as "there are more of these" rather than as a glitch. `--turn` is deliberately
 * slower than the composer's growth: this one is unprompted, and unprompted motion that moves
 * quickly reads as something going wrong. */
.hint-line.leaving { transform: translateY(-105%); opacity: 0; }
.hint-line.arriving { transform: translateY(105%); opacity: 0; transition: none; }
/* Nothing to hint at once there is a question in the box. */
.composer.typed .hint { opacity: 0; visibility: hidden; transition: opacity 90ms linear, visibility 0s linear 90ms; }

/* The invisible twin the height is measured on. Everything that affects where a line breaks
 * has to match the real field exactly — family, size, line height, letter spacing, wrapping —
 * or the box will be a line short of what it needs at some widths and a line over at others.
 * Its width is set from the field at measuring time, because the field's width depends on the
 * pill, which depends on the viewport. */
.composer-mirror {
  position: absolute; left: -9999px; top: 0; visibility: hidden; pointer-events: none;
  font-family: var(--sans); font-size: 18px; line-height: 1.35;
  white-space: pre-wrap; overflow-wrap: break-word;
}

/* Send.
 *
 * An arrow, in a filled circle, at the end of the pill. It read "Return ↵" — which is a caption
 * for a key, not a label for a button: it told a keyboard user something they could have
 * discovered by pressing Enter, and told a pointer user, who is the only person who needs this
 * control at all, the name of a key they were not reaching for. The arrow is the action.
 *
 * Filled, and it is the only filled control on the page. That is deliberate and it is narrow: a
 * bordered circle the same weight as everything else made the one thing on the screen you press
 * indistinguishable from the six shapes around it that you do not. The caution panel is the only
 * other fill in the design, and it is a warning; this is the verb.
 *
 * 60px. 44 is the smallest square a finger reliably hits, so a circle at 44 is the *floor* — and
 * a target at the floor, on the end of a box 840 wide, reads as an afterthought stuck on. It was
 * briefly a 60×44 oval on the same reasoning and put back: an oval is a shape that holds a word,
 * and there is no word in it. The pill does not grow to fit this — its vertical padding came
 * down by the same eight pixels at each end that the button went up, so the composer is the
 * height it always was, and the button now sits 16px inside the pill's corner rather than 32.
 *
 * Not there at all until there is something to send. It was there always, in a lighter fill,
 * on the reasoning that a control which appears and disappears is harder to aim at than one
 * that is simply quiet — and the reasoning was for a button, not for this. A 60px disc of solid
 * colour is the heaviest mark on the home screen; parking it beside an empty field points the
 * whole composition at a press that does nothing, because the field is `required` and the
 * browser swallows the submit. So it arrives with the first character and leaves with the last,
 * and while it is gone the sentence has the width back.
 *
 * `:not(.is-stop)` is what keeps that from breaking Stop. A turn in flight clears the composer,
 * which drops `.typed` — so the rule below would take the only control that can cancel a
 * one-minute run off the screen at the exact moment it becomes the thing you need. */
.composer:not(.typed) button[type="submit"]:not(.is-stop) { display: none; }

.composer button[type="submit"] {
  flex: none;
  display: grid; place-items: center;
  width: 60px; height: 60px; border-radius: 50%;
  background: var(--slate); color: var(--ground);
}

/* The mark inside it, drawn rather than typeset — `↑` is a glyph most sans faces do not carry,
 * so it arrives from whatever fallback the system has at whatever weight that font offers, and
 * neither its weight nor the size of its head is a thing you can ask a substituted glyph for.
 * Two strokes at a weight this file sets: 1.75 on a 24 grid drawn at 30px, a little over 2px on
 * screen, which is the magnifier's weight at the other end of the pill. Half the disc, so the
 * mark and its ground keep their proportion whatever size the button settles at.
 *
 * Upwards, not rightwards. Right is the direction of the next word — it reads as "carry on",
 * which is what the field is already doing. Up is out of the box and onto the page above it,
 * which is where the answer appears. */
.composer button[type="submit"] svg {
  width: 30px; height: 30px;
  fill: none; stroke: currentColor; stroke-width: 1.75;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Stopping a turn in flight, on the same button and in the same place. A square rather than the
 * word, because the word no longer fits the shape — filled, because a stop is the one mark here
 * that should read as a block rather than a direction. Both are in the markup and one is shown:
 * `setStreaming` toggles a class and writes one string, so there is nothing for it to destroy. */
.composer button[type="submit"] .halt { display: none; }
.composer button[type="submit"] .halt rect { fill: currentColor; stroke: none; }
.composer button[type="submit"].is-stop { background: var(--slate); }
.composer button[type="submit"].is-stop .arrow { display: none; }
.composer button[type="submit"].is-stop .halt { display: block; }
/* After an answer the box is still the box — pinned in the dock, saying what it is now for.
 * The frames draw the follow-up as a quiet italic link that expands on hover, and that only
 * works because a static comp has nothing after it: in a session the transcript grows without
 * limit, so a link at the end of it is a link nobody scrolls to. The words survive; they are
 * the placeholder, set by `setPhase`. */

.banner {
  max-width: var(--pill); margin: var(--s-tight) auto 0;
  font-family: var(--serif); font-style: italic; font-size: 15px; color: var(--caution);
}

/* ------------------------------------------------------------------ a turn */

.transcript { min-height: 0; }
.transcript:empty { display: none; }
/* The gap below a turn is the gap *between* turns, so the last one on the page does not need
 * it — with the composer docked below the canvas, it read as a hundred pixels of dead page
 * between the answer and the box. */
/* Exactly one hero step between turns, and none below the last one.
 *
 * It used to be a bottom margin *and* a top padding — 128px of nothing — which was defensible
 * while a rule sat in the middle of it and is not now that the question carries the divider.
 * One gap, the largest on the page, so it can mean "new question" without anything else. */
.turn { margin-bottom: 0; }
.turn:last-child { margin-bottom: var(--s-prose); }
.turn + .turn { padding-top: var(--s-hero); }

/* The question, in the form it will keep — and the title of the report.
 *
 * It used to arrive as a pill — the same rounded box as the composer — and become a heading
 * only once the answer landed. That put two pills on screen at once, one at the top holding
 * what had just been asked and one docked below waiting for the next thing, and then replaced
 * the first with something that looked nothing like it the moment the answer arrived.
 *
 * So the question is written once, in its final shape, and never restyled. What changes under
 * it is the *content*: the run panel gives way to the answer. That is the only transition on
 * this screen, which is what makes it legible.
 *
 * It was 16px sans, on the reasoning that the words should look like the ones the reader typed
 * rather than a label the system applied. That reasoning held and the size did not: at 16px the
 * question was smaller than every paragraph under it and a third the size of the answer's
 * opening line, so the one thing on the page that says *what this is* was the quietest thing on
 * it. A reader landing on a scrolled page could not find where a turn began.
 *
 * 28px serif now — the largest text in a turn, and the answer's opening sentence has stepped
 * down to make room. This is the swap the screen was missing: one title per report, and it is
 * the question, because that is what a reader navigates by. Serif because a title belongs to the
 * document rather than to the interface. The 1.5px rule — the design's one heavy divider — stays
 * under it, and still means "a section starts here". */
.question {
  display: block; max-width: none;
  padding: 0 0 var(--s-tight); background: none; min-height: 0;
  border: 0; border-bottom: var(--rule-strong); border-radius: 0;
  margin: 0 0 var(--s-prose);
  font-family: var(--serif); font-weight: 400; font-size: 28px; line-height: 1.25;
  letter-spacing: -0.01em; color: var(--ink);
}

/* ------------------------------------------------------------------ the run
 * The unfurling panel. Bordered rather than filled — the fill is a hair off the ground and is
 * only there to stop the panel reading as a hole in the page. */

.timeline {
  border: 1px solid var(--rule-colour); border-radius: 8px; background: var(--raised);
  padding: 40px; margin-bottom: var(--s-section);
  /* Folds rather than vanishes. `display: none` on the panel and the answer simply appeared
   * where it had been, which is a cut in the one place this product spends most of its time —
   * the wait is tens of seconds, and the moment it ends is the moment worth designing. The
   * height is set explicitly by `foldTimeline` before the fold starts; transitioning from a
   * guessed `max-height` eases wrongly for the first half of the travel. */
  overflow: hidden;
  transition:
    max-height var(--shift) var(--ease),
    opacity var(--shift) var(--ease),
    margin var(--shift) var(--ease),
    padding var(--shift) var(--ease),
    border-color var(--shift) var(--ease);
}
/* Gone once there is an answer, and only then. The stages are how the wait is spent; the
 * moment the finding is on screen they are history, and leaving them above it would push the
 * thing the reader waited for down the page. A turn that ended *without* an answer keeps them:
 * that is the turn whose stages the reader most needs, because they say how far it got. */
.timeline.collapsed {
  max-height: 0; opacity: 0;
  margin-bottom: 0; padding-top: 0; padding-bottom: 0; border-color: transparent;
}
/* The cube, in the middle of the wait.
 *
 * Centred and given its own band between the heading and the stages, because it is the only
 * thing on this screen that moves continuously and the eye settles where it is put. In a corner
 * it would read as a spinner — a thing that says "still going" and nothing else. Here it is the
 * subject, and the stages beneath it are the caption.
 *
 * Sized in `rem` rather than as a share of the panel: the panel's width follows the content
 * column, and a cube that grew with the window would be a different object on a laptop and on a
 * monitor. */
/* Sized to the row it sits in, and **not** `overflow: visible`.
 *
 * That is the one thing `.brand-cube` needs and this must not have. The hover frames the mark's
 * own 180-unit window and lets the cube bloom outside it; this uses the engine's default frame,
 * which is the whole cube plus the room its faces travel through mid-turn. Letting that overflow
 * put sliding faces outside the box and down the panel — it read as the cube melting.
 *
 * 6rem for a cube that measures 44px. The frame is deliberately roomy: measured, the cube paints
 * across 46% of its width and 53% of its height, so the box has to be about twice what the cube
 * should look like. At 2.75rem it rendered around 20px, which is where "very small" came from. */
.wait-cube {
  display: flex; justify-content: center; align-self: center;
}
.wait-cube svg { width: 6rem; height: 6rem; }

/* The hop, and it is deliberately the *panel's* animation rather than the cube's — it says a
 * turn is in flight, which is a fact about the request and not about the solve. `mark.js` drives
 * rotation on its own clock through `requestAnimationFrame`; putting a transform on the wrapper
 * keeps the two from arguing over the same element.
 *
 * Slower than it wants to be. At half a second this reads as impatience, which is the wrong
 * thing to tell somebody eighty seconds into a query they cannot hurry. */
.wait-cube.is-solving svg { animation: cube-hop 1400ms var(--ease) infinite; }
@keyframes cube-hop {
  0%, 100% { transform: translateY(4%); }
  50% { transform: translateY(-7%); }
}

/* The cube, in the heading row.
 *
 * A grid rather than `space-between`, because the cube has to be centred in the *panel* and the
 * two labels beside it are different widths — with `space-between` it would sit wherever the
 * text left it, drifting as "Step 2 of 4" became "Step 4 of 4". Equal `1fr` tracks on either
 * side pin it to the middle whatever the labels say.
 *
 * Baseline for the words, centre for the cube: the two labels are 20px serif and 11px mono and
 * only sit on one line because they share a baseline, which is not a thing a square has. */
.timeline-head {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: baseline;
  gap: var(--s-prose);
  padding-bottom: var(--s-section); border-bottom: var(--rule);
}
.timeline-head .of { justify-self: end; }
.timeline-head .doing {
  font-family: var(--serif); font-style: italic; font-size: 20px; color: var(--ink);
}
.timeline-head .of { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; color: var(--slate); }

.stages { display: grid; gap: 16px; padding: 32px 0; }
.stage { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-prose); }
.stage .named { display: grid; gap: 4px; min-width: 0; }
.stage .what {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-2);
}
/* The value the stage resolved to, set in the serif the finding uses — because it *is* the
 * finding, one narrowing at a time. This is the whole idea of the wait screen: not a progress
 * bar, but the scope closing in on an answer in front of you. */
.stage .value { font-family: var(--serif); font-weight: 600; font-size: 16px; color: var(--ink); }
.stage .state {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em;
  color: var(--ink-2); white-space: nowrap; display: flex; align-items: center; gap: 8px;
}

.stage.active .what { color: var(--slate); }
.stage.active .value { font-style: italic; }
.stage.active .state { color: var(--slate); }
.stage.active .state::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none;
  /* The only thing moving on the screen during a run, and it is beside the stage that is
   * running — which is where somebody watching is already looking. It replaced three dots in
   * the question pill, where the motion was furthest from the thing it described. */
  animation: breathe 1.8s ease-in-out infinite;
}
@keyframes breathe { 50% { opacity: 0.25; } }
/* Not started. Quieted by ink alone — no strikethrough, no half-opacity, because opacity would
 * take the text below the contrast floor rather than merely making it recede. */
.stage.queued .what, .stage.queued .state { color: var(--ink-3); }
.stage.queued .value { color: var(--ink-3); font-style: italic; font-weight: 400; }
.stage.failed .what, .stage.failed .state, .stage.failed .value { color: var(--caution); }

/* ------------------------------------------------------------------ the answer */

/* Applied once, when the first token lands. An animation rather than a transition, and
 * `both` rather than a resting rule, so a turn read back from history — which never gets the
 * class — is simply visible instead of depending on it. */
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
.answer.arriving { animation: rise var(--shift) var(--ease) both; }

.answer > :first-child { margin-top: 0; }
.answer > :last-child { margin-bottom: 0; }

/* Two prose sizes and no third. The specimen sheet names 32px for the finding and 18px italic
 * for the supporting prose, so everything after the first paragraph is 18px — and the first of
 * those is the italic one, because that is the sentence qualifying the finding.
 *
 * The finding takes the full canvas, as the frames set it: at 32px that is around sixty
 * characters to the line, which reads. The prose under it is capped at `--measure`, which the
 * frames are not — at 18px the full canvas is about a hundred and ten characters, and a line
 * that long is one a reader loses their place in on the way back. */
.answer > p {
  font-family: var(--serif); font-size: 18px; line-height: 1.5;
  margin: 0 0 var(--s-prose); max-width: var(--measure);
}
/* The opening paragraph, at 20px — down from 26, and before that from the specimen sheet's 32.
 *
 * 32 was right for what the sheet was typeset against — *"Chrome leads Windows 11 engagement in
 * the United States, taking 21.4% of all foreground minutes."* One sentence, one clause of
 * consequence, two lines at most. It is not what the agent actually writes first. A real opening
 * paragraph is often the method — *"Ranked by TotalEngagementMinutes_R28 over the 28 days ending
 * 2026-08-09, restricted to Windows 11 devices…"* — thirty words with three identifiers in them,
 * which at 32px is three lines of display type shouting the qualification rather than the result.
 * 26 was that argument applied halfway and it did not go far enough: nothing here can know which
 * paragraph is the finding, so a lead treatment large enough to be a headline is a headline
 * applied to whatever happens to be first.
 *
 * So there is no headline in the answer at all now. The question above it is the title, at 28,
 * and this is one step over the body — enough to read as the start of something, not enough to
 * be mistaken for the point of it. The measure comes back with the size: at 26px on the full
 * canvas this ran to a line a reader loses their place in. */
.answer > p:first-of-type {
  font-size: 20px; line-height: 1.45; margin-bottom: var(--s-prose);
}
/* And identifiers inside it do not scale with it. `0.88em` of 32px put `TotalEngagementMinutes_R28`
 * on screen at 28px of monospace — a table column name set larger than most headlines. Mono
 * already reads a size bigger than the sans beside it, so the lead's code is pinned rather than
 * proportional. */
.answer > p:first-of-type code { font-size: 17px; }
.answer > p:first-of-type strong { font-weight: 600; }
.answer > p:first-of-type + p { font-style: italic; color: var(--ink-2); }

.answer h3, .answer h4, .answer h5, .answer h6 {
  font-family: var(--mono); font-size: 12px; font-weight: 400;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-2);
  margin: var(--s-prose) 0 var(--s-tight);
}
.answer ul, .answer ol {
  margin: 0 0 var(--s-prose); padding-left: var(--s-prose);
  max-width: var(--measure); font-family: var(--serif); font-size: 18px;
}
.answer li { margin: 4px 0; }
.answer code { font-family: var(--mono); font-size: 0.88em; }
.answer pre {
  font-family: var(--mono); font-size: 12px; line-height: 1.55;
  border-left: var(--rule); padding-left: var(--s-prose);
  overflow-x: auto; margin: 0 0 var(--s-prose); color: var(--ink-2);
}

/* ------------------------------------------------------------------ the table
 * Sans for words, mono for figures, and that division is the whole design. A number set in the
 * same face as the name beside it reads as a name; set in mono, and lined up on the right, it
 * reads as a measurement. */
/* 24 above rather than 48: inside a turn the table is the next thing to read, not a new
 * section. The 48 is spent between turns instead, where it means something. */
.table-scroll { overflow-x: auto; max-width: 100%; margin: var(--s-prose) 0 var(--s-prose); }
/* The rows past the preview, hidden rather than never built — `tableOf` explains why the whole
 * table has to stay in the DOM. `n + 6` is `TABLE_PREVIEW + 1` and the two are checked against
 * each other by `test_the_preview_row_count_is_the_same_number_in_both_files`, because a
 * stylesheet and a script disagreeing about this shows up as an off-by-one in a button's
 * count and nowhere else. */
.answer .table-scroll.is-clipped tbody tr:nth-child(n + 6) { display: none; }
.result { margin: var(--s-prose) 0; position: relative; }

/* The table's own footing: what was left out on the left, what to do with it on the right.
 *
 * This sat level with the header row for a while, which cost a gutter — a result's last
 * column is its figures and is right aligned, so its heading ends at the table's right edge
 * and a control placed there lands on the word. Narrowing the table to make room meant every
 * row paying width for something that appears once. Below the table there is nothing to
 * collide with, and the table is full width again. */
/* Table or chart, never both. The frame is built once on first use and then simply hidden,
 * because rebuilding an SVG the reader has already seen would also drop the scroll position
 * of the table beside it. */
.chart-frame { display: none; margin-top: var(--s-prose); }
.result.showing-chart .chart-frame { display: block; }
.result.showing-chart > .table-scroll { display: none; }
/* The fold count belongs to the table and says nothing about a chart, which draws every row
 * it was given. Left showing, it offers to open a dialog full of the rows the reader is
 * already looking at. */
.result.showing-chart .more-rows { display: none; }

/* Fills the frame, which is set to the table's own height so the block never changes size when
 * the view does. `meet` rather than `none`: if the two ever disagree — a window resized after
 * the chart was drawn — the drawing keeps its shape and gains a margin, rather than stretching. */
.chart { width: 100%; height: 100%; display: block; }
/* Gridlines are scaffolding and must read as less than the data. The zero line is not
 * scaffolding — it is where every bar's claim is measured from — so it is darker. */
.chart .axis-rule { stroke: var(--pane-edge); stroke-width: 1; }
.chart .axis-zero { stroke: var(--ink-3); stroke-width: 1; }
/* Real pixels now: the viewBox is the measured block, so one unit is one pixel and these
 * are the sizes that arrive on screen. They used to be multiplied by whatever the block's
 * width divided by 640 came to — about 1.66 — which turned a ten-pixel axis into a
 * seventeen-pixel one shouting over the figures it was labelling.
 *
 * Smaller than the table's own type on purpose. An axis is scaffolding: it is read once to
 * get the scale and then ignored, and anything louder competes with the data. */
.chart .axis-figure {
  font-family: var(--mono); font-size: 10px; fill: var(--ink-3);
}
.chart .bar-name {
  font-family: var(--sans); font-size: 11px; fill: var(--ink-2);
}
.chart .bar { fill: var(--slate); }
/* The figure on the mark, so a value is read off the chart rather than off the axis by eye.
 * `--ground` when it sits inside a bar, which is the only place on this page where type is
 * set on a filled shape rather than on the page's own background. */
.chart .mark-value { font-family: var(--mono); font-size: 10px; fill: var(--ink-3); }
.chart .mark-value.is-inside { fill: var(--ground); }
/* On a line, the figure takes its series' colour, which is what makes it attributable when two
 * lines are being compared. On a bar it does not: bars are one colour and the figure is set on
 * the page's own ground beside them. */
.chart .mark-value.on-series { fill: currentColor; }

/* A series sets `color` and nothing else; the marks take their stroke and fill from it.
 *
 * The obvious arrangement — `.series-0 { stroke: slate; fill: slate }` — filled the line.
 * `.chart .line { fill: none }` and `.chart .series-0 { fill: slate }` have identical
 * specificity, so the later rule won, and a four-point series rendered as a solid blue
 * triangle. `currentColor` removes the competition rather than adjudicating it: there is
 * one declaration of the colour and each mark says what to do with it.
 *
 * Six of them, because the chart refuses to draw a seventh series. Slate leads: it is the
 * page's own accent and the single-series case is the common one. */
.chart .series-0 { color: var(--slate); }
.chart .series-1 { color: #a8562f; }
.chart .series-2 { color: #3f7d5c; }
.chart .series-3 { color: #7a5aa6; }
.chart .series-4 { color: #96702a; }
.chart .series-5 { color: #4a6d84; }
.chart .line {
  fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round;
}
.chart .dot { fill: currentColor; stroke: none; }
.chart .series-name {
  font-family: var(--sans); font-size: 10px; font-weight: 700; fill: currentColor;
}

.view-toggle { display: flex; align-items: center; gap: 2px; }
.view-table, .view-chart { padding: 3px 5px; line-height: 0; border-radius: 2px; }
.view-toggle button[aria-pressed="true"] { background: var(--pane-ground); }
.glyph { width: 12px; height: 12px; display: block; }
.glyph path {
  fill: none; stroke: var(--ink-3); stroke-width: 1.2;
  stroke-linecap: round; stroke-linejoin: round;
}
.view-toggle button[aria-pressed="true"] .glyph path { stroke: var(--slate); }
.view-toggle button:hover .glyph path { stroke: var(--slate); }

.result-foot {
  display: flex; align-items: center; gap: var(--s-prose);
  margin-top: var(--s-tight);
}
.result-tools {
  position: relative; margin-left: auto;
  display: flex; align-items: center; gap: 6px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
}
.result-tools button { color: var(--ink-3); text-transform: uppercase; }
.result-tools button:hover { color: var(--slate); }
/* Underlined at rest rather than on hover. This is the one control on the answer screen a
 * reader has to *find* — everything else is either prose or a figure — and a quiet button that
 * only declares itself on hover is invisible to anyone who has not already guessed it is
 * there. `.link` elsewhere is reached by reading; this one is reached by looking. */
.table-copy .copy-word { text-decoration: underline; text-underline-offset: 2px; }
.table-copy { display: inline-flex; align-items: center; gap: 5px; }
.table-copy.is-done { color: var(--rule-blue); }
.table-copy.is-failed { color: var(--slate); }

/* Drawn, and shown only once the copy has landed. `stroke-dasharray` on a 16-unit path with
 * the offset animated to zero draws the tick on rather than blinking it in — the motion is
 * what carries "this just happened" at eleven pixels. */
.tick { width: 11px; height: 11px; display: none; overflow: visible; }
.table-copy.is-done .tick { display: block; }
.tick path {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 16; stroke-dashoffset: 16;
  animation: tick-on 260ms ease-out forwards;
}
@keyframes tick-on { to { stroke-dashoffset: 0; } }
.table-more { padding: 0 2px; line-height: 1; }
.table-more::after { content: "\25BE"; }

.table-menu {
  position: absolute; top: calc(100% + 5px); right: 0; z-index: 3;
  background: var(--ground); border: var(--rule); border-radius: 2px;
  padding: 4px; min-width: 148px;
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.10);
}
.table-menu button {
  display: block; width: 100%; text-align: left;
  padding: 6px 8px; font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
  color: var(--ink-2);
}
.table-menu button:hover { background: var(--pane-ground); color: var(--ink); }
.result .table-scroll { margin-bottom: 0; }
.more-rows { font-size: 13px; }

/* The whole result, over the page. Sized against the viewport rather than the content so a
 * 250-row result scrolls inside the dialog instead of growing past the screen. */
.table-view {
  width: min(92vw, 900px); max-height: 86vh;
  padding: 0; border: var(--rule); border-radius: 2px;
  background: var(--ground); color: var(--ink);
  overflow: hidden;
}
/* `display` belongs on `[open]` and nowhere else, which is not a stylistic preference.
 *
 * A dialog is hidden by the user agent's own `dialog:not([open]) { display: none }`, and an
 * author declaration beats a user-agent one whatever the selectors say — origin is settled
 * before specificity is ever consulted. So a bare `.table-view { display: flex }` overrides it
 * in both states: the box is painted before it has ever been opened, and `close()` then takes
 * it out of the top layer, drops the backdrop, releases focus and leaves the thing sitting on
 * the page. Which reads exactly as a Close button that does not close. */
.table-view[open] { display: flex; flex-direction: column; }
.table-view::backdrop { background: rgb(0 0 0 / 0.44); }
.table-view-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-prose);
  padding: var(--s-prose) var(--s-prose) var(--s-tight);
  border-bottom: var(--rule);
}
.table-view-head h2 {
  font-family: var(--sans); font-weight: 700; font-size: 13px;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3); margin: 0;
}
#table-view-body { overflow: auto; padding: 0 var(--s-prose) var(--s-prose); min-height: 0; }
#table-view-body .table-scroll { margin-top: 0; }
.answer table { border-collapse: collapse; width: 100%; }
.answer th {
  font-family: var(--sans); font-weight: 700; font-size: 12px;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3);
  text-align: left; white-space: nowrap;
  padding: var(--s-tight) 16px; border-bottom: var(--rule);
}
.answer td {
  font-family: var(--sans); font-size: 14px; color: var(--ink-2);
  padding: 14px 16px; border-bottom: var(--rule);
}
/* The first column of words is the row's subject — the application, the country, the build —
 * and it is what a reader scans down. The frames set it bold; everything beside it is not. */
.answer tbody td:first-child, .answer tbody td:nth-child(2) { color: var(--ink); }
.answer tbody td:nth-child(2) { font-weight: 700; }
.answer tbody tr:last-child td { border-bottom: 0; }
/* `.num` is written by the renderer wherever the source said `|---:|`. */
.answer td.num { font-family: var(--mono); font-size: 13px; color: var(--ink); }
.answer tbody td.num:last-child { font-weight: 700; }
.answer td code, .answer th code { white-space: nowrap; }

/* ------------------------------------------------------------------ what the answer rests on
 * The metadata row: the caution on the left, the provenance on the right, one line apart from
 * each other and from everything above. Both are quiet, and neither is allowed to outweigh the
 * finding — which is the failure an earlier version shipped, with four grey strips each in
 * their own small size, collectively louder than the sentence they annotated. */

.footing {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s-section); margin-top: var(--s-prose);
}

/* The one filled panel in the design, and it has to earn it: a caution that fires on every
 * stale table is one people learn to read past, so this appears only when the data actually
 * cannot support the answer. */
.band { max-width: 40rem; }
.band:empty { display: none; }
.band p {
  display: flex; align-items: baseline; gap: 8px; margin: 0;
  padding: 8px 16px; border: 1px solid var(--caution); border-radius: 6px;
  background: var(--caution-tint);
  font-family: var(--serif); font-style: italic; font-size: 13px; line-height: 1.5;
  color: var(--caution);
}
.band p::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none; transform: translateY(-2px);
}
/* Freshness was never checked. Weaker than a warning and reads as one — no tint, no border,
 * because an absent check is not a finding about the data. */
.band.unknown p {
  border-color: transparent; background: none; padding-left: 0; color: var(--ink-3);
}

.evidence-slot:empty { display: none; }
/* Provenance and the copy controls, stacked in the right-hand column of the footing. One
 * place, so the answer screen ends with a finding, a table and a single quiet block — rather
 * than the four competing grey strips it used to. */
/* `flex: none` because the default `flex-shrink: 1` measured this column down to 174px — the
 * caution beside it is allowed 40rem, and a block whose content can wrap has a min-content
 * width of about one word, so the flex algorithm took everything it was offered. The
 * provenance line then broke across three lines in the corner of the page. */
.aside { margin-left: auto; text-align: right; flex: none; max-width: 26rem; }
.evidence { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.evidence summary { cursor: pointer; padding: 4px 0; letter-spacing: 0.04em; list-style-position: inside; }
.evidence summary:hover { color: var(--ink); }
.evidence .facts { display: block; font-size: 12px; color: var(--ink-2); margin-top: 4px; }

/* Opened, the panel is a definition list and reads left to right like every other table on the
 * page — so the right-alignment of the summary above it has to be undone here. */
.evidence dl {
  display: grid; grid-template-columns: 8rem 1fr; gap: 6px var(--s-prose);
  margin: var(--s-tight) 0 0; padding: var(--s-tight) 0 0;
  border-top: var(--rule); text-align: left; max-width: 46rem;
}
.evidence dt { letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }
.evidence dd { margin: 0; color: var(--ink-2); min-width: 0; }
.evidence .quality { margin-bottom: 4px; }
.evidence dd code { white-space: nowrap; margin-right: var(--s-tight); }
/* Each executed statement, folded. The panel above is where a reader goes to *check* an
 * answer — the row count, the freshness verdict, the tables — and a twenty-line query opened
 * by default pushed all of that off the screen. The summary is a compressed first line, so
 * with several statements you can tell which is which without opening any of them. */
/* Unscoped, because a folded statement now appears in two places: the evidence panel, and the
 * answer itself wherever the agent fences SQL into its prose. Scoped to `.evidence` these rules
 * left that second one an unstyled `<details>` — no pointer, no hover, no ellipsis, nothing
 * that reads as a control. A fold nobody can see is a fold that does not exist. */
.sql-block { margin-bottom: 6px; }
.sql-block > summary {
  cursor: pointer; padding: 2px 0; letter-spacing: 0;
  color: var(--ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sql-block > summary:hover { color: var(--slate); }
.sql-block[open] > summary { color: var(--ink-2); margin-bottom: 4px; }
/* One handle at a time. Closed it previews the statement; open it names it, because the
 * statement is right underneath and a handle that quotes its first line makes the block look
 * like it restarted rather than continued. */
.sql-block[open] > summary .peek { display: none; }
.sql-block:not([open]) > summary .name { display: none; }
/* The panel sets mono/11px on itself and the summary inherits it. The answer is serif prose, so
 * a preview of a query has to say for itself what it is. */
.answer .sql-block > summary { font-family: var(--mono); font-size: 11px; }
.evidence pre.sql {
  font-family: var(--mono); font-size: 11px; line-height: 1.6;
  white-space: pre-wrap; word-break: break-word;
  margin: 0 0 6px; padding: 0 0 0 var(--s-tight); border-left: var(--rule); color: var(--ink-2);
}

.actions { display: flex; gap: var(--s-prose); justify-content: flex-end; margin-top: 8px; font-family: var(--mono); font-size: 11px; }
.action-slot:empty { display: none; }
.actions button { color: var(--ink-3); letter-spacing: 0.04em; }
.actions button:hover { color: var(--slate); }

/* ------------------------------------------------------------------ outcome */

.outcome { display: flex; align-items: baseline; gap: var(--s-prose); margin-top: var(--s-prose); }
.outcome:empty { display: none; }
.outcome .said { font-family: var(--serif); font-style: italic; font-size: 18px; color: var(--ink-2); }
.outcome .recover {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--slate); border-bottom: 1px solid currentColor; padding-bottom: 1px; white-space: nowrap;
}
.outcome.no_match .said, .outcome.incomplete .said { color: var(--ink-2); }
.outcome.unavailable .said { color: var(--caution); }
/* A turn the reader stopped. Quieter than a failure, because nothing went wrong. */
.outcome.cancelled .said { color: var(--ink-3); }
