:root {
  --ink: #1d1d1f;
  --paper: #fdfcf7;
  --rule: #e0dccf;
  --accent: #b4451f;
  --code-bg: #f5f2e8;
  --ok: #2f6b3a;
  --err: #a52014;
  --hand: "Bradley Hand", "Segoe Print", "Comic Sans MS", cursive;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 3rem 1.5rem 6rem;
  background: var(--paper);
  color: var(--ink);
  font: 17px/1.65 Georgia, "Iowan Old Style", serif;
}

main { max-width: 46rem; margin: 0 auto; }

h1 { font-size: 2.1rem; line-height: 1.15; margin: 0 0 .3rem; }
h2 {
  font-size: 1.35rem;
  margin: 3rem 0 .8rem;
  padding-bottom: .3rem;
  border-bottom: 2px solid var(--rule);
}
.kicker {
  font: 600 .8rem/1 system-ui, sans-serif;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 1.4rem;
}

p { margin: 0 0 1rem; }
code { font: .92em ui-monospace, "SF Mono", Menlo, monospace; background: var(--code-bg); padding: .1em .3em; border-radius: 3px; }

/* --- cells --- */

.cell {
  margin: 1.6rem 0;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: #fff;
  overflow: hidden;
}
.cell .bar {
  display: flex;
  align-items: center;
  /* Wraps rather than overflows: the tick is a sentence when something is wrong
     ("program changed since this ran") and a narrow column must not clip it. */
  flex-wrap: wrap;
  gap: .6rem;
  padding: .45rem .7rem;
  background: var(--code-bg);
  border-bottom: 1px solid var(--rule);
  font: 600 .72rem/1 system-ui, sans-serif;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: #6b6455;
}
.cell .bar .spacer { flex: 1; }

