/* ============================================================
   felix et al. Lab companion.
   Extends styles.css and 00-foundation/design/tokens.css. Every colour,
   space and radius below reads from a variable already defined in
   styles.css. Nothing here redeclares .btn-primary, .btn-ghost, .container,
   .prose, .kicker, .display-title or .lead, all of which the page already
   uses as written. Dark is the default surface here for the same reason it
   is everywhere else on the site, and light only ever arrives through the
   header toggle, never through prefers-color-scheme.
   ============================================================ */

/* ---------- One radius, twice ----------
   Felix, looking at the board: "Keep a uniform radius." The same view
   used to show five different corner values on cards, inputs, small
   controls and the tray. Two numbers now, and nothing else anywhere in
   this file or in lab-widgets.css. A control is anything typed into,
   picked from, or clicked: an input, a select, a textarea, a button.
   A surface is anything things sit on or inside: a widget, a panel,
   the rail, the tray, a dropdown, a card. Felix, September 3 2026: no
   pills anywhere on the site, buttons now take the control radius, the
   same one a field already uses, not a third number. Small icon-only
   chrome that was already designed, the widget bar's own buttons, the
   rail star, the drag ghost, keeps its own small radius: it is
   furniture, not a control or a surface, and the brief for this pass
   says to leave the chrome alone unless a change here makes it
   inconsistent. These two alias the sitewide --radius-control/-surface
   in styles.css rather than redeclare the pixel values. */
:root {
  --lab-radius-control: var(--radius-control);
  --lab-radius-surface: var(--radius-surface);
}

/* ---------- One field, everywhere ----------
   Settled, after several passes and one live correction from Felix, in
   `80-website/lab-widgets.css`, in the block headed "Fields, settled"
   at the end of that file. That block is authoritative for every field
   inside a widget or the timer tray: a quiet recessed field at rest, a
   little more weight once it holds a value, one outline around a value
   and its unit with a straight seam and no second material. It loads
   after this file and wins on purpose, so the widget and tray rules
   that used to live here were removed rather than left to compete with
   it, per the instruction that produced this comment.

   What is left here is the one place that block does not reach: a
   standalone tool page, `.lab-tool .lab-field`, which carries no
   `.lab-widget-body` or `.lab-tray-body` ancestor. Same values, so nobody
   reading the two files side by side finds a field that looks different
   for no reason. */
.lab-tool .lab-field input[type="text"],
.lab-tool .lab-field input[type="number"],
.lab-tool .lab-field select,
.lab-tool .lab-field textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-control);
  padding: var(--sp-2);
  font: inherit;
  font-size: 0.9rem;
  min-height: 36px;
}
.lab-tool .lab-field textarea {
  min-height: 5.5rem;
  resize: vertical;
}
.lab-tool .lab-field input:focus-visible,
.lab-tool .lab-field select:focus-visible,
.lab-tool .lab-field textarea:focus-visible {
  border-color: var(--focus);
}
.lab-tool .lab-field input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}
.lab-tool .lab-field input[type="number"]::-webkit-inner-spin-button,
.lab-tool .lab-field input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* The one addition to a paired value and unit that the settled block
   does not already cover: centring, Felix's own sketch, which touches
   no colour, no weight and no radius, so it cannot make a field louder
   than the settled rule already allows, and cannot outweigh a result. */
.lab-input-pair input[type="number"] { text-align: center; }

/* ---------- A table, bare in the markup ----------
   batch-normalise and master-mix each build a plain <table> in a <div>
   with an inline overflow-x, and nothing in this file used to answer
   either element: no row rhythm, a bold centred browser default for
   every header cell, and a native scrollbar cutting across the card the
   moment the table outgrew its widget. Row separators, not shading: a
   filled stripe is a decoration sitting on top of a number, and this
   project's own rule is that nothing decorates a number. A header row
   earns a hairline and a small caps label instead of shouting in bold.
   font-variant-numeric carries through to every cell so a column of
   values keeps its digits the same width top to bottom, which visible
   alignment depends on more than the font size does. */
.lab-widget-body table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
.lab-widget-body caption {
  caption-side: top;
  text-align: left;
  margin-bottom: var(--sp-1);
}
.lab-widget-body th,
.lab-widget-body td {
  padding: var(--sp-1) var(--sp-2);
  text-align: left;
  vertical-align: middle;
  white-space: nowrap;
}
.lab-widget-body thead th {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-faint);
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: var(--sp-1);
}
.lab-widget-body tbody tr {
  border-bottom: 1px solid var(--rule);
}
.lab-widget-body tbody tr:last-child {
  border-bottom: 0;
}
.lab-widget-body td input {
  min-width: 4.5rem;
}

/* The wrapping div carries no class, only an inline overflow-x, which
   :has() reaches without a tool file needing to hand it one. A table
   wider than its widget now scrolls inside a bordered, rounded strip of
   its own rather than the page's raw scrollbar cutting straight across
   the card. */
.lab-widget-body div:has(> table) {
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-control);
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}
.lab-widget-body div:has(> table)::-webkit-scrollbar {
  height: 8px;
}
.lab-widget-body div:has(> table)::-webkit-scrollbar-track {
  background: transparent;
}
.lab-widget-body div:has(> table)::-webkit-scrollbar-thumb {
  background: var(--rule-strong);
  border-radius: 999px;
}
.lab-widget-body div:has(> table) table {
  padding: 0 var(--sp-2);
}

/* ---------- Hero strip, above the shell ---------- */

.lab-promise {
  color: var(--fg-muted);
  font-size: 0.9375rem;
}

.lab-noscript {
  color: var(--fg-muted);
  font-style: italic;
}

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

.lab-shell .lab-search-wrap {
  position: relative;
  margin-bottom: var(--sp-8);
}

.lab-panel {
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 12px;
  padding: var(--sp-6);
}
.lab-panel + .lab-panel {
  margin-top: var(--sp-8);
}

.lab-h2 {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  margin: 0 0 var(--sp-4);
}
.lab-h3 {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  margin: 0 0 var(--sp-2);
}

.lab-label {
  display: block;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  margin-bottom: var(--sp-1);
}

/* Felix: "without box they seem lost but with they would interfere with
   the design." A border around a note gives a secondary line the same
   weight as the control it explains, and no boundary at all reads as
   debris under the field. The third answer is a hairline that says
   relationship rather than container: it sits flush with the field's own
   left edge rather than framing the note, so the eye reads control and
   note as one continuing block, not a form field with a card stapled
   under it. It holds at one line and at four, and two open notes in the
   same widget each carry their own line, so neither reads as attached to
   the other. */
.lab-note {
  font-size: 0.8125rem;
  color: var(--fg-faint);
  margin: var(--sp-1) 0 0;
  padding-left: var(--sp-2);
  border-left: 2px solid var(--rule);
  max-width: none;
}
.lab-note.is-error {
  color: var(--ember);
  font-weight: 600;
  border-left-color: var(--ember);
}

.lab-warn {
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 40%, transparent);
  border-radius: var(--lab-radius-control);
  padding: var(--sp-4);
  font-size: 0.875rem;
  color: var(--fg);
  margin: var(--sp-4) 0;
}

.lab-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  margin-top: var(--sp-4);
}

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

/* Reused, unlabelled, as the scratch pad's textarea: see "One field,
   everywhere" near the top of this file. This class carries no field
   styling of its own any more, so both the top search box, if this is
   ever placed outside a widget again, and that textarea, read as the
   one field. */
.lab-search {
  width: 100%;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-control);
  padding: var(--sp-2);
  font: inherit;
  font-size: 0.9rem;
  min-height: 36px;
}
.lab-search:focus-visible {
  border-color: var(--focus);
}

.lab-search-results {
  list-style: none;
  margin: var(--sp-1) 0 0;
  padding: 0;
  position: absolute;
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--bg-deep);
  border: 1px solid var(--rule-strong);
  border-radius: var(--lab-radius-surface);
  max-height: 20rem;
  overflow-y: auto;
  box-shadow: var(--shadow-2, 0 18px 44px rgba(0, 0, 0, 0.28));
}

.lab-search-result {
  padding: var(--sp-3, var(--sp-2)) var(--sp-4);
  border-top: 1px solid var(--rule);
  cursor: pointer;
  display: grid;
  gap: 2px;
}
.lab-search-result:first-child {
  border-top: 0;
}
.lab-search-result.is-active {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

.lab-search-name {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--fg);
}
.lab-search-why {
  display: block;
  font-size: 0.75rem;
  color: var(--accent);
}
.lab-search-status {
  display: block;
  font-size: 0.75rem;
  color: var(--fg-faint);
}

/* ---------- Buttons, the base the page markup expects ----------
   styles.css only ships .btn-primary and .btn-ghost, applied directly.
   This page's markup carries a bare .btn on several controls, so this is
   the one new rule that base class needs. It reads exactly like
   .btn-ghost, the closest existing shape, and .btn-primary layered on top
   of it still wins on background and label colour, so "btn btn-primary"
   keeps working exactly as it does everywhere else on the site. */
.btn {
  display: inline-block;
  background: transparent;
  color: var(--fg);
  border: 1px solid var(--rule-strong);
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1;
  padding: 12px var(--sp-4);
  border-radius: var(--lab-radius-control);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--motion) var(--ease), background var(--motion) var(--ease);
}
.btn:hover {
  border-color: var(--fg);
  background: rgba(169, 201, 201, 0.06);
}

/* ---------- Timer form ---------- */

.lab-timer-form {
  display: grid;
  gap: var(--sp-4);
}
/* Values match "One field, everywhere" above verbatim, kept as its own
   rule because this one has to reach the timer panel on the page itself,
   outside any widget or tray. */
