/* Palette is defined once per scheme; everything else reads the variables.
   Every pair below was measured from rendered pixels, not from intent: body
   text clears 4.5:1 on the surface it sits on, and rules, focus rings and the
   selection fill clear 3:1. That is why the greys are darker than a hairline
   usually wants to be. */
:root {
  color-scheme: light dark;
  --bg: #fbfbf9;
  --surface: #ffffff;
  --ink: #1b1b1a;
  --ink-soft: #5c5c58;
  --ink-faint: #67675f;      /* 5.70:1 on surface, 5.50:1 on bg */
  --rule: #8f8f89;           /* 3.25:1 on surface */
  --rule-strong: #7f7f79;    /* 4.03:1 on surface */
  --accent: #3d5a80;
  --select-fill: #e9eff7;    /* selection tint; the bar carries the 3:1 */
  --hover: #f0f0ec;
  --danger: #8c3b2f;
  --danger-soft: #f7edeb;
  --focus: #2f6fb3;
  --kind-user: #6b6b64;
  --kind-thought: #3d5a80;
  --kind-call: #5a5030;
  --kind-result: #2f5d4a;
  --kind-final: #4a3663;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16171a;
    --surface: #1d1f23;
    --ink: #e7e6e2;
    --ink-soft: #a9a8a2;
    --ink-faint: #8f8e89;    /* 5.03:1 on surface, 5.46:1 on bg */
    --rule: #656e7a;         /* 3.19:1 on surface */
    --rule-strong: #7a828e;  /* 4.25:1 on surface */
    --accent: #9db9dc;
    --select-fill: #212630;  /* selection tint; the bar carries the 3:1 */
    /* Below the selection fill, not above it: at #26282d the row under the
       pointer measured 1.119:1 against the surface while the *chosen* row
       measured 1.088:1, so the brightest row in the list was whichever one the
       mouse happened to be resting on. 1.075:1 now — present, and outranked. */
    --hover: #232529;
    --danger: #e2938a;
    --danger-soft: #33231f;
    --focus: #7fb0e8;
    --kind-user: #a5a49d;
    --kind-thought: #9db9dc;
    --kind-call: #d3bd85;
    --kind-result: #8dc4a9;
    --kind-final: #c0a8dd;
  }
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  /* The detail pane is rebuilt on every selection and its height changes with
     it. Left to the browser's scroll anchoring, that resize dragged the page
     under the reader by however much the pane had grown — up to 300px per
     arrow key. Nothing on this page loads in above the fold, so there is
     nothing for anchoring to earn back. */
  overflow-anchor: none;
  background: var(--bg);
  color: var(--ink);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Every other size on this page is in rem, so the base is too: pinned to px
     it was the one declaration that ignored a reader who had raised the
     browser's own font size. Identical at the 16px default. */
  font-size: 1rem;
  line-height: 1.55;
  /* Long unbroken tokens in hostile input must never widen the page. */
  overflow-wrap: anywhere;
}

/* One deliberate scale: 0.7rem for the small-caps labels, 0.8rem for asides,
   0.875rem for interface text, 1rem for prose, and a clamp for the title. Ten
   sizes inside a 5px band said nothing that four do not. */

.topbar, main { max-width: 76rem; margin: 0 auto; padding: 0 clamp(0.75rem, 3vw, 2rem); }

.topbar { padding-top: clamp(1rem, 4vw, 2rem); }

h1 {
  font-size: clamp(1.25rem, 1rem + 1.2vw, 1.6rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 0;
}

.tagline { margin: 0.25rem 0 0; color: var(--ink-soft); font-size: 0.875rem; }

/* The paste box sits under the run, which on a phone is well below the fold.
   One line above the fold says where it is, and goes there.

   Hiding it above the breakpoint was tried and reverted: the premise was that
   the box is on the first screen at those widths, and it is not. At a viewport
   640px tall the TRANSCRIPT JSON label sits 654-733px down at every width from
   737 to 1920 — measured with this line hidden, which is the case being tested;
   on the shipped page, with the line shown, the same label sits 677-756px down;
   at 700 tall it is still below the fold at 737, 800 and 900; and the Render
   button is below the fold at every width until the viewport is about 1010px
   tall. A maximised window on a 1366x768 laptop has roughly 657px of viewport.
   Whether the box is on the first screen is a question about height, and a
   width query cannot answer it. Shown everywhere. */
.signpost { margin: 0.25rem 0 0; font-size: 0.8rem; }
.signpost a { color: var(--accent); text-underline-offset: 3px; }

.h2-aside {
  margin-left: 0.5rem;
  letter-spacing: 0.04em;
  text-transform: none;
  font-weight: 400;
  white-space: nowrap;
}

h2 {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  font-weight: 600;
  color: var(--ink-faint);
  margin: 0 0 0.5rem;
}

/* The run comes first at every width: the loop this page exists to show is on
   the first screen, with the paste box under it and a line in the header
   pointing at it. This used to be done with flex `order` over an unchanged
   source order, on the reasoning that leaving the DOM alone left Tab alone.
   It did — and that was the bug: Tab kept following the source, so from a cold
   1280x800 load focus dived 680px to the page foot and jumped 714px back up
   (WCAG 2.4.3, focus order). The panel now sits last in the markup, which is
   where it is drawn, so the two orders are the same order. */
main { display: flex; flex-direction: column; padding-bottom: 4rem; }
.input-panel { margin-top: clamp(1rem, 3vw, 1.5rem); }

.field-label {
  display: block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-faint);
  margin-bottom: 0.25rem;
}

