:root {
  --bg: #0b0e14;
  --panel: #131824;
  --line: #232a3a;
  --text: #e6e9f0;
  --muted: #8b94a8;
  --accent: #7cc4ff;

  /* Cell states. A live cell's brightness IS its transaction's progress:
     dim when merely broadcast, solid once mined. */
  --dead: #161b26;
  --pending: #2d3550;
  --broadcast: #3f6fa8;
  --seen: #62a8e8;
  --mined: #b9e0ff;
  --failed: #e0575b;
  /* A failure on a DEAD cell. It has to be visible — a stalled run must not
     look healthy — but not bright, or the diagram would read it as alive and
     the pattern would be wrong. */
  --failed-dead: #5c2226;

  /* The funding panel. It has three urgencies and they must be legible against
     a dark diagram without shouting in the resting state. */
  --fund-good: #7fd6a0;
  --fund-warn: #e8c46a;
  --fund-warn-bg: #241f14;
  --fund-bad: var(--failed);
  --fund-bad-bg: #2a1519;

  /* This page is a hand-rolled dark theme, but the controls in it are not: both
     range sliders, both checkboxes, the number spinners and every <details>
     marker are drawn by the browser, and without this it draws them for a light
     page — white tracks and grey boxes on a #0b0e14 background. Barely
     noticeable on a desktop, where the footer is a thin strip; impossible to
     miss on a phone, where those controls are most of what you can see. */
  color-scheme: dark;
  accent-color: var(--accent);
}

* { box-sizing: border-box; }

/* The whole page is one unscrollable shell, so a touch that runs off the end of
   the diagram has nowhere to go and the browser gives it to the document
   instead — which on a phone means pull-to-refresh, i.e. scrolling up through
   history far enough reloads the page. Contained here rather than on <body>
   because it is the inner scrollers that overscroll. */
#canvas-wrap, .explainer article { overscroll-behavior: contain; }

/* Hover states are for devices that can hover. On a touchscreen the browser
   fakes :hover on the last thing tapped and leaves it there, so a tapped button
   keeps an accent border until something else is tapped — an element that looks
   focused and is not. */
@media (hover: hover) {
  button:hover { border-color: var(--accent); }
  #fundBtn:hover:not(:disabled) { filter: brightness(1.1); }
  .explain-card:hover { border-color: var(--accent); background: #1a2234; }
  .explain-card:hover .explain-go { color: var(--accent); }
  .explain-close button:hover { color: var(--text); border-color: var(--accent); }
  .explainer summary:hover { color: var(--accent); }
  .linkish:hover { color: var(--mined); }
  .rule-out:hover { border-color: var(--accent); }
}

/* [hidden] has to win, and by default it does not.
 *
 * The UA stylesheet's `[hidden] { display: none }` is an ordinary author-beatable
 * rule, so ANY rule here that sets `display` on the same element silently
 * outranks it — and several do, because the footer controls are flex rows.
 * The locked-controls change set `hidden` on the rate label and it stayed on
 * screen: the property was set, the element was still displayed, and a
 * deployment that refuses rate changes went on offering a rate slider.
 *
 * Not catchable by the renderer tests, which drive a DOM stub with no CSS. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  display: flex;
  flex-direction: column;
  /* Fixed viewport height, not min-height: the diagram scrolls INSIDE its own
     pane. With min-height the page itself grew instead, and the pane never
     became scrollable — which capped the visible history at roughly one
     screenful. */
  height: 100vh;
  /* svh, and specifically not dvh. On a phone `vh` is the LARGE viewport —
     measured with the browser toolbars retracted — so the footer was laid out
     underneath the URL bar, and since this element is overflow:hidden there was
     no way to scroll down to it. Every clock and zoom control on the page was
     simply unreachable.
     dvh would also fix that, but it tracks the toolbar as it retracts, and each
     change reflows main, re-runs windowRows() and reallocates the canvas. This
     document never scrolls, so the toolbar has nothing to retract in response
     to: dvh would buy a resize hazard and no pixels. svh is the toolbars-shown
     height, which is the one that is always true here. */
  height: 100svh;
  overflow: hidden;
}

