/* ═══════════════════════════════════════════════════════════════════════════════════════
   quackd — the browser simulator.

   Hand-authored CSS in the quackd-web design language (www.quackd.org): cream ground with a
   radial wash, film grain, purple/gold brand, Nunito Sans + DM Mono, 1rem radii, hard-drop
   button shadows, a 2px #7c4a9e focus ring.

   THREE RULES THAT DECIDE EVERY LATER QUESTION
   R1  The page has exactly two objects plus a brandline: .brandline, section.console,
       footer. Everything needed during a run lives inside the console: the credential band
       across the top, the arena beneath it with the cockpit as a full-height right column,
       and the deck — lane A "say it", lane B the log — under the arena.
   R2  Both ways of driving are visible AT ONCE, without scrolling. That simultaneity is the
       dual-control argument, and the LAYOUT is what makes it. This rule used to demand that
       the sentence box and the keycaps be adjacent in one lane, and they were — adjacent to
       each other and both under the fold, together with the key box, so a visitor typed a
       sentence, pressed Run and got an error about a credential the page had never shown
       them. The console is one screen now: the switch and 03 in a full-width band across the
       top, the arena with 01 and 04 beneath it, 02 down the right side spanning both. Nothing
       added here may push the band, the arena, the sentence box, the log or the keycaps off a
       real 1366×768 laptop — 641px of viewport once the browser has taken its chrome, which
       is the number to test against, not 768. Adjacency was a proxy for simultaneity; this is
       the thing itself. Do not restore the vertical stack.
       The bays are named, not numbered. They were numbered when the page was one column
       and the numbers read down it; in this layout the eye goes key, arena, sentence, log,
       keycaps, so a number on each heading only claimed an order that is not there.
   R3  Machine speech is mono, human speech is sans. Verb calls, results, #readout, keycaps,
       eyebrows and bay names are DM Mono; the goal line, the refusal paragraph and every
       .fine are Nunito Sans. This is deliberate — do not "fix" the mixed-family log.

   Exactly ONE dark surface exists on this page: .terminal-window (#483652). The log is
   content, not chrome. Everything else is cream.

   Section order below: tokens · reset · class registry · base/type · shell · brandline ·
   console/stage · the model bar · stage chrome · deck · controls · cockpit · key box ·
   terminal · footer · keyframes · responsive.
   ═════════════════════════════════════════════════════════════════════════════════════ */

/* ── tokens ──────────────────────────────────────────────────────────────────────────── */

:root {
  /* Was `dark`. Every native control — the <select> popup, the password reveal eye — renders
     wrong against a cream page under a dark scheme. */
  color-scheme: light;

  /* ground */
  --cream: #fffdf7;
  --card: #ffffff;
  --card-soft: linear-gradient(145deg, rgba(255, 255, 255, 0.94), rgba(250, 243, 255, 0.94));
  --lilac: #f2e9fa;
  --lilac-edge: #e2d2ef;
  --lilac-2: #f7f1fb;
  --tint: #f8f0ff;        /* the hover wash under every outline control */

  /* ink, every value checked against --cream */
  --ink: #3e294e;    /* 12.6:1 */
  --ink-2: #4e365b;  /* 10.6:1 */
  --body: #6b5875;   /*  5.9:1 */
  --meta: #7a6784;   /*  4.9:1 */
  --btn-ink: #5f4577;     /* quackd-web's control ink, 7.3:1 */

  /* edges */
  --edge: #dfcce9;
  --edge-3: #ecdff2;
  --edge-guard: #e4d3ee;  /* quackd-web's .guard-card border */
  --edge-soft: #cdb3e6;
  --field: #ddc8ea;

  /* brand */
  --primary: #8a5cc4;
  --primary-deep: #7c4a9e;
  --primary-ink: #573c68;
  --pink: #a34a72;        /* 5.4:1 — eyebrows, links */
  --pink-hi: #ad5079;     /* the pink button's hover, 5.0:1 against white */
  --pink-deep: #7c3457;   /* its plinth and its pressed link */
  --pink-edge: #e2c3d6;
  --pink-rule: #e7a7c4;
  --gold: #f5c533;
  --gold-hi: #f8cb43;
  --gold-deep: #d59f18;
  --gold-press: #cf9a15;
  --gold-dot: #d99a12;    /* the live dot, and the hand pill's ink lives beside it */
  --gold-ink: #4b3828;    /* text ON gold, 8:1 */
  --gold-ink-warm: #6d4c10;
  --bad: #a3312f;
  /* Deliberately the same hue as --pink: with the language layer off the page has ONE
     off-brand accent, not two. The pink #run is a two-colour object — face, ink, plinth and
     hover — and a single custom property cannot express that, so it is spelled out under
     `body.no-quackd #run` rather than smuggled in here. */
  --off: var(--pink);
  --ring: #7c4a9e;
  --duck: var(--gold);

  /* the one dark panel */
  --plum: #483652;
  --plum-edge: rgba(255, 255, 255, 0.14);
  --plum-rule: rgba(255, 255, 255, 0.07);
  --log-ink: #e8dcf2;
  --log-meta: #c0a9c2;
  --log-dim: #9c87a8;
  --log-gold: #f1c642;
  --log-gold-soft: #f6dc93;
  --log-purple: #c08ae8;
  --log-purple-soft: #e2d3ee;
  --log-green: #cfe9d5;
  --log-green-soft: #a9e0bd;
  --log-green-dot: #7fc48f;
  --log-bad: #f3b3b0;
  --log-bad-dot: #e08a86;
  /* #7c4a9e on #483652 is 1.7:1 and FAILS the 3:1 non-text minimum, i.e. the focus ring is
     invisible on the dark panel. Gold #f1c642 is 8:1 there. */
  --ring-dark: #f1c642;

  /* shadow — the four that quackd-web casts, at quackd-web's own strength */
  --shadow-card: 0 24px 60px rgba(118, 81, 140, 0.1);
  --shadow-term: 0 22px 48px rgba(79, 55, 85, 0.18);
  --shadow-guard: 0 16px 40px rgba(118, 81, 140, 0.08);
  --shadow-pill: 0 8px 24px rgba(146, 91, 137, 0.08);
  --shadow-glass: 0 10px 28px rgba(113, 80, 130, 0.14);
  --shadow-nav: 0 12px 35px rgba(113, 80, 130, 0.12);

  /* fonts — always a real fallback stack, the page must be readable before Google answers */
  --sans: "Nunito Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: "DM Mono", ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, Consolas, monospace;

  /* type scale */
  --t-micro: 0.6875rem;
  --t-eyebrow: 0.6875rem;
  --t-mono: 0.78125rem;
  --t-fine: 0.8125rem;
  --t-ui: 0.875rem;
  --t-body: 0.9375rem;
  --t-input: 1rem;        /* NEVER lower: iOS Safari zooms a focused input under 16px, and
                             that zoom destroys a stage-first layout mid-interaction. */
  --t-h1: 1.125rem;
  --t-h2: 1.25rem;
  --t-lede: clamp(1.35rem, 2.6vw, 1.75rem);

  /* motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --t-fast: 0.14s;
  --t-base: 0.2s;
  --t-slow: 0.3s;

  /* radius */
  --r-pill: 99px;
  --r-xs: 0.375rem;
  --r-sm: 0.5rem;
  --r-md: 0.75rem;
  --r: 1rem;
  --r-lg: 1.25rem;

  /* spacing, a 0.25rem ladder. quackd-web's section rhythm is py-20 sm:py-28 (5rem/7rem) and
     NONE of it is ported: a landing page can afford 7rem between ideas, a tool cannot afford
     it between the arena and the button that drives it. The ladder stops at --s-6. */
  --s-0: 0.25rem;
  --s-1: 0.5rem;
  --s-2: 0.75rem;
  --s-3: 1rem;
  --s-4: 1.5rem;
  --s-5: 2rem;
  --s-6: 3rem;
  --gutter: clamp(0.875rem, 3vw, 1.25rem);
}