.lab-field input[type="text"] {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-control);
  padding: var(--sp-2);
  font: inherit;
  font-size: 0.9rem;
  min-height: 36px;
  color: var(--fg);
}
.lab-field input[type="text"]:focus-visible {
  border-color: var(--focus);
}
.lab-field input[aria-invalid="true"] {
  border-color: var(--ember);
}

.lab-toggles {
  border: 0;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-6);
}
.lab-toggles legend {
  width: 100%;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  margin-bottom: var(--sp-1);
  padding: 0;
}
.lab-toggles label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: 0.875rem;
  color: var(--fg);
  cursor: pointer;
}
.lab-toggles input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
}

/* ---------- Timer list ----------
   Remaining time is the one thing on this page that has to read across a
   room, so the clock is large, bold and tabular regardless of viewport,
   never the body text scale the rest of the page uses. */

.lab-timer-list {
  list-style: none;
  margin: var(--sp-6) 0 0;
  padding: 0;
  display: grid;
  gap: var(--sp-4);
}

.lab-timer {
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-surface);
  padding: var(--sp-4);
  display: grid;
  gap: var(--sp-4);
  background: var(--bg);
}
@media (min-width: 40rem) {
  .lab-timer {
    grid-template-columns: 1fr auto;
    align-items: center;
  }
}

.lab-timer-info {
  display: grid;
  gap: 2px;
}
.lab-timer-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  color: var(--fg);
  margin: 0;
}
.lab-timer-clock {
  font-family: var(--font-body);
  font-variant-numeric: tabular-nums;
  font-size: clamp(2.25rem, 1.6rem + 2vw, 3rem);
  font-weight: 700;
  line-height: 1;
  color: var(--fg);
  margin: var(--sp-1) 0 0;
}
.lab-timer.is-paused .lab-timer-clock {
  color: var(--fg-muted);
}
.lab-timer.is-done .lab-timer-clock {
  color: var(--fg-faint);
}
.lab-timer-status {
  font-size: 0.875rem;
  color: var(--accent);
  margin: var(--sp-1) 0 0;
}

.lab-timer-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.lab-timer-btn {
  padding: 8px var(--sp-4);
  font-size: 0.8125rem;
}

/* ---------- Phone hand off ----------
   The QR itself keeps a white ground no matter the theme. A dark module on
   a dark surface simply will not scan, and this is the one place on the
   page where that matters more than matching the palette. */

.lab-handoff {
  margin-top: var(--sp-6);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule);
}
.lab-qr {
  display: block;
  background: #FFFFFF;
  padding: var(--sp-4);
  border-radius: 8px;
  line-height: 0;
}
.lab-qr svg {
  display: block;
  width: 100%;
  height: auto;
  max-width: 20rem;
}
.lab-handoff-payload {
  font-family: monospace;
  font-size: 0.8125rem;
  word-break: break-all;
  background: var(--bg-deep);
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-surface);
  padding: var(--sp-2);
  max-width: 32rem;
}

/* ---------- Working log ---------- */

.lab-trail {
  margin: var(--sp-4) 0 0;
  padding-left: 1.25rem;
  display: grid;
  gap: var(--sp-2);
}
.lab-trail li {
  font-family: monospace;
  font-size: 0.875rem;
  color: var(--fg);
}

/* ---------- Consent, the legally load bearing card ----------
   Not a full screen overlay. It sits inline where the action that
   triggered it happened, which is enough to be unmissable on a page this
   short, and it keeps the focus management simple: one card in normal
   document flow, not a trap to build and maintain. */

.lab-consent {
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  padding: var(--sp-6);
  margin: var(--sp-6) 0;
}
.lab-consent p {
  color: var(--fg);
}

/* ---------- Scanned timer offer ----------
   Built entirely at runtime by lab.js when a QR hand off lands back on
   this page with ?end= in the address, so there is no markup for it to
   redefine. Placed first in the shell, ahead of search, since it is the
   one thing on the page that is time sensitive. */

.lab-adopt {
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-2));
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  padding: var(--sp-6);
  margin-bottom: var(--sp-8);
}
.lab-adopt p {
  margin: 0;
  color: var(--fg);
}

.lab-footnote {
  margin-top: var(--sp-8);
  text-align: center;
}
.lab-link {
  background: none;
  border: 0;
  padding: 0;
  color: var(--fg-faint);
  font-size: 0.8125rem;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
}
.lab-link:hover {
  color: var(--fg-muted);
}

.lab-visually-hidden {
  position: absolute;
  left: -9999px;
  top: -9999px;
}

/* ---------- Source button and panel ----------
   One panel behind every tool's code icon, built once by lab.js and
   reused for whichever tool asked for it last. The icon sits inline next
   to a tool heading, not above it, so it never reads as its own row. The
   overlay only gets display: flex through the :not([hidden]) rule below.
   Setting display directly on .lab-source-overlay would fight the hidden
   attribute lab.js toggles, since an author rule beats the browser's own
   default display: none for [hidden]. */

.lab-source-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  margin-left: var(--sp-2);
  background: transparent;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  color: var(--fg-muted);
  cursor: pointer;
  vertical-align: middle;
  transition: border-color var(--motion) var(--ease), color var(--motion) var(--ease);
}
.lab-source-btn:hover {
  color: var(--fg);
  border-color: var(--fg);
}
.lab-source-btn svg {
  display: block;
}

.lab-source-overlay {
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  padding: var(--sp-4);
  background: rgba(20, 26, 28, 0.72);
  z-index: 100;
}
.lab-source-overlay:not([hidden]) {
  display: flex;
}

.lab-source-panel {
  background: var(--surface-2);
  border: 1px solid var(--rule-strong);
  border-radius: 12px;
  width: 100%;
  max-width: 40rem;
  max-height: calc(100vh - var(--sp-8));
  overflow-y: auto;
  padding: var(--sp-6);
  box-shadow: var(--shadow-2, 0 18px 44px rgba(0, 0, 0, 0.28));
}
.lab-source-panel:focus {
  outline: none;
}
.lab-source-panel:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

.lab-source-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}
.lab-source-head .lab-h3 {
  margin: 0;
  padding-right: var(--sp-4);
}

.lab-source-close {
  flex-shrink: 0;
  background: transparent;
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  color: var(--fg-muted);
  font: inherit;
  font-size: 0.8125rem;
  padding: 6px var(--sp-4);
  cursor: pointer;
}
.lab-source-close:hover {
  color: var(--fg);
  border-color: var(--fg);
}

/* styles.css gives every section 64px of vertical padding, which is the
   page's own rhythm and completely wrong inside a panel: it put 160px of
   dead space between every block here. The panel sets its own spacing. */
.lab-source-body section {
  padding: 0;
  overflow: visible;
}
.lab-source-body section.lab-source-limits { padding: var(--sp-4); }

.lab-source-body section + section {
  margin-top: var(--sp-6);
}


.lab-source-subhead {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0 0 var(--sp-2);
}

/* The explanation is the passage meant to survive being read on its own,
   so it gets normal body type at the panel's full width rather than the
   small, muted treatment .lab-note carries elsewhere on this page. It has
   no subhead of its own: the question just above it, as the panel's
   heading, is the question this paragraph answers. */
.lab-source-explanation {
  margin: 0;
  color: var(--fg);
  font-size: 1rem;
  line-height: 1.55;
  max-width: none;
}

/* One paragraph per idea. Placed after the rule above on purpose: that one
   sets margin to zero, so at equal specificity this has to come later to
   win. A single block of twenty lines is the shape people skip. */
.lab-source-explanation + .lab-source-explanation {
  margin-top: var(--sp-4);
}

/* Equations are typeset from mathml where an entry has it, one browser
   native <math> element per line, and fall back to the plain ASCII line
   as monospace text where it does not, in the same box so the two read
   as one language rather than two. Each equation keeps its own scroll
   container, so one wide equation never pushes the panel wider than the
   text around it.

   The box itself always keeps a light ground and dark ink regardless of
   the page's own theme, the same reasoning .lab-qr already carries for
   the QR code above: an equation is read like a printed formula, not
   themed like the rest of the interface, and paper-0 over ink-0 keeps
   every fraction bar and radical legible in both this panel's dark
   default and the light toggle. */
.lab-source-equations {
  display: grid;
  gap: var(--sp-2);
}
.lab-source-equation {
  padding: var(--sp-3, var(--sp-2)) var(--sp-4);
  overflow-x: auto;
  color: var(--ink-0);
  background: var(--paper-0);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.lab-source-equation math {
  font-size: 1.0625rem;
  color: var(--ink-0);
}
.lab-source-equation-text {
  margin: 0;
  font-family: monospace;
  font-size: 0.9375rem;
  color: var(--ink-0);
  white-space: pre;
}

.lab-source-vars {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-1) var(--sp-4);
}
.lab-source-vars dt {
  font-family: monospace;
  font-weight: 600;
  color: var(--fg);
}
.lab-source-vars dd {
  margin: 0;
  color: var(--fg-muted);
}

/* The limits section is the one part of this panel that has to outrank
   the formula rather than sit quietly under it, so it borrows the same
   warm, bordered treatment .lab-warn already uses elsewhere on this page
   rather than reading as a plain paragraph. */
.lab-source-limits {
  padding: var(--sp-4);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 40%, transparent);
  border-radius: 8px;
}
.lab-source-limits .lab-source-subhead {
  color: var(--fg);
}
.lab-source-limits p {
  margin: 0;
  color: var(--fg);
  font-size: 0.9375rem;
}

.lab-source-sources-note {
  margin: 0 0 var(--sp-2);
}

.lab-source-sources {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-4);
}
.lab-source-source {
  padding: var(--sp-4);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.lab-source-source a {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--link);
}
.lab-source-source a:hover {
  color: var(--link-hover);
}
.lab-source-states {
  margin: var(--sp-2) 0 0;
  font-size: 0.875rem;
  color: var(--fg-muted);
}

/* Modest by design, unlike limits above: a worked example backing the
   sources up, not a warning outranking the formula. Small type, muted
   colour, no border of its own beyond the section rhythm every other
   part of this panel already keeps. */