header, footer {
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  padding: 12px 18px;
  /* Clear of the notch in landscape. env() is 0 everywhere else, so max() keeps
     the 18px this has always had. */
  padding-left: max(18px, env(safe-area-inset-left));
  padding-right: max(18px, env(safe-area-inset-right));
}
footer {
  border-bottom: none;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  /* And of the home indicator. */
  padding-bottom: max(12px, env(safe-area-inset-bottom));
}

header {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: baseline;
  justify-content: space-between;
}
.title { display: flex; gap: 12px; align-items: baseline; }
h1 { font-size: 18px; margin: 0; letter-spacing: .04em; }
.sub { color: var(--muted); font-size: 12px; }

.stats { display: flex; flex-wrap: wrap; gap: 18px; font-size: 12px; }
/* tabular-nums because this row is above the diagram in a flex column that is
   exactly the viewport tall, so anything that changes the header's height comes
   straight out of the canvas — which is then reallocated and repainted. These
   numbers change several times a second; proportional digits make the row
   breathe by a pixel or two on every one of them, and at a wrap boundary that
   is a whole line of header and a visible jump in the diagram. It does not fix
   a digit-count change (9,999 to 10,000), which is rarer and left alone. */
.stat b { color: var(--accent); font-weight: 600; font-variant-numeric: tabular-nums; }
.stat span { color: var(--muted); }

main { flex: 1; display: flex; min-height: 0; }
#canvas-wrap {
  flex: 1;
  position: relative;
  overflow: auto;
  /* min-height:0 lets a flex child actually shrink below its content and
     scroll; without it the pane stretches to fit the canvas instead. */
  min-height: 0;
  padding: 12px 0 12px 0;
}
/* The spacer. Its height is the whole run; it draws nothing. */
#surface { position: relative; }
/* And the part that stays in view. Sticky rather than fixed so it keeps the
   scroller's coordinate space and the horizontal scrollbar still works. */
#pinned {
  position: sticky;
  top: 0;
  display: flex;
  align-items: flex-start;
}
#gutter {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--bg);
  padding-right: 8px;
  text-align: right;
  color: var(--muted);
  font-size: 10px;
  user-select: none;
  white-space: pre;
  line-height: 1;
}
canvas { display: block; image-rendering: pixelated; }

#empty {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: var(--muted);
  pointer-events: none;
}
/* An explicit display beats the UA stylesheet's [hidden] { display: none },
   so the attribute has to be honoured here or the overlay never goes away. */
#empty[hidden] { display: none; }

.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* display:contents, so above the breakpoint #more contributes its children
   straight to .controls and this element has no layout of its own — the footer
   is laid out exactly as it was before the wrapper existed. Below the
   breakpoint the same element becomes a sheet; see the phone block. */
.more { display: contents; }
.more-toggle { display: none; }
/* Its own row, as it has always had: at every width the controls alone are
   wider than half the footer, so the legend wrapped anyway. Saying so directly
   keeps that true now that it is inside .controls rather than beside it.
   The margin makes up the difference between .controls' 8px row gap and the
   16px the footer used to put between the two of them, so the footer is laid
   out to the pixel it was before #more existed. */
.legend { flex-basis: 100%; margin-top: 8px; }