/* ── reset ───────────────────────────────────────────────────────────────────────────── */

* { box-sizing: border-box; }

/* The user-agent rule for [hidden] is `display: none`, and it loses to any author rule that
   sets `display` at equal specificity. `.overlay` sets `display: grid`, so `loading.hidden =
   true` left the loading panel covering the canvas and the whole stage bar for good. Anything
   the JS hides with `.hidden` — #loading, #key-box, #manual, #base-url, #share — needs this
   to actually go away. It must stay in THIS file: tests/test_web.py reads only style.css. */
[hidden] { display: none !important; }

/* ── class registry ──────────────────────────────────────────────────────────────────────
   tests/test_web.py asserts every class the JavaScript passes to classList.add/remove/toggle
   or querySelectorAll is defined here. The matcher wants `.name` immediately followed by a
   comma, an opening brace or a colon — `body.no-quackd #run {` does NOT register `no-quackd`,
   and `.a .b {` registers only `b`.

   The other five checked classes register themselves where they are actually styled and need
   no stub: `bad` below, `on` at `.camera-buttons button.on`, `live` at `#record.live`, `chip`
   at `.chip`, `down` at `kbd.down`. Only `no-quackd` needs a line of its own, because every
   rule that uses it is a descendant selector the matcher cannot see. */

.bad { color: var(--bad); }
body.no-quackd { --duck: var(--off); }       /* standalone: a descendant selector will not do */

/* ── base and typography ─────────────────────────────────────────────────────────────── */

body {
  margin: 0;
  /* clip, not hidden: overflow-x:hidden on body makes position:sticky scroll away. */
  overflow-x: clip;
  font: 400 var(--t-body) / 1.6 var(--sans);
  color: var(--body);
  -webkit-font-smoothing: antialiased;
  background:
    radial-gradient(circle at 81% 5%, rgba(229, 213, 255, 0.72), transparent 26rem),
    radial-gradient(circle at 5% 54%, rgba(255, 222, 232, 0.42), transparent 25rem),
    var(--cream);
}

/* Film grain. Sits above the page and must never eat a click. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.75'/%3E%3C/svg%3E");
}

::selection { background: #e8c9db; color: #3e294e; }

img { max-width: 100%; }

a[href],
button,
summary,
label[for],
select,
[role="button"] { cursor: pointer; }

button:disabled,
[aria-disabled="true"] { cursor: not-allowed; }

/* A visible, high-contrast focus ring everywhere, not only where somebody remembered it.
   NO border-radius here: an outline already follows the element's own corners, and a bare
   `:focus-visible` (0-1-0) outranks `input, select` (0-0-1), so squaring the radius from this
   rule snapped every field's corners on keyboard focus and put them back on blur. */
:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 3px;
}

.eyebrow {
  font: 500 var(--t-eyebrow) var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--pink);
}
.eyebrow-muted {
  font: 500 var(--t-eyebrow) var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--meta);
}

.fine {
  margin: 0.45rem 0 0;
  max-width: 62ch;
  font: 400 var(--t-fine) / 1.55 var(--sans);
  color: var(--body);
}

.skip-link {
  position: absolute;
  left: 1rem;
  top: -4rem;
  z-index: 70;
  background: var(--cream);
  color: var(--ink);
  border: 1px solid var(--edge-soft);
  padding: 0.7rem 1.1rem;
  border-radius: var(--r-md);
  font: 700 0.85rem var(--sans);
  text-decoration: none;
  transition: top 0.2s ease;
}
.skip-link:focus { top: 1rem; }

/* ── shell and header ────────────────────────────────────────────────────────────────── */

/* quackd.org's nav is a DETACHED rounded pill sitting inside the page's own 1180px column,
   not an app bar flush to the viewport. As a full-bleed bar the mark landed ~190px left of the
   console it labels on a 1600px screen — three different left edges stacked down one page. */
.nav-shell {
  position: sticky;
  top: var(--s-1);
  z-index: 40;
  width: min(1180px, 100% - 2.5rem);
  margin: var(--s-1) auto 0;
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0.6rem var(--s-3);
  background: rgba(255, 253, 247, 0.85);
  backdrop-filter: blur(18px);
  border: 1px solid rgba(216, 195, 232, 0.82);
  border-radius: var(--r);
  box-shadow: var(--shadow-nav);
}
.nav-shell h1 {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  margin: 0;
  font: 800 var(--t-h1) / 1.1 var(--sans);
  letter-spacing: -0.03em;
  color: var(--ink);
}
/* The lockup is the way back to www.quackd.org, so the mark and the word are inside one
   anchor — the heading is still an h1, only its text is wrapped. It must not read as a link:
   it inherits the h1's ink and weight rather than the user-agent's link blue and underline,
   which is what an unstyled <a> in this header would otherwise get — nothing in this file
   colours a bare anchor. A wordmark that underlines on hover reads as broken. It is
   NOT a .nav-link either; that class sets its own 600/--t-ui font and would shrink the
   brand to the size of the nav beside it. Hover is the same primary-deep .nav-link uses.
   The focus ring is the global `:focus-visible` and nothing more: 2px at 3px offset clears
   the nav pill's 0.6rem padding, so there is nothing here to fight it with. */
.nav-shell h1 a {
  display: flex;   /* a flex item: `inline-flex` would be blockified to this anyway */
  align-items: center;
  gap: var(--s-1);
  color: inherit;
  text-decoration: none;
  transition: color var(--t-base) ease;
}
.nav-shell h1 a:hover { color: var(--primary-deep); }
.nav-shell h1 .mark {
  width: 30px;
  height: 30px;
  display: block;
  filter: drop-shadow(0 2px 4px rgba(119, 83, 165, 0.18));
}
.nav-shell h1 small {
  font: 400 var(--t-fine) / 1 var(--sans);
  color: var(--body);
}

/* quackd-web's own pill, at its own strength: #5f4577 ink, a soft pink shadow, and the live
   dot the hero puts in front of the text. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: auto;
  color: var(--btn-ink);
  background: rgba(255, 253, 247, 0.75);
  border: 1px solid var(--pink-edge);
  border-radius: var(--r-pill);
  padding: var(--s-0) 0.7rem;
  font: 500 var(--t-eyebrow) var(--mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: var(--shadow-pill);
}
.status-pill::before {
  content: "";
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--gold-dot);
}

.nav-shell nav { display: flex; gap: var(--s-0); }
.nav-link {
  color: var(--body);
  text-decoration: none;
  padding: 0.35rem 0.6rem;
  border-radius: var(--r-sm);
  font: 600 var(--t-ui) var(--sans);
  transition: color var(--t-base) ease, background-color var(--t-base) ease,
    border-color var(--t-base) ease;
  border: 1px solid transparent;
}
.nav-link:hover { color: var(--primary-deep); background: rgba(235, 221, 250, 0.7); }
/* quackd-web's .nav-star: the one visible action in the nav, so the bar is not two identical
   pieces of grey text. */
