/* =============================================================================
   starti.app dev — an app, not a page.
   -----------------------------------------------------------------------------
   This runs in a brand's mobile webview, so it is built as an app shell: the
   viewport never scrolls, only the screen inside it does; chrome is translucent
   and content passes under it; screens push and pop.

   Two ideas carry the whole design.

   1. The type is the interface. Every SDK signature, argument name and returned
      value is monospace, and that is most of what is on screen. The system sans
      is reserved for chrome — titles, tabs, section labels.

   2. The grouped inset list. The ground is darker than the cards sitting on it,
      which is what makes a list of capabilities read as an app screen rather
      than a document. Depth comes from that step, not from shadows.

   Signal Orange is the brand's, and marks interaction only. Green and red are
   load-bearing: a console that cannot tell a return from a throw is not a
   console.
   ============================================================================ */

/* ---------- Tokens ---------- */

:root {
  color-scheme: light;

  --bg:         hsl(32 12% 94%);
  --surface:    hsl(0 0% 100%);
  --pressed:    hsl(32 10% 93%);
  --sep:        hsl(32 8% 88%);
  --field:      hsl(32 12% 96%);

  --text:       hsl(30 10% 12%);
  --text-2:     hsl(30 6% 38%);
  /* Small mono — parameter names, timestamps. 42%, not the 54% that reads as
     "tertiary": at 54% it is 3.4:1 on a white card, which is not readable text. */
  --text-3:     hsl(30 6% 42%);

  /*
   * Signal Orange, taken down to the lightness that carries text on a white
   * card. The brand's own 52% is 2.8:1 there, so every small orange label on a
   * light ground would be under half the readable minimum. Dark mode keeps the
   * bright value, where it is already well clear.
   */
  --accent:     hsl(27 95% 36%);
  --accent-bg:  hsl(27 95% 36% / 0.1);
  --on-accent:  hsl(0 0% 100%);

  --ok:         hsl(150 72% 27%);
  --ok-bg:      hsl(150 60% 94%);
  --err:        hsl(2 74% 44%);
  --err-bg:     hsl(2 74% 95%);
  --warn:       hsl(35 88% 32%);
  --warn-bg:    hsl(38 92% 92%);

  --chrome:     hsl(32 12% 94% / 0.8);
  --scrim:      hsl(30 10% 8% / 0.28);

  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, "Roboto Mono", Consolas, monospace;

  --chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");

  /* The content column, and the only number the chrome is measured from. */
  --column: 40rem;

  --navbar-h: 2.75rem;
  --sheet-bar-h: 2.75rem;
  --gutter: 1rem;
  --radius: 0.75rem;
  --r-pill: 0.8125rem;

  /* The curve UIKit pushes with: fast out of the gate, long quiet settle. */
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --push: 380ms;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg:         hsl(30 9% 7%);
    --surface:    hsl(30 8% 12%);
    --pressed:    hsl(30 8% 17%);
    --sep:        hsl(30 7% 20%);
    --field:      hsl(30 8% 9%);

    --text:       hsl(32 8% 96%);
    --text-2:     hsl(32 6% 64%);
    --text-3:     hsl(32 6% 47%);

    --accent:     hsl(27 92% 57%);
    --accent-bg:  hsl(27 92% 57% / 0.16);
    --on-accent:  hsl(30 20% 8%);

    --ok:         hsl(150 62% 58%);
    --ok-bg:      hsl(150 40% 15%);
    --err:        hsl(2 88% 70%);
    --err-bg:     hsl(2 45% 17%);
    --warn:       hsl(38 92% 63%);
    --warn-bg:    hsl(38 50% 15%);

    --chrome:     hsl(30 9% 7% / 0.78);
    --scrim:      hsl(0 0% 0% / 0.5);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg:         hsl(30 9% 7%);
  --surface:    hsl(30 8% 12%);
  --pressed:    hsl(30 8% 17%);
  --sep:        hsl(30 7% 20%);
  --field:      hsl(30 8% 9%);

  --text:       hsl(32 8% 96%);
  --text-2:     hsl(32 6% 64%);
  --text-3:     hsl(32 6% 47%);

  --accent:     hsl(27 92% 57%);
  --accent-bg:  hsl(27 92% 57% / 0.16);
  --on-accent:  hsl(30 20% 8%);

  --ok:         hsl(150 62% 58%);
  --ok-bg:      hsl(150 40% 15%);
  --err:        hsl(2 88% 70%);
  --err-bg:     hsl(2 45% 17%);
  --warn:       hsl(38 92% 63%);
  --warn-bg:    hsl(38 50% 15%);

  --chrome:     hsl(30 9% 7% / 0.78);
  --scrim:      hsl(0 0% 0% / 0.5);
}