button {
  background: #1d2434;
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 6px 12px;
  cursor: pointer;
  font: inherit;
}
button[aria-pressed="true"] { background: var(--accent); color: #07101c; border-color: var(--accent); }

.zoom { display: flex; gap: 8px; align-items: center; color: var(--muted); }
.zoom input { width: 110px; }
.follow { display: flex; gap: 6px; align-items: center; color: var(--muted); }
.jump { display: flex; gap: 6px; align-items: center; color: var(--muted); }
.jump input {
  width: 11ch;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 4px 6px;
  font: inherit;
  font-size: 12px;
}

.rate { display: flex; gap: 8px; align-items: center; color: var(--muted); }
.rate input { width: 160px; }
.rate output { color: var(--text); min-width: 92px; }

.legend { display: flex; gap: 14px; flex-wrap: wrap; color: var(--muted); font-size: 12px; }
.key { display: flex; gap: 6px; align-items: center; }
.sw { width: 11px; height: 11px; border-radius: 2px; display: inline-block; }
.sw.dead { background: var(--dead); outline: 1px solid var(--line); }
.sw.pending { background: var(--pending); }
.sw.broadcast { background: var(--broadcast); }
.sw.seen { background: var(--seen); }
.sw.mined { background: var(--mined); }
.sw.failed { background: var(--failed); }
.sw.failed-dead { background: var(--failed-dead); }

#err {
  background: #3a1c22;
  color: #ffc9cc;
  border-top: 1px solid #6b2b33;
  padding: 8px 18px;
  font-size: 12px;
  white-space: pre-wrap;
}

#tip {
  position: fixed;
  z-index: 10;
  background: #0f141f;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 12px;
  pointer-events: none;
  /* Was a flat 420px, which is wider than the phone it would have to fit in. */
  max-width: min(420px, calc(100vw - 16px));
  word-break: break-all;
  box-shadow: 0 6px 18px rgba(0,0,0,.5);
}
#tip b { color: var(--accent); }

#tip .hint { color: var(--muted); }

/* The touch counterpart of #tip.
   Anchored to the bottom rather than to the cell: a panel that follows the tap
   sits under the finger that made it, and the thing a reader most wants to see
   after tapping a cell is the cell. */
#cellsheet {
  position: fixed;
  z-index: 30;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 12px 14px;
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  background: #0f141f;
  border-top: 1px solid var(--line);
  font-size: 13px;
  line-height: 1.55;
  word-break: break-all;
  box-shadow: 0 -8px 24px rgba(0,0,0,.5);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
#cellsheet b { color: var(--accent); }
#cellsheet .hint { color: var(--muted); }
#cellsheet .err { color: var(--failed); }
.cellsheet-act { display: flex; gap: 10px; align-items: center; }
#cellsheetOpen { flex: 1; min-height: 44px; }
#cellsheetClose { min-width: 44px; min-height: 44px; }

/* ---------------------------------------------------------------------------
   Funding

   Always present, the first thing under the header and above both the
   explainers and the diagram. What changes is urgency, not visibility: `low`
   and `stopped` are set by app.js from the engine's own numbers. See renderFund
   for why this is not conditional.
   --------------------------------------------------------------------------- */

#fund {
  margin: 0;
  padding: 10px 18px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  /* A left rule is the whole escalation cue in the resting state: present
     enough to read as part of the furniture, quiet enough not to compete with
     the diagram, and already in place so that turning it amber or red is a
     change of colour rather than a change of layout. */
  border-left: 3px solid var(--line);
}

/* At rest, one line.
 *
 * The panel is permanent on purpose — see renderFund — but permanent was being
 * read as three lines of payment copy standing between the header and the
 * diagram at all times, on a page whose whole subject is below it. Collapsed,
 * the standing offer and the button that takes it are still there; the
 * paragraph about fees is a chevron away.
 *
 * Only `resting` collapses. low, stopped and working all set their own class
 * and are excluded, so need still opens the panel without being asked — the
 * escalation is on the engine's evidence, not on whether anyone clicked. */
#fund.resting { padding: 6px 18px; }
#fund.resting:not([data-expanded]) #fundWhy { display: none; }
/* The coin count that goes with it is not lost: the header carries `fuel` as a
   standing stat, which is where a number that changes every second belongs. */

/* Deliberately tiny. On a 390px screen the title and the amount-plus-button
   group together come to within a few pixels of the bar's width, so anything
   wider than this tips the whole thing onto a second row and costs more height
   than collapsing the panel saved. */