.lab-source-check {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-1) var(--sp-4);
  font-size: 0.8125rem;
  color: var(--fg-muted);
}
.lab-source-check dt {
  font-weight: 600;
  color: var(--fg-muted);
}
.lab-source-check dd {
  margin: 0;
}

/* ---------- Reduced motion ----------
   The blanket rule in styles.css already collapses every transition and
   animation duration to near zero. Nothing here introduces a keyframe
   animation, so this block only exists to state plainly that the search
   highlight and the timer controls carry no motion of their own beyond
   that shared rule, and to give this file's own reduced motion answer
   rather than relying silently on someone else's. */
@media (prefers-reduced-motion: reduce) {
  .btn,
  .lab-search-result {
    transition: none;
  }
}

/* ---------- Print, the bench copy ----------
   Only the two things worth having on paper survive: the timer list as a
   record of what ran and for how long, and the working log. Everything
   that exists to be clicked, typed into or scanned does not. */

@media print {
  .lab-head .lab-promise,
  .lab-search-wrap,
  .lab-timer-form,
  .lab-warn,
  .lab-handoff,
  .lab-consent,
  .lab-adopt,
  .lab-footnote,
  .lab-timer-controls,
  .lab-trail-panel .lab-row {
    display: none !important;
  }
  .lab-panel {
    background: #FFFFFF !important;
    border-color: #999999 !important;
    break-inside: avoid;
  }
  .lab-timer {
    background: #FFFFFF !important;
    border-color: #999999 !important;
    break-inside: avoid;
  }
  .lab-timer-name,
  .lab-timer-clock,
  .lab-trail li {
    color: #000000 !important;
  }
  .lab-timer-status {
    color: #333333 !important;
  }

  /* A source panel someone actually opened is worth having on the printed
     protocol next to the formula it belongs to, so it drops out of its
     fixed overlay and flows into the page like any other panel. A closed
     one prints nothing, since :not([hidden]) never matches it and the
     button that opens it is hidden below like every other control on
     this page. */
  .lab-source-btn {
    display: none !important;
  }
  .lab-source-overlay:not([hidden]) {
    position: static !important;
    display: block !important;
    padding: 0 !important;
    background: none !important;
  }
  .lab-source-panel {
    max-width: none !important;
    max-height: none !important;
    overflow: visible !important;
    box-shadow: none !important;
    background: #FFFFFF !important;
    border-color: #999999 !important;
    break-inside: avoid;
  }
  .lab-source-close {
    display: none !important;
  }
  .lab-source-equation,
  .lab-source-limits {
    background: #FFFFFF !important;
    border-color: #999999 !important;
  }
  .lab-source-source {
    border-color: #999999 !important;
  }
  .lab-source-panel,
  .lab-source-explanation,
  .lab-source-equation,
  .lab-source-equation math,
  .lab-source-equation-text,
  .lab-source-vars dt,
  .lab-source-vars dd,
  .lab-source-limits p,
  .lab-source-source a,
  .lab-source-states,
  .lab-source-subhead,
  .lab-source-sources-note,
  .lab-source-check dt,
  .lab-source-check dd {
    color: #000000 !important;
  }
}

/* ============================================================
   Tool calculators.
   The two centrifuge tools, and every future one built the same way:
   an article.lab-tool holding its question, its answer in prose, a live
   form, and a details.lab-source disclosure carrying the formula and its
   sources, rendered at build time by _src/render.py's formula_block().
   That disclosure is a different piece of markup from the older
   lab-source-btn overlay above, which no tool on this page still opens,
   so nothing below touches those selectors.

   styles.css gives every section 64px of vertical padding, its own
   rhythm, and .lab-tool is not a section element, so nothing here
   inherits that. It still gets its own explicit padding below, on
   purpose, so a compact card never depends on what a bare section tag
   happens to give it. ============================================== */

.lab-tools .container {
  display: grid;
  gap: var(--sp-8);
}

.lab-tool {
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: 12px;
  padding: var(--sp-6);
}

.lab-tool-question {
  margin: 0 0 var(--sp-4);
}

.lab-tool-answer {
  margin: 0 0 var(--sp-6);
}
.lab-tool-answer p {
  color: var(--fg-muted);
}

/* ---------- The calculator itself ---------- */

.lab-form {
  display: grid;
  gap: var(--sp-4);
}

.lab-tool .lab-field {
  display: grid;
  gap: var(--sp-1);
}
.lab-tool .lab-field > label,
.lab-tool .lab-field > legend {
  display: block;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  padding: 0;
}
fieldset.lab-field {
  border: 0;
  margin: 0;
}

/* The one field's fill, border, radius, height and padding are declared
   once, near the top of this file, against the same selectors repeated
   here. Kept as a bare comment rather than a duplicated rule: see "One
   field, everywhere" above. */

/* The RPM or RCF radio choice. A different name from .lab-toggles above,
   the timer's own checkbox row, so the two never collide, even though
   they end up reading almost identically. */
.lab-tool .lab-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-6);
}
.lab-tool .lab-toggle > label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--fg);
  cursor: pointer;
}
.lab-tool .lab-toggle input[type="radio"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
}

.lab-out {
  display: block;
  min-height: 1.5em;
  padding: var(--sp-4);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 8px;
  font-size: 0.9375rem;
  color: var(--fg);
}
/* Nothing typed yet, or nothing usable yet, and lab-tools.js has left
   this element with no text in it at all. Empty stays invisible rather
   than an empty box sitting under the form, waiting. */
.lab-out:empty {
  display: none;
}

/* ---------- Formula and sources, the details element ---------- */

.lab-tool .lab-source {
  margin-top: var(--sp-6);
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule);
}
/* The summary is the one clickable control in this whole disclosure, so
   it reads as a small button rather than a plain line of text: a
   border, a hover state and a visible focus ring. */
.lab-tool .lab-source summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  list-style: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--fg-muted);
  padding: 6px var(--sp-4);
  border: 1px solid var(--rule-strong);
  border-radius: var(--lab-radius-control);
  transition: color var(--motion) var(--ease), border-color var(--motion) var(--ease);
}
.lab-tool .lab-source summary::-webkit-details-marker {
  display: none;
}
.lab-tool .lab-source summary:hover {
  color: var(--fg);
  border-color: var(--fg);
}
.lab-tool .lab-source summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}
.lab-tool .lab-source[open] summary {
  color: var(--fg);
  border-color: var(--fg);
  margin-bottom: var(--sp-4);
}
.lab-tool .lab-source-icon {
  flex-shrink: 0;
}

.lab-source-inner > * + * {
  margin-top: var(--sp-4);
}

.lab-source-inner h4 {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0;
}

/* Typeset from mathml, stored at build time by
   10-system/methods/latex-to-mathml.py. The box keeps a light ground and
   dark ink regardless of the page's own theme, the same reasoning .lab-qr
   above already carries for the QR code: a formula is read like a printed
   equation, not themed like the rest of the interface, and paper-0 over
   ink-0 keeps every fraction bar and radical legible whether the toggle
   is set to dark or to light. A wide equation scrolls inside its own box
   rather than ever widening the card around it. */
.lab-eq {
  padding: var(--sp-4);
  overflow-x: auto;
  background: var(--paper-0);
  color: var(--ink-0);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.lab-eq math {
  font-size: 1.0625rem;
  color: var(--ink-0);
}

.lab-symbols {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-1) var(--sp-4);
}
.lab-symbols dt {
  font-family: monospace;
  font-weight: 600;
  color: var(--fg);
}
.lab-symbols dd {
  margin: 0;
  color: var(--fg-muted);
  font-size: 0.9375rem;
}

/* The one part of this panel that has to outrank the formula rather than
   sit quietly under it, so it borrows the same warm, bordered treatment
   .lab-warn already uses elsewhere on this page. */
.lab-limits {
  padding: var(--sp-4);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 40%, transparent);
  border-radius: 8px;
}
.lab-limits h4 {
  color: var(--fg);
  margin: 0 0 var(--sp-2);
}
.lab-limits p {
  margin: 0;
  color: var(--fg);
  font-size: 0.9375rem;
}

.lab-sources {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-4);
}
.lab-sources li {
  padding: var(--sp-4);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.lab-sources a {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--link);
}
.lab-sources a:hover {
  color: var(--link-hover);
}

/* Modest by design, unlike limits above: a worked example backing the
   sources up, not a warning outranking the formula. */
.lab-check {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-1) var(--sp-4);
  font-size: 0.8125rem;
  color: var(--fg-muted);
}
.lab-check dt {
  font-weight: 600;
  color: var(--fg-muted);
}
.lab-check dd {
  margin: 0;
}

/* ============================================================
   The dashboard
   Added 2026-08-31.

   Two arguments were commissioned for this, warmth and precision, and
   they are in 20-evidence/2026-08-31-visual-direction-warmth.md and
   ...-precision.md. Neither wins outright, and the split is not a
   compromise, it is where the two agreed once pushed: warmth owns the
   chrome, precision owns the data. Rounded, lit, slightly soft
   surfaces, because this page is meant to be left open all day and a
   surface you look at for eight hours should be pleasant. Nothing
   decorative anywhere near a number, a unit, a formula or the line that
   separates a fact from a judgment call, because a decorated number is a
   number somebody misreads.
   ============================================================ */

/* ---------- Layout ---------- */

.lab-layout {
  display: grid;
  gap: var(--sp-4);
  align-items: start;
}

@media (min-width: 62rem) {
  .lab-layout {
    grid-template-columns: 15rem minmax(0, 1fr);
    gap: var(--sp-6);
  }
  .lab-layout:has(.lab-rail.is-collapsed) {
    grid-template-columns: 3rem minmax(0, 1fr);
  }
}

/* ---------- The rail ---------- */