textarea {
  width: 100%;
  min-height: 5rem;
  resize: vertical;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--rule-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 0.875rem;
  line-height: 1.5;
}

.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-top: 0.5rem;
}

button {
  font: inherit;
  font-size: 0.875rem;
  min-height: 2.75rem;              /* 44px: a target a thumb can hit */
  padding: 0.5rem 1rem;
  border-radius: 5px;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--bg);
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}

button.ghost { background: transparent; color: var(--accent); }

/* Restrained hover feedback: the pointer should get an answer from anything
   that answers a click. */
button:hover:not([disabled]) { background: color-mix(in srgb, var(--accent) 85%, var(--ink)); }
button.ghost:hover:not([disabled]) { background: var(--hover); color: var(--accent); }

button[disabled] { opacity: 0.55; cursor: default; }

/* Busy state: reading a multi-megabyte file blocks the thread, and silence for
   several seconds reads as a hung page. */
body.is-busy { cursor: progress; }
.status.busy { color: var(--ink); }
.status.busy::before {
  content: '';
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  margin-right: 0.25rem;
  border-radius: 50%;
  background: var(--accent);
  animation: busy-pulse 1s ease-in-out infinite;
}
@keyframes busy-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }

/* The rule that marks a link has to sit on the text. As a bottom border it
   sat on the bottom of a 44px tap target, 15px clear of the words and the full
   width of the button, which read as a stray section rule. */
button.link {
  background: none;
  border: none;
  border-radius: 0;
  color: var(--accent);
  padding: 0;
  min-height: 2.75rem;
  display: inline-flex;
  align-items: center;
  font-size: 0.875rem;
  text-decoration: underline;
  text-underline-offset: 3px;
}
button.link:hover:not([disabled]) { background: none; color: var(--ink); }

.hint { color: var(--ink-faint); font-size: 0.8rem; margin: 0.25rem 0 0; }

.status { min-height: 1.5rem; margin: 0.5rem 0 0; font-size: 0.875rem; color: var(--ink-soft); }
.status.error { color: var(--danger); }

.summary {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.5rem;
  margin-top: 1rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  font-size: 0.875rem;
  color: var(--ink-soft);
}
.summary:empty { display: none; }
.summary .metric-value { color: var(--ink); font-variant-numeric: tabular-nums; }
/* A metric keeps its place in the strip, so the button stays the size of the
   number it shows — and carries a 44px target around it that costs no layout. */
.summary button.link.metric-value { min-height: 0; color: var(--accent); position: relative; }
.summary button.link.metric-value::after {
  content: '';
  position: absolute;
  left: -0.75rem;
  right: -0.75rem;
  top: 50%;
  height: 2.75rem;
  transform: translateY(-50%);
}
.summary button.link.metric-value:hover { color: var(--ink); }
.summary .metric-label { color: var(--ink-faint); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; white-space: nowrap; }
.summary .metric { display: flex; flex-direction: column; min-width: 0; }
/* The tool list wraps and is never cut mid-identifier. */
.summary .metric-tools { flex: 1 1 18rem; }
/* A folded disclosure is one short line and has nothing to grow into, so it
   should not take the leftover width: growing it is what pushed FORMAT onto a
   row of its own between 704 and 760px. Where `:has()` is unsupported the whole
   rule is dropped and the strip is exactly what it was. */
.summary .metric-tools:has(.tools-disclosure:not([open])) { flex: 0 1 auto; }
.summary .metric-tools .metric-value { white-space: normal; overflow-wrap: anywhere; }
.tools-disclosure { min-width: 0; }
.tools-disclosure > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  position: relative;
}
/* The target is drawn, not laid out — the same trick the error count uses, so a
   44px tap area costs the strip no height. It was the other way round at first:
   44px on the desktop, where the list is already open and nothing needs
   tapping, and 28px on the phone, the one width where it starts folded. */