/*
 * Safe areas, the two-source way the Manager does it: in a browser `env()` is
 * all there is, but inside the wrapper Android reports it as 0 and the native
 * layer publishes the real notch and home-indicator sizes as `--startiapp-inset-*`.
 * Take whichever is larger.
 */
:root {
  --inset-top: env(safe-area-inset-top, 0px);
  --inset-bottom: env(safe-area-inset-bottom, 0px);
  --inset-left: env(safe-area-inset-left, 0px);
  --inset-right: env(safe-area-inset-right, 0px);
}

/*
 * All four, not just the vertical pair. The shell gates the top and bottom on
 * whether it already inset the webview, but publishes left and right as the
 * real values unconditionally — it never insets horizontally — so in landscape
 * on a notched device those are the only thing standing between the content and
 * the cutout.
 */
body[startiapp] {
  --inset-top: max(env(safe-area-inset-top, 0px), var(--startiapp-inset-top, 0px));
  --inset-bottom: max(env(safe-area-inset-bottom, 0px), var(--startiapp-inset-bottom, 0px));
  --inset-left: max(env(safe-area-inset-left, 0px), var(--startiapp-inset-left, 0px));
  --inset-right: max(env(safe-area-inset-right, 0px), var(--startiapp-inset-right, 0px));
}

/* ---------- Reset ---------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html, body {
  height: 100%;
  overflow: hidden;              /* the app scrolls, the document never does */
  overscroll-behavior: none;
}

body {
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.45;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; text-align: left; }
ul, ol { list-style: none; padding: 0; }
svg { display: block; }
code, pre, kbd { font-family: var(--mono); }

::selection { background: var(--accent-bg); }
:root { accent-color: var(--accent); caret-color: var(--accent); }

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

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    transition-duration: 1ms !important;
  }
}

/* ---------- Shell ---------- */

.app {
  position: fixed;
  inset: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  background: var(--bg);
}

/* ---------- Nav bar ---------- */

/*
 * Translucent, with the screen scrolling under it. The title here is empty
 * until the screen's own large title scrolls up behind the bar, then it fades
 * in — so you always know where you are without spending a row on it.
 */
.navbar {
  position: relative;
  z-index: 3;
  padding-top: var(--inset-top);
  /* The bar's material reaches the edge; only its contents step in. */
  padding-inline: var(--inset-left) var(--inset-right);
  background: var(--chrome);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
}

@supports not (backdrop-filter: blur(1px)) {
  .navbar { background: var(--bg); }
}