#fundToggle {
  display: none;
  padding: 0 4px;
  margin-left: 0;
  border: none;
  background: none;
  color: var(--muted);
  font-size: 11px;
  line-height: 1;
  vertical-align: 1px;
  transition: transform .15s ease;
}
/* Only where there is something behind it. In every other state the detail is
   already on screen and a control that expands what is expanded is furniture. */
#fund.resting #fundToggle { display: inline-block; }
/* The message row is the control — see index.html — so it carries the pointer
   and, on a touchscreen, the height that makes it hittable. The chevron itself
   stays 17px wide: taking 44 out of this row would come straight out of the
   title, and there is nothing spare in it at 390px. */
#fund.resting .fund-msg { cursor: pointer; }
@media (pointer: coarse) {
  #fund.resting .fund-msg { min-height: 44px; }
}
#fund.resting[data-expanded] #fundToggle { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  #fundToggle { transition: none; }
}

#fund.low {
  border-left-color: var(--fund-warn);
  background: var(--fund-warn-bg);
}

/* Stopped is the one state that may interrupt. The automaton is not running,
   the diagram is frozen, and without this the page looks broken rather than
   hungry. */
#fund.stopped {
  border-left-color: var(--fund-bad);
  background: var(--fund-bad-bg);
}

#fund.working {
  border-left-color: var(--fund-good);
}

/* The spinner, for the gap between a payment being credited and the fuel it
   buys becoming spendable. That takes about a minute -- the keeper has to mint
   the pool from the deposit -- and without a sign of life a paid-for automaton
   that has not started yet is indistinguishable from one that ate the money. */
.spinner {
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-left: 8px;
  vertical-align: -1px;
  border: 2px solid var(--line);
  border-top-color: var(--fund-good);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Someone who has asked the system not to animate things has asked for a
   reason. Keep the affordance, drop the motion. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; border-top-color: var(--fund-good); opacity: 0.7; }
}

.fund-bar {
  display: flex;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}

.fund-msg { min-width: 0; }
.fund-msg b { color: var(--text); }
#fund.low .fund-msg b { color: var(--fund-warn); }
#fund.stopped .fund-msg b { color: var(--fund-bad); }
.fund-msg #fundWhy { display: block; }

.fund-act {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-shrink: 0;
}

/* The button is the point of the panel, so it does not look like the zoom
   control. Solid, not outlined. */
#fundBtn {
  background: var(--accent);
  color: #06101c;
  border-color: var(--accent);
  font-weight: 600;
}
#fund.stopped #fundBtn { background: var(--fund-bad); border-color: var(--fund-bad); color: #fff; }

.muted { color: var(--muted); font-size: 12px; }

#fund input[type="number"] {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 5px 8px;
  font: inherit;
  width: 12ch;
}

.fund-status { font-size: 12px; color: var(--muted); margin: 8px 0 0; }
.fund-status.good { color: var(--fund-good); }
.fund-status.bad { color: var(--fund-bad); }

#fund button:disabled {
  opacity: 0.5;
  cursor: default;
}

/* --- The two explainers -------------------------------------------------- */

/* The trigger strip. Same panel treatment as #fund so the top of the page
   reads as one stack of bars rather than three unrelated widgets, but with no
   left border: #fund uses that edge to signal urgency, and a second element
   wearing the same stripe would dilute a signal that has to be noticed. */
#explain {
  display: flex;
  gap: 12px;
  padding: 10px 18px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

/* Cards, not buttons: an invitation to read, sized so the pair fills the bar
   evenly at any width. Overriding the shared button rule wholesale because
   almost nothing about a 6px-by-12px control transfers. */
.explain-card {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
  align-items: flex-start;
  text-align: left;
  padding: 9px 12px;
  background: #161d2c;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--text);
  cursor: pointer;
  font: inherit;
}
.explain-card b { color: var(--accent); font-weight: 600; }
.explain-card .muted { line-height: 1.4; }
.explain-go { color: var(--muted); font-size: 11px; align-self: flex-end; margin-top: 2px; }