.nav-star {
  color: var(--btn-ink);
  border-color: #d9c4ec;
  background: rgba(255, 253, 247, 0.7);
  border-radius: var(--r-pill);
}
.nav-star:hover { color: var(--btn-ink); border-color: #b98cd7; background: var(--tint); }

.wrap {
  width: min(1180px, 100% - 2.5rem);
  margin-inline: auto;
  padding-block: var(--s-4) var(--s-6);
}

/* ── brandline ───────────────────────────────────────────────────────────────────────── */

.brandline { margin: 0 0 var(--s-4); }
.brandline .eyebrow { margin: 0 0 0.35rem; }
.lede {
  margin: 0;
  font: 800 var(--t-lede) / 1.1 var(--sans);
  letter-spacing: -0.03em;
  color: var(--ink);
}

/* ── console and stage ───────────────────────────────────────────────────────────────── */

/* The console is the grid. Below 62rem it is one column in document order — 03 the key, the
   arena, 01, 04, 02 — and at 62rem it becomes the band / arena+02 / 01+04 layout in B8. */
.console {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* below 62rem: one column, DOM order */
  gap: 0;                                  /* REQUIRED. Every division on this card is a
                                              border, never a gap: applyToggle() sets
                                              keyBox.hidden = !on, and a row-gap would
                                              survive that collapse as a band of cream. */
  background: var(--card-soft);
  border: 1px solid var(--edge);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;              /* the canvas clips to the card's top corners */
}

/* ── the model bar ───────────────────────────────────────────────────────────────────────
   The switch and the credential, in one full-width band ABOVE the arena. Stacked here;
   a two-track grid from 62rem up. */
.model-bar {
  padding: var(--s-3) var(--gutter);
  border-bottom: 1px solid var(--edge-3);
}
.model-bar > * + * { margin-top: var(--s-3); }

.stage { position: relative; }

/* HARD RULE: never transition or animate any property that changes the box of .stage,
   canvas#view or .console. view.js reads clientWidth/clientHeight every frame and calls
   setSize when the CSS box has actually changed, so an animating box reallocates the WebGL
   drawing buffer 60×/s. (The guard compares against the pixel-ratio-scaled backing store; it
   used to compare backing-store pixels to CSS pixels, which on any retina display was true
   every frame and reallocated regardless of whether anything moved.) */
canvas#view {
  display: block;
  width: 100%;
  /* The one-column value, for below 62rem. From 62rem up "THE FOLD, AND HOW THE ARENA PAYS
     FOR IT" at the foot of the file replaces it with `calc(100vh - the measured chrome)`,
     which is what actually delivers the fold.
     Viewport-relative either way, and never a percentage of a `1fr` row: every row in the
     console grid stays `auto`, so the failure mode is the page scrolling, never a
     scissored Run button. */
  height: clamp(12rem, 38vh, 30rem);
  background: var(--lilac-2);    /* the one frame before MuJoCo draws; never white */
  touch-action: none;            /* REQUIRED: view.js binds pointer drag and a passive:false wheel */
  cursor: grab;
}
canvas#view:active { cursor: grabbing; }
/* NEGATIVE offset: .console has overflow:hidden, so an outset ring is scissored away. */
canvas#view:focus-visible { outline: 3px solid var(--ring); outline-offset: -3px; }

/* ── stage chrome ────────────────────────────────────────────────────────────────────── */

.stage-bar {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;          /* or the canvas drag dies under the bar */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--s-2);
  padding: var(--s-2);
}
.bar-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s-2);
  flex-wrap: wrap;
}
.stage-bar .glass { pointer-events: auto; }

.glass {
  display: inline-flex;
  align-items: center;
  gap: var(--s-0);
  padding: var(--s-0);
  background: rgba(255, 253, 247, 0.86);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(216, 195, 232, 0.82);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-glass);
}
@supports not (backdrop-filter: blur(1px)) {
  .glass { background: rgba(255, 253, 247, 0.96); }
}
.glass :focus-visible { outline-offset: 2px; }   /* 3px bleeds into the render */

.hud { gap: var(--s-1); padding: var(--s-0) 0.6rem var(--s-0) 0.35rem; }

/* #loading (z:3) covers .stage-bar (z:2), so shipping #owner in its `live` state in the HTML
   is not a lie: nobody can see it while it is still false. */
.overlay {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  /* The boot panel is ~279px of content. A 205px arena inside `.console { overflow: hidden }`
     scissors centred content at BOTH ends — the duck mark off the top, "Nothing is uploaded"
     off the bottom. Start-safe and scrollable, it loses neither. This used to be fixed only
     below 48rem, because that was the only place the arena got short. */
  align-content: center;
  align-content: safe center;   /* two declarations, not one: a browser without the `safe`
                                   keyword drops the whole line, and with nothing centring
                                   left the boot panel would spread instead. */
  justify-content: center;
  overflow-y: auto;
  justify-items: center;
  gap: var(--s-2);
  text-align: center;
  padding: var(--s-5);
  background: linear-gradient(150deg, #fdf6ff, #fff8f4);
}
.overlay h2 {
  margin: 0;
  font: 800 var(--t-h2) / 1.2 var(--sans);
  letter-spacing: -0.03em;
  color: var(--ink);
}
.overlay .fine { text-align: center; max-width: 44ch; }
.boot-mark { width: 64px; height: 64px; }
.bar {
  width: min(22rem, 60vw);
  height: 6px;
  background: #ece0f3;
  border-radius: var(--r-pill);
  overflow: hidden;
}
.bar > div {
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, #b48ae4, var(--gold));
  transition: width 0.25s ease;
}
/* #loading-phase is textContent-owned. ::before is generated content, not a DOM child, so it
   survives `textContent = message` — the only safe way to decorate any of the six elements
   the JS overwrites (#loading-phase #toggle-label #toggle-note #run #record #readout). */
#loading-phase {
  margin: 0;
  min-height: 1.2em;             /* no layout jump between load phases */
  color: var(--body);
  font-size: var(--t-fine);
}
#loading-phase.bad { color: var(--bad); font-weight: 600; }
#loading-phase.bad::before { content: "⚠ "; }

/* the owner pill: who is holding the twist. aria-live lives here, and paintOwner() must
   early-return on an unchanged state or it announces every keystroke. */
#owner {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.28rem 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font: 500 var(--t-eyebrow) var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background-color var(--t-base), border-color var(--t-base), color var(--t-base);
}
#owner::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  flex: none;
}
#owner[data-state="live"] {
  background: rgba(255, 253, 247, 0.9);
  border-color: #e2c3d6;
  color: var(--ink-2);
}
#owner[data-state="live"]::before { box-shadow: inset 0 0 0 2px var(--gold-dot); }
#owner[data-state="hand"] { background: #fdf3dd; border-color: #ecd9ae; color: var(--gold-ink-warm); }
#owner[data-state="hand"]::before { background: var(--gold-dot); }
#owner[data-state="pilot"] { background: var(--lilac); border-color: var(--lilac-edge); color: var(--primary-ink); }
#owner[data-state="pilot"]::before { background: var(--primary); }
#owner[data-state="typing"] { background: #f4ecfa; border-color: #e3d4ef; color: #5f4a6b; }
#owner[data-state="typing"]::before { display: none; }

/* textContent-owned, so it is ONE colour: an asked→sent arrow form cannot be two-toned.
   The ellipsis is what stops a long arrow form pushing the pill wider than the arena. */
#readout {
  font: 400 var(--t-micro) / 1 var(--mono);
  color: var(--body);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: min(52ch, 60vw);
}

/* ── deck and lanes ──────────────────────────────────────────────────────────────────── */

.deck {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
  border-top: 1px solid var(--edge-3);
}
.lane { padding: var(--s-3) var(--gutter); min-width: 0; }
/* The lanes' own inter-block spacing is set per width, not here: `.lane-a > * + *` above
   62rem and `.deck > .lane > * { margin-top: 0 }` below it, where the rule between bays does
   the separating instead. A base `* + *` rule here would be overridden at every width. */

/* Four named bays are the page's entire heading system:
   Your API key · Command the duck · Drive the duck · Transcript. The first two are
   deliberately parallel: one sentence, one set of keys, the same duck. */
/* No `font-size: 0` here. It was carried over to "kill the whitespace between the inline
   spans", but a flex container already drops whitespace-only anonymous items — `summary`
   below uses the same span pattern without it and renders identically. */
.lane-label {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  margin: 0 0 var(--s-2);
}
.lane-label .eyebrow { margin: 0; }
.lane-label .tag {
  margin-left: auto;
  font: 500 var(--t-eyebrow) var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--meta);
}

/* Bay 01's label and its instruction on ONE baseline row. Two stacked lines here cost the
   fold ~20px and say nothing the one line does not. */