.navbar-inner {
  height: var(--navbar-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.5rem;
  /* Less the back button's own inset, so its label lands on the column edge
     rather than the button's box doing. */
  padding-inline: calc(var(--gutter) - 0.375rem);
}

/*
 * The bar always names the screen you are on. It used to fade in only once the
 * large title scrolled under it — which meant any screen short enough not to
 * scroll, most of them on a phone, never named itself at all.
 */
.navbar-title {
  grid-column: 2;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  /* The outgoing name slides out under the bar's edge rather than past it. */
  overflow: hidden;
}

.navbar-title-text {
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* Out of flow while it leaves, so the slot is already the new name's width. */
.navbar-title-text.is-leaving {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.navbar-left  { grid-column: 1; justify-self: start; }

.navbar-right {
  grid-column: 3;
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  min-width: 0;
}

/*
 * The wordmark, set the way the Manager sets its own
 * (startiapp-manager/src/components/Wordmark.tsx): `starti.app` extralight in
 * Signal Orange, the product word semibold, tracking pulled tight. The one
 * place orange carries something you cannot press — it is the mark, not a label.
 *
 * One name per bar: the mark is the root screen's name, and it steps aside the
 * moment a screen has one of its own. A back button, a centred title and a
 * wordmark do not all fit across 390px anyway.
 */
.navbar .mark {
  /* Same size as the screen title it trades places with, so the bar reads as
     one slot holding one name rather than two things of different weight. */
  font-size: 0.9375rem;
  line-height: 1;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

.navbar .mark .lead { font-weight: 200; color: var(--accent); }
.navbar .mark .tail { font-weight: 600; color: var(--text); }

.navbar .mark[hidden] { display: none; }

.back {
  display: inline-flex;
  align-items: center;
  gap: 0.125rem;
  height: 2.25rem;
  padding-inline: 0.375rem 0.625rem;
  color: var(--accent);
  font-size: 0.9375rem;
  border-radius: 0.5rem;
  transition: opacity 120ms linear;
}

.back svg { width: 1.125rem; height: 1.125rem; }
.back:active { opacity: 0.4; }
.back[hidden] { display: none; }

/* ---------- Screen stack ---------- */

.stack {
  position: relative;
  overflow: hidden;
  background: var(--bg);
}

.screen {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--bg);
  /* The console bar floats over the bottom of every screen; clear it. */
  padding-block: 0.75rem calc(var(--sheet-bar-h) + var(--inset-bottom) + 2rem);
  padding-inline: calc(var(--gutter) + var(--inset-left)) calc(var(--gutter) + var(--inset-right));
  will-change: transform;
}

/*
 * Push and pop, the platform way: the incoming screen comes in from the right
 * at full speed while the one behind it slides a third of the way out and dims.
 * Leaving reverses exactly — a screen that arrived from the right leaves to the
 * right, so the spatial model survives.
 */
/*
 * Layering matters as much as the transform: the screen you are leaving sits
 * under the dimming layer, the one arriving sits over it. Get that backwards
 * and every pushed screen renders behind a scrim.
 */
.screen[data-state="ahead"]  { transform: translate3d(100%, 0, 0); visibility: hidden; z-index: 2; }
.screen[data-state="active"] { transform: translate3d(0, 0, 0); z-index: 2; }
.screen[data-state="behind"] { transform: translate3d(-28%, 0, 0); visibility: hidden; z-index: 0; }

.screen[data-animating="true"] {
  visibility: visible;
  transition: transform var(--push) var(--ease);
}

.screen-dim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--push) var(--ease);
}

.screen-dim[data-shown="true"] { opacity: 1; }

/*
 * While a finger owns the stack, nothing may animate: the screens are being
 * positioned every frame from the pointer, and a transition on top of that is
 * lag. `visibility` is lifted too, because the screen being dragged back into
 * view is the one CSS has parked as hidden.
 */
.stack[data-dragging="true"] .screen,
.stack[data-dragging="true"] .screen-dim {
  transition: none;
  visibility: visible;
}

/*
 * Vertical panning stays the browser's; horizontal is ours. Without this the
 * browser claims the gesture before the first pointermove arrives.
 */
.screen { touch-action: pan-y; }

/* ---------- Large title ---------- */

/* All that is left of the screen heading is the namespace it exercises; the
   name itself lives in the bar, where it cannot scroll away. */
.title {
  padding: 0.125rem 0.25rem 0.75rem;
}

.title code {
  display: block;
  font-size: 0.75rem;
  color: var(--text-3);
}

/* ---------- Grouped inset list ---------- */

.group { margin-bottom: 1.375rem; }
.group:last-child { margin-bottom: 0; }

.group-label {
  padding: 0 0.75rem 0.375rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.group-note {
  padding: 0 0.75rem 0.5rem;
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--text-2);
}

.group-note code { font-size: 0.75rem; color: var(--text); }

.list {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
}

.list > li + li { border-top: 1px solid var(--sep); }

/* ---------- Row: one row is one SDK call ---------- */

.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  min-height: 2.875rem;
  padding: 0.625rem 0.875rem;
  transition: background 90ms linear;
}