/* On a phone the strip is competing with the diagram for a viewport that is
   mostly diagram, so the hook line goes and each card becomes a single row.

   An earlier version of this block said the cards "stack" here. They never did
   — nothing set flex-direction, and .explain-card keeps `flex: 1 1 0` — so the
   result was two half-width boxes with their explanation deleted. Laying them
   out as rows on purpose is cheaper in height than either.

   .explain-go stays. It was hidden here, which took the only "this is a link"
   cue away from precisely the devices that cannot hover to find the other
   one. */
@media (max-width: 640px) {
  #explain { gap: 8px; padding: 8px 12px; }
  .explain-card {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    font-size: 13px;
  }
  .explain-card .muted { display: none; }
  /* The arrow alone. Half a card's width at this size is about seventeen
     monospace characters, and "What is Rule 110?" is exactly that — so the word
     in front of the arrow is what pushed the title onto a second line and cost
     the diagram a row it could not spare. The arrow still says "link". */
  .explain-go { align-self: center; margin: 0 0 0 auto; }
  .go-word { display: none; }
}

/* The dialogs themselves. Native <dialog>, so the focus trap, Esc and the
   backdrop are the browser's job and not ours. */
.explainer {
  /* In pixels, not ch. `ch` here would be measured in the monospace font this
     element inherits from body, while the prose inside is set in a sans — so
     the measure would be decided by a font the reader never sees. 880 puts the
     prose at a bit under 80 characters and is just wide enough that the opcode
     listing does not need its scrollbar. */
  width: min(880px, calc(100vw - 32px));
  max-height: 86vh;
  padding: 0;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 24px 64px rgba(0,0,0,.6);
  overflow: hidden;
}
.explainer::backdrop { background: rgba(6, 9, 15, .78); }

/* The article scrolls, not the dialog, so the close button can stay put. */
.explainer article {
  max-height: 86vh;
  overflow-y: auto;
  padding: 26px 34px 34px;

  /* The only place in this application that is not monospace, deliberately.
     Everything else here is data — addresses, hex, counters — where a fixed
     advance is doing real work. This is three thousand words of prose, and
     prose set in a terminal font is prose people give up on. Code inside it
     goes back to monospace below. */
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 15px;
  line-height: 1.62;
}

.explain-close { position: sticky; top: 0; height: 0; z-index: 2; text-align: right; }
.explain-close button {
  position: relative;
  top: 10px;
  right: 10px;
  padding: 2px 9px;
  font-size: 15px;
  line-height: 1.4;
  color: var(--muted);
}

.explainer h2 {
  margin: 0 0 4px;
  font-size: 22px;
  letter-spacing: .01em;
}
.explainer h3 {
  margin: 30px 0 8px;
  font-size: 15px;
  color: var(--accent);
  letter-spacing: .04em;
  text-transform: uppercase;
}
.explainer p { margin: 0 0 14px; }
.explainer ol, .explainer ul { margin: 0 0 14px; padding-left: 22px; }
.explainer li { margin-bottom: 6px; }
.explainer b { color: #f2f5fb; }

/* The opening line carries the whole explanation for anyone who reads exactly
   one sentence, which is most people. */
.lede {
  font-size: 17px;
  line-height: 1.5;
  color: var(--mined);
  margin: 0 0 22px !important;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--line);
}

.explainer code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1px 5px;
  color: var(--seen);
  word-break: break-word;
}
.explainer pre.code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.5;
  background: var(--bg);
  border: 1px solid var(--line);
  border-left: 3px solid var(--broadcast);
  border-radius: 4px;
  padding: 10px 14px;
  margin: 0 0 16px;
  /* Wide listings scroll inside their own box. The page must never gain a
     horizontal scrollbar because of one long opcode line. */
  overflow-x: auto;
}
.explainer pre.asm { font-size: 12px; line-height: 1.45; }
/* Annotations beside the opcodes. Dim enough that the opcode column reads as
   the content and the commentary as the margin. */