textarea, input {
  width: 100%;
  border: 0;
  padding: .8rem .9rem;
  font: 14px/1.55 ui-monospace, "SF Mono", Menlo, monospace;
  color: var(--ink);
  background: #fff;
  resize: none;
}
textarea:focus, input:focus { outline: 2px solid #cfd8e8; outline-offset: -2px; }

input { border-bottom: 1px solid var(--rule); }
.query .prompt { display: flex; align-items: baseline; }
.query .prompt span {
  padding-left: .9rem;
  font: 14px ui-monospace, Menlo, monospace;
  color: #8a8371;
  white-space: nowrap;
}

button {
  font: 600 .74rem/1 system-ui, sans-serif;
  padding: .42rem .7rem;
  border: 1px solid #c8c1ae;
  border-radius: 4px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
}
button:hover:not(:disabled) { background: var(--code-bg); }
button:disabled { opacity: .38; cursor: default; }
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
button.primary:hover { background: #98380f; }

/* A standing property of the cell, not a passing state — so it sits apart from
   the tick, which is about what just happened. Marks the one cell whose state a
   re-consult does not undo (format §8). */
.badge {
  font: 700 .62rem/1 system-ui, sans-serif;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .22rem .5rem;
  border-radius: 999px;
  border: 1px solid currentColor;
  opacity: .75;
}
.badge.stateful { color: #a8730f; }

.status { font: 400 .74rem/1.3 system-ui, sans-serif; text-transform: none; letter-spacing: 0; text-align: right; }
.status.ok { color: var(--ok); }
.status.err { color: var(--err); }
.status.busy { color: #8a8371; }
/* Loaded, but SWI said something the reader needs to see — most often that this
   cell has redefined a predicate another cell owns. */
.status.warn { color: #a8730f; white-space: pre-line; line-height: 1.4; }

.out {
  font: 13.5px/1.7 ui-monospace, Menlo, monospace;
  padding: .6rem .9rem;
  max-height: 16rem;
  overflow-y: auto;
  background: #fbfaf5;
  border-top: 1px solid var(--rule);
}
.out:empty { display: none; }
.line.echo { color: #8a8371; }
/* Attribution. An output is never shown without saying whose it is
   (docs/modes.md §3), so this line is structural rather than decorative. */
.line.from {
  color: #8a8371;
  font: 600 .74rem/1.6 system-ui, sans-serif;
  letter-spacing: .06em;
  text-transform: uppercase;
}
/* Saved answers that no longer follow from the program above them. Marked, never
   silently dropped and never silently trusted. */
.line.warn { color: #a8730f; }
/* Put away, never discarded: a chapter shows its answers (docs/modes.md §2), but
   prose that says "press Run, then ; next to walk through the rest" is arguing
   with a page that has already printed all six. The attribution line stays
   visible while they are hidden, so the reader can always see that they exist. */
.out.answers-hidden .line:not(.from) { display: none; }
.line.from button.peek {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  vertical-align: -.15em;
  margin-left: .6rem;
  padding: .16rem .55rem .16rem .45rem;
  border-radius: 999px;
  font: 600 .68rem/1.4 system-ui, sans-serif;
  letter-spacing: .06em;
  text-transform: uppercase;
  vertical-align: baseline;
}
.line.sol { color: var(--ok); }
.line.done { color: #8a8371; font-style: italic; }
/* "more solutions may follow." must never be mistaken for "no more solutions." at
   a glance, so the sequence is marked as continuing rather than closed. This is
   also where the cold-page `; next` affordance will attach (869ectt0y). */
.line.done.partial::before { content: '\22EE  '; opacity: .7; }
.line.err { color: var(--err); }

/* The page's own controls. Chrome, not content: built by notebook.js at runtime,
   so a printed or GitHub-rendered chapter never shows a button that cannot work.

   A LOZENGE, AND A CARD THAT RISES OUT OF IT. Closed, it is a dot and a word in
   the corner of the viewport. Clicking it raises a card immediately above it —
   right edges aligned, so the thing the reader pressed is visibly the thing that
   opened — with one row per thing the page controls.

   IT MOVES; IT DOES NOT MEASURE. The first version was one pill that WIDENED,
   which meant JavaScript had to measure its contents on every state change, since
   CSS cannot transition to `auto`. Any re-render landing mid-animation then
   snapped it to its final width — and export's "has anything changed?" listener
   did exactly that on the next frame after every click, including the click that
   opened it. A transform needs no measuring, cannot be cancelled by a re-render,
   and stays on the compositor. The bug class goes away rather than being patched.

   Rows also mean the next unit costs vertical space, which nobody is short of.
   The widening pill had reached the edge of the viewport with three.

   Fixed to the VIEWPORT rather than sticky inside the column, because the column
   is where the prose is, and a control floating over the sentence being read is
   worse than one that is hard to find. */
.page-controls {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 5;
  display: flex;
  justify-content: flex-end;
}

.page-controls .panel {
  position: absolute;
  right: 0;
  bottom: 100%;
  margin-bottom: .5rem;
  /* TWO COLUMNS, AND A WIDTH THAT DOES NOT MOVE. Every button sits in the second
     column, so they are all as wide as the widest of them — and because the card
     itself is a fixed width, nothing shifts when a label changes under the
     reader's cursor. Sized by content, it twitched on every click: "Hide saved
     answers" and "Show saved answers" are not the same length, and neither are
     "Start engine" and "Restart engine". */
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  width: 25rem;
  max-width: calc(100vw - 2rem);
  padding: .1rem .9rem;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--paper);
  box-shadow: 0 6px 24px rgba(0, 0, 0, .14);
  /* HIDDEN, not merely transparent. An opacity of 0 leaves a button clickable,
     and this card lies over the page: invisible things must not be touchable, and
     visibility says so in a way opacity does not. */
  visibility: hidden;
  opacity: 0;
  transform: translateY(.7rem);
  /* The fade is quicker than the rise, so it is already legible while it is still
     arriving rather than sliding into place as a blank rectangle. */
  transition: opacity .18s ease, transform .28s cubic-bezier(.32, .72, 0, 1), visibility .28s;
}
.page-controls[data-open="true"] .panel {
  visibility: visible;
  opacity: 1;
  transform: none;
}

/* One row per thing the page controls: what is true, then the button that changes
   it. Ruled rather than merely spaced, because two state phrases in a column
   otherwise read as one paragraph. */
/* The unit is a grouping, not a box: its two children join the card's own grid so
   that every state phrase and every button line up down the card. */
.page-controls .unit { display: contents; }
/* HIDDEN MEANS HIDDEN. The `hidden` attribute is only a UA rule — `[hidden] {
   display: none }` — so ANY display declaration of ours outranks it and the
   element stays on screen. That is how the version picker came to sit next to
   the phrase it replaces, both visible at once, and it is the same principle
   already written into the panel's own fade: invisible things must not be
   touchable, and something told to hide must actually go. */
.page-controls [hidden] { display: none !important; }

/* The one row whose state is a CHOICE. A native select, because it works on a
   touch screen and from a keyboard without anything being written here — but
   stripped of its chrome, so a card of quiet phrases does not suddenly grow a
   form control. The chevron is the same glyph the lozenge uses. */
.page-controls .picker {
  display: inline-flex;
  align-items: center;
  gap: .1rem;
}
.page-controls .picker select {
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.page-controls .picker svg {
  width: .7rem;
  height: .7rem;
  /* The glyph is a "<": 270° points it down, which is the direction this one
     opens. The lozenge's own chevron uses 90° because its panel rises. */
  transform: rotate(270deg);
  opacity: .7;
}
.page-controls .picker:hover { color: var(--ink); }
.page-controls .unit > .state { padding: .6rem 1.4rem .6rem 0; }
.page-controls .unit + .unit > * { border-top: 1px solid var(--rule); }

/* WHAT THIS IS, not what it is doing. It spans both columns because it has no
   button to line up with: every row above is a state and its verb, and this is
   neither. Quieter than the states above it, and not selectable-looking — it is
   a colophon, and a reader should be able to ignore it at a glance. */
.page-controls .about {
  grid-column: 1 / -1;
  margin: 0;
  padding: .55rem 0 .6rem;
  border-top: 1px solid var(--rule);
  font: 400 .66rem/1.5 system-ui, sans-serif;
  color: #a49c88;
  /* The banner is one long sentence in a narrow card. Balanced rather than left
     to strand "License." on a line of its own; ignored where unsupported. */
  text-wrap: pretty;
}
/* Two lines: what is running, then who owns it. Each is short enough not to wrap
   at this width, which is why the split is by MEANING rather than by shrinking
   the type until the sentence fits. */
.page-controls .about > span { display: block; }
/* Inline, because it belongs to the sentence above it rather than to a line of
   its own — and absent until the engine has been asked, which cannot happen
   until it is running. */
.page-controls .about .engine-version { display: inline; }

/* Free to wrap, now that there is a column to wrap in. The widening pill had to
   ellipsise this sentence to keep the buttons on screen; a row has the width it
   needs and the card grows downward instead. */
.page-controls .state {
  min-width: 0;
  font: 400 .74rem/1.4 system-ui, sans-serif;
  color: #8a8371;
}

.page-controls .handle {
  display: flex;
  align-items: center;
  gap: .45rem;
  flex: 0 0 auto;
  padding: .42rem .75rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--paper);
  box-shadow: 0 2px 12px rgba(0, 0, 0, .09);
  font: 600 .7rem/1 system-ui, sans-serif;
  letter-spacing: .04em;
  color: #8a8371;
  white-space: nowrap;
  transition: box-shadow .3s ease;
}
/* Lifted a little while open, so the pair reads as being in front of the page
   rather than printed on it. */
.page-controls[data-open="true"] .handle { box-shadow: 0 5px 22px rgba(0, 0, 0, .14); }
.page-controls .handle:hover:not(:disabled) { background: var(--code-bg); }
/* A LIGHT, NOT AN ICON — and the distinction is the whole vocabulary. Every glyph
   in a button here depicts that button's verb; this one thing depicts a state, so
   it is deliberately not drawn as a symbol. Grey for no engine, amber while one is
   arriving, green once it is running: readable without being read. */
.page-controls .handle .dot {
  width: .48rem;
  height: .48rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: #c8c1ae;
  transition: background .3s ease;
}
.page-controls.live .handle .dot { background: var(--ok); }
.page-controls.busy .handle .dot {
  background: #d09a2c;
  animation: engine-pulse 1.1s ease-in-out infinite;
}
@keyframes engine-pulse {
  50% { opacity: .3; }
}

/* The lozenge's own verb: it opens the panel, and it points the way the panel
   goes — up to raise it, down to put it away. Rotated, not swapped for a
   different glyph, so the turn itself reads as the state changing. The drawn
   chevron points left, so up is a quarter turn from where it starts. */
.page-controls .chev {
  display: inline-flex;
  flex: 0 0 auto;
  margin-left: .1rem;
  opacity: .55;
  transform: rotate(90deg);
  transition: transform .3s cubic-bezier(.32, .72, 0, 1);
}
.page-controls[data-open="true"] .chev { transform: rotate(270deg); }

.page-controls .panel button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .38rem;
  /* Fills its column, so all three are one width. */
  width: 100%;
  margin: .6rem 0;
  padding: .4rem .8rem;
  border-radius: 999px;
  border-color: transparent;
  background: var(--code-bg);
}
/* THREE PEERS, ONE WEIGHT. Download was filled with the accent, on the argument
   that the reader's copy is the point of export. That held while the panel was a
   row of units; as three rows it reads as though the other two were lesser
   versions of it — and hiding the answers or restarting the engine is what a
   reader reaches for far more often.

   The accent belongs to Run and Consult, which are the actions the CHAPTER is
   about. A control in the corner shouting at the same volume is a tool insisting
   on itself, which is the thing this panel was shaped to avoid. */
/* Line icons, inherited colour, sized in ems so they follow the label rather than
   the page. Decoration for the eye and a second reading for everyone else — the
   words are always there too. */
.page-controls .icon {
  display: inline-flex;
  flex: 0 0 auto;
  opacity: .75;
}
.page-controls svg,
.line.from .icon svg {
  width: 1.05em;
  height: 1.05em;
}
.line.from .icon { display: inline-flex; opacity: .8; }
.page-controls .panel button:hover:not(:disabled) { background: #ebe5d5; }

/* A phone has no corner to spare: the card takes the width of the screen rather
   than a column of it, and the lozenge stays where it was. */
@media (max-width: 30rem) {
  .page-controls { left: 1rem; }
  .page-controls .panel { left: 0; width: auto; max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
  .page-controls .panel, .page-controls .handle, .page-controls .chev { transition: none; }
  .page-controls .panel { transform: none; }
  /* The amber still says "busy"; only the blinking goes. */
  .page-controls.busy .handle .dot { animation: none; }
}

/* --- teaching devices --- */

.predict {
  margin: 1.6rem 0;
  padding: 1rem 1.2rem;
  border: 2px dashed #c8b98a;
  border-radius: 6px;
  background: #fffdf2;
}
.predict h3 {
  margin: 0 0 .5rem;
  font: 700 .95rem/1.3 system-ui, sans-serif;
  color: var(--accent);
}
.predict textarea {
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: #fff;
  min-height: 3.4rem;
}
.predict summary {
  cursor: pointer;
  font: 600 .85rem/1 system-ui, sans-serif;
  color: var(--accent);
  margin-top: .7rem;
}
.predict[open] summary { margin-bottom: .6rem; }
.predict details p:last-child { margin-bottom: 0; }

.aside {
  margin: 1.8rem 0;
  padding: .9rem 1.1rem;
  border-left: 4px solid var(--accent);
  background: #fbf6ef;
  font-size: .95rem;
}
.aside strong { display: block; margin-bottom: .3rem; }

/* Generated containers wrap their prose in <p>, which the hand-written markup did
   not. Without this the last paragraph adds a trailing 1rem inside every box. */
.aside p:last-child,
.predict > p:last-child,
.bullets p:last-child { margin-bottom: 0; }

.note {
  font-family: var(--hand);
  font-size: 1.12rem;
  color: var(--accent);
  transform: rotate(-1.2deg);
  margin: .6rem 0 1.4rem 2rem;
}
.note::before { content: "↖ "; }

.bullets { border-top: 3px double var(--rule); padding-top: 1rem; margin-top: 2.5rem; }
.bullets h2 { border: 0; margin-top: 0; }
.bullets li { margin-bottom: .5rem; }

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e8e4da; --paper: #16151a; --rule: #35323c;
    --code-bg: #22212a; --accent: #e2825c; --ok: #7fc98d; --err: #f0806f;
  }
  .cell, textarea, input { background: #1c1b22; }
  .out { background: #1a191f; }
  .line.warn, .status.warn, .badge.stateful { color: #d9a441; }
  /* A prediction box is where the reader writes, so it must not be the one white
     rectangle on a dark page. */
  .predict textarea { background: #1c1b22; }
  .predict { background: #201e19; border-color: #5a4f36; }
  .aside { background: #201d1c; }
  button { background: #24232b; border-color: #454150; color: var(--ink); }
  button:hover:not(:disabled) { background: #2d2b35; }
  button.primary { background: var(--accent); color: #16151a; }
  /* A shadow is invisible on a dark page, so the pill leans on its edge instead. */
  .page-controls .panel,
  .page-controls .handle { background: #1c1b22; border-color: #4b4656; box-shadow: none; }
  .page-controls .handle:hover:not(:disabled) { background: #2d2b35; }
  .page-controls .panel button { background: #2a2833; }
  .page-controls .panel button:hover:not(:disabled) { background: #35323f; }
}

/* Shown until mount() runs; see notebook.js. Its presence means the page is inert. */
/* THE PAGE THAT CANNOT RUN HAS TO SAY SO, WHERE IT WILL BE READ (869erqq1u).

   Two properties, and both are the point:

   FIXED, so it takes no layout. A working page carries this element for the
   fraction of a second before mount() removes it, and a banner in the flow would
   push the whole chapter down and then let it snap back — a visible jolt on every
   single load, to warn about something that is not happening.

   REVEALED ON A DELAY, in CSS. mount() removes it long before the animation
   fires, so a page that boots never shows it at all; a page that cannot boot
   shows it a second and a half in. It cannot be a timer, because script is the
   thing that is broken. `forwards` holds the final frame rather than snapping
   back to invisible. */
#boot-warning {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  margin: 0;
  padding: 1rem 1.2rem;
  border-bottom: 2px solid var(--err);
  background: #fff5f3;
  font-size: .95rem;
  opacity: 0;
  animation: boot-warning .25s ease 1.5s forwards;
}
@keyframes boot-warning { to { opacity: 1; } }
/* ROOM FOR IT, made on the same delay and only while it is there. A fixed banner
   would otherwise sit over the chapter's title — the page is broken, but it
   should not also look broken. `:has` is what keeps this honest: the moment
   mount() removes the element the selector stops matching, so a page that boots
   never reserves the space, and a browser too old for `:has` merely goes back to
   the banner overlapping. */
body:has(#boot-warning) { animation: boot-warning-room .25s ease 1.5s forwards; }
@keyframes boot-warning-room { to { padding-top: 7rem; } }
/* Somebody who has asked not to be moved still gets the message — only the fade
   goes. Without this the whole rule is skipped and the warning never appears. */
@media (prefers-reduced-motion: reduce) {
  #boot-warning { animation-duration: 0s; }
}
#boot-warning strong { color: var(--err); }
#boot-warning pre {
  margin: .6rem 0 0;
  padding: .6rem .8rem;
  background: var(--code-bg);
  border-radius: 4px;
  font: 13px/1.5 ui-monospace, Menlo, monospace;
  overflow-x: auto;
}
@media (prefers-color-scheme: dark) { #boot-warning { background: #2a1d1b; } }

/* ---------------------------------------------------------------- navigation

   Where the page sits, and how to leave it (869eun9qa). Generated matter that a
   binder puts AROUND a chapter, never part of the notebook — so these rules do
   nothing at all on a page built without a book.
*/
.crumbs {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem;
  margin: 0 0 2rem; font-size: .8rem; letter-spacing: .04em; text-transform: uppercase;
}
.crumbs a { color: inherit; opacity: .65; text-decoration: none; }
.crumbs a:hover { opacity: 1; color: var(--accent); }
.crumbs span { opacity: .35; }

/* THE TITLE IS THE CONTROL, NOT THE ARROW, so the card is the click target and it
   is as large as the words in it. Two columns on a wide screen, stacked on a
   phone — and when there is no previous chapter the next one keeps its side,
   because a card that slides left would read as the wrong one. */
.chapter-nav {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
  margin: 4rem 0 0; padding-top: 2rem; border-top: 1px solid var(--rule);
}
.chapter-nav.only-next { grid-template-columns: 1fr; justify-items: end; }
.chapter-nav a {
  display: flex; flex-direction: column; gap: .35rem;
  padding: .9rem 1rem; border: 1px solid var(--rule); border-radius: 8px;
  text-decoration: none; color: inherit;
}
.chapter-nav a:hover { border-color: var(--accent); color: var(--accent); }
.chapter-nav .next { text-align: right; align-items: flex-end; }
.chapter-nav .dir {
  font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; opacity: .55;
}
.chapter-nav .name { font-weight: 600; line-height: 1.3; }
.chapter-nav.only-next a { max-width: 22rem; }
p.up { margin: 1.25rem 0 0; text-align: center; font-size: .85rem; }
p.up a { color: inherit; opacity: .6; text-decoration: none; }
p.up a:hover { opacity: 1; color: var(--accent); }
@media (max-width: 34rem) {
  .chapter-nav, .chapter-nav.only-next { grid-template-columns: 1fr; justify-items: stretch; }
  .chapter-nav .next { text-align: left; align-items: flex-start; }
  .chapter-nav.only-next a { max-width: none; }
}