.say-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  column-gap: var(--s-2);
  row-gap: var(--s-0);
}
.say-head > .lane-label { margin: 0; flex: 0 0 auto; }
/* No `display` here: this is a flex item, so any inline value is blockified anyway. */
.say-head > label[for="goal"] { margin: 0; flex: 1 1 16ch; min-width: 0; }

/* ── controls ────────────────────────────────────────────────────────────────────────── */

button, .button {
  font: 700 var(--t-ui) / 1 var(--sans);
  border-radius: var(--r-md);
  cursor: pointer;
  transition:
    background-color var(--t-base) var(--ease),
    border-color var(--t-base) var(--ease),
    color var(--t-base) var(--ease),
    transform var(--t-base) var(--ease),
    box-shadow var(--t-base) var(--ease);
}

/* #run — the hard-shadow object, quackd-web's .button-yellow at full 7px. The same physics
   appears again at keycap scale on <kbd>: one object, two sizes. */
#run {
  background: var(--duck);
  color: var(--gold-ink);
  border: 0;
  padding: 0.7rem 1.1rem;
  border-radius: var(--r-md);
  font-weight: 800;
  font-size: var(--t-body);
  box-shadow: 0 7px 0 var(--gold-deep), 0 12px 25px rgba(227, 169, 34, 0.18);
}
#run::after { content: " ›"; }   /* pseudo, so it survives textContent Run ↔ Send */
#run:hover:not(:disabled) {
  transform: translateY(-2px);
  background: var(--gold-hi);
  box-shadow: 0 9px 0 var(--gold-press), 0 16px 28px rgba(227, 169, 34, 0.22);
}
#run:active:not(:disabled) {
  transform: translateY(2px);
  box-shadow: 0 4px 0 var(--gold-press);
}

/* With the language layer off the button is a PINK object, not a pink face on a gold shelf.
   `--duck: var(--off)` swaps the background alone, and the rest of #run stayed gold: --gold-ink
   on pink is 1.99:1, the 7px plinth stayed #d59f18, and the hover flipped the whole thing back
   to gold under the cursor. A custom property cannot express a four-part object, so the object
   is spelled out. These sit BEFORE #run:disabled on purpose — equal specificity, and the
   disabled state must stay the same grey in both worlds. */
body.no-quackd #run {
  color: #fff;                                   /* 5.6:1 on #a34a72 */
  box-shadow: 0 7px 0 var(--pink-deep), 0 12px 25px rgba(163, 74, 114, 0.18);
}
body.no-quackd #run:hover:not(:disabled) {
  background: var(--pink-hi);
  box-shadow: 0 9px 0 var(--pink-deep), 0 16px 28px rgba(163, 74, 114, 0.22);
}
body.no-quackd #run:active:not(:disabled) { box-shadow: 0 4px 0 var(--pink-deep); }

#run:disabled {
  background: #f0e6d3;
  color: #9a8c74;
  box-shadow: 0 4px 0 #ddd0b8;
  transform: none;
  cursor: not-allowed;
}

/* .btn only. The one `.button` on the page is #share, and it lives inside .record-buttons,
   whose own rules (0-2-0) beat every declaration here — so a `.button` arm added nothing but
   the impression that it did. `.button:disabled` could never match at all: #share is an <a>. */
.btn {
  color: var(--btn-ink);
  border: 1px solid #c9aede;
  background: rgba(255, 253, 247, 0.65);
  padding: 0.55rem 0.85rem;
  border-radius: var(--r-sm);
}
.btn:hover:not(:disabled) { border-color: #a874cf; background: var(--tint); transform: translateY(-1px); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
.share { text-decoration: none; }

.camera-buttons button, .record-buttons button, .record-buttons .button {
  color: var(--btn-ink);
  border: 1px solid transparent;
  background: transparent;
  padding: 0.4rem 0.7rem;
  border-radius: var(--r-pill);
  font-size: var(--t-ui);
}
.camera-buttons button:hover:not(:disabled),
.record-buttons button:hover:not(:disabled),
.record-buttons .button:hover { background: var(--tint); border-color: var(--edge-soft); }
.camera-buttons button:disabled, .record-buttons button:disabled { opacity: 0.45; cursor: not-allowed; }
.camera-buttons button.on {
  background: #f1e6fd;
  border-color: #b98cd7;
  color: var(--btn-ink);
  box-shadow: inset 0 0 0 1px var(--edge-soft);
}
#record.live {
  color: var(--bad);
  border-color: #e6b3b1;
  background: #fdf0ef;
}

.link {
  background: none;
  border: 0;
  padding: 0;
  color: var(--pink);
  font: 600 var(--t-fine) / 1 var(--sans);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link:hover { color: var(--pink-deep); }

/* form fields */
input, select {
  width: 100%;
  font: 400 var(--t-input) / 1.3 var(--sans);
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--field);
  border-radius: var(--r-sm);
  padding: 0.55rem 0.7rem;
}
#goal { border-radius: var(--r-md); box-shadow: inset 0 1px 2px rgba(62, 41, 78, 0.05); }
#key, #base-url, #model { font-family: var(--mono); }
#provider {
  appearance: none;
  padding-right: 1.9rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%236b5875' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 12px 8px;
}
/* IN ADDITION TO the global :focus-visible ring, never instead of it: this gives a mouse
   user a visible active field without weakening the keyboard ring. */
input:focus, select:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(138, 92, 196, 0.18);
  outline: none;
}
input:focus-visible, select:focus-visible { outline: 2px solid var(--ring); outline-offset: 3px; }
/* No opacity: --body at 0.65 composites to #a396a9 on white, which is 2.95:1 and fails AA.
   --meta is the dimmest ink on the page that still passes against a white field. */
::placeholder { color: var(--meta); opacity: 1; }

label {
  display: block;
  font: 600 var(--t-fine) / 1.4 var(--sans);
  color: var(--body);
  margin-bottom: 0.35rem;
}

.row { display: flex; gap: var(--s-2); }
.row > * { flex: 1; min-width: 0; }
/* flex-start and the extra bottom margin are what keep #run's 7px plinth off .examples. */
.row.actions {
  align-items: flex-start;
  margin: var(--s-2) 0 var(--s-1);
}
.row.actions > * { flex: 0 0 auto; }

.examples {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--t-fine);
  color: var(--body);
}

/* chip.textContent is copied VERBATIM into #goal, so the visible label is the sentence that
   gets typed. A ::before glyph is safe — generated content is not a text node and never
   appears in textContent. A nested <span> or <img> is not. Never nest anything in a .chip. */