.explainer .cm { color: var(--muted); font-style: italic; }

/* Disclosure blocks: the same idiom as the funding bar's chevron, which is
   already how this application says "there is more here if you want it".
   Everything genuinely technical lives in one of these, so the main thread
   stays readable. */
.explainer details {
  margin: 0 0 16px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0 14px;
}
.explainer details[open] { padding-bottom: 4px; }
.explainer summary {
  cursor: pointer;
  padding: 9px 0;
  color: var(--muted);
  font-size: 13px;
}
.explainer details p:last-child { margin-bottom: 12px; }

/* A button that reads as a link, for the cross-reference at the foot of each
   dialog. */
.explain-next { margin-top: 26px !important; padding-top: 16px; border-top: 1px solid var(--line); }
.linkish {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* --- The demos ----------------------------------------------------------- */

.demo {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 14px;
  margin: 0 0 16px;
}
.demo-note { margin: 10px 0 0 !important; font-size: 13px; }
.demo-note input[type="number"] {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 3px 6px;
  width: 7ch;
  font: inherit;
}

/* The eight neighbourhoods. Each is three cells over one answer cell; the
   answer is the only part that is clickable, because the whole point is that
   the rule IS those eight bits and nothing else. */
.rule-table {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: space-between;
}
.rule-case {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}
.rule-nbhd { display: flex; gap: 2px; }
.rule-bit {
  width: 15px;
  height: 15px;
  border: 1px solid var(--line);
  background: var(--dead);
}
.rule-bit.on { background: var(--mined); border-color: var(--mined); }
.rule-arrow { color: var(--muted); font-size: 10px; line-height: 1; }
/* The answer cell is a button so it is reachable by keyboard: the demo is the
   explanation here, not decoration around it. */
.rule-out {
  width: 15px;
  height: 15px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--dead);
  cursor: pointer;
}
.rule-out.on { background: var(--mined); border-color: var(--mined); }
.rule-out:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.rule-readout {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  margin: 14px 0 12px !important;
}
.rule-readout b { color: var(--accent); font-size: 16px; }
.rule-readout button { padding: 3px 10px; font-size: 12px; }

.demo-canvas {
  display: block;
  width: 100%;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--dead);
  image-rendering: pixelated;
}

/* The bit-extraction readout. A grid of label/value pairs, because the thing
   being explained is which number came from where. */
.bit-out {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.7;
}
.bit-out .bit-row { display: flex; gap: 10px; flex-wrap: wrap; }
.bit-out .bit-k { color: var(--muted); min-width: 17ch; }
.bit-out .bit-v { color: var(--seen); }
.bit-out .bit-hi { color: var(--mined); font-weight: 600; }
.bit-out hr { border: none; border-top: 1px solid var(--line); margin: 9px 0; }

/* ---------------------------------------------------------------------------
   Touch

   Keyed on the POINTER, not on the width. A 15px control is just as hard to hit
   on an 11-inch tablet as on a phone, and a mouse on a 500px window has no
   trouble with either — so these two axes get two sets of rules and this is the
   one about fingers. 44px is the figure both platform guidelines land on.
   --------------------------------------------------------------------------- */