/* Feedback on press, not on release. */
.row:active { background: var(--pressed); transition: none; }

.row code {
  font-size: 0.8125rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

.row small {
  grid-column: 1;
  font-family: var(--sans);
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--text-2);
  margin-top: 0.125rem;
}

.row-go {
  grid-row: 1 / -1;
  grid-column: 2;
  width: 1.125rem;
  height: 1.125rem;
  color: var(--text-3);
  transition: color 90ms linear, transform 90ms linear;
}

.row:active .row-go { color: var(--accent); transform: translateX(1px); }

.row[data-busy="true"] .row-go { color: var(--accent); animation: blink 900ms steps(2, end) infinite; }

@keyframes blink { 50% { opacity: 0.25; } }

/* A row that only navigates, no call. */
.row-nav { padding-block: 0.5rem; row-gap: 0.0625rem; }
.row-nav code { color: var(--text); font-family: var(--sans); font-size: 0.9375rem; font-weight: 500; }

/*
 * The method list under an index row is search text that happens to be visible:
 * it hints at what is inside without spending three lines on it. `filterIndex`
 * still matches the whole string, not the part that fits.
 */
.row-nav small {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 0;
}
.row-nav .row-meta {
  grid-row: 1 / -1;
  grid-column: 2;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--text-3);
}

/* ---------- Return value ---------- */

.out {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 260ms var(--ease);
}

.out > div { overflow: hidden; }
.out[data-shown="true"] { grid-template-rows: 1fr; }

.out pre {
  margin: 0;
  padding: 0.5rem 0.875rem 0.625rem;
  font-size: 0.75rem;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 12rem;
  overflow: auto;
}

.out[data-kind="ok"]    pre { background: var(--ok-bg);  color: var(--ok); }
.out[data-kind="error"] pre { background: var(--err-bg); color: var(--err); }

/* ---------- Arguments ---------- */

.args {
  padding: 0.5rem 0.875rem 0.625rem;
  display: grid;
  gap: 0.5rem;
}

.arg { display: grid; gap: 0.1875rem; }

/* The label is the parameter's real name, so it is set like one. */
.arg-name {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--text-3);
}

.arg-name em {
  font-style: normal;
  color: var(--text-3);
  opacity: 0.75;
}

.arg input, .arg select, .arg textarea {
  width: 100%;
  height: 2.25rem;
  padding: 0 0.5rem;
  border: 1px solid var(--sep);
  border-radius: 0.5rem;
  background: var(--field);
  font-family: var(--mono);
  /* 1rem, so iOS does not zoom the whole app when a field takes focus. */
  font-size: 1rem;
  transition: border-color 120ms linear;
}

.arg textarea {
  height: auto;
  min-height: 4.5rem;
  padding: 0.4375rem 0.5rem;
  font-size: 0.8125rem;
  line-height: 1.5;
  resize: vertical;
}

.arg input:focus, .arg select:focus, .arg textarea:focus {
  outline: none;
  border-color: var(--accent);
}

/*
 * A pointer gets the border colour and nothing else — a ring on every tap is
 * noise. A keyboard gets the ring: the border alone is a one-pixel colour
 * change, which is not somewhere you can see you are.
 */
.arg input:focus-visible,
.arg select:focus-visible,
.arg textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.arg input[type="color"] { padding: 0.1875rem; cursor: pointer; }
.arg input[type="range"] { height: 1.75rem; padding: 0; border: 0; background: none; }

.arg select {
  appearance: none;
  padding-right: 1.75rem;
}

/*
 * The chevron is masked rather than painted, so its colour comes from a token
 * like every other colour here. A data URI cannot read a custom property, which
 * is why this was a hardcoded hex — and then a second hardcoded hex to undo it
 * in dark mode. A mask reads only the alpha channel, so the stroke colour
 * inside the SVG is arbitrary and one definition serves both themes.
 */
.arg:has(select) { position: relative; }