.lab-rail {
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-surface);
  padding: var(--sp-4);
  position: relative;
}

@media (min-width: 62rem) {
  .lab-rail {
    position: sticky;
    top: 96px;
    max-height: calc(100vh - 128px);
    display: flex;
    flex-direction: column;
  }
}

.lab-rail-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); margin-bottom: var(--sp-2);
}

.lab-rail-label {
  margin: 0; font-family: var(--font-body); font-size: 0.75rem;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-faint);
}

.lab-rail-toggle {
  background: none; border: 0; color: var(--fg-muted); cursor: pointer;
  padding: var(--sp-1); border-radius: 8px; line-height: 0;
  min-width: 34px; min-height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color var(--motion) var(--ease), transform var(--motion) var(--ease);
}
.lab-rail-toggle:hover { color: var(--fg); }
.lab-rail-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* Values match "One field, everywhere" above verbatim: the rail search
   is the same control as everything else typed into on this page. */
.lab-rail-filter {
  width: 100%; box-sizing: border-box;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--rule); border-radius: var(--lab-radius-control);
  padding: var(--sp-2); font: inherit; font-size: 0.9rem;
  min-height: 36px;
  margin-bottom: var(--sp-2);
}
.lab-rail-filter:focus-visible { border-color: var(--focus); }

.lab-rail-list {
  list-style: none; margin: 0; padding: 0;
  overflow-y: auto; flex: 1 1 auto;
}

.lab-rail-head {
  font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fg-faint); margin: var(--sp-2) 0 var(--sp-1);
}
.lab-rail-head:first-child { margin-top: 0; }

.lab-rail-item {
  display: flex; align-items: center; gap: var(--sp-1);
  border-radius: var(--lab-radius-control);
  transition: background var(--motion) var(--ease);
}
.lab-rail-item:hover { background: rgba(169, 201, 201, 0.06); }

.lab-rail-add {
  flex: 1 1 auto; text-align: left; background: none; border: 0;
  color: var(--fg); font: inherit; font-size: 0.9rem;
  padding: var(--sp-2); cursor: grab; border-radius: var(--lab-radius-control);
  touch-action: none; min-height: 34px;
}
.lab-rail-add:active { cursor: grabbing; }
.lab-rail-add:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; border-radius: var(--lab-radius-control); }

.lab-rail-star {
  background: none; border: 0; color: var(--fg-faint); cursor: pointer;
  padding: var(--sp-1); line-height: 0; border-radius: 6px;
  min-width: 34px; min-height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color var(--motion) var(--ease), transform 180ms var(--ease);
}
.lab-rail-star:hover { color: var(--accent); transform: scale(1.15); }
.lab-rail-star[aria-pressed="true"] { color: var(--accent); }
.lab-rail-star:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

.lab-rail-empty, .lab-rail-foot {
  font-size: 0.8rem; color: var(--fg-faint); margin: var(--sp-2) 0 0;
}
.lab-rail-foot a { color: var(--link); }

.lab-rail.is-collapsed .lab-rail-label,
.lab-rail.is-collapsed .lab-rail-filter,
.lab-rail.is-collapsed .lab-rail-list,
.lab-rail.is-collapsed .lab-rail-foot { display: none; }
.lab-rail.is-collapsed { padding: var(--sp-2); }
.lab-rail.is-collapsed .lab-rail-toggle { transform: rotate(180deg); }

/* The thing under the cursor while a tool is being dragged out of the
   rail. Deliberately light: it says what is being carried and nothing
   else, since a full preview under a moving cursor reads as lag. */
.lab-rail-ghost {
  position: fixed; z-index: 90; pointer-events: none;
  transform: translate(8px, 8px);
  background: var(--surface); color: var(--fg);
  border: 1px solid var(--rule-strong); border-radius: 10px;
  padding: var(--sp-1) var(--sp-2); font-size: 0.85rem;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
}

/* The board bar itself, .lab-board-bar and .lab-board-btn, is styled in
   lab-widgets.css beside the pin and the other host-added controls,
   not here: a second definition in this file would only fight it for
   the same properties at the same specificity. */

/* ---------- The canvas ---------- */

.lab-canvas {
  display: grid; gap: var(--sp-4);
  min-height: 12rem;
}

/* Felix: "grid intercalates with the background." There were two grids:
   the page's own coarse one from styles.css, painted on the body, and
   this one, finer, painted on a transparent canvas that let the first
   one show through. Different origins, different spacings, so wherever
   they crossed the two produced interference rather than a surface,
   worst exactly where the board is empty and there is nothing else to
   look at. One grid now: the canvas gets its own opaque ground, the
   page grid stops being visible through it, and this is the only grid
   drawn from here down. Same spacing and the same faint weight as the
   page's own, on purpose, so it reads as the same fabric continuing
   rather than a second texture arguing with the first. */
.lab-canvas.is-free {
  display: block; position: relative;
  border-radius: var(--lab-radius-surface);
  background-color: var(--surface-2);
  background-image:
    linear-gradient(var(--grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: 64px 64px, 64px 64px;
}

.lab-canvas-empty {
  border: 1px dashed var(--rule-strong); border-radius: var(--lab-radius-surface);
  padding: var(--sp-6); text-align: center; color: var(--fg-muted);
}
.lab-canvas.is-free .lab-canvas-empty {
  position: absolute; inset: var(--sp-4);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.lab-canvas-empty-lead {
  font-family: var(--font-editorial); font-size: 1.15rem; color: var(--fg);
  margin: 0 0 var(--sp-2);
}
.lab-canvas-empty p { max-width: 34ch; margin-left: auto; margin-right: auto; }

/* ---------- A widget ---------- */

/* Felix, with real Revolut screens open beside the board: "use the full
   height... this is not revolut style." In list mode, not free mode, a
   grid row stretches every widget in it to the height of the tallest
   one, and until now the content inside just sat at the top of that
   taller box with dead air underneath. A flex column lets that content
   answer the question instead: .lab-widget-body below grows to fill
   whatever height the row gives it, and its own last child carries
   margin-top: auto, so a short tool settles its answer at the bottom of
   the space rather than leaving the gap after it. In free mode a widget
   has no forced height, so there is nothing to distribute and this is a
   no-op there. */
.lab-widget {
  display: flex;
  flex-direction: column;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-surface);
  overflow: hidden;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset,
              0 10px 30px rgba(0, 0, 0, 0.28);
  transition: box-shadow var(--motion) var(--ease), border-color var(--motion) var(--ease),
              opacity var(--motion) var(--ease);
  /* Lets .lab-widget-kind below ask about the widget's own rendered
     width rather than the viewport's: a widget can be dragged down to
     280px wide on a screen that is nowhere near that narrow. */
  container-type: inline-size;
  container-name: lab-widget;
}
.lab-canvas.is-free .lab-widget { position: absolute; }
.lab-widget:focus-within { border-color: var(--rule-strong); }
.lab-widget.is-dragging {
  box-shadow: 0 22px 50px rgba(0, 0, 0, 0.45);
  border-color: var(--rule-strong);
  cursor: grabbing;
}

/* Opening a tool is provisional now: it is on the board to be looked at,
   and it leaves again when another one opens, unless it is pinned or
   typed into. Unpinned has to read as temporary, not as broken, and it
   has to survive a drag, a fold and a duplicate, so nothing here removes
   interactivity or full opacity, and .lab-widget-pin below carries the
   only badge, not a whole faded card, since a faded card is what a
   disabled tool would look like. */
.lab-widget.is-unpinned { border-style: dashed; }

.lab-widget-bar {
  display: flex; align-items: center; gap: var(--sp-2);
  flex-shrink: 0;
  padding: var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-4);
  background: linear-gradient(180deg, rgba(169, 201, 201, 0.07), transparent);
  border-bottom: 1px solid var(--rule);
  cursor: grab;
  touch-action: none;
}
.lab-canvas:not(.is-free) .lab-widget-bar { cursor: default; }

.lab-widget-name {
  flex: 1 1 auto; min-width: 4rem;
  background: none; border: 0; border-bottom: 1px solid transparent;
  color: var(--fg); font-family: var(--font-body); font-size: 0.95rem;
  font-weight: 600; padding: 2px 0;
}
.lab-widget-name:hover { border-bottom-color: var(--rule); }
.lab-widget-name:focus { outline: none; border-bottom-color: var(--focus); }

/* The bar already carries the user's own name for the tool, the pin
   already says what "provisional" means, the (i) opens the full title
   in the source panel. A third statement of the same fact, our own name
   for the tool, only earns its place once the bar is wide enough to
   show more than a fragment of it: capped by a percentage of the bar it
   used to resolve to one letter and an ellipsis on a narrow widget,
   which read as broken rather than as absent. Hidden by default, shown
   only once its own container has room for a couple of words. */
.lab-widget-kind {
  display: none;
  font-size: 0.7rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--fg-faint); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
@container lab-widget (min-width: 340px) {
  .lab-widget-kind { display: block; max-width: 40%; }
}

.lab-widget-actions { display: flex; align-items: center; gap: 2px; }

.lab-widget-btn {
  background: none; border: 0; color: var(--fg-faint); cursor: pointer;
  padding: var(--sp-1); border-radius: 6px; line-height: 0;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 34px; min-height: 34px;
  text-decoration: none; font-size: 0.8rem; font-weight: 700;
  transition: color 160ms var(--ease), background 160ms var(--ease);
}
.lab-widget-btn:hover { color: var(--fg); background: rgba(169, 201, 201, 0.08); }
.lab-widget-btn:focus-visible {
  outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 6px;
}

.lab-widget-body {
  padding: var(--sp-2) var(--sp-4) var(--sp-4);
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
}
.lab-widget.is-collapsed .lab-widget-body { display: none; }
.lab-widget.is-collapsed .lab-widget-collapse svg { transform: rotate(180deg); }
/* A folded widget is one bar tall: nothing left in it to resize, and the
   handle used to sit in the corner regardless, overlapping the bar's
   own rounded edge. */
.lab-widget.is-collapsed .lab-widget-resize { display: none; }
.lab-widget-collapse svg { transition: transform 200ms var(--ease); }

.lab-widget-resize {
  position: absolute; right: 0; bottom: 0; width: 16px; height: 16px;
  cursor: ew-resize; touch-action: none; display: none;
}
.lab-canvas.is-free .lab-widget-resize { display: block; }
.lab-canvas.is-free .lab-widget.is-collapsed .lab-widget-resize { display: none; }
.lab-canvas.is-free .lab-widget-resize::after {
  content: ""; position: absolute; right: 4px; bottom: 4px;
  width: 6px; height: 6px; border-right: 2px solid var(--rule-strong);
  border-bottom: 2px solid var(--rule-strong);
}

/* ---------- The timer tray ----------
   Fixed to the top right and offset below the site header rather than
   over it. The header is 70px unstuck and sticky at z-index 50, so the
   tray sits under it in both senses: lower on the page and lower in the
   stack, which is what keeps the navigation reachable while six timers
   are running. */

/* Felix wants the tray warm, one of the orange tones, because it is an
   overlay sitting over the work and should read as clearly not part of
   the board. Picked clay-d, and picked it fixed, the same hex in light
   and dark, the way .lab-eq below already keeps a light ground and dark
   ink regardless of the page's own theme: an overlay that changes
   colour with the toggle underneath it is harder to recognise as the
   same object at a glance, and this one only has one job.

   Measured against paper-1, the fixed text colour used everywhere in
   here: 5.09 to 1, comfortably past the 4.5 small text needs. Against
   paper-0, used nowhere here but checked anyway: 5.50. Against white,
   5.87, which is the same number styles.css already prints next to
   clay-d as the light theme accent, so the pairing is not a new
   guess. Not gold: gold is already the accent almost everywhere else on
   this page, and reusing it here would make the tray look like another
   piece of the board rather than a thing sitting over it. Not clay or
   clay-l: paper-1 on clay is 3.14, on clay-l 2.31, both well under the
   4.5 small text needs. Not ember as the base: it reads as an alarm at
   rest, which a tray full of running timers is not. */
/* Felix, first pass: pick one of the orange tones and hold it across
   both themes, the way .lab-eq keeps a fixed ground regardless of the
   toggle. Felix, second pass, having seen it: "darker orange for the
   timers night theme, for day a bit more washed out." Not one hex in
   both themes after all, one hue pulled to two different depths, which
   is what the fixed pair of local properties below gives each theme
   without either one guessing at the other's number.

   Dark: clay-d flat, the deepest of the three named clay tones. Text at
   paper-1 measures 5.09 to 1 against it, comfortably past the 4.5 small
   text needs, and it is the same pairing styles.css already prints as
   the light theme's own accent contrast (5.87 to 1 against white), so
   it is a measured pairing, not a new guess. Plain clay was tried first
   and rejected: paper-1 on clay is 3.14, under the minimum.

   Light: clay mixed a third of the way toward paper-0, worked out as
   30% clay because it is the palest mix that still reads as clay rather
   than as tinted paper, not chosen for round numbers. Computed at
   #E7D0C4, ink-0 on it measures 11.06 to 1 and slate 5.41 to 1, both
   comfortably clear of what small text needs, so the lighter theme can
   afford an actual muted tier where the dark one could not. */
.lab-tray {
  --lab-tray-fill: var(--clay-d);
  --lab-tray-fg: var(--paper-1);
  --lab-tray-fg-muted: var(--paper-1);
  --lab-tray-edge: rgba(255, 255, 255, 0.18);
  --lab-tray-scrim: rgba(0, 0, 0, 0.32);
  /* Raised from 40 when a maximised widget started covering the whole
     viewport at 55, header included, because Felix asked for fullscreen
     to mean fullscreen. The tray has to sit over that widget: a timer
     hidden behind a calculator is the one failure this page cannot
     have. It still never draws over the navigation, because it is
     fixed at 88px, below a 70px header, and is a bottom bar on a
     phone. Consent at 60 and the row card at 70 stay above it. */
  position: fixed; z-index: 56;
  top: 88px; right: var(--sp-4);
  width: min(21rem, calc(100vw - 2 * var(--sp-4)));
  background: var(--lab-tray-fill);
  color: var(--lab-tray-fg);
  border: 1px solid var(--lab-tray-fill);
  border-radius: var(--lab-radius-surface);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38);
  overflow: hidden;
}
[data-theme='light'] .lab-tray {
  --lab-tray-fill: color-mix(in srgb, var(--clay) 30%, var(--paper-0));
  --lab-tray-fg: var(--ink-0);
  --lab-tray-fg-muted: var(--slate);
  --lab-tray-edge: rgba(26, 33, 35, 0.14);
  --lab-tray-scrim: rgba(26, 33, 35, 0.08);
  border-color: var(--clay);
  box-shadow: 0 16px 40px rgba(26, 33, 35, 0.22);
}