.tools-disclosure > summary::before {
  content: '';
  position: absolute;
  left: -0.5rem;
  right: -0.5rem;
  top: 50%;
  height: 2.75rem;
  transform: translateY(-50%);
}
.tools-disclosure > summary::-webkit-details-marker { display: none; }
/* Shown open as well as shut: it is now the only place the tool count lives. */
.tools-count { color: var(--ink); font-variant-numeric: tabular-nums; }
/* A caret, drawn in text so it inherits the label's colour and needs no asset. */
.tools-disclosure > summary::after {
  content: '\25B8';
  font-size: 0.95em;
  color: var(--ink-faint);
  transition: transform 120ms ease;
  display: inline-block;
}
.tools-disclosure[open] > summary::after { transform: rotate(90deg); }
.tools-disclosure > summary:hover { color: var(--ink); }
.tools-disclosure > summary:hover::after { color: var(--ink); }

.run-notes { margin-top: 0.5rem; }
.run-notes[hidden] { display: none; }
.run-notes .note { margin: 0.25rem 0 0; max-width: 60rem; }

/* One breakpoint, and it governs the whole page. The columns used to be
   container-driven (`auto-fit` over `minmax(min(100%, 20rem), 1fr)`) while the
   rules at the foot of this file and the script's `NARROW` were viewport-driven
   at 46rem. The two agreed nowhere in particular: for a viewport W the padding
   and gap are both 3vw, so two tracks appear once 640 <= 0.91W — from 703.3px
   up. Between there and 736px the grid had already split while the narrow rules
   still ran, so the timeline gave up 168px of height beside a column that was
   already there, and `revealPair` — whose premise is that at this width the
   detail sits *under* the list — scrolled the window for a pane sitting next to
   the row. The script cannot read a container query, so the layout follows the
   script's breakpoint rather than the other way round.

   They agree at 43.99rem, not at 46rem, because the band belongs to the wide
   side: 703.3px is where two 20rem columns fit, and unifying upward gave the
   band the phone's rules instead. Measured at 720x800, as document height /
   timeline rows visible / detail card top — base 901 / 3 / 305; unified at
   46rem 1219 / 4 / 624; unified here 1144 / 5 / 381. The band reads better as
   a small tablet than as a large phone, and the extra height over base is the
   timeline's wide max-height, which is what buys the two extra rows. 43.99rem
   puts the flip between 703 and 704. */
.panes {
  display: grid;
  gap: clamp(1rem, 3vw, 2rem);
  grid-template-columns: 1fr 1fr;
  margin-top: 1.5rem;
  align-items: start;
}

.pane { min-width: 0; }

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--surface);
  max-height: clamp(16rem, 55vh, 34rem);
  overflow-y: auto;
  overflow-x: hidden;
}

.step {
  display: block;
  position: relative;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--rule);
  border-left: 4px solid transparent;
  cursor: pointer;
  min-width: 0;
  transition: background-color 120ms ease;
}
.step:last-child { border-bottom: none; }
/* Never on the selected row: measured, the hover fill was *brighter* than the
   selection fill in dark (1.119:1 against 1.088:1 on the surface), so a mouse
   resting anywhere in the list made the row under the pointer read as the
   chosen one and left the 4px bar carrying the selection alone. */
.step:hover:not([aria-selected="true"]) { background: var(--hover); }
/* The selection is carried by the bar, not by the fill. A fill saturated
   enough to clear 3:1 on its own was the loudest thing on the page in dark
   mode, and forcing one ink over it switched off the very kind colour the row
   you are reading is there to show. The bar clears 3:1 against the surface on
   its own; the fill is a tint, so every label keeps its own hue and its own
   measured ratio. */
.step[aria-selected="true"] { background: var(--select-fill); border-left-color: var(--accent); }
.step:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

.step-head { display: flex; gap: 0.5rem; align-items: baseline; }
.step-index { color: var(--ink-faint); font-size: 0.7rem; font-variant-numeric: tabular-nums; min-width: 1.5rem; }
.step-kind { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
.step-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  color: var(--ink-soft);
  font-size: 0.875rem;
  /* Reasoning rows wrap to three lines: following a loop should not cost one
     click per step. The detail pane still carries the whole thing. */
  overflow: hidden;
}

/* A failure is named, not only tinted: the two hues sat at near-identical
   luminance in 10.88px type. */
.step-flag {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 600;
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: 3px;
  padding: 0 0.25rem;
}

/* The call/result pairing, visible in the list itself rather than only behind
   a link in the detail pane. */