@media (pointer: coarse) {
  button { min-height: 44px; padding: 10px 14px; }

  /* Under 16px, iOS zooms the whole page in when the field takes focus and
     leaves it zoomed — on a layout with no room to spare, that is a page the
     reader now has to pan around by hand.
     Spelled out per field rather than as a bare `input`, because every one of
     these already has a rule setting its size and a media query adds no
     specificity: `input { font-size: 16px }` loses to `.jump input` and changes
     nothing at all. Each selector below matches the weight of the rule it is
     overriding, and wins by coming later. */
  .jump input,
  #fund input[type=number],
  .demo-note input[type="number"] { font-size: 16px; }

  /* Height, which is a separate problem: a range input is 16px tall and a
     checkbox 13px, so the controls a phone most needs — zoom and follow — were
     the two hardest things on the page to hit. */
  input[type=range] { height: 44px; }
  input[type=checkbox] { width: 22px; height: 22px; }
  .jump input, #fund input { min-height: 44px; }
  /* The label is the target for a checkbox, so it is the label that has to be
     big enough. */
  .follow, .demo-note { min-height: 44px; display: flex; align-items: center; }

  .explain-close button { min-width: 44px; min-height: 44px; }

  /* Disclosure triangles are the smallest targets on the page after the rule
     bits, and these sit in the middle of prose where a missed tap selects a
     word instead. */
  .explainer summary { min-height: 44px; display: flex; align-items: center; }

  .linkish { display: inline-block; padding: 11px 0; }

  /* The rule bits stay 15px. The demo's whole claim is that the rule IS these
     eight squares, and a square scaled up to thumb size stops reading as one
     bit of a number — so the hit area grows and the paint does not. */
  .rule-out { position: relative; }
  .rule-out::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }
  /* ...which needs room around each case, or neighbouring hit areas overlap and
     the top half of one bit belongs to the bit above it. */
  .rule-table { gap: 20px 12px; }

}

/* ---------------------------------------------------------------------------
   Phones

   The chrome used to take 656 of a 664px viewport — header, explainers, funding
   bar and footer all sized for a desktop, with `main` the only item able to
   shrink. The diagram, which is the entire point of the page, was left with
   8px. Everything below is about giving that space back.
   --------------------------------------------------------------------------- */