.chip {
  display: inline-block;
  border-radius: var(--r-pill);
  padding: 0.3rem 0.7rem;
  font: 600 var(--t-fine) / 1.2 var(--sans);
  border: 1px solid;
  cursor: pointer;
  transition: transform var(--t-base) var(--ease), border-color var(--t-base) var(--ease);
}
.chip::before { content: "›"; margin-right: 0.35rem; opacity: 0.55; }
.chip:hover { transform: translateY(-1px); }
.chip:nth-of-type(1) { background: var(--lilac); border-color: var(--lilac-edge); color: var(--primary-ink); }
.chip:nth-of-type(2) { background: #fbf1da; border-color: #eddcb6; color: var(--gold-ink-warm); }
.chip:nth-of-type(3) { background: #fbe9f1; border-color: #efd2df; color: var(--pink-deep); }

/* the switch — the language layer, and nothing else. It never gates the keyboard. */
.toggle-row { display: flex; gap: 0.8rem; align-items: flex-start; }
.switch { position: relative; width: 2.75rem; height: 1.5rem; flex: none; }
.switch input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}
.slider {
  position: absolute;
  inset: 0;
  border-radius: var(--r-pill);
  background: #e0d0ec;
  pointer-events: none;
  transition: background-color var(--t-base) var(--ease);
}
.slider::before {
  content: "";
  position: absolute;
  width: 1.1rem;
  height: 1.1rem;
  left: 0.2rem;
  top: 0.2rem;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(79, 55, 85, 0.35);
  transition: transform var(--t-base) var(--ease);
}
.switch input:checked + .slider { background: var(--duck); }
.switch input:checked + .slider::before { transform: translateX(1.25rem); }
/* NEW. The switch had no visible focus at all — a keyboard bug the redesign closes. */
.switch input:focus-visible + .slider { outline: 2px solid var(--ring); outline-offset: 3px; }
/* textContent-owned: no child elements, no aria-live wrapper inside either of these. */
#toggle-label { display: block; font: 700 var(--t-body) / 1.3 var(--sans); color: var(--ink); }
#toggle-note { margin-top: 0.2rem; }

/* ── the cockpit ─────────────────────────────────────────────────────────────────────── */

/* quackd-web's .guard-card, whole: its gradient, its #e4d3ee border and its shadow. Two of
   the three were dropped on the way over, which left the same object reading flat here. */
.cockpit {
  background: linear-gradient(150deg, #fbf5ff, #fff6f3);
  border: 1px solid var(--edge-guard);
  border-radius: var(--r);
  box-shadow: var(--shadow-guard);
  padding: var(--s-3);
  transition:
    background-color var(--t-slow) var(--ease),
    border-color var(--t-slow) var(--ease),
    box-shadow var(--t-slow) var(--ease);
}
/* The ONE place "controls recede" is broken on purpose: with the language layer off, the
   keyboard strip is the only thing on the page that still does anything, so it says so. */
body.no-quackd .cockpit {
  border-color: #e0b93f;
  box-shadow: 0 0 0 3px rgba(245, 197, 51, 0.22), var(--shadow-guard);
}

.keycaps {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: start;
  margin-top: var(--s-2);
}
/* A literal 3×2 keyboard grid, so the map is read off the shape rather than off a sentence:
   top row is the head and forward (Q W E), bottom row is the feet (A S D), Space spans them. */
.cluster { display: grid; grid-template-columns: repeat(3, 2.1rem); gap: 0.3rem; }
.cluster-2 { grid-template-columns: repeat(2, 2.1rem); }

kbd {
  display: grid;
  place-items: center;
  min-width: 2.1rem;
  height: 2rem;
  padding: 0 0.35rem;
  font: 500 var(--t-micro) / 1 var(--mono);
  color: #4e3b5c;
  background: linear-gradient(180deg, #fff, #f7f1fb);
  border: 1px solid var(--field);
  border-radius: var(--r-sm);
  box-shadow: 0 3px 0 #d9c6e6;    /* #run's physics, at keycap scale */
  transition:
    background-color var(--t-fast) var(--ease),
    border-color var(--t-fast),
    color var(--t-fast),
    transform var(--t-fast),
    box-shadow var(--t-fast);
}
kbd.wide { grid-column: span 3; width: 100%; font-size: 0.6875rem; letter-spacing: 0.04em; }
/* transition-duration:0s is the whole trick — a key that lights up 140 ms late feels broken.
   The release is still eased, because that is where the softness belongs. */
/* `kbd.down` and nothing else: a bare `.down` was a global that would paint any element on the
   page gold, and paintKeycaps() only ever toggles the class on a <kbd>. */
kbd.down {
  background: linear-gradient(180deg, #f8cf4d, #f2be24);
  color: var(--gold-ink);
  border-color: var(--gold-deep);
  box-shadow: 0 1px 0 var(--gold-press);
  transform: translateY(2px);
  transition-duration: 0s;
}

.legend {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--s-2);
  row-gap: 0.3rem;
  margin: 0.9rem 0 0;
  font-size: var(--t-fine);
}
.legend dt {
  font: 500 var(--t-micro) var(--mono);
  color: var(--primary-ink);
  white-space: nowrap;
}
.legend dd { margin: 0; color: var(--ink-2); max-width: 62ch; }

/* Plain blocks below 62rem — nothing scrolls inside a scrolling page. From 62rem up
   .key-notes becomes the cockpit's scroll region; see the responsive section. */
.key-notes { min-width: 0; }
.why > summary { font-size: var(--t-fine); color: var(--body); }
.why > summary::after { margin-left: var(--s-1); }   /* not `auto`: no full-width row to push to */
.why[open] > summary { margin-bottom: var(--s-1); }
.why > .fine:first-of-type { margin-top: 0; }

.touch-note {
  display: none;
  margin: var(--s-2) 0 0;
  font-size: var(--t-fine);
  color: var(--meta);
}
@media (pointer: coarse) { .touch-note { display: block; } }

/* ── the key box ─────────────────────────────────────────────────────────────────────────
   Deliberately the ONE component with no gold, no animation and no hard shadow: playfulness
   lives everywhere else, the credential field stays sober. It also stays a real
   <details>/<summary>, because the keydown guard's selector lists `summary` and that is what
   keeps Space activating it (WCAG 2.1.1). */

details { border: 0; }
summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--t-input);
}
summary::-webkit-details-marker { display: none; }
summary::after {
  content: "▸";
  color: var(--pink);
  font-size: 0.8em;
  margin-left: auto;
  transition: transform var(--t-base) var(--ease);
}
details[open] summary::after { transform: rotate(90deg); }
details[open] summary { margin-bottom: var(--s-2); }
#key-box > * + * { margin-top: var(--s-2); }
#provider-note a {
  color: var(--pink);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ── the terminal ────────────────────────────────────────────────────────────────────── */

.terminal-window {
  background: var(--plum);
  border-radius: var(--r);
  overflow: hidden;
  box-shadow: var(--shadow-term);
  min-width: 0;
}
.term-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid var(--plum-edge);
}
.term-head h2 { margin: 0; display: flex; align-items: center; gap: var(--s-1); }
.term-head .eyebrow { color: #d79ac2; }   /* 4.8:1 on plum */
.term-head::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(241, 198, 66, 0.95), transparent);
  opacity: 0.5;
}
.terminal-window .link { color: #d6c3d8; }
.terminal-window .link:hover { color: #fff; }
.terminal-window :focus-visible { outline-color: var(--ring-dark); }

/* NEVER add scroll-behavior:smooth here. app.js sets scrollTop = scrollHeight synchronously
   on every append, and smooth makes a fast run's log lag permanently behind its own tail.
   The <ol> ITSELF is the scroll container — app.js scrolls this element, so moving max-height
   or overflow onto a wrapper silently stops the auto-scroll. */
#transcript {
  list-style: none;
  margin: 0;
  padding: var(--s-1) 0.85rem 0.85rem;
  max-height: clamp(14rem, 40vh, 30rem);
  overflow-y: auto;
  font: 400 var(--t-mono) / 1.55 var(--mono);
  color: var(--log-ink);
  font-variant-numeric: tabular-nums;
  scrollbar-width: thin;
  scrollbar-color: #6b5375 var(--plum);
}
#transcript::-webkit-scrollbar { width: 8px; }
#transcript::-webkit-scrollbar-thumb { background: #6b5375; border-radius: var(--r-pill); }
/* --log-dim is 3.35:1 on the plum, which is enough for the 6px dots below (3:1 non-text) and
   not enough for 13px prose. This is the one sentence that teaches dual control, so it takes
   --log-meta at 5.04:1. */
#transcript:empty::after {
  content: "nothing yet — take the keys, or type a sentence.";
  display: block;
  padding: 0.6rem 0 1rem;
  color: var(--log-meta);
  font: 400 var(--t-fine) var(--sans);
  font-style: italic;
}
/* CLASS-scoped, never `#transcript li`. Every row modifier below is a class selector (0-2-0)
   and an id-scoped base rule (1-0-1) silently outranks all of them: the refusal panel lost its
   padding and kept a hairline through it, .handover lost its indent, .goal and .end lost the
   air that brackets a run. Keep the base row at class weight so a modifier can win. */
.transcript li {
  position: relative;
  padding: 0.32rem 0 0.32rem 1.1rem;
  border-bottom: 1px solid var(--plum-rule);
  overflow-wrap: anywhere;
  animation: row-in 0.28s var(--ease) both;
}
.transcript li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
}