.step-pair {
  margin-left: auto;
  font-size: 0.7rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A result is indented under its call and joined to it by an elbow. */
.step.kind-tool-result { padding-left: 1.75rem; }
.step.kind-tool-result::before {
  content: '';
  position: absolute;
  left: 0.75rem;
  top: 0;
  width: 0.5rem;
  height: 1rem;
  border-left: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-bottom-left-radius: 3px;
}

.kind-user .step-kind { color: var(--kind-user); }
.kind-thought .step-kind { color: var(--kind-thought); }
.kind-thinking .step-kind { color: var(--kind-thought); font-style: italic; }
.kind-tool-call .step-kind { color: var(--kind-call); }
.kind-tool-result .step-kind { color: var(--kind-result); }
.kind-final .step-kind { color: var(--kind-final); }
.kind-note .step-kind, .kind-system .step-kind { color: var(--ink-faint); }
.step.is-error .step-kind { color: var(--danger); }

.detail {
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--surface);
  padding: clamp(0.75rem, 2vw, 1rem);
  min-width: 0;
}

.detail-kind { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.09em; font-weight: 600; color: var(--ink-faint); }
.detail-title { font-size: 1rem; font-weight: 600; margin: 0.25rem 0 0; }
.detail-meta { color: var(--ink-faint); font-size: 0.8rem; margin: 0.25rem 0 0; }
.detail-section { margin-top: 1rem; }
.detail-section > h3 {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--ink-faint); font-weight: 600; margin: 0 0 0.25rem;
}

.error-flag {
  display: inline-block;
  margin-top: 0.5rem;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--danger);
  border-radius: 4px;
  color: var(--danger);
  background: var(--danger-soft);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

/* Clamp bounds the idle height only. The class is applied at render time solely
   when the content overflows AND a reveal control exists, so nothing is ever
   clipped without a way to open it. */
.value-slot { position: relative; }
.value-slot.clamped .value-body { max-height: 12rem; overflow: hidden; }
.value-fade {
  display: none;
  height: 2.5rem;
  margin-top: -2.5rem;
  position: relative;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none;
}
.value-slot.clamped .value-fade { display: block; }
.value-body { position: relative; min-width: 0; }
/* Open is a taller region that scrolls itself, not a value that runs on for
   thousands of pixels with its only control at the far end. */
.value-slot.expanded .value-body {
  max-height: 60vh;
  overflow: auto;
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.5rem;
}

pre.value, p.value {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 0.8rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
  /* Contain any bidi control characters that survive sanitising. */
  unicode-bidi: isolate;
}

.value-toggle { margin-top: 0.25rem; }

.note { color: var(--ink-faint); font-size: 0.8rem; margin: 0.5rem 0 0; }
.empty { color: var(--ink-faint); font-size: 0.875rem; }

.drop-veil {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  border: 2px dashed var(--accent);
  color: var(--accent);
  font-size: 1rem;
  /* Defensive: whatever else goes wrong with a drag, this layer can never eat
     a click. The drop itself is handled on window, not on the veil. */
  pointer-events: none;
}
.drop-veil[hidden] { display: none; }

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

/* One column. The list keeps its own bounded scroll here, because letting it
   run to its full height put the detail pane three and a half screens down and
   sent the row you tapped off the top of the window. It is bounded to a little
   over a third of the screen, so the list, the row you are on and the top of
   the detail are on the same screen and the page still has plenty of surface
   left to swipe on. */
@media (max-width: 43.99rem) {
  .timeline { max-height: clamp(9rem, 34vh, 20rem); overflow-y: auto; }
  .summary {
    gap: 0.25rem 1rem;
    font-size: 0.8rem;
    margin-top: 0.75rem;
  }
  .summary .metric { flex-direction: row; gap: 0.4rem; align-items: baseline; }
  .summary .metric-tools { flex: 1 1 100%; }
  /* Wrapping, here as at every other width: an ellipsis in the middle of
     query_wareh… names no tool anyone can look up. */
  .summary .metric-tools .metric-value { display: block; }
  /* Folded shut here, so the strip is a line rather than a screen. */
  .tools-disclosure { display: block; }
  /* The label repeats the placeholder, so it is kept for screen readers and
     taken out of the flow. */
  .field-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  textarea { min-height: 3.5rem; }
  /* The hint is the only text on the page that names the drop target and the
     Ctrl/Cmd + Enter shortcut. It used to be hidden here to keep the form off
     the first screen — but the form is at the foot now, so hiding it bought
     nothing and left two affordances documented nowhere on a phone. It reads
     as its own line rather than trailing the buttons. */
  .hint { display: block; }
  .panes { grid-template-columns: 1fr; margin-top: 0.75rem; }
}

@media (prefers-reduced-motion: reduce) {
  /* The pseudo-elements too: `*` alone does not match them, and the caret on
     the tools disclosure is drawn by summary::after, so its 120ms rotation
     went on running under reduce — on a control that is now folded by default
     below 48rem and toggled at more widths than it used to be. */
  *, *::before, *::after { transition: none !important; }
  .status.busy::before { animation: none; opacity: 1; }
  html { scroll-behavior: auto; }
}