@media (max-width: 640px) {
  /* Title and stats share one row, and the stats scroll inside what is left of
     it. All seven numbers survive — they are the run's vital signs — but four
     stacked rows of them cost 110px of a screen with none to give. */
  header {
    padding: 8px 12px;
    flex-wrap: nowrap;
    gap: 12px;
    align-items: center;
  }
  h1 { font-size: 15px; }
  /* The one thing here a reader does not need twice: the explainer card
     directly below says the same in more words. */
  .sub { display: none; }
  .title { flex: 0 0 auto; }
  .stats {
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 14px;
    /* Without this the stats refuse to shrink below their content and push the
       title off the side instead of scrolling. */
    min-width: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .stats::-webkit-scrollbar { display: none; }
  .stat { flex: 0 0 auto; }

  #fund { padding: 8px 12px; }
  .fund-bar { gap: 8px; }
  .fund-act { flex-shrink: 1; min-width: 0; }
  #fund input[type=number] { width: 9ch; min-width: 0; }
  #fund.resting { padding: 5px 12px; }
  .fund-btn-tail { display: none; }
  #fund input[type=number] { width: 8ch; }
  .fund-act { gap: 6px; }
  /* The whole saving depends on this staying one row, and at 360px the title,
     the chevron and the amount-plus-button group do not fit in the bar's width.
     Left to wrap they cost more height than collapsing the panel saved, and
     they wrap in three different places — the bar, the title against the
     chevron, and the words inside the button. So nothing here wraps, and the
     title is the one thing allowed to give way. A clipped word is a smaller
     loss than the row it prevents. */
  #fund.resting:not([data-expanded]) .fund-bar { flex-wrap: nowrap; }
  #fund.resting:not([data-expanded]) .fund-msg {
    display: flex;
    align-items: center;
    min-width: 0;
  }
  #fund.resting:not([data-expanded]) #fundTitle {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  #fund.resting:not([data-expanded]) #fundToggle { flex: 0 0 auto; }
  /* "Fund it" is two words and the button is the last thing with room. */
  .fund-act button { white-space: nowrap; }

  #canvas-wrap { padding: 6px 0; }
  /* Eight characters at 6px was neither readable nor cheap: ~37px of a 390px
     screen spent on a column nobody could make out. Narrower and legible. */
  #gutter { padding-right: 4px; }

  footer { padding: 8px 12px; gap: 8px; padding-bottom: max(8px, env(safe-area-inset-bottom)); }
  .controls { row-gap: 8px; font-size: 13px; }
  /* The clock and the zoom, on one row. Everything here is about the width that
     row has: a desktop's 12px button padding and a 110px slider add up to more
     than a phone is wide, and the overflow lands as a second row of footer that
     comes straight out of the diagram. */
  .controls > button { padding: 8px 10px; }
  .zoom { gap: 6px; }
  .zoom input { width: 84px; }
  /* The slider position is the readout. The number beside it was saying the
     same thing again in a place with no room to say anything twice. */
  #zoomOut { display: none; }
  .more-toggle { display: inline-flex; align-items: center; margin-left: auto; }

  /* The seven remaining control groups and the legend, as a sheet. Fixed rather
     than in flow so raising it costs the diagram nothing, and translated rather
     than display:none so it can slide. */
  .more {
    display: flex;
    flex-direction: column;
    gap: 14px;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 25;
    padding: 14px;
    padding-bottom: max(14px, env(safe-area-inset-bottom));
    background: var(--panel);
    border-top: 1px solid var(--line);
    box-shadow: 0 -8px 24px rgba(0,0,0,.5);
    transform: translateY(110%);
    visibility: hidden;
    transition: transform .18s ease, visibility .18s;
  }
  .more[data-open] { transform: none; visibility: visible; }
  .more .jump, .more .follow, .more .rate { width: 100%; }
  .more .rate input { flex: 1; width: auto; }
  .more .jump input { flex: 1; width: auto; }
  .legend { gap: 10px 14px; margin-top: 0; }

  /* Full-bleed. A centred dialog on a 390px screen spends its margins, its
     border radius and 68px of article padding on being a box, and what is
     inside it is three thousand words that need the width. */
  .explainer {
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    height: 100svh;
    max-height: none;
    margin: 0;
    border: none;
    border-radius: 0;
  }
  /* [open], and not the bare element. A <dialog> is hidden by the UA's
     `dialog:not([open]) { display: none }`, which is an ordinary rule any author
     `display` outranks — so `.explainer { display: flex }` would put both
     explainers permanently on the page. Same trap as the [hidden] rule at the
     top of this file. */
  .explainer[open] { display: flex; flex-direction: column; }
  /* A real header bar rather than a zero-height sticky overlay, so the close
     control is a 44px target with somewhere to sit instead of a 27px glyph
     printed on top of the first paragraph.
     Note it is a SIBLING of <article>, not a child: it is already the full
     width of the dialog, and pulling it out to the article's padding edge with
     a negative margin put it 16px off the side of the screen. */
  .explain-close {
    position: static;
    height: auto;
    flex: 0 0 auto;
    margin: 0;
    padding: 6px 10px;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
  }
  .explain-close button { top: 0; right: 0; }
  /* The article is the only part that scrolls, so it takes what the bar leaves
     rather than a viewport height of its own — svh minus a header is not a
     length CSS can spell without hard-coding the header. */
  .explainer article {
    flex: 1;
    min-height: 0;
    max-height: none;
    height: auto;
    padding: 14px 16px 32px;
    padding-bottom: max(32px, env(safe-area-inset-bottom));
    font-size: 16px;
  }

  .explainer h2 { font-size: 20px; }
  .explainer h3 { font-size: 14px; margin-top: 24px; }
  .lede { font-size: 16px; }
  .demo { padding: 10px; }
  /* space-between on a wrapped row leaves the last line ragged and the gaps
     uneven; there is no longer enough width for the tidiness it was buying. */
  .rule-table { justify-content: flex-start; }
  /* 17ch of label in a 390px viewport leaves nothing for the value it labels. */
  .bit-out .bit-k { min-width: 0; }
}