/* R3 again, and this is where a later pass is most tempted to "fix" it: the verb calls and
   results are the machine talking, so they are mono. The goal line and the refusal are the
   human's own words read back, so they are sans. No test covers any of these rows — dropping
   one is a silent regression that makes success and failure indistinguishable. */
.transcript .fine { color: var(--log-meta); font-size: var(--t-micro); }
.transcript code {
  background: rgba(255, 255, 255, 0.1);
  color: var(--lilac);
  padding: 0 0.25rem;
  border-radius: var(--r-xs);
}
.transcript kbd {
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  border-color: rgba(255, 255, 255, 0.2);
  box-shadow: 0 2px 0 rgba(0, 0, 0, 0.25);
  display: inline-grid;
  height: auto;
  min-width: 1.4rem;
  padding: 0.05rem 0.3rem;
}

.transcript .goal {
  color: var(--log-gold-soft);
  font: 700 var(--t-ui) / 1.5 var(--sans);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  margin-top: 0.35rem;
  padding-top: var(--s-1);
}
.transcript .goal::before { background: var(--log-gold); }
.transcript .call { color: var(--log-ink); }
.transcript .call::before { background: var(--log-purple); }
.transcript .call b { color: #d9b6f5; }
.transcript .result { color: var(--log-green); }
.transcript .result::before { background: var(--log-green-dot); }
.transcript .result.bad { color: var(--log-bad); }
.transcript .result.bad::before { background: var(--log-bad-dot); }
.transcript .note { color: var(--log-meta); }
.transcript .note::before { background: var(--log-dim); }

/* the human's discrete acts: kick, stand up, stop, read state */
.transcript .hand { color: var(--log-purple-soft); }
.transcript .hand::before { box-shadow: inset 0 0 0 2px var(--log-gold); }
.transcript .hand b { color: var(--log-gold); }
/* the moment the robot changed hands mid-run */
.transcript .handover {
  color: var(--log-gold-soft);
  border-left: 2px solid var(--log-gold);
  padding-left: 0.6rem;
  background: rgba(241, 198, 66, 0.06);
}
.transcript .handover::before { background: var(--log-gold); }

/* the payoff line, and it must NOT read as a log row */
.transcript .refused {
  color: #f4e6c9;
  font: 400 var(--t-fine) / 1.6 var(--sans);
  background: rgba(241, 198, 66, 0.08);
  border-left: 2px solid var(--log-gold);
  border-bottom: 0;              /* a card, not a row: no rule across its foot */
  padding: 0.6rem 0.7rem;
  border-radius: var(--r-sm);
}
.transcript .refused::before { display: none; }

/* every run is visually bracketed: a top rule opens it at .goal and closes it at .end */
.transcript .end {
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  margin-top: 0.35rem;
  padding-top: var(--s-1);
}
.transcript .end.success { color: var(--log-green-soft); }
.transcript .end.success::before { background: var(--log-green-dot); }
.transcript .end.failure, .transcript .end.error { color: var(--log-bad); }
.transcript .end.failure::before, .transcript .end.error::before { background: var(--log-bad-dot); }
.transcript .end.aborted { color: var(--log-meta); }
.transcript .end.aborted::before { background: var(--log-dim); }
.transcript .end.budget { color: var(--log-gold-soft); }
.transcript .end.budget::before { background: var(--log-gold); }

/* ── footer ──────────────────────────────────────────────────────────────────────────── */

/* The band is full-bleed; its TEXT is not. The inline padding resolves to the same left edge
   .wrap has — (100% − 1180px) / 2 on a wide screen, 1.25rem once the column is narrower than
   its own maximum — so the footer, the console and the nav all start on one line. */
.footer-shell {
  border-top: 1px solid #e9d9e7;
  background: rgba(255, 253, 247, 0.8);
  padding: var(--s-5) max(1.25rem, (100% - 1180px) / 2);
}
.footer-shell p { margin: 0; max-width: 78ch; font-size: 0.75rem; color: var(--body); }
.footer-shell a { color: var(--pink); }

/* ── keyframes ───────────────────────────────────────────────────────────────────────── */

@keyframes pulse-dot {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(217, 154, 18, 0.25); }
  50% { opacity: 0.6; box-shadow: 0 0 0 6px rgba(217, 154, 18, 0); }
}
@keyframes rec-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(163, 49, 47, 0.28); }
  50% { box-shadow: 0 0 0 5px rgba(163, 49, 47, 0); }
}
@keyframes row-in {
  from { opacity: 0; transform: translateX(-6px); }
}
@keyframes nudge {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  75% { transform: translateX(3px); }
}

/* Every state these carry is ALSO carried by a colour and by a word, so the reduced-motion
   page loses only the flourish. */
@media (prefers-reduced-motion: no-preference) {
  #owner[data-state="hand"]::before { animation: pulse-dot 2s ease-in-out infinite; }
  #record.live { animation: rec-pulse 1.8s ease-in-out infinite; }
  /* #run.textContent silently becomes "Send" when the switch flips off; this wobble is the
     only thing that makes that change perceptible. */
  body.no-quackd #run { animation: nudge 0.45s var(--ease) 1; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  kbd.down { transform: none; }   /* colour still carries the state */
  #run:hover, #run:active, .btn:hover, .chip:hover { transform: none; }
}

/* ── responsive ──────────────────────────────────────────────────────────────────────── */