.arg:has(select)::after {
  content: "";
  position: absolute;
  right: 0.5rem;
  /* Centred on the control, which is the last thing in the field. */
  bottom: calc((2.25rem - 0.875rem) / 2);
  width: 0.875rem;
  height: 0.875rem;
  background: var(--text-3);
  -webkit-mask: var(--chevron-down) center / contain no-repeat;
  mask: var(--chevron-down) center / contain no-repeat;
  pointer-events: none;
}

.args-row { display: flex; gap: 0.5rem; }
.args-row > .arg { flex: 1 1 0; min-width: 0; }

/* ---------- Toggle row ---------- */

.toggle {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: 2.875rem;
  padding: 0.5rem 0.875rem;
  /* The signature step, so the label sits level with the `code` beside it. */
  font-size: 0.8125rem;
  cursor: pointer;
}

.toggle code { font-size: 0.8125rem; }
.toggle input { margin-left: auto; width: 1.125rem; height: 1.125rem; flex: none; }

/* ---------- Plain button rows ---------- */

.actions { padding: 0.625rem 0.875rem; display: flex; flex-wrap: wrap; gap: 0.5rem; }

.btn {
  flex: 1 1 5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.25rem;
  padding-inline: 0.875rem;
  border-radius: 0.5rem;
  background: var(--field);
  border: 1px solid var(--sep);
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
  transition: background 90ms linear, transform 90ms var(--ease);
}

.btn:active { background: var(--pressed); transform: scale(0.98); }
.btn-accent { background: var(--accent); border-color: transparent; color: var(--on-accent); }
.btn-accent:active { filter: brightness(0.92); }

/* A segmented choice has to say which one is chosen. */
.btn[aria-pressed="true"] {
  background: var(--accent-bg);
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

/* ---------- Chips ---------- */

.chips { padding: 0.625rem 0.875rem; display: flex; flex-wrap: wrap; gap: 0.375rem; }
.chips:empty { display: none; }

.chip {
  height: 1.625rem;
  padding-inline: 0.625rem;
  border-radius: var(--r-pill);
  background: var(--accent-bg);
  color: var(--accent);
  font-family: var(--mono);
  font-size: 0.6875rem;
}

.chip:active { filter: brightness(0.9); }

/* ---------- Notices and links ---------- */

.notice {
  display: grid;
  gap: 0.125rem;
  padding: 0.625rem 0.875rem;
  background: var(--warn-bg);
  border-radius: var(--radius);
  margin-bottom: 1.375rem;
  color: var(--warn);
  font-size: 0.8125rem;
  line-height: 1.4;
}

.notice b { font-weight: 600; }
.notice[hidden] { display: none; }

.links { padding: 0.5rem 0.875rem; display: grid; gap: 0.375rem; font-size: 0.8125rem; }
.links a { color: var(--accent); text-underline-offset: 0.2em; }

.empty {
  padding: 3rem 1rem;
  text-align: center;
  color: var(--text-3);
  font-size: 0.8125rem;
}

/* ---------- Console screen ---------- */

.console-log {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 0.5rem 0;
  overflow: hidden;
}

.console-line {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.5rem;
  padding: 0.125rem 0.875rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.5;
}

.console-time {
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  font-size: 0.6875rem;
  padding-top: 0.0625rem;
}

.console-text { white-space: pre-wrap; overflow-wrap: anywhere; }

.console-line[data-kind="call"]  .console-text { color: var(--text-2); }
.console-line[data-kind="ok"]    .console-text { color: var(--ok); }
.console-line[data-kind="error"] .console-text { color: var(--err); }
.console-line[data-kind="event"] .console-text { color: var(--text); }
.console-line[data-kind="info"]  .console-text { color: var(--text-2); }

/* ---------- Search ---------- */

.search {
  position: relative;
  margin-bottom: 0.875rem;
}

.search input {
  width: 100%;
  height: 2.25rem;
  padding: 0 0.625rem 0 2rem;
  border: 0;
  border-radius: 0.5rem;
  background: var(--surface);
  font-size: 1rem;
}

.search input:focus { outline: 2px solid var(--accent); outline-offset: -2px; }

.search svg {
  position: absolute;
  left: 0.625rem;
  top: 50%;
  translate: 0 -50%;
  width: 1rem;
  height: 1rem;
  color: var(--text-3);
  pointer-events: none;
}

/* ---------- Icon button ---------- */

.icon-btn {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 0.5rem;
  color: var(--text-2);
  transition: color 120ms linear;
}

.icon-btn svg { width: 1.125rem; height: 1.125rem; }
.icon-btn:active { color: var(--accent); }

/* ---------- Index footer ---------- */

.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.75rem;
  padding: 1.25rem 0.25rem 0;
  font-size: 0.75rem;
  color: var(--text-3);
}