.lab-tray-toggle {
  width: 100%; display: flex; align-items: center; gap: var(--sp-2);
  background: linear-gradient(180deg, var(--lab-tray-scrim), transparent);
  border: 0; border-bottom: 1px solid var(--lab-tray-edge);
  color: var(--lab-tray-fg); font: inherit; text-align: left;
  padding: var(--sp-2) var(--sp-4); cursor: pointer;
}
.lab-tray-title { font-weight: 600; font-size: 0.95rem; }
.lab-tray-count {
  flex: 1 1 auto; font-size: 0.8rem; color: var(--lab-tray-fg-muted);
  font-variant-numeric: tabular-nums;
}
.lab-tray-arrow { line-height: 0; color: var(--lab-tray-fg); }
.lab-tray-arrow svg { transition: transform 220ms var(--ease); }
.lab-tray.is-folded .lab-tray-arrow svg { transform: rotate(180deg); }
.lab-tray.is-folded .lab-tray-body { display: none; }
.lab-tray.is-folded .lab-tray-toggle { border-bottom: 0; }

/* A finished timer used to turn the tray's border to the accent colour,
   the one thing it was allowed while folded away. The tray is warm all
   the time now, so a border colour change has nothing left to move
   from, and a timer also rings until it is acknowledged, which means
   the visual only has to carry the message once the sound is already
   gone and the tray may already be folded to one line. Ember laid over
   either fill fails as running text (1.19 to 1 on clay-d; worse on the
   light mix, both far under 4.5), so it never sits directly on the
   tray's own ground: a small solid chip instead, its own fill, paper-0
   on ember measured at 4.64. That chip sits on .lab-tray-count, which
   stays in the bar when folded, so calm orange holding steady and then
   a red badge that was not there before reads as a real change in both
   themes and in both the open and the folded state. */
.lab-tray.has-done { box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38), 0 0 0 2px var(--ember); }
[data-theme='light'] .lab-tray.has-done { box-shadow: 0 16px 40px rgba(26, 33, 35, 0.22), 0 0 0 2px var(--ember); }
.lab-tray.has-done .lab-tray-count {
  display: inline-flex; align-items: center;
  background: var(--ember); color: var(--paper-0);
  padding: 2px var(--sp-2); border-radius: 999px;
  flex: 0 1 auto;
}

.lab-tray-body {
  padding: var(--sp-4);
  max-height: calc(100vh - 176px);
  overflow-y: auto;
}
/* Labels, notes and the warning box inherited their colour from the
   board's own tokens, tuned for the page's own background, not for
   this fixed one. Each theme gets its own scrim under the warning box
   rather than one value guessed to work for both: measured at 8.47 to 1
   in dark, where a black scrim under clay-d only raises the 5.09
   baseline further, and at 9.56 to 1 in light, where a black scrim
   under the pale mix costs a little of its very large 11.06 headroom
   but stays nowhere near the 4.5 floor. */
.lab-tray-body .lab-label,
.lab-tray-body .lab-note { color: var(--lab-tray-fg); }
.lab-tray-body .lab-note { border-left-color: var(--lab-tray-edge); }
.lab-tray-body .lab-warn {
  background: var(--lab-tray-scrim);
  border-color: var(--lab-tray-edge);
  color: var(--lab-tray-fg);
}
.lab-tray-body .lab-timer-form { margin-bottom: var(--sp-2); }
.lab-tray-more summary {
  cursor: pointer; font-size: 0.8rem; color: var(--fg-muted);
  margin-bottom: var(--sp-2);
}
.lab-tray-more .lab-toggles { border: 0; padding: 0; margin: 0 0 var(--sp-2); }

/* The sheet is for a phone and only for a phone.

   The breakpoint was at 62rem, which is where free positioning starts, and
   that was the wrong reason: free positioning needs width for two columns,
   the tray only needs width for itself. A browser window at half a large
   monitor is around 960 pixels wide and very tall, so it fell into the
   phone layout and the tray became a full width sheet six hundred pixels
   high. Felix caught it: fine on a phone, fine full screen, wrong in
   between. The corner tray now holds down to 48rem, which is the width
   below which 19rem of tray genuinely stops fitting beside anything. */
@media (max-width: 61.99rem) and (min-width: 48rem) {
  /* Still the corner tray, but the board no longer reserves a gutter for
     it at this width, so it overlays. The board is a single column here
     and folding the tray is a real answer rather than a workaround. */
  .lab-tray { width: 19rem; }
}

@media (max-width: 47.99rem) {
  /* A sheet at the bottom, where a thumb is. Folded by default is not set
     here: the preference is the same one the user already chose. */
  .lab-tray {
    top: auto; bottom: 0; right: 0; left: 0;
    width: auto; border-radius: var(--lab-radius-surface) var(--lab-radius-surface) 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
  }
}