@media (min-width: 62rem) {
  /* Four children, three areas. 03 and the switch span the full card because a visitor who
     never sees the key field presses Run and gets an error about something nobody asked them
     for; 02 is a full-height column beside the arena AND the sentence box, so neither way of
     driving waits for the other. */
  .console {
    grid-template-columns: minmax(0, 1fr) clamp(15rem, 22%, 20rem);
    grid-template-areas:
      "key   key"
      "stage drive"
      "say   drive";
  }
  .model-bar { grid-area: key; }
  .stage     { grid-area: stage; }
  .deck      { grid-area: say; }
  #manual    { grid-area: drive; }
  /* 22% of a 1178px console = 259px; the clamp floors it at 240px down at 992px wide and caps
     it at 320px on a wide screen. The left group is 74–79% throughout — "about 80/20" holds. */

  /* the band: the switch on the left, the credential on the right */
  .model-bar {
    display: grid;
    grid-template-columns: minmax(19rem, 30rem) minmax(0, 1fr);
    column-gap: var(--s-4);
    align-items: start;
    padding: var(--s-2) var(--s-4);   /* 12px vertical is the minimum that clears the global
                                         :focus-visible ring's 3px offset against
                                         .console { overflow: hidden } */
  }
  .model-bar > * + * { margin-top: 0; }
  /* With the language layer off, #key-box is `hidden` and the right track has nothing in it. */
  body.no-quackd .model-bar { grid-template-columns: minmax(0, 1fr); }
  .toggle-row > div > #toggle-note { font-size: var(--t-micro); }

  /* <details> interposes a ::details-content box between #key-box and everything after the
     <summary>, so WITHOUT this the flex container has exactly two items — the summary and
     that box — and every `flex:` below is dead: .row, #key and .fine each render full-track
     on their own line and the band is a 185px vertical stack instead of the row described
     here. `display: contents` dissolves it so the fields are flex items again; the box
     carries no styling of its own, and every rule here is scoped to [open], so the closed
     state is untouched. Worth 24px, straight out of the fold. */
  #key-box[open]::details-content { display: contents; }

  #key-box[open] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: var(--s-2);
    row-gap: var(--s-1);
  }
  #key-box[open] > * { margin-top: 0; }
  #key-box[open] > summary        { flex: 0 0 auto; margin-bottom: 0; }
  #key-box[open] > summary::after { margin-left: var(--s-1); }   /* `auto` would shove the caret
                                                                    across the whole card */
  /* grow 3 against #key's 2, not 2 against 2. The row carries TWO controls, and the <select>
     takes its own min-content width (181px for "Anthropic (Claude)") off the top regardless
     of `min-width: 0`, so an even split left the model field 162px for a 166px value and
     "claude-sonnet-5" read as "claude-sonnet-". #key still gets 246px for an "sk-ant-…". */
  #key-box[open] > .row           { flex: 3 1 18rem; }
  #key-box[open] > #key           { flex: 2 1 12rem; }
  #key-box[open] > #base-url      { flex: 2 1 12rem; }
  #key-box[open] > .fine          { flex: 1 1 100%; margin: 0; max-width: none;
                                    font-size: var(--t-micro); }
  #key-box[open] > #provider-note { flex: 1 1 100%; margin: 0; font-size: var(--t-micro); }
  #provider-note:empty { display: none; }
  /* The privacy note is 228 characters and it takes TWO 17px lines at this width. It is the
     reason a visitor is willing to paste a key at all, so it is never truncated. If it is ever
     "tidied" back to --t-fine with its 62ch measure the band grows ~35px and takes all of it
     out of the arena. */

  /* the deck: 01 | 04, 50/50, and lane B fills the row rather than floating in it */
  .deck { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: stretch; }
  .lane { padding: var(--s-3); }
  .lane + .lane { border-left: 1px solid var(--edge-3); }
  .lane-a > * + * { margin-top: var(--s-2); }
  .deck > .lane:last-child { display: flex; }
  .transcript-box { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
  #transcript { flex: 1 1 auto; max-height: clamp(6rem, 17vh, 26rem); }
  /* max-height and overflow-y stay ON THE <ol> — that is the element app.js scrolls. flex-grow
     only lets it reach that cap; it never replaces it. The cap is what stops a fifty-line log
     from making bay 04 taller than bay 01 and growing the whole card. */

  /* 02, the right column, spanning the arena and the deck */
  #manual {
    display: grid;
    grid-template-rows: auto auto minmax(0, 1fr);
    min-height: 0;                 /* lets THIS grid's own 1fr row shrink below its content.
                                      Necessary, and on its own not sufficient — see the
                                      `contain: size` on .key-notes, which is what actually
                                      stops #manual inflating the console. */
    margin: 0;
    padding: var(--s-3);
    border-width: 0 0 0 1px;
    border-color: var(--edge-guard);
    border-radius: 0;
  }
  /* The off-state signal was an OUTSET 3px ring; this column's own edges and the console's
     `overflow: hidden` would scissor it, so it goes inset. */
  body.no-quackd .cockpit {
    border-color: #e0b93f;
    box-shadow: inset 0 0 0 2px rgba(245, 197, 51, 0.55), var(--shadow-guard);
  }
  /* "DRIVE THE DUCK … ALWAYS LIVE" is a longer string than the numbered version it
     replaced, but it also lost the 24px separator rule that used to be hidden here for
     exactly this reason. The two roughly cancel. Verified not to wrap in the column at
     this tier; if a longer name is ever given to this bay, check it again. */
  .keycaps { gap: var(--s-2); }
  .legend { font-size: var(--t-micro); row-gap: 0.3rem; margin-top: 0; }
  .legend dd { max-width: none; }
  .key-notes {
    /* THE ONE DECLARATION THAT KEEPS #manual OUT OF THE CONSOLE'S ROW SIZING, and it looks
       like a micro-optimisation. #manual spans rows 2 and 3, and a row-spanning grid item
       grows every `auto` track it spans to fit its own content. `min-height: 0` zeroes only
       the MIN-content contribution; the MAX-content contribution still grows those tracks, so
       the legend used to stretch the arena's row and the deck's row by ~34px each — 67px past
       the fold, a 34px strip of dead card under the arena, and the glass HUD (which anchors
       to .stage, not to the canvas) hanging off the bottom of the render. `contain: size`
       makes this box contribute NOTHING to intrinsic sizing, so the rows are sized by the
       arena and the deck alone, #manual stretches to whatever they came to, and the 1fr row
       above hands the remainder here to scroll. The cheap regression check is that .stage's
       height equals canvas#view's height at 1366×641.
       min-height is the floor if a browser ever leaves this row indeterminate: with
       containment an auto height would collapse the legend to nothing. */
    contain: size;
    min-height: 7rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    margin-top: var(--s-2);
    padding-top: var(--s-2);
    /* the ring is 2px at a 3px offset and outlines do not scroll into view, so a scroll
       container flush with its content clips it left and right (FINDING: .why's summary) */
    padding-inline: 5px;
    margin-inline: -5px;
    border-top: 1px solid var(--edge-3);
    scrollbar-width: thin;
    scrollbar-color: var(--edge-soft) transparent;
  }
  /* The MAP is pinned and the WORDS scroll. All thirteen keycaps are visible at every height;
     only the reading matter behind them gives way, and only when the arena and the deck did
     not leave it room. */
}

/* THE FOLD, AND HOW THE ARENA PAYS FOR IT.
   ---------------------------------------------------------------------------------------
   Everything in the console except the arena is a fixed number of pixels tall: the band is
   the summary, the privacy note, one field row and the provider note; the deck is the 01
   head, the input, the button row and the chips; the cockpit no longer contributes anything
   (see `contain: size` above). So the console's height is that constant plus the canvas, and
   the canvas is the only part that can give.

   That is why the height below is `calc(100vh - <constant>)` and not a bare `Nvh`. A
   fraction of the viewport is only ACCIDENTALLY right at one height: 32vh fitted a 641px
   laptop and overflowed a 768px one by 70px, because the fixed remainder does not scale.
   Subtracting the measured remainder makes the fit a straight line instead of a staircase,
   so every height from ~590px up lands the same way and there is no dead zone where a TALLER
   viewport overflows worse than a shorter one. It is still viewport-relative, as it must be;
   it is just relative to the space that is actually left.

   The constant is the measured chrome above and below the arena, rounded up for slack, and
   it therefore changes when that chrome does. If the privacy note ever wraps to a third line
   or a bay grows a row, this number is what you re-measure — the symptom will be the card
   going back under the fold. The clamp floor is the point at which the arena stops being the
   centrepiece and we let the page scroll instead.
   HARD RULE unaffected: this resolves on resize only, never per frame, so view.js's WebGL
   buffer is reallocated exactly as often as it was before. */

@media (min-width: 62rem) {
  :root {
    /* everything above and below the arena, measured: the nav and the wrap's top padding,
       the brandline, the band, the deck, and a few px of slack. */
    --fold-chrome: 35rem;
    --arena-floor: 12rem;
    --arena-cap: 30rem;
    /* and what the band costs when it does not fit on one line at this WIDTH — see below. */
    --band-wrap: 0rem;
  }
  /* `vh`, not `dvh`: this rule only runs at 62rem and up, where there is no collapsing URL
     bar to care about, and `dvh` inside a var()-bearing declaration cannot fall back to the
     line above it — it would compute to `auto` on a browser that lacks the unit. */
  canvas#view {
    height: clamp(var(--arena-floor),
                  calc(100vh - var(--fold-chrome) - var(--band-wrap)),
                  var(--arena-cap));
  }
}

/* THE BAND'S OWN WIDTH TIER. Below ~72rem the credential track gets too narrow for the
   provider/model row and the key field to share a line, so the band takes an extra line
   (and the privacy note a third) — 65px the height tiers cannot see, because it is a
   function of width. Narrowing the switch's track buys most of it back; the rest is
   declared here so the arena still knows what is left. */
@media (min-width: 62rem) and (max-width: 72rem) {
  .model-bar { grid-template-columns: minmax(0, 19rem) minmax(0, 1fr); }
  :root { --band-wrap: 0.25rem; }
}
/* and narrower still, where the deck's own bays start wrapping too */
@media (min-width: 62rem) and (max-width: 68rem) {
  :root { --band-wrap: 2rem; }
}

/* THE LAPTOP TIER, and it starts far higher than it used to. It used to open at 46rem
   (736px) and so a literal 768px viewport got the TALLEST variant and none of these
   reductions — the reason the required 1366×768 case overflowed by 70px while the shorter
   1280×720 overflowed by 35px. Opening it at 58rem (928px) means the two reductions that
   cost the least — a brand line that repeats the nav and a second row of optional chips —
   are spent well before the fold is actually in danger. */