.footer a { color: var(--accent); text-underline-offset: 0.2em; }

/* ---------- Console drawer ---------- */

/*
 * A sheet you drag, not a tab you leave the screen for: the console is what you
 * are watching while you fire calls, so it has to be reachable without losing
 * your place. Its transform is driven from JS during a drag and handed back to
 * this transition on release.
 */
.sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  height: min(62dvh, 30rem);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-top: 1px solid var(--sep);
  border-radius: 0.875rem 0.875rem 0 0;
  box-shadow: 0 -12px 40px -14px hsl(0 0% 0% / 0.35);
  padding-bottom: var(--inset-bottom);
  padding-inline: var(--inset-left) var(--inset-right);
  will-change: transform;
}

.sheet[data-settling="true"] { transition: transform 420ms var(--ease); }

.sheet-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  height: var(--sheet-bar-h);
  padding-inline: var(--gutter);
  cursor: grab;
  /* The bar owns the vertical gesture; the browser must not scroll instead. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.sheet-bar:active { cursor: grabbing; }

.sheet-icon { width: 0.9375rem; height: 0.9375rem; color: var(--text-3); flex: none; }

.sheet-peek {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--text-2);
}

.sheet-count {
  flex: none;
  min-width: 1.25rem;
  height: 1.25rem;
  padding-inline: 0.3125rem;
  border-radius: var(--r-pill);
  background: var(--bg);
  color: var(--text-3);
  font-family: var(--mono);
  font-size: 0.6875rem;
  line-height: 1.25rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
  transition: background 150ms linear, color 150ms linear;
}

/* Something arrived while you were not looking. */
.sheet-count[data-unread="true"] { background: var(--accent-bg); color: var(--accent); }

.sheet-chevron {
  flex: none;
  width: 0.9375rem;
  height: 0.9375rem;
  color: var(--text-3);
  transition: transform 220ms var(--ease);
}

.sheet[data-open="true"] .sheet-chevron { transform: rotate(180deg); }

.sheet-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-top: 1px solid var(--sep);
  padding-block: 0.375rem;
}

.sheet-actions {
  flex: none;
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem var(--gutter);
  border-top: 1px solid var(--sep);
}

.sheet-actions .btn { flex: 1; }

/* Nothing below the bar is reachable while it is down. */
.sheet[data-open="false"] .sheet-body,
.sheet[data-open="false"] .sheet-actions { visibility: hidden; }

.sheet .console-log { background: none; border-radius: 0; padding: 0; }
.sheet .empty { padding: 2rem 1rem; }

/* ---------- Tablet ---------- */

/*
 * One step wider and then a stop. There is no desktop layout: the tool runs on
 * the device it tests, and a laptop is only where the source gets read.
 */
@media (min-width: 44rem) {
  .screen {
    padding-inline:
      max(calc(var(--gutter) + var(--inset-left)), calc((100% - var(--column)) / 2))
      max(calc(var(--gutter) + var(--inset-right)), calc((100% - var(--column)) / 2));
  }

  /* The chrome stops where the column stops; a nav bar wider than the list it
     sits over leaves its back button hanging outside the content's edge. */
  .navbar-inner,
  .sheet {
    width: min(100%, calc(var(--column) + var(--gutter) * 2));
    margin-inline: auto;
  }

  .sheet { left: 50%; translate: -50%; }
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