/* One height cap for every mode. A tall window used to give the tray
   permission to be as tall as it liked, and a timer form is the same size
   whatever the window does. */
.lab-tray-body { max-height: min(58vh, 30rem); }

/* And the form itself is tighter in the tray than it is on a page. This
   is a panel that sits over somebody's work all day, so it takes the
   space it needs and not the space the page would give it. */
.lab-tray-body .lab-field { margin-bottom: var(--sp-2); }
.lab-tray-body input[type="text"] { padding: var(--sp-2); }
.lab-tray-body .lab-note { font-size: 0.78rem; margin-bottom: 0; }
.lab-tray-body .lab-warn { font-size: 0.8rem; padding: var(--sp-2); }
.lab-tray-body .btn { padding: var(--sp-2) var(--sp-4); }

/* ---------- The index page ----------
   Felix, in the same words he used about the widgets: "looks cheap,
   learn from the revolut agent." Fifty rows of underlined links on a
   bare grid, every one carrying the same weight whether it does
   something or not. A settings list in the real Revolut app answers
   this with no per row border at all, an icon, a title and a muted
   line under it, separation coming entirely from consistent padding
   and left alignment rather than from a ruled table. This keeps the
   hairline, since fifty rows of pure whitespace on a page this dense
   drift without one, but quiets everything else: a group heading reads
   as a label above its rows, not as a second level of page title, an
   action that does something is a small chip rather than a link lost
   in a wall of links, and a row that does nothing dims as a whole
   rather than making a reader parse "Not built yet" fifty times. */
.lab-index-group { margin-bottom: var(--sp-8); }
.lab-index-group + .lab-index-group {
  padding-top: var(--sp-6);
  border-top: 1px solid var(--rule);
}
.lab-index-group .lab-h2 {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-bottom: var(--sp-4);
}
.lab-index-list { list-style: none; margin: 0; padding: 0; }
.lab-index-row {
  display: grid; gap: var(--sp-1);
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--rule);
  align-items: baseline;
  transition: opacity 160ms var(--ease);
}
@media (min-width: 48rem) {
  .lab-index-row { grid-template-columns: 16rem minmax(0, 1fr) auto; gap: var(--sp-4); padding: var(--sp-2) 0; }
}
.lab-index-name { font-weight: 600; }
.lab-index-de { color: var(--fg-faint); font-size: 0.875rem; }
.lab-index-act { font-size: 0.875rem; display: flex; align-items: center; gap: var(--sp-2); }
/* An action reads as an action: a small bordered chip rather than an
   underlined link, the same quiet-pill language .lab-out-copy already
   uses for the one other small in-flow action button on this page. */
.lab-index-add {
  display: inline-flex; align-items: center;
  color: var(--fg); text-decoration: none;
  border: 1px solid var(--rule-strong); border-radius: 999px;
  padding: 5px var(--sp-4); min-height: 34px; box-sizing: border-box;
  transition: color 160ms var(--ease), border-color 160ms var(--ease), background 160ms var(--ease);
}
.lab-index-add:hover { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.lab-index-read {
  color: var(--fg-muted); text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
}
.lab-index-read:hover { color: var(--link); border-bottom-color: var(--link); }
.lab-index-soon { color: var(--fg-faint); font-style: italic; }
/* A row that cannot be acted on dims as a whole rather than leaving the
   reader to notice "Not built yet" in isolated small text: :has() finds
   the marker the row already carries in its markup, no tool file needs
   a class added to say so. */
.lab-index-row:has(.lab-index-soon) { opacity: 0.55; }
.lab-index-foot { margin-top: var(--sp-6); }

/* ---------- Motion ----------
   Everything above that moves, moves because something moved it. There
   is no ambient animation on the working surface. */

@media (prefers-reduced-motion: reduce) {
  .lab-widget, .lab-rail-star, .lab-rail-toggle,
  .lab-tray-arrow svg, .lab-widget-collapse svg, .lab-widget-btn {
    transition: none;
  }
}

/* ---------- Print ----------
   What is on screen is what prints, minus the furniture that cannot be
   pressed on paper. */

@media print {
  .lab-rail, .lab-tray, .lab-search-wrap, .lab-canvas-empty,
  .lab-widget-actions, .lab-widget-resize { display: none; }
  .lab-canvas.is-free { display: block; background: none; min-height: 0; }
  .lab-canvas.is-free .lab-widget {
    position: static; width: auto; margin: 0 0 var(--sp-4);
    break-inside: avoid; box-shadow: none;
  }
  .lab-widget.is-collapsed .lab-widget-body { display: block; }
}

/* ---------- Three fixes found in the browser, not in the code ----------
   Recorded rather than folded silently into the rules above, because each
   one is a mistake worth not making twice. */

/* The empty state carries the hidden attribute when something is on the
   board, and a display rule beats hidden every time. So the widgets
   appeared with "nothing on the board yet" printed underneath them. Any
   element given a display value has to say what hidden means for it. */
.lab-canvas-empty[hidden] { display: none; }

/* Superseded: the kind label used to truncate against a percentage of
   the bar and hide below a viewport width, which is the wrong ruler for
   something that lives inside an independently resizable widget. It now
   hides by default and shows itself once its own container, not the
   window, has room for a couple of words. See .lab-widget-kind and the
   @container rule beside it, above, near .lab-widget-bar. */

/* The tray is fixed over the top right, so on a wide screen the board
   reserved no room for it and a widget dropped at the right edge landed
   underneath it. Only above 84rem, where there is width to give away:
   narrower than that, the tray overlaps and folding it is the answer. */
/* Superseded by the pair at the end of this file, which starts the
   gutter at 62rem instead. Left as a comment rather than deleted so the
   reason the threshold moved stays with the rule that moved. */

/* ============================================================
   The explanations page
   Added 2026-08-31.

   It had no rules at all. Forty five questions, a table of contents
   and a back link, every one of them rendering at browser defaults,
   which is exactly what "looks like a 2010 blog" describes. It is not
   a taste problem, it is a missing stylesheet.

   The page is prose, so it takes the editorial face and a real measure,
   where the board takes the interface face and tabular figures. Two
   different jobs, and the type is what says so.
   ============================================================ */

.lab-faq { padding-top: var(--sp-6); }

@media (min-width: 62rem) {
  .lab-faq-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 17rem;
    gap: calc(var(--sp-8) * 1.2);
    align-items: start;
  }
}

/* ---------- Contents ---------- */

.lab-faq-layout > .lab-faq-toc { order: 2; }
.lab-faq-layout > .lab-faq-body { order: 1; }

.lab-faq-toc {
  border: 1px solid var(--rule);
  border-radius: var(--lab-radius-surface);
  padding: var(--sp-4);
  background: var(--surface-2);
}
@media (min-width: 62rem) {
  .lab-faq-toc {
    position: sticky; top: 96px;
    max-height: calc(100vh - 128px);
    overflow-y: auto;
    background: none; border: 0; padding: 0;
    border-left: 1px solid var(--rule);
    padding-left: var(--sp-4);
  }
}

.lab-faq-toc > summary {
  font-family: var(--font-body); cursor: pointer;
  font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--fg-faint); margin: 0 0 var(--sp-2);
  list-style-position: outside;
}
.lab-faq-toc > summary:hover { color: var(--fg-muted); }
@media (min-width: 62rem) {
  /* On a wide screen the contents sit in their own column with nothing
     competing for the space, so the marker is noise. It stays operable,
     it just stops announcing itself. */
  .lab-faq-toc > summary::marker { content: ""; }
}
.lab-faq-toc ul { list-style: none; margin: 0; padding: 0; }
.lab-faq-toc > ul > li { margin-bottom: var(--sp-4); }
.lab-faq-toc-group > a {
  display: block; font-weight: 600; font-size: 0.9rem;
  color: var(--fg); text-decoration: none; margin-bottom: var(--sp-1);
}
.lab-faq-toc-group > a:hover { color: var(--link-hover); }
.lab-faq-toc-group ul { border-left: 1px solid var(--rule); padding-left: var(--sp-2); }
.lab-faq-toc-group ul li { margin: 0 0 2px; }
.lab-faq-toc-group ul a {
  display: block; font-size: 0.82rem; line-height: 1.4;
  color: var(--fg-muted); text-decoration: none;
  padding: 3px 0 3px var(--sp-1); border-left: 2px solid transparent;
  margin-left: -1px;
}
.lab-faq-toc-group ul a:hover {
  color: var(--fg); border-left-color: var(--accent);
}

/* ---------- The answers ---------- */

.lab-faq-tool {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.1rem);
  line-height: 1.1; letter-spacing: -0.01em;
  margin: calc(var(--sp-8) * 0.9) 0 var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--rule-strong);
  scroll-margin-top: 96px;
}
.lab-faq-tool:first-child { margin-top: 0; border-top: 0; padding-top: 0; }

.lab-faq-item {
  margin: 0 0 var(--sp-6);
  scroll-margin-top: 96px;
}

/* The question is the heading, because the question is what the reader
   arrived with. It is set in the editorial face at reading size rather
   than shouted in the display face: this is the answer to something
   somebody asked, not a section of a brochure. */
.lab-faq-item h3 {
  font-family: var(--font-editorial);
  font-size: 1.18rem; font-weight: 400; line-height: 1.35;
  color: var(--fg); margin: 0 0 var(--sp-2); max-width: 34ch;
}

.lab-faq-item p {
  font-size: 1rem; line-height: 1.65; color: var(--fg-muted);
  max-width: 62ch; margin: 0 0 var(--sp-4);
}
/* The first paragraph is the answer. The ones after it are the detail,
   and a reader who only takes the first sentence should still leave
   with something true. */
.lab-faq-item p:first-of-type { color: var(--fg); }

.lab-faq-back {
  display: inline-block;
  font-size: 0.78rem; letter-spacing: 0.04em;
  color: var(--fg-faint); text-decoration: none;
  border-bottom: 1px solid transparent;
}
.lab-faq-back:hover { color: var(--link); border-bottom-color: var(--rule-strong); }