@media (min-width: 62rem) and (max-height: 58rem) {
  :root { --fold-chrome: 28rem; --arena-floor: 11rem; --arena-cap: 28rem; }

  .wrap { padding-block: var(--s-1) var(--s-4); }
  .brandline .lede { display: none; }
  .brandline { margin-bottom: var(--s-2); }

  /* The chips become ONE SCROLLING ROW, worth 25px and the cheapest 25px on the page. Three
     declarations, and all three are load-bearing:
       · nowrap on the container, or it is two rows;
       · `flex: 0 0 auto; white-space: nowrap` on the CHIPS, or nowrap only squeezes them and
         each one takes two text lines — taller than the two-row version it replaced;
       · padding-block for the focus ring. Outlines do not contribute to scrollable overflow,
         so a scroll container flush with its content clips the ring instead of scrolling to
         it. The negative margin hands the padding back, so the row occupies what it did.
     Never a `text-overflow` here: a half-shown example is not an example. */
  .examples {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: thin;
    padding-block: 5px;
    margin-block: -5px;
  }
  .examples .chip { flex: 0 0 auto; white-space: nowrap; }

  .row.actions { margin: var(--s-1) 0 var(--s-1); }  /* the bottom margin stays --s-1: #run's
                                                        7px plinth must keep clearing .examples */
  .keycaps { gap: var(--s-1); margin-top: var(--s-1); }

  /* The boot panel is 289px of content against an arena of ~217px here, and `safe center`
     lets it scroll rather than clip its head off — correct, but a half-shown line of the
     45MB note reads as a bug and not as an affordance. Tightened by 72px it fits whole at
     the sizes this tier is actually about; below 600px of viewport it scrolls again, which
     is what the overlay was built to do. */
  .overlay { padding: var(--s-3); gap: var(--s-1); }
  .boot-mark { width: 38px; height: 38px; }
}

/* THE VALVE — a real laptop: 1366×768 minus Windows Chrome's chrome is 641px, 1280×720 is
   593px, and a bookmarks bar takes another 40. The eyebrow goes here, and the 26px it costs
   go straight into the arena: the nav already carries the wordmark, "a Microduck with a
   brain" and a status pill that says "MuJoCo · WASM · your key stays in this tab", which is
   very nearly what the eyebrow says. The page is not left mute — it stops saying the same
   thing twice at the one height where the duck is fighting for pixels.
   Opening at 46rem (736px) rather than the 40rem this used to use is deliberate: 641px is
   40.06rem, so the old boundary sat ONE CSS pixel from the single most important case and a
   bookmarks bar flipped it. */
@media (min-width: 62rem) and (max-height: 46rem) {
  .brandline { display: none; }
  :root { --fold-chrome: 26.5rem; --arena-floor: 9.5rem; }
}

@media (max-width: 61.99rem) {
  /* display:contents dissolves the lane boxes so BOTH columns' children become direct grid
     items of .deck and stack in one column. Safe here: both lanes are plain <div>s with no
     implicit ARIA role and no border or background of their own below 62rem.
     WARNING: anything later given .lane as a visual box will silently vanish at this width.

     NO `order` here, and that is the point. Document order already reads 01 Say it, 02 Drive
     it, 03 Your API key, 04 Transcript — the numbers in their own sequence, with the sentence
     box and the keycaps adjacent, which is R2. `order` had it running 01 → 04 → 02 → 03 with
     the log wedged between the two halves of the dual-control argument, and it moved only the
     paint: Tab still followed the DOM, so focus jumped from the last chip to the visually last
     block and then back up to the transcript's clear button (WCAG 2.4.3).

     #key-box and #manual are no longer lane children — #key-box lives in .model-bar above the
     arena and #manual is the console's last child — so the rule that used to reset their
     margins names .console > #manual instead. Document order is still the paint order and the
     Tab order: 03 the key, the arena, 01, 04, 02. The key box being FIRST on a phone is the
     brief, and the keycap map being last is right, because a phone has no keyboard. */
  .lane { display: contents; }
  /* Not `* + *`: a sibling combinator reads the DOM, so the transcript — the first child of
     the second lane — would be the only block in the stack with no rule above it. */
  .deck > .lane > * {
    padding: var(--s-3) var(--gutter);
    margin-top: 0;
    border-top: 1px solid var(--edge-3);
  }
  /* It carries an id, so the 0-2-0 rule above cannot reach the margin its base rule sets. */
  .console > #manual {
    margin: 0;
    padding: var(--s-3) var(--gutter);
    border-width: 1px 0 0;
    /* .cockpit's own --edge-guard is the card's border colour; in the stack this line is one
       of the rules BETWEEN bays, and those are all --edge-3. */
    border-color: var(--edge-3);
    border-radius: 0;
  }
  /* 01's head and its field are ONE bay. The label moved out of the form into .say-head so
     that "01 SAY IT" and "Tell the duck what to do" share a baseline above 62rem; down here
     that would put the between-bays rule and 33px of padding between a label and the input
     it names, so the head keeps the rule that .deck already draws above it and the form
     keeps none at all. Worth 33px of phone height as well. */
  .lane-a > .say-head { border-top: 0; }
  .lane-a > #goal-form { border-top: 0; padding-top: 0; }
}

/* Un-floating the stage chrome is a design decision, not a fallback: a glass pill over a
   320px-wide arena covers the duck. */
@media (max-width: 48rem) {
  .stage-bar {
    position: static;
    padding: var(--s-2);
    border-top: 1px solid var(--edge-3);
    background: rgba(255, 255, 255, 0.7);
    backdrop-filter: none;
    flex-direction: column;
    align-items: stretch;
  }
  .glass {
    border: 0;
    box-shadow: none;
    background: none;
    border-radius: 0;
    flex-wrap: wrap;
    padding: 0;
  }
  canvas#view { height: clamp(15rem, 42vh, 22rem); }
  /* The centring fix that used to live here is now on the base `.overlay` rule — the shorter
     desktop arena exposed the same clipping — so only the tighter padding remains. */
  .overlay { padding: var(--s-3); }
}

@media (max-width: 60rem) {
  /* The pill's claim is not lost with it: the key box repeats "The key stays in this tab" in
     full, one screen down, where the field it is about actually lives. */
  .status-pill { display: none; }
}
/* Short screens lose the lede, never the eyebrow: a 1366×768 laptop is about 660px of viewport
   after browser chrome, and dropping both left the page with no brand voice at all above the
   fold. The eyebrow is one 11px line and it says what this is. */
@media (max-height: 46rem) { .brandline .lede { display: none; } }
@media (max-width: 30rem) {
  /* The provider select and the model name share a row everywhere else. At this width the
     model track is ~128px and "claude-sonnet-5" needs 166, so the value the visitor most
     needs to READ is silently clipped to "claude-sonn". Two rows instead. */
  #key-box .row { flex-wrap: wrap; }
  #key-box .row > * { flex: 1 1 100%; }
  .legend { grid-template-columns: 1fr; }
  .legend dt { margin-top: 0.35rem; }
  .row.actions { flex-wrap: wrap; }
}
@media (max-width: 26rem) {
  .nav-shell h1 small { display: none; }
  /* .cluster-2 also carries .cluster, and both selectors weigh 0-1-0, so a bare `.cluster`
     override here won on source order and reflowed the 2×3 block into 3×2 — K R G / O 1 2.
     kbd's own min-width has to come down with the track or every cap overflows it. */
  .cluster { grid-template-columns: repeat(3, 1.9rem); }
  .cluster-2 { grid-template-columns: repeat(2, 1.9rem); }
  kbd { min-width: 1.9rem; }
}

/* A finger is not a mouse pointer. */
@media (pointer: coarse) {
  button, .button, select, input, summary { min-height: 2.75rem; }
}