@media (prefers-reduced-motion: reduce) {
  .lab-faq-toc-group ul a { transition: none; }
}

@media print {
  .lab-faq-toc, .lab-faq-back { display: none; }
  .lab-faq-item { break-inside: avoid; }
}

/* A widget is a panel, not a page. The field notes inside one were
   setting at body size, which made a two line hint about which radius to
   use louder than the answer above it. */
.lab-widget-body .lab-note,
.lab-widget-body p { font-size: 0.82rem; line-height: 1.5; }
.lab-widget-body .lab-label,
.lab-widget-body label { font-size: 0.85rem; }
/* Felix, with real Revolut screens open: "no border for results. The
   number explains itself." A balance in every reference he sent is set
   large directly on the surface, nothing around it, no fill, no rule:
   size, weight and position do what a box used to do. Felix wins where
   this disagrees with anything written earlier in this file, so the
   box .lab-out carried outside widgets, still correct for a standalone
   tool page's own single answer, is switched off here specifically. */
.lab-widget-body .lab-out {
  background: none;
  border: 0;
  padding: 0;
  /* Not every tool's answer is a bare number: several print a full
     sentence, "10 cm at 4000 RPM converts to about 1789 x g, RCF." A
     balance's own type scale, taken directly, would set three lines of
     that sentence in a weight built for four digits. Sized down a step
     from the first draft and off full bold, enough to still read as the
     one answer on the widget and clearly ahead of a 0.9rem field, not
     enough to read as a shout on a tool whose correct answer is a
     clause rather than a number. */
  font-size: 1.1875rem;
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}
/* An empty result is a prompt, not an answer yet, so it stays at the
   note's own quiet weight rather than the answer's. */
.lab-widget-body .lab-out:empty {
  font-size: 0.85rem; font-weight: 400; letter-spacing: normal;
  color: var(--fg-faint);
}
/* Several outputs in one widget: the first is the answer, everything
   after it is support, the way Revolut sets one large balance beside
   smaller, lighter figures rather than repeating the same size down the
   card. Neither gets a box: the size and colour difference alone is the
   hierarchy, per the no border rule above. */
.lab-widget-body .lab-out ~ .lab-out,
.lab-widget-body .lab-out-wrap ~ .lab-out-wrap .lab-out {
  font-size: 1rem;
  font-weight: 500;
  color: var(--fg-muted);
}
/* Clicking a result now copies it, added elsewhere as .is-copyable and
   briefly .is-copied on .lab-out. Numbers are never decorated, so the
   invitation is an underline rather than a colour change on the digits
   themselves, quiet until the pointer is actually over it, and the
   confirmation is a small word appended after the value rather than a
   popup: nothing about the number's own size, weight or ink moves. */
.lab-widget-body .lab-out.is-copyable { cursor: pointer; }
.lab-widget-body .lab-out.is-copyable:hover {
  text-decoration: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--fg-faint);
  text-underline-offset: 0.2em;
}
.lab-widget-body .lab-out.is-copied::after {
  content: "Copied";
  display: inline-block;
  margin-left: var(--sp-2);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--accent);
  vertical-align: middle;
}
.lab-widget-body legend { font-size: 0.85rem; }
/* Felix, looking at a note widget: "too much space lost on padding...
   this is gonna get crowded." A board holds six or ten of these at
   once, so every widget's own air is a tax on how many fit. The gap
   between one field and the next drops by half, and the row that holds
   a widget's own buttons drops with it. The one exception runs the
   other way: the gap right before a result grows, because that is the
   one place on this page where space is doing real work, telling input
   apart from answer, and it is not for sale to make room for the rest. */
.lab-widget-body .lab-field { margin-bottom: var(--sp-2); }
.lab-widget-body .lab-row { margin-top: var(--sp-2); }
/* padding-top is the guaranteed gap before a result, always present
   regardless of how much room the row gives the widget: this is the one
   separation the density pass is not allowed to spend. margin-top: auto
   is the separate, additional amount that only exists in list mode, when
   a grid row has stretched this widget taller than its own content: it
   absorbs whichever part of that extra height is not already spoken
   for, so a short tool's answer settles toward the bottom of the space
   instead of leaving dead air under it. In free mode there is no forced
   height and auto resolves to nothing, leaving exactly the padding. */
.lab-widget-body .lab-out-wrap,
.lab-widget-body .lab-out {
  margin-top: auto;
}
/* The gap itself: on the bare element for a tool with no copy control,
   and on the wrap for a tool with one, where lab-widgets.css reserves
   more room again, 38px rather than 16, to clear the button sitting
   over the top right corner. Declared on the wrap only there, not
   here, so the two are never two padding-top values on the one element
   arguing about which wins. */
.lab-widget-body .lab-out { padding-top: var(--sp-4); }
.lab-widget-body .lab-out-wrap .lab-out { padding-top: 0; }
.lab-widget-body > *:last-child { margin-bottom: 0; }

/* The gutter this pair used to reserve is gone: see "The grid goes to
   the right edge" further down, beside the pair that actually reserved
   it. Only the tray's own width narrowing survives here. */
@media (min-width: 62rem) {
  .lab-tray { width: 19rem; }
}

/* ---------- The rail becomes the search, 2026-08-31 ----------
   Felix: "like the search should be more of a side bar", and the board
   "leaves much unused space". Two problems with one cause. A full width
   search bar sat above the board, so the board started a screen down the
   page and the search was nowhere near the list it searched. The search is
   in the rail now, the rail is the result list, and the board starts at the
   top of the page. */

.lab-head-tight { padding-top: var(--sp-6); padding-bottom: 0; }
.lab-head-tight .lab-head-title {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 1.1rem + 2vw, 2.6rem);
  line-height: 1; margin: var(--sp-1) 0 var(--sp-2);
}
.lab-head-tight .lab-promise {
  margin: 0; font-size: 0.85rem; color: var(--fg-faint); max-width: 62ch;
}
.lab-shell { padding-top: var(--sp-4); }

.lab-rail-hint {
  font-size: 0.72rem; color: var(--fg-faint);
  margin: calc(var(--sp-1) * -1) 0 var(--sp-2);
}

.lab-rail-item.is-unbuilt {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-2); padding: var(--sp-2); color: var(--fg-faint);
  font-size: 0.9rem;
}
.lab-rail-item.is-unbuilt:hover { background: none; }
.lab-rail-soon {
  font-size: 0.68rem; letter-spacing: 0.05em; text-transform: uppercase;
  white-space: nowrap;
}

/* The two toggle icons. Only one is ever in the flow. */
.lab-rail-icon-find { display: none; }
.lab-rail.is-collapsed .lab-rail-icon-fold { display: none; }
.lab-rail.is-collapsed .lab-rail-icon-find { display: block; }
/* Folded, the rail is a 3rem column and the toggle used to stretch full
   width and height inside it with only a small glyph centred in all
   that room, which read as an oversized empty button floating beside
   the board rather than as the edge of a folded panel. It is now a
   compact chip sized to itself, centred in the column instead of
   filling it, with its own quiet fill so it reads as a handle that
   belongs to the rail rather than a control that happens to sit on it. */
.lab-rail.is-collapsed .lab-rail-toggle {
  transform: none; color: var(--fg);
  width: 34px; height: 34px; margin: 0 auto;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border-radius: 999px;
  background: rgba(169, 201, 201, 0.08);
}
.lab-rail.is-collapsed .lab-rail-toggle:hover { color: var(--accent); background: rgba(169, 201, 201, 0.14); }
.lab-rail.is-collapsed .lab-rail-top { margin: 0; }
.lab-rail.is-collapsed #lab-search,
.lab-rail.is-collapsed .lab-rail-hint,
.lab-rail.is-collapsed .lab-search-results { display: none; }

/* The board takes the width it is given. With the search gone from above
   it, the canvas starts where the rail starts and the unused band across
   the top of the page is not there to be unused. */
@media (min-width: 62rem) {
  .lab-canvas.is-free { min-height: 70vh; }
}

/* The question is the control now, so it has to look like one without
   looking like a button. The marker stays: a reader needs to know there is
   something behind it. */
.lab-faq-item > summary {
  font-family: var(--font-editorial);
  font-size: 1.08rem; font-weight: 400; line-height: 1.4;
  color: var(--fg); cursor: pointer; padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--rule);
  max-width: 62ch;
}
.lab-faq-item > summary:hover { color: var(--link-hover); }
.lab-faq-item[open] > summary { border-bottom-color: var(--rule-strong); }
.lab-faq-item { margin: 0 0 var(--sp-2); }
.lab-faq-item[open] { margin-bottom: var(--sp-6); }
.lab-faq-item > .prose { padding-top: var(--sp-4); }

/* The code has to be scannable across a bench, so the tray gives it room
   rather than the code shrinking to fit the tray. */
.lab-tray.has-qr { width: min(24rem, calc(100vw - 2 * var(--sp-4))); }
@media (max-width: 47.99rem) {
  .lab-tray.has-qr { width: auto; }
}

/* ---------- The board takes the whole window ----------
   Felix, looking at a wide monitor: "All this screen real estate".

   Every other page on this site is reading, and reading wants a measure,
   which is what the 76rem container is for. The board is not reading. It is
   a work surface, and a work surface that leaves four hundred pixels of
   nothing down each side while the user drags widgets into a column in the
   middle is refusing the space it was given.

   So the lab pages, and only the lab pages, drop the measure. The prose on
   them keeps its own, because a promise line at 62ch is still a promise
   line at 62ch no matter how wide the window is. */

.lab-head-tight > .container-wide,
.lab-shell > .container-wide {
  max-width: none;
  padding-left: var(--sp-6);
  padding-right: var(--sp-6);
}

/* Felix: "grid intercalates with the background", about the double grid
   fixed above, and in the same look at the board: the right edge should
   run the board's own grid to the window's edge rather than reserving a
   dead band for the tray. The tray is fixed and already draws above
   everything else on the board, so it does not need a gutter kept empty
   underneath it, it can simply sit over the grid the way an overlay
   should. This used to reserve 21 to 22rem here so the canvas never ran
   under the tray; it no longer does, on purpose, and the host reclamps
   a widget's position against the canvas width on drag and on resize,
   so a widget dragged to the far right and then narrowed stays
   reachable rather than stranded under the overlay. */
@media (min-width: 62rem) {
  /* Wider windows can afford a wider rail, which is what stops a tool
     called "Nucleic acid concentration from A260" wrapping onto three
     lines in a column two words wide. */
  .lab-layout { grid-template-columns: 17rem minmax(0, 1fr); }
}
@media (min-width: 90rem) {
  .lab-layout { grid-template-columns: 19rem minmax(0, 1fr); }
}

.lab-source-close {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 34px; min-height: 34px; padding: 0;
  border-radius: 999px; line-height: 0;
}

/* The sources, on the page people actually read. */
.lab-faq-sources {
  margin: 0 0 var(--sp-6);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--rule);
  max-width: 62ch;
}
.lab-faq-sources h3 {
  font-family: var(--font-body);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-faint);
  margin: 0 0 var(--sp-1);
}
.lab-faq-sources ul { list-style: none; margin: 0; padding: 0; }
.lab-faq-sources li { margin-bottom: 2px; }
.lab-faq-sources a { font-size: 0.85rem; color: var(--link); }

/* ---------- One scrollbar treatment across this page ----------
   Felix, pointing at a heavy system bar down the middle of the
   explanations page: "scrollbar". The tray and the rail already had a
   thin one, so the page had two kinds of scrollbar depending on which
   panel you were in. Every panel that scrolls uses the same one. */
.lab-faq-toc,
.lab-faq-toc > ul,
.lab-widget-body,
.lab-search-results {
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}
.lab-faq-toc::-webkit-scrollbar,
.lab-faq-toc > ul::-webkit-scrollbar,
.lab-widget-body::-webkit-scrollbar,
.lab-search-results::-webkit-scrollbar { width: 8px; height: 8px; }
.lab-faq-toc::-webkit-scrollbar-thumb,
.lab-faq-toc > ul::-webkit-scrollbar-thumb,
.lab-widget-body::-webkit-scrollbar-thumb,
.lab-search-results::-webkit-scrollbar-thumb {
  background: var(--rule-strong);
  border-radius: 999px;
}
.lab-faq-toc::-webkit-scrollbar-track,
.lab-faq-toc > ul::-webkit-scrollbar-track,
.lab-widget-body::-webkit-scrollbar-track,
.lab-search-results::-webkit-scrollbar-track { background: transparent; }

/* ---------- The bottom of the page ----------
   Felix: "this in the bottom center with a double click necesssarx".
   Erasing everything and choosing to keep it are the two settings this
   page has, they belong together, and they belong at the end rather than
   in the flow of the work. The erase control arms on the first press and
   acts on the second, which is handled in lab.js. */
.lab-footnote {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  align-items: center;
  justify-content: center;
  text-align: center;
  margin-top: var(--sp-6, 3rem);
}
.lab-link.is-armed { color: var(--ember); }

/* The one setting on this page that is a choice rather than a switch. */
.lab-setting {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--fg-faint);
  font-size: 0.8125rem;
}
.lab-setting .lab-select { width: auto; min-height: 32px; font-size: 0.8125rem; }

/* A clock and a countdown never move their digits under a changing
   character. Felix: "Check alignment after +". */
.lab-timer-clock,
.lab-tray-count,
.lab-tray-unit { font-variant-numeric: tabular-nums; }
/* The overrun is the alarm colour. Written with the same weight as the
   rule it replaces, because .lab-timer.is-done .lab-timer-clock counts an
   element as well as two classes and a plainer selector loses to it. */
.lab-timer.is-done .lab-timer-clock.is-over { color: var(--ember); }

/* The /lab teaser, shown while LAB_PUBLIC is False. Two real captures of
   the board sit behind a blur, and the signup card is the one thing that
   is not blurred, so it is the one thing the eye lands on. This is the
   one page on the site with text over an image; the rest of the site
   never does that (Felix's rule, August 28 2026), because a photograph
   there is a person or a place and needs to read clean. This backdrop is
   our own UI, already softened past the point of reading as a photo, and
   the blur exists so text can sit on it at all. */
.lab-teaser { position: relative; isolation: isolate; padding: var(--sp-8) 0; overflow: visible; }
/* One capture, one blur, everywhere. There used to be a second, more
   heavily blurred copy masked to a soft ellipse behind the signup card,
   so blur intensified toward it. Felix, September 3 2026: "no
   intensifying around the newsletter button, the intensity at the
   corner is enough." Gone, along with --lab-teaser-focus, which existed
   only to position that mask.

   background-size is cover, settled last after 100% 100% (fills edge to
   edge but stretches the image out of proportion at any width far from
   the source's own, which Felix flagged on a narrow phone width) and
   contain (never stretches or crops, but leaves a gap where the box and
   the image's aspect ratio disagree). Felix's own call between the two
   remaining defects: "proportion is more important". Cover never
   distorts and never gaps: it crops one axis instead, and center top
   keeps the crop on the bottom and sides so the board's own title stays
   in frame. No transform: scale layered on top: it was tried, measured
   to leave an unfilled band at the section's bottom edge with the old
   100% 100% sizing, and was never revisited once sizing changed. */
.lab-teaser-bg {
  --lab-teaser-img: url(/assets/img/lab-teaser-dark.jpg);
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--lab-teaser-img);
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center top;
  filter: blur(3px) saturate(1.05);
}
[data-theme="light"] .lab-teaser-bg { --lab-teaser-img: url(/assets/img/lab-teaser-light.jpg); }
/* The scrim, a flat colour layer between the capture and the copy. It
   sits on the same element as the blur, so the blur is untouched: the
   pseudo element paints over the already blurred image, it is not
   itself blurred.

   Felix, September 4 2026: "make the blurring for the PJ and the lab
   darker in dark mode and brighter in bright mode, so it is better
   readable." One shared 42 percent of var(--bg) used to serve both
   themes and served neither well. Measured at 1280 wide before the
   change: in light mode the lead over the darkest part of the capture
   ran at 3.5 to 1 and the median was 4.3, under the 4.5 normal text
   needs; in dark mode the headline's worst patch was 2.4, under the 3.0
   large text needs. After: light worst 5.8, median 6.6; dark lead worst
   7.6, headline worst 3.9.

   Per theme, and both away from var(--bg): --bg in light mode is true
   white, and the site does not paint pure white or pure black. --ink-0
   and --paper-0 are the two ends of the palette and are what the rest
   of the page already sits on. */
.lab-teaser-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ink-0) 58%, transparent);
}
[data-theme="light"] .lab-teaser-bg::after {
  background: color-mix(in srgb, var(--paper-0) 62%, transparent);
}
.lab-teaser-inner {
  display: grid;
  gap: var(--sp-8);
  justify-items: center;
  text-align: center;
}
.lab-teaser-copy { max-width: 100%; }
/* .lead is var(--fg-muted) everywhere else on the site, fine over a
   plain background but too low contrast over the blurred board capture
   in light mode (Felix: "subtext in bright mode difficult to read,
   make it almost black"). var(--fg) is --ink-0 in light mode, close to
   black, and stays the lighter --paper-1 in dark mode, so this only
   changes the one theme where it was actually a problem. */
.lab-teaser-copy .lead { margin-top: var(--sp-4); max-width: 34rem; color: var(--fg); }
/* PCR, spelled by the first letter of each line rather than written out
   as a label above the headline. Same device as the home page ampersand:
   one letter in the editorial serif, italic, in the accent colour.
   Grid rather than inline text: display: contents on the line wrapper
   lets each cap and each word join the SAME two columns across three
   separate rows, so all three caps share one left edge (Felix: "leftbound")
   at whatever width the widest of P, C and R needs, with a real column
   gap to the word instead of the italic's own cramped side bearing. */
.lab-teaser-title { display: grid; grid-template-columns: auto 1fr; column-gap: 0.12em; row-gap: 0; justify-content: center; }
.lab-teaser-line { display: contents; }
.lab-teaser-cap {
  grid-column: 1;
  justify-self: start;
  font-family: var(--font-editorial);
  font-style: italic;
  /* Same color as "et al." in the wordmark (.wordmark .etal), not
     --accent: Felix wants the two reading as the same brand mark. */
  color: var(--clay-l);
  /* Noto Serif italic's own baseline sits lower than Archivo's at the
     same font-size, so P/C/R hung visibly below IPETTE/RY/EPEAT with no
     offset at all. Tuned by eye against the rendered page, not a
     metrics calculation: em based so it holds across the clamp(). */
  vertical-align: 0.12em;
}
.lab-teaser-line > span:last-child { grid-column: 2; justify-self: start; }
@media (min-width: 62rem) {
  .lab-teaser-title { justify-content: start; }
}
.lab-teaser-signup {
  width: 100%;
  max-width: 26rem;
  text-align: left;
  box-shadow: 0 22px 48px rgba(7, 16, 18, 0.32);
}
[data-theme="light"] .lab-teaser-signup { box-shadow: 0 22px 48px rgba(31, 41, 46, 0.16); }
.lab-teaser-signup h2 { margin-top: var(--sp-2); }
.lab-teaser-signup .newsletter-form { margin-top: var(--sp-4); }
@media (min-width: 62rem) {
  .lab-teaser-inner { grid-template-columns: 1fr auto; align-items: center; text-align: left; justify-items: start; }
  .lab-teaser-copy { justify-self: start; }
}
