/*
  Box Discovery, in a browser.

  Palette taken from the world's own materials rather than a mood board: wet slate,
  pale limestone, oxidised copper, ember, and the straw colour white-hot metal passes
  through on its way down. Two accents and both semantic — patina is what is known,
  ember is what is hot or failing.

  Mono carries the structure, the way a specimen label does in a handbook; a serif
  carries the world's voice. Numbers are tabular everywhere they line up, because the
  entire intellectual act of this game is comparing them.

  One rule governs size here: **the bench is bigger than everything else**. Type,
  padding, borders and colour all say it. The first version of this page set every
  panel at the same width and every control at eleven pixels, and the honest result was
  that a newcomer could not tell where the game was. Emphasis is not decoration — it is
  the only thing telling somebody what to touch first.
*/

/*
  Both themes, stated once each, in `light-dark()`.

  This was a light `:root` and a `@media (prefers-color-scheme: dark)` block restating
  forty-one of the same tokens — which was fine while the *system* was the only thing
  that could choose. A switch on the page needs the dark palette addressable by selector
  as well as by media query, and the obvious way to get that is to write it out twice.
  Forty-one colours in two places is exactly the arrangement this codebase keeps finding
  faults in: a second place to say a thing is a place for it to drift.

  `light-dark()` says it once, with the pair adjacent, which reads better than the split
  did — the dark value for `--ink` used to be a hundred and sixty lines from the light
  one. It needs `color-scheme` set, which is below, and that is also what makes the
  switch a single declaration rather than a second palette.

  The merge was done by script rather than by hand, because forty-one pairs is exactly
  the number where one transcribed digit goes unnoticed until somebody using the other
  theme complains. Every token's computed value was then read back in both modes and
  compared against the values from before the change.

  **One thing cannot live here**: `light-dark()` takes two *colours*, and the nightfall
  opacity is a number. It keeps a media query, and is the only place in this file where
  a theme value is written twice — see `--sc-nightfall-depth`.
*/
:root {
  /*
    What the two halves of `light-dark()` mean, and what the browser paints behind us.

    Set on the root so it reaches everything, and overridden by `[data-theme]` below —
    which is the whole of the switch. It also tells the browser which way to render the
    furniture it owns and we do not: form controls, and the canvas behind the page
    before the first paint.
  */
  color-scheme: light dark;

  --slate: #18201f;
  --slate-lift: #1f2928;
  --charcoal: #0d1211;
  --limestone: #e7e8e2;
  --patina: #2f7f6b;
  --ember: #b64d28;
  --straw: #9a7a24;

  --ground: light-dark(var(--limestone), var(--slate));
  --panel: light-dark(#f2f3ee, var(--slate-lift));
  --well: light-dark(#e0e2da, var(--charcoal));
  --ink: light-dark(#1b2322, #dfe3e0);
  --ink-soft: light-dark(#55605e, #9aa6a3);
  /*
    Faint, not unreadable. This was #7e8987, which is 3.24 against the panel and **2.76
    against the well** — below the 4.5 a body of small text wants, and failing outright
    on the darker surface. It carries every section label (IN THE GROUND, BUILT HERE),
    every footnote, every `tip-more` and every disabled technique's `needs Fire Pit or
    Kiln or Bloomery`, which is a great deal of the page to have to squint at.

    A quarter of the way to `--ink`: 4.53 on the panel, and still faint by a factor of
    three against body prose at 14.37, so the hierarchy it exists for is untouched.
  */
  --ink-faint: light-dark(#66716f, #85908d);
  --rule: light-dark(#cdd0c8, #2c3736);
  --rule-strong: light-dark(#b2b7ad, #3d4a48);
  /*
    A line *between* things, and the edge *of* a thing, are two jobs and were one colour.

    Counted over every element on the page that draws a border: **31 controls had a
    boundary under 3:1 against what they stand on, and two cleared it** — Strike and
    Heat, which happen to use `--ink-soft` for an unrelated reason. `--rule-strong` sat
    at 1.83 on the light panel and **1.62 on the dark**, and it was drawing the command
    box, every gather button, both ledger filters, all six reading rooms, `give`,
    `teach`, `put up` and the day count's `select`.

    For a divider that is right and this file argues for it at length — a hairline
    between two rows is furniture, and 1.21 is exactly as loud as furniture should be.
    But **the outline of a control is not decoration, it is the control**: WCAG 1.4.11
    wants 3:1 for anything whose boundary is what tells you it is one, and this is the
    same distinction docs/attention.md §9 had to make between a *material* and the ink
    derived from it. So `--rule` and `--rule-strong` keep every divider they have, and
    `--edge` is the new one, used only where something can be pressed or typed into.

    Solved for the ratio rather than picked, on the same hue ramp — and solved against
    the **darkest surface it lands on** rather than the panel, which is the trap this
    file already records one token over: *the well, the transcript and the command box
    are a darker surface than the panel*. The first value cleared 3.22 on the panel and
    then sat at **2.75 on the well** and 2.91 on the ground, which is where the command
    box actually is. Light: 3.71 / 3.36 / 3.16 on panel, ground and well. Dark: 3.21 on
    the panel, which is that theme's tightest case because there the panel is the
    *lightest* surface, and 3.57 on the ground.
  */
  --edge: light-dark(#757f7b, #6b7774);
  /*
    Patina is the material; this is the material made legible on paper. Raw `--patina`
    is 4.31 on the panel — under the bar for the colour that carries good news and every
    link — and it is 4.54 four percent darker, which nobody can see and a contrast
    checker can. They are kept separate rather than darkening `--patina` itself, because
    that token is also the scene's green and a swatch should not shift to settle a
    question about text. In the dark theme the two are already different for the same
    reason, in the other direction.
  */
  --known: light-dark(#2e7b68, #6fc0a8);
  --hot: light-dark(var(--ember), #e0774b);
  /*
    Same again, and this one matters most: `--first` is what a world first is written
    in, and a world first is the best sentence this game ever prints. Raw `--straw` is
    3.63 on the panel and the line is set at 19px, which is not large text — WCAG's
    large is 24px at normal weight — so it wanted 4.5 and did not have it. Sixteen
    percent toward the ink. `--straw` itself stays exactly as it is, because it is also
    the sky at dusk and the thatch on a roof.
  */
  /*
    The eight a player may choose to be (docs/faces.md).

    The game holds the **words** and no colour at all, which is what lets a terminal
    reach for an ANSI code and this file reach for a hex without either of them being
    the authority. So this is the browser's answer to what `amber` looks like, and it
    is a theme's answer rather than a colour's: the dark block redeclares all eight.

    Measured, not picked, against **two** thresholds — because each of these is a name in
    running text *and* a figure standing on ground:

    - **4.5:1 against `--panel` and `--well`**, which is what small text wants. The light
      set runs 5.95 to 7.64 on the panel, the dark set 5.51 to 10.97.
    - **3:1 against every rock the scene draws**, which is what a graphical object wants
      — counting the halo below, since an outline that reads is a shape that reads.

    The second one is why the light set is darker than a first pass made it. Sandstone is
    the Forest's ground and therefore the ground of the first hour, and at `#c7a17f` it is
    a mid-tone that five of the eight sat 2.6 against. The same measurement taken of what
    was already there: **the player's figure has been 2.13 on that ground for the life of
    the project**, which is how this came to be looked at at all.

    The closest two are 50 apart in RGB, after a first pass put amber and rust 38 apart
    and reading as one warm colour.

    They are **not** mixed from the five palette stones the way the scene's own fills
    are, and that is deliberate. The stones make a landscape — slate, limestone, patina,
    ember, straw — and a person is the one thing in the drawing that is not made of the
    ground. The scene already conceded this: `.f-other` takes theme ink and not scene
    ink, "because a visitor has to be readable against the night".
  */
  --who-bone: light-dark(#4f544a, #dcded2);
  --who-moss: light-dark(#33591f, #8fc46f);
  --who-teal: light-dark(#125a51, #6fc0a8);
  --who-amber: light-dark(#6b4d00, #e2b444);
  --who-rust: light-dark(#8c2f17, #f4926c);
  --who-rose: light-dark(#84284e, #e88aa6);
  --who-plum: light-dark(#523378, #b39ae4);
  --who-sky: light-dark(#1d5285, #79b6e8);
  /*
    What a figure is outlined in, so a colour is legible on any ground.

    The scene's ground runs from pale limestone to near-black basalt *within one theme*,
    so no single figure colour can clear a ratio against all of it. A halo is the answer
    this file already reached for once, for exactly this reason — `.sc-exit-name` carries
    the page's own colour as a halo, because "a word half-legible against a bank of
    material is worse than a word standing slightly proud of it". A person is worth the
    same courtesy.
  */
  --who-halo: light-dark(var(--limestone), var(--charcoal));
  --first: light-dark(#866c24, #d8c07c);
  --tab: light-dark(rgba(47, 127, 107, 0.09), rgba(111, 192, 168, 0.1));
  --tab-strong: light-dark(rgba(47, 127, 107, 0.16), rgba(111, 192, 168, 0.18));
  --bar: light-dark(#c6cabf, #2f3b39);
  --shade: light-dark(rgba(13, 18, 17, 0.55), rgba(6, 10, 9, 0.72));

  /*
    Charter for the world's voice: a workhorse text face with blunt, low-contrast
    serifs, drawn to stay readable small — a handbook face rather than a literary one,
    which is the register this game speaks in. Sitka and Constantia are the same idea
    on Windows; Georgia is the sturdy floor.

    Plex Mono carries every label and every number. It was drawn for technical
    documents and it looks like it, which is exactly right for a world whose whole
    intellectual act is comparing readings off a table.

    No webfont. This project has no runtime dependencies and a CDN request on every
    load would be the first — for a face most machines already have something honest
    for.
  */
  --serif: Charter, "Bitstream Charter", "Sitka Text", Constantia, "Iowan Old Style",
    Georgia, ui-serif, serif;
  --mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono",
    Menlo, Consolas, "Liberation Mono", monospace;

  /*
    One scale, and everything on it. Every step is a size somebody can read across a
    desk — the old scale bottomed out at eleven pixels and used it for buttons.
  */
  --t-tiny: 0.75rem;
  --t-small: 0.875rem;
  --t-body: 1rem;
  --t-lead: 1.1875rem;
  --t-title: 1.4375rem;
  --t-hero: 1.875rem;
  --t-figure: 2rem;

  /*
    The one theme value `light-dark()` cannot hold, because it is not a colour.

    Night falls on the ground as an opacity, and the dark theme already dims the light
    on the scene — so night on top of dusk needs a lighter hand or the ground goes to
    pitch. A number, so it keeps a media query and is written twice; it is the only
    place in this file where that is true, and the pair is named here rather than left
    inline so the two halves are at least adjacent.
  */
  --sc-nightfall-depth: 0.3;
}

/*
  The switch, and it is three states rather than two.

  No attribute means *follow the system*, which is the right default and the one most
  pages throw away the moment they add a toggle. `light` and `dark` are explicit choices,
  and each has to win against a system that says the opposite — which is why both are
  declared rather than only the one that differs from the media query.

  One declaration each, because the palette above already knows both halves. That is the
  whole return on merging it.
*/
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; --sc-nightfall-depth: 0.2; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --sc-nightfall-depth: 0.2; }
}

* { box-sizing: border-box; }

/*
  Scrollbars, made to belong to the page.

  The default is a wide pale slab that reads as part of the browser rather than part of
  the world, and there are several scrolling regions here — it was among the loudest
  things on screen. A transparent border with `background-clip: padding-box` insets the
  thumb without knowing what colour it is sitting on, which matters because the panels,
  the well and the ground are three different shades.
*/
* {
  scrollbar-width: thin;
  scrollbar-color: var(--rule-strong) transparent;
}
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--rule-strong);
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

/*
  One focus ring, and the keyboard finds out where it is in the world's own colour.

  Eleven `:focus-visible` rules had grown one at a time, each on the thing somebody
  happened to be working on, all of them `2px solid var(--known)`. Counted: **41 of the
  83 tabbable things on the page had one and 42 did not** — every technique, every
  gather button, both ledger filters, all six reading rooms, every *put up*. Those fell
  back to the browser's own ring, which on this platform is a **bright blue** and is the
  one colour in the interface that belongs to nothing in it. So a player working by
  keyboard crossed between two focus languages roughly every other press.

  This is the base, so a bare `<button>` is right by default rather than by being
  remembered. The eleven specific rules stay where they are: some need a different
  offset (a ledger row insets so the ring does not sit over the rule below it), and one
  is not an outline at all (the chart's marks draw a halo, because a browser's ring
  around an SVG `<g>` boxes the label with the mark).
*/
:focus-visible { outline: 2px solid var(--known); outline-offset: 2px; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.label {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0;
}

.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.dash { color: var(--ink-faint); }
/* Temperature, as temperature. The fuel ladder is visible before anybody sorts it. */
.t1 { color: #8a6a52; }
.t2 { color: #b4713c; }
.t3 { color: #d08b2e; }
.t4 { color: #e0b054; }
.t5 { color: #efd79a; }

code {
  font-family: var(--mono);
  font-size: 0.9em;
  background: var(--tab);
  padding: 0.05em 0.3em;
  color: var(--known);
}

/* ------------------------------------------------------------------- shell */
/*
  A dashboard, not a document: it is scanned and operated rather than read down, so it
  fills the window and each column keeps its own scroll. Nothing important should ever
  be below a fold that the page as a whole has to move to reach.
*/
.shell {
  height: 100%;
  max-width: 1800px;
  margin: 0 auto;
  padding: clamp(0.9rem, 1.6vw, 1.5rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 2rem;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--rule-strong);
}

.who { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.wordmark {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.34em;
  text-transform: uppercase;
  margin: 0;
  font-weight: 600;
  color: var(--ink-faint);
}
.whoami { margin: 0; font-size: var(--t-lead); line-height: 1.2; }
.whoami b { font-weight: 600; }
.whoami span { color: var(--ink-faint); font-style: italic; }
/* Quiet, and quieter still until pointed at: it is the way out, not a thing to do. */
.notyou {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  background: none;
  border: none;
  border-bottom: 1px dotted var(--edge);
  color: var(--ink-faint);
  cursor: pointer;
  padding: 0;
  margin-left: 0.5rem;
}
.notyou:hover { color: var(--known); border-bottom-color: var(--known); }

/* ------------------------------------------------------------ a face of your own
   The colour and the mood, drawn wherever somebody is named (docs/faces.md §4).

   `--who-teal` is the fallback everywhere and not a neutral grey: everybody has a colour
   from the moment they join, because the world derives one from the name, so a figure
   with no colour on it is a bug rather than a state worth designing for. */
.mug { display: block; flex: none; overflow: visible; }
.mug-head { fill: var(--who-teal); }
.mug[data-colour="bone"] .mug-head { fill: var(--who-bone); }
.mug[data-colour="moss"] .mug-head { fill: var(--who-moss); }
.mug[data-colour="teal"] .mug-head { fill: var(--who-teal); }
.mug[data-colour="amber"] .mug-head { fill: var(--who-amber); }
.mug[data-colour="rust"] .mug-head { fill: var(--who-rust); }
.mug[data-colour="rose"] .mug-head { fill: var(--who-rose); }
.mug[data-colour="plum"] .mug-head { fill: var(--who-plum); }
.mug[data-colour="sky"] .mug-head { fill: var(--who-sky); }
/*
  The face itself, in the page's ground rather than in ink.

  A dark line on a dark colour and a dark line on a pale one cannot both work, and every
  one of the eight is a *contrasting* colour against the panel by construction — so the
  face is drawn in the surface the head is standing on, which is the one colour guaranteed
  to differ from every head by at least 4.5:1. It is the same trick the marks use, one
  layer up.
*/
/*
  Scaled strokes here, unlike the scene — and that is the right way round.

  `non-scaling-stroke` earns its place where a stroke would otherwise fall under a pixel,
  which is the scene's 6.5 px head. A mug is drawn at 22 px on a person's row, 26 px in
  the header and 30 px in the picker, all far above that, so the only thing a fixed
  pixel width buys here is three different-looking faces at three sizes. Scaled keeps
  them one drawing at three scales, which is what an icon set is.
*/
.mug-face {
  fill: none;
  stroke: var(--panel);
  stroke-width: 0.34;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.mug-face .eye { stroke-width: 0.46; }
.mug-face ellipse { fill: var(--panel); stroke: none; }

/*
  A name in its owner's colour, wherever one is printed.

  One opt-in class rather than a selector per surface. It was `[data-colour].person-name,
  [data-colour]#v-player` — two places named in eight rules, and a third surface wanting
  the same thing (who left word) would have made it three. `in-colour` says *this element
  carries somebody's colour*, and anything may say it.
*/
.in-colour[data-colour="bone"] { color: var(--who-bone); }
.in-colour[data-colour="moss"] { color: var(--who-moss); }
.in-colour[data-colour="teal"] { color: var(--who-teal); }
.in-colour[data-colour="amber"] { color: var(--who-amber); }
.in-colour[data-colour="rust"] { color: var(--who-rust); }
.in-colour[data-colour="rose"] { color: var(--who-rose); }
.in-colour[data-colour="plum"] { color: var(--who-plum); }
.in-colour[data-colour="sky"] { color: var(--who-sky); }

/* The header's own face, which is also the button that changes it. */
.mugshot {
  background: none;
  border: 1px solid transparent;
  border-radius: 50%;
  padding: 2px;
  margin-right: 0.4rem;
  cursor: pointer;
  /*
    Tuned by measurement against the name's cap-to-baseline centre, not by eye: -0.34em
    left the face **1.47px high**, and -0.45em lands on **0.00**. Stated in `em` so it
    tracks the type size rather than a pixel count.

    Flexing `.whoami` and centring was tried first and is *worse* here — 5px out, because
    this line's height is set by the `not you?` button rather than by the name, so
    "centre of the line" and "centre of the name" are different places. The person's row
    can use `align-self` precisely because its line holds nothing taller than its text.
  */
  vertical-align: -0.45em;
  line-height: 0;
}
.mugshot:hover, .mugshot:focus-visible { border-color: var(--edge); }
.mugshot[aria-expanded="true"] { border-color: var(--known); }

.who { position: relative; }
/*
  The sheet, over the page rather than in the header's flow.

  The header is four things wide already and the one row it has is the tightest strip on
  the page — a picker opening *inside* it would push the vitals down every time somebody
  looked at their own face. Absolute, so nothing above or beside it moves at all.
*/
.mugs {
  position: absolute;
  z-index: 40;
  top: calc(100% + 0.5rem);
  left: 0;
  /* Measured on the page, and the arithmetic was wrong the first two times — 17rem
     wrapped two swatches onto a second row, 19.5rem still wrapped one by 1.8px, because
     1.9rem rounds down to 30 rendered pixels and eight of those plus seven 6.4px gaps
     want 284.8. 20.5rem leaves 300 for 285 and stops the row reading as two groups of
     colours rather than one row of eight. */
  min-width: 20.5rem;
  max-width: min(22rem, calc(100vw - 2rem));
  padding: 0.75rem 0.85rem 0.6rem;
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  box-shadow: 0 8px 28px var(--shade);
}
.mugs-head {
  margin: 0 0 0.4rem;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.mugs-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.7rem; }
.mugs-note { margin: 0; font-size: var(--t-tiny); font-style: italic; color: var(--ink-faint); }

/*
  A swatch is a control, so its boundary clears 3:1 — the rule this file argues at length
  for every other bordered thing on the page, and one a coloured square makes easy to
  forget, because the *fill* being visible is not the same as the *control* being visible.
  44px on a phone, from the same touch floor the chips keep.
*/
.swatch {
  width: 1.9rem;
  height: 1.9rem;
  border: 1px solid var(--edge);
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  background: var(--who-teal);
}
.swatch[data-colour="bone"], .swatch[data-pick="bone"] { background: var(--who-bone); }
.swatch[data-colour="moss"], .swatch[data-pick="moss"] { background: var(--who-moss); }
.swatch[data-colour="teal"], .swatch[data-pick="teal"] { background: var(--who-teal); }
.swatch[data-colour="amber"], .swatch[data-pick="amber"] { background: var(--who-amber); }
.swatch[data-colour="rust"], .swatch[data-pick="rust"] { background: var(--who-rust); }
.swatch[data-colour="rose"], .swatch[data-pick="rose"] { background: var(--who-rose); }
.swatch[data-colour="plum"], .swatch[data-pick="plum"] { background: var(--who-plum); }
.swatch[data-colour="sky"], .swatch[data-pick="sky"] { background: var(--who-sky); }
/* Chosen is a ring rather than a tick: a tick has to be drawn in a colour, and there is
   no colour that reads on all eight. */
.swatch[aria-pressed="true"] {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-color: var(--ink);
}

/* Nine of them, so three by three — a grid rather than a wrapping row, because wrapping
   laid them 4, 4, 1 and a lone `awed` on a row of its own reads as a different kind of
   thing. Three columns is the one arrangement nine has that is not ragged, and a grid
   holds it whatever the labels or the font do. */
.mugs-moods { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.3rem; }
.mood-pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  padding: 0.35rem 0.2rem;
  background: none;
  border: 1px solid transparent;
  color: var(--ink-faint);
  font-family: var(--mono);
  font-size: var(--t-tiny);
  cursor: pointer;
}
.mood-pick:hover, .mood-pick:focus-visible { border-color: var(--edge); color: var(--ink-soft); }
.mood-pick[aria-pressed="true"] { border-color: var(--known); color: var(--known); }

/*
  The person's row grows a face and a colour. Row, not column, because a picture beside
  a name is one thing being named and the busy note stays under both.

  **Centred on the line, while the words keep their shared baseline.** The row is
  `align-items: baseline` so `malte` and `glad` sit on one baseline, which is right for
  two pieces of text at different sizes — but an SVG on a baseline row puts its *bottom
  edge* on that baseline, so the face floated. Measured against the name's optical
  centre (a `Range` over the text node, not the line box, which is the only honest way to
  ask where text actually sits): **5.3px high**, a quarter of the face's own diameter and
  plainly visible. `align-self` corrects the one item without disturbing the two that
  were already right — 0.9px after.
*/
.person-row .mug { align-self: center; }
/* On the name's baseline, not centred on the row: centred, a tiny word beside a body-size
   name reads as a superscript, and what somebody says about themselves is not a footnote
   to their name. */
.person-mood {
  font-family: var(--mono);
  font-size: var(--t-small);
  color: var(--ink-faint);
}

/* The door's swatches. A fieldset because it is a choice among alternatives, and the
   legend is the only label a screen reader needs for the row. */
.swatches { border: none; padding: 0; margin: 0; }
.swatches legend {
  padding: 0;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.swatch-row { display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0.5rem 0 0.4rem; }

/*
  The vitals are the clock. Fed-for is the only number that can stop you doing
  anything, so it is set at the size of a number you glance at rather than one you go
  looking for.
*/
.vitals { margin: 0 0 0 auto; display: flex; flex-wrap: wrap; gap: 0.5rem 1.75rem; }
.vital { display: flex; flex-direction: column; align-items: flex-start; }
.vital dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.vital dd {
  margin: 0;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-figure);
  line-height: 1.05;
  font-weight: 500;
}
.vital dd.low { color: var(--hot); }
.vital dd.busy { color: var(--first); }

/*
  The day fills; everything else counts down. Only the day has a known start, so only
  the day gets a bar — `freeOn` and `readyOn` are arrivals, and an arrival gets a
  countdown in chair time beside the world's own "day 15". Both exist only where the
  world keeps real time; under the eager clock a day has no width, and every one of
  these renders as nothing rather than as zero. Fixed sizes throughout: a countdown
  changing its digits must never move the row it sits in.
*/
.dayline {
  display: block;
  width: 3.4rem;
  height: 3px;
  margin-top: 0.35rem;
  background: var(--bar);
  overflow: hidden;
}
.dayline-fill {
  display: block;
  height: 100%;
  width: 0;
  background: var(--known);
  transition: width 1.1s linear;
}
.soon {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-tiny);
  color: var(--ink-faint);
  white-space: nowrap;
}
.vital .soon { margin-top: 0.2rem; }
.building-state .soon:not(:empty)::before { content: " · "; }

.help {
  font-family: var(--mono);
  font-size: var(--t-lead);
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--edge);
  color: var(--ink-soft);
  cursor: pointer;
  flex: none;
}
.help:hover { border-color: var(--known); color: var(--known); }

/* The two page controls, in one box so they wrap as a pair. The margin that pushes them
   to the end of the header lives here rather than on either button — see index.html. */
.corner { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
.corner .help { margin-left: 0; }

/* The rooms fold, on narrow screens only. Not rendered at all above the breakpoint. */
.rooms { display: none; }

/* The theme cycle, built to `?`'s pattern rather than a new one — same circle, same
   quiet border, one size down because it is the lesser of the two. */
.theme {
  font-family: var(--mono);
  /* `--t-body` put a sun in here at 16px and it read as a speck — ☀ spends most of its
     em on whitespace between the rays, so it needs the size `?` gets even though the
     button around it is smaller. Checked against nine candidate glyphs at this size
     before settling on sun, crescent and half-disc. */
  font-size: var(--t-lead);
  line-height: 1;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: transparent;
  border: 1px solid var(--rule-strong);
  color: var(--ink-faint);
  cursor: pointer;
  flex: none;
  margin-left: auto;
}
.theme:hover, .theme:focus-visible { border-color: var(--known); color: var(--known); }

/*
  Three columns, and they are deliberately unequal. The bench is widest because it is
  the panel being used; it is also first in the source, so at every narrower width it
  stays at the top without any reordering trickery.
*/
.columns {
  flex: 1;
  min-height: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr) minmax(0, 1.05fr);
  grid-template-areas: "bench ledger place";
  align-items: stretch;
}
.bench-panel { grid-area: bench; }
.ledger-panel { grid-area: ledger; }
.place-panel { grid-area: place; }

/*
  Below the width three columns can honestly carry, the bench takes a full row of its
  own and the reference panels sit under it. The page scrolls from here down; a fixed
  viewport with four stacked scroll regions is worse than a page that just moves.
*/
/*
  Too narrow for three columns, or too short for three columns — the same answer.

  The height half was added after a short wide window was measured properly: at
  1512 × 621 the bench alone wants more than the window has, so the fixed viewport was
  already putting something below a fold, and the only question left was whether it is
  reached by scrolling the page or by finding an inner scroll region inside a panel with
  a heading pinned above it. Stacking answers it the way the narrow case has always been
  answered, and *deletes* the third behaviour that used to live at the foot of this file
  (a bench with its own scrollbar and a sticky command line over the transcript's newest
  line — see there).

  `min-width: 1421px` on the tightening tiers below still holds, so this is one rule with
  one threshold on each axis rather than a matrix (docs/attention.md §12.2).
*/
@media (max-width: 1420px), (max-height: 720px) {
  .columns {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "bench bench" "ledger place";
  }
  .shell { height: auto; min-height: 100%; }
  .bench-panel { min-height: 32rem; }
  /* Everything else this layout needs is at the **end** of the file, because every one
     of those rules names an element defined below this point and lost to it on source
     order. See there — the list of what was inert is worth reading before adding to it. */
}
/*
  Stacked, *Where you stand* keeps second place — and the bench stops needing the ledger.

  Reordering was tried first and it only moved the problem. Measured at 420px, which is
  a phone:

      bench, ledger, place    bench to the ledger 0.84 screens, to a gather button 2.05
      bench, place, ledger    bench to the ledger 3.15 screens, to a gather button 1.25

  Whichever of them is second, the third is two or three screens off, because the page
  is three full panels long and *Where you stand* is much the biggest. Trading a
  three-screen scroll for a two-screen one is not a fix.

  So the bench carries what it needs with it — a strip of what you are in fact carrying,
  under the well, which is the only part of the ledger the bench ever wanted — and this
  stays the order that keeps *gathering* close, which is the thing people do most and
  the thing somebody ran out of food failing to reach (docs/attention.md §11).
*/
@media (max-width: 900px) {
  .columns { grid-template-columns: minmax(0, 1fr); grid-template-areas: "bench" "place" "ledger"; }
  .vitals { margin-left: 0; }
  /* Only here. With three columns the ledger is already beside the bench, and a second
     way to pick a thing up would blur what the ledger row teaches. */
  .carry:not([hidden]) { display: flex; }
}

/* What you are carrying, within reach of the bench. Chips in the register of the bench's
   own, a shade quieter, because these are a way *to* the well and not what is in it. */
.carry { display: none; flex-wrap: wrap; gap: 0.4rem; }
.carry-chip { padding: 0.45rem 0.6rem; font-size: var(--t-small); background: none; }
.carry-chip .count {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--known);
  margin-left: 0.15rem;
}


.panel {
  background: var(--panel);
  border: 1px solid var(--rule);
  padding: 1.1rem 1.2rem 1.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
}

/*
  There is more of this below, and nothing used to say so.

  Two regions scroll inside themselves. Measured on the first morning at 1512 × 743, the
  ledger holds **1.93 screens** and *Where you stand* holds **2.88** — and the platform's
  scrollbars are overlay ones, 0 px and 2 px wide, invisible until something is already
  moving. No mask, no shadow, no rule on either edge. `Ways on` — one of the four things
  a player can do with a place — sat 258 px below a cut with nothing to say it was a cut.

  A gradient the colour of the panel, at the bottom edge, only while there is more below
  (`has-more`, kept in step by `markEdges`). Three details are load-bearing:

  **Bottom only.** Both regions carry `position: sticky` headings — the ledger's `thead`,
  the place column's section labels — and a fade at the top would wash out the one thing
  pinned there to be read.

  **A sticky pseudo-element and not `mask-image`.** A mask fades the *content*, and half
  a gather button faded out reads as a button somebody has disabled. This lies over the
  content instead, and only the last two lines of it.

  **`flex: none`.** The panels are `flex-direction: column`, so the `::after` is a flex
  item; without it the box shrinks to nothing and the rule silently does nothing at all —
  which is exactly the failure this file has caught four times in the responsive blocks.

  And it is **2 rem of pure ramp**, which is the second thing measured rather than
  guessed. At 2.75 rem with a 15% opaque foot it covered a whole 41 px ledger row, and a
  row faded to a third of its ink reads as a row somebody has disabled — on rows that are
  still perfectly clickable. Two thirds of the last line is enough to say *there is more*.
*/
.has-more::after {
  content: "";
  display: block;
  flex: none;
  position: sticky;
  bottom: 0;
  height: 2rem;
  margin-top: -2rem;
  pointer-events: none;
  background: linear-gradient(to top, var(--panel), transparent);
}
/* The table scrolls inside its own padding, so the fade has to reach the full width. */
.ledger-scroll.has-more::after { margin-inline: -1.2rem; }
/* The folio body already sits on `--panel`, so it takes the same gradient — but it is
   inside a card with its own padding, and the last line must fade to the card's edge. */
.folio-body.has-more::after { margin-inline: -0.2rem; }

.panel-head { display: flex; flex-direction: column; gap: 0.1rem; }
.panel-title {
  margin: 0;
  font-size: var(--t-title);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
}
.panel-sub { margin: 0; font-size: var(--t-small); color: var(--ink-faint); font-style: italic; }

/*
  The bench, said in the frame itself: a heavier border and a shade of the accent
  behind the heading. It is the only panel that gets either.
*/
.bench-panel {
  border: 1px solid var(--rule-strong);
  border-top: 3px solid var(--known);
  overflow: hidden;
}
.bench-panel .panel-title { color: var(--known); }
/*
  The bench never scrolls as a whole — the transcript inside it does. So everything
  around the transcript keeps its size and the transcript takes whatever is left, down
  to a floor. Without this, a short window pushed the heading and the first steps up out
  of a panel that clips, and the one thing a newcomer is meant to look at was the one
  thing off screen.
*/
/*
  The transcript takes what is left, down to a floor.

  **With three columns up and room to divide, the floor does not decide this** — flex
  does, and it hands over the whole remainder. Measured at 1512 × 982 the transcript is
  334 px and at 868 it is 299 px; the floor of 4.5 rem is nowhere near either. Letting
  the well shrink alongside changes nothing, because a flex item's automatic minimum is
  its content and a bench with eight chips on it is already as short as it can be
  (docs/attention.md §10).

  **Everywhere else the floor decides it entirely, and the two places it does are
  overridden further down.** Dividing a remainder only works where there is one. Once
  the shell is `height: auto` — every stacked layout, so two windows side by side and
  every phone — the bench is its own `min-height`, the basis of `1 1 0` is zero, and
  this box sits on its floor at any window height whatsoever: 72 px at 1100 × 800 and
  72 px at 756 × 900. And on a short wide window the tier at the end of the file lowers
  the floor to 3.2 rem, which took it to 54 px — under one line. Both are answered where
  they happen (`max-width: 1420px`, `max-height: 720px`), because the answers are
  opposite: one lets the page grow, the other lets the bench scroll.

  The lesson worth keeping is the one this comment used to get wrong by asserting it:
  every measurement here had been taken in the three-column desktop layout, and that is
  the single shape where flex is in charge. Measure the stacked one too, or the number
  written down is about one window out of four (docs/attention.md §12).
*/
.bench-panel > *:not(.transcript) { flex: none; }
.bench-panel > .transcript { flex: 1 1 0; min-height: 4.5rem; }

.footnote { font-size: var(--t-small); color: var(--ink-faint); margin: 0; font-style: italic; }

/* --------------------------------------------------------------- the guide
   Four steps, ticked off by watching what the player has actually done, and gone for
   good once they are through it. It says nothing about content: no property, no
   combination, no material. Where the buttons are is not a hint. */
.guide {
  border: 1px dashed var(--first);
  background: color-mix(in srgb, var(--first) 7%, transparent);
  padding: 0.55rem 0.8rem 0.65rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.guide-head { display: flex; align-items: baseline; gap: 0.5rem; }
.guide-title {
  margin: 0;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--first);
  flex: 1;
}
/*
  A dismiss control is not smaller on a desktop, it is only easier to miss slowly.

  This was 17 × 14 — a note in the mobile block already calls that "a pointer size on a
  surface that has no pointer" and grows it to 44 there, and then left the pointer with
  238 px² to aim at. It is the only way out of a reading room besides Escape, it sits in
  a corner of a dialog rather than at a screen edge so Fitts gives it nothing, and it is
  the same element the first-steps guide is closed with.
*/
.guide-close {
  background: none;
  border: none;
  color: var(--ink-faint);
  cursor: pointer;
  font-size: var(--t-small);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  min-height: 2rem;
  margin: -0.4rem -0.5rem -0.4rem 0;
}
.guide-close:hover { color: var(--ink); }

/*
  The page is out of touch with the world.

  Said at the command box and nowhere else, because that is the one control everything
  else is a shortcut for and the place the next attempt is going to be made. Ember, which
  is the colour `Fed for` and `Carrying` already use for *this is a state you should know
  about*, and it stays until a poll gets through — a standing fact, but one that makes
  every number on the page a lie, which is what earns it a permanent mark.

  No spinner and no overlay. The world is still readable and everything on the page is
  still true as of the last time it was heard from; what has stopped is the asking.
*/
body.adrift .commandline input {
  border-color: var(--hot);
  color: var(--hot);
}
body.adrift .commandline input::placeholder { color: var(--hot); opacity: 0.85; }
body.adrift .commandline .verb.big { border-color: var(--hot); color: var(--hot); }
.guide-steps { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 0.1rem; }
.guide-steps li {
  display: grid;
  grid-template-columns: 1.35rem 1fr;
  align-items: baseline;
  gap: 0.4rem;
  font-size: var(--t-body);
  line-height: 1.4;
  color: var(--ink);
}
.guide-steps .tick { font-family: var(--mono); font-size: var(--t-small); color: var(--ink-faint); }
.guide-steps li[data-done="yes"] { color: var(--ink-faint); text-decoration: line-through; }
.guide-steps li[data-done="yes"] .tick { color: var(--known); text-decoration: none; }
/* Done steps compress into a quiet row of ticks: progress felt, never scored. */
.guide-steps li.guide-gone { text-decoration: none; grid-template-columns: auto 1fr; }
.guide-steps li.guide-gone .tick { font-size: var(--t-tiny); letter-spacing: 0.2em; }
.guide-steps .gone-say {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
}
.guide.done { border-style: solid; border-color: var(--known); background: var(--tab); }
.guide.done .guide-title { color: var(--known); }

/* --------------------------------------------------------------- the bench */
/*
  Large on purpose. This is the object the whole game is played on, and at eighty
  pixels of grey it read as a spacer between two things that mattered more.
*/
.well {
  background: var(--well);
  border: 2px dashed var(--rule-strong);
  min-height: 6.5rem;
  padding: 1rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  align-content: flex-start;
  transition: border-color 140ms ease;
}
.well:not(.empty) { border-style: solid; border-color: var(--known); }
.well.empty { align-items: center; justify-content: center; }
/* `--ink-soft`, not `--ink-faint`, and the same for every other line in this file that
   sits on `--well`: the well, the transcript and the command box are a darker surface
   than the panel, and even the corrected faint is only 3.86 against it. Soft is 4.99.
   The token is chosen by what it stands on rather than by how quiet it wants to be. */
.well-hint {
  font-style: italic;
  color: var(--ink-soft);
  font-size: var(--t-body);
  text-align: center;
  max-width: 34ch;
  margin: 0;
}

.chip {
  font-family: var(--mono);
  font-size: var(--t-body);
  background: var(--panel);
  border: 1px solid var(--edge);
  color: var(--ink);
  padding: 0.5rem 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
}
.chip:hover { border-color: var(--hot); color: var(--hot); }
.chip .x { color: var(--ink-faint); font-size: var(--t-small); }
.chip:hover .x { color: var(--hot); }
/* Laid out but not carried. A dashed edge and faded ink, in the register of the
   `first steps` box rather than of a refusal — it is not a mistake, it is a thing you
   have put down to look at and do not have. The truth used to arrive two steps later,
   from the world, after a technique had been chosen (docs/attention.md §7). */
.chip[data-short] {
  border-style: dashed;
  /* Sits on `--well`, having no background of its own — so soft rather than faint. */
  color: var(--ink-soft);
  background: none;
}
.chip[data-short] .x { color: var(--ink-soft); }
.chip[data-short]:hover, .chip[data-short]:hover .x { color: var(--hot); }

.verbs { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.verb {
  font-family: var(--mono);
  font-size: var(--t-small);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.6rem 1rem;
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--edge);
  cursor: pointer;
}
.verb:hover:not(:disabled) { border-color: var(--known); color: var(--known); background: var(--tab); }
.verb:disabled { color: var(--ink-faint); border-style: dashed; cursor: not-allowed; }
/*
  The world just got bigger because of what you did: a technique crossing from
  refusal to button pulses once, on the crossing only. The button itself is the
  announcement — no toast, no sound, no text.
*/
@keyframes verb-lit {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--known) 60%, transparent); }
  to { box-shadow: 0 0 0 16px transparent; }
}
.verb.lit { animation: verb-lit 1.6s ease-out 1; border-color: var(--known); color: var(--known); }
@media (prefers-reduced-motion: reduce) { .verb.lit { animation: none; } }
.verb.big { font-size: var(--t-body); padding: 0.7rem 1.4rem; }
.verb.go { border-color: var(--known); color: var(--known); background: var(--tab); font-weight: 600; }
.verb.go:hover { background: var(--tab-strong); }
.verb.quiet-verb { border-color: transparent; color: var(--ink-faint); }

/*
  A technique that is available is a button. One that is not says what would have to
  stand here, on its face rather than in a tooltip — and says it quietly, because a row
  of loud refusals is how the old right-hand column stopped being readable.
*/
.verbs .verb { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; }
.verbs .verb:not(:disabled) { font-weight: 600; border-color: var(--ink-soft); }
/*
  A button must not be sized by its own excuse.

  Measured on the bench at day 10, with two techniques open and three shut:

  | Strike | open | 93 px | 4,814 px² |
  | Heat | open | 73 px | 3,797 px² |
  | Crush | shut | 236 px | 12,201 px² |
  | Mix | shut | 166 px | 8,601 px² |
  | Cultivate | shut | 122 px | 6,341 px² |

  **27,143 px² of what you cannot do against 8,611 px² of what you can** — 3.2 to one,
  and Crush alone was 3.2× Heat. Nothing had gone wrong except that the width came off
  the `<small>`, so the more thoroughly a technique was unavailable the bigger its button
  grew. The most emphatic objects on the panel the whole game is played on were the three
  things the player is not allowed to touch.

  This is the fault the structures list was fixed for a long time ago — *refusals are
  quiet; what can go up here comes first, and being short of stone is a fact about a
  satchel rather than an error* — and the technique row never learned it. The **order**
  already had (`drawBench` sorts available first); it was only ever the sizing.

  The first fix was to give every button one size. It worked — 3.2 to one became 1.5,
  and no shut button was bigger than an open one — and it cost **64 px of bench height**,
  which the bench has not got: 609 px of content in a 613 px panel. So instead, the split
  the structures list has used for a long time. What you can do is a row of buttons; what
  you cannot is a quiet line beneath it, one item per technique, the excuse on the same
  line as the name. 112 px → 78 px, and it shrinks and then vanishes as the game opens up.

  It is still `<button disabled>` with the same tip on it: nothing about the behaviour
  was at fault, only how loud it was. And the need text stays on the face, because a
  player who cannot find out what a technique wants is stuck on the interface rather
  than on the game.
*/
/*
  A ladder for type, because there was not one where it mattered most.

  Counted over every control on the page: **28 were at 12 px** — all six reading rooms,
  both ledger filters, the day count's `select`, and `give`, `teach`, `offer a trade`,
  `leave word here` and `put up`. Which is to say **`put up` — days of work, materials
  spent, nothing undoable — was set at exactly the size of the word *Built here* above
  it.** The interface did not distinguish an action from a label by size at all, while
  the biggest type on the page, 32 px, sat on the four vitals, which are the only things
  up there nobody can press.

  Three rungs now, and the rule is what a thing *is* rather than where it lives:

  - **12 px** — labels, section headings, counts. What you read.
  - **14 px** — a secondary action: a room to open, a filter, a person to give to, a
    building to raise.
  - **16 px** — the two things pressed most, which the measured session put at 29 of 81
    commands and the whole of the bench: a technique, and a seam.

  The vitals keep 32 px. They are a figure rather than a heading, they are read across
  the room, and shrinking status is not the same problem as growing action.
*/
.verbs .verb:not(:disabled) { min-width: 6.5rem; align-items: center; font-size: var(--t-body); }
.verbs-shut {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 1.5rem;
  margin-top: -0.1rem;
}
.verbs-shut .verb:disabled {
  flex-direction: row;
  align-items: baseline;
  gap: 0.45rem;
  padding: 0.1rem 0;
  border: 0;
  font-size: 0.6875rem;
  color: var(--ink-soft);
  cursor: default;
}
.verbs-shut .verb:disabled > span { letter-spacing: 0.09em; }
.verb small {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.03em;
  text-transform: none;
  color: var(--ink-faint);
  font-weight: 400;
  line-height: 1.35;
}

/* The world's voice. Sensory, never instructional. */
.transcript {
  background: var(--well);
  border: 1px solid var(--rule);
  padding: 1rem 1.1rem;
  overflow-y: auto;
  flex: 1;
  min-height: 12rem;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}
.said { display: flex; flex-direction: column; gap: 0.15rem; }
/* The echo of what was typed, on `--well` like the rest of the transcript — so soft. It
   is also the line that says *which* command an answer belongs to, which is a poor thing
   to have to squint at when reading back what happened. */
.said .echo { font-family: var(--mono); font-size: var(--t-small); color: var(--ink-soft); }
.said p { margin: 0; font-size: var(--t-body); white-space: pre-wrap; }
.said p.first { color: var(--first); font-size: var(--t-lead); }
.said p.good { color: var(--known); }
.said p.bad { color: var(--hot); }
.said.fresh { animation: settle 280ms ease-out; }
@keyframes settle { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .said.fresh { animation: none; } }

/* Out at work, between the transcript and the command box — where the refusal for being
   busy is about to arrive. Never under the eager clock, where nobody is ever still busy
   and a row of furniture for a state that cannot happen is worse than nothing. */
.atwork {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-soft);
  border-left: 2px solid var(--first);
  padding: 0.25rem 0 0.25rem 0.55rem;
}
.atwork .soon { color: var(--first); }

.commandline { display: flex; gap: 0.6rem; align-items: stretch; }
.commandline input {
  flex: 1;
  font-family: var(--mono);
  font-size: var(--t-body);
  background: var(--well);
  color: var(--ink);
  border: 1px solid var(--edge);
  padding: 0.75rem 0.85rem;
  min-width: 0;
}
.commandline input:focus-visible { outline: 2px solid var(--known); outline-offset: -1px; }

/* ------------------------------------------------------------- the ledger */
.controls { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.controls input[type="search"] {
  font-family: var(--mono);
  font-size: var(--t-small);
  background: var(--ground);
  color: var(--ink);
  border: 1px solid var(--edge);
  padding: 0.5rem 0.65rem;
  flex: 1 1 8rem;
  min-width: 0;
}
.chipbtn {
  font-family: var(--mono);
  font-size: var(--t-small);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.5rem 0.7rem;
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--edge);
  cursor: pointer;
}
.chipbtn[aria-pressed="true"] { color: var(--known); border-color: var(--known); background: var(--tab); }

/*
  The table scrolls; the panel does not. What a thing *is* — twenty readings, the whole
  vocabulary the game is reasoned in — sits under the table and was reachable only by
  scrolling past thirty-odd rows, which meant the panel that exists to be read pushed
  the reading off the bottom of itself.
*/
/*
  The table is the fixed part and the reading takes what is left — that way round.

  Fixing the *reading* instead also stops the flicker, but it caps it at a box most
  substances overflow, and the readings are the vocabulary the whole game is reasoned
  in. Fixing the table gets the same stability — rows cannot move if the box holding
  them never changes size — and hands every remaining pixel to the reading, which is the
  part that varies. Both are constant for a given window, which is all stability needs.
*/
.ledger-panel { overflow: hidden; }
.ledger-panel > *:not(.detail) { flex: none; }
.ledger-scroll {
  flex: none;
  height: clamp(11rem, 30vh, 26rem);
  overflow-y: auto;
  margin: 0 -1.2rem;
  padding: 0 1.2rem;
}
table.ledger { width: 100%; border-collapse: collapse; }
table.ledger thead th {
  position: sticky;
  top: 0;
  background: var(--panel);
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 400;
  text-align: right;
  padding: 0.3rem 0 0.4rem 0.6rem;
  white-space: nowrap;
  border-bottom: 1px solid var(--rule-strong);
  cursor: pointer;
  user-select: none;
}
table.ledger thead th:first-child { text-align: left; padding-left: 0; }
table.ledger thead th[aria-sort]:not([aria-sort="none"]) { color: var(--known); }
table.ledger td {
  padding: 0.45rem 0 0.45rem 0.6rem;
  border-top: 1px solid var(--rule);
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  color: var(--ink-soft);
  white-space: nowrap;
}
table.ledger td:first-child {
  text-align: left;
  padding-left: 0;
  font-family: var(--serif);
  font-size: var(--t-body);
  color: var(--ink);
  width: 99%;
}
tr.thing { cursor: pointer; }
tr.thing:hover td { background: var(--tab); }
tr.thing:focus-visible { outline: 2px solid var(--known); outline-offset: -2px; }
tr.thing.carried td:first-child { font-weight: 600; }

/* ---------------------------------------------------------------- marks
   A small picture of a thing, beside its name.

   The markup arrives on `Held` from the server, which sends one only for what is
   already in this player's ledger — see `content/marks.ts` for why it is not a folder
   of PNGs in `public/`.

   Sized in `em` so it tracks the row's own type rather than a number picked once, and
   `vertical-align: -0.18em` puts its optical centre on the text baseline: a 16 × 16 box
   sitting on the baseline rides visibly high beside lower-case letters.

   ---- Shape is shared; the tint is what tells a family apart

   Every ore is a rock with flecks, every metal is an ingot, every blade is a blade. So
   the *shape* classes below carry no colour of their own — they take it from a `t-`
   tint class sitting alongside, which is the only thing that differs between Copper Ore
   and Tin Ore. A new ore is one line here and one line there.

   Colours are the **material** tokens — `--patina`, `--straw`, `--ember`, `--limestone`
   — which are things in a drawing rather than text colours, so a mark can use them
   without touching a decision about legibility (`CLAUDE.md`). Both themes get the same
   markup and the tokens do the work; every tint is mixed against `--ground`, which is
   the page's own paper, so a pale material stays pale on a dark page rather than
   turning into a hole.
*/
.mk {
  width: 1.15em;
  height: 1.15em;
  vertical-align: -0.18em;
  margin-right: 0.45em;
  flex: none;
}
/* Holds the column for a substance with no picture, and draws nothing in it. */
.mk-none { display: inline-block; }

/*
  On a chip the picture is the subject, not a column marker.

  The bench is where the game actually happens — *you can only strike things together* —
  and the things on it were words. Bigger than the ledger's, because a chip is a single
  object being held up rather than a row being scanned, and the chip's own `gap` does the
  spacing, so the ledger's `margin-right` is cleared or the two of them double up.

  Sized in `em` like everything else here, so it follows the type rather than fighting it,
  and `flex: none` (inherited from `.mk`) keeps it from being squeezed by a long name.
*/
.chip .mk { width: 1.6em; height: 1.6em; margin-right: 0; vertical-align: baseline; }

/* ---- the shared shapes. No colour here; the tint class carries it. */
.mk .mk-rock { fill: var(--mk-body); }
.mk .mk-facet { fill: var(--mk-lit); }
.mk .mk-fleck { fill: var(--mk-fleck); }
.mk .mk-metal { fill: var(--mk-body); }
.mk .mk-sheen { fill: var(--mk-lit); }
.mk .mk-edge { fill: var(--mk-lit); }
.mk .mk-heap { fill: var(--mk-body); }
.mk .mk-lump { fill: var(--mk-body); }
.mk .mk-pool { fill: var(--mk-body); opacity: 0.85; }
.mk .mk-pot { fill: var(--mk-body); }
.mk .mk-lip { fill: var(--mk-lit); }
.mk .mk-stalk { fill: none; stroke: var(--mk-body); stroke-width: 1.05; stroke-linecap: round; }

/* ---- the tints.

   **Mixed from materials, never from the page.** The first version mixed each tint
   against `--ground` and `--ink`, which are the paper and the writing and are *swapped
   by the theme*: in dark, `--ground` is slate and `--ink` is pale, so `13% ink on
   ground` — meant to be chalk — came out very nearly black. Every rock in the set was a
   dark blob and the palest material in the game was the darkest mark on the page.

   The scene had already solved this and it is worth copying rather than re-deriving:
   `--sc-rock-limestone` is `limestone 27% on slate`, a **material** against a fixed
   base. `--limestone`, `--straw`, `--ember`, `--patina`, `--slate` and `--charcoal` are
   literal values that no theme touches, because they are things in a drawing rather
   than text colours (`CLAUDE.md`) — so a mix of them is the same colour on both pages,
   which is what a material's colour ought to be.

   Everything is kept inside a band — nothing paler than about 88% limestone, nothing
   darker than about 22% — so that no mark disappears into either surface. Coal is not
   jet black, and that is the price of it being visible on a dark page. */
.mk .t-stone { --mk-body: color-mix(in srgb, var(--limestone) 42%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 58%, var(--slate)); }
.mk .t-pale { --mk-body: color-mix(in srgb, var(--limestone) 62%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 76%, var(--slate)); }
.mk .t-chalk { --mk-body: color-mix(in srgb, var(--limestone) 68%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 84%, var(--slate)); }
.mk .t-dark { --mk-body: color-mix(in srgb, var(--limestone) 26%, var(--charcoal)); --mk-lit: color-mix(in srgb, var(--limestone) 40%, var(--charcoal)); }
/* The black country. Both sit near the dark end of the band and are told apart by the
   lit facet: basalt catches the light dully all over, obsidian catches it like glass. */
.mk .t-basalt { --mk-body: color-mix(in srgb, var(--limestone) 24%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 38%, var(--slate)); }
.mk .t-obsidian { --mk-body: color-mix(in srgb, var(--limestone) 23%, var(--charcoal)); --mk-lit: color-mix(in srgb, var(--limestone) 62%, var(--slate)); }
.mk .f-iron { --mk-fleck: color-mix(in srgb, var(--straw) 70%, var(--limestone)); }
.mk .f-copper { --mk-fleck: color-mix(in srgb, var(--ember) 76%, var(--straw)); }
.mk .f-tin { --mk-fleck: color-mix(in srgb, var(--limestone) 80%, var(--patina)); }
.mk .f-lead { --mk-fleck: color-mix(in srgb, var(--limestone) 58%, var(--slate)); }
.mk .f-silver { --mk-fleck: color-mix(in srgb, var(--limestone) 88%, var(--patina)); }

.mk .t-copper { --mk-body: color-mix(in srgb, var(--ember) 62%, var(--straw)); --mk-lit: color-mix(in srgb, var(--ember) 40%, var(--limestone)); }
.mk .t-tin { --mk-body: color-mix(in srgb, var(--limestone) 60%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 84%, var(--patina)); }
.mk .t-bronze { --mk-body: color-mix(in srgb, var(--straw) 64%, var(--ember)); --mk-lit: color-mix(in srgb, var(--straw) 48%, var(--limestone)); }
.mk .t-iron { --mk-body: color-mix(in srgb, var(--limestone) 40%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 70%, var(--slate)); }
.mk .t-steel { --mk-body: color-mix(in srgb, var(--limestone) 46%, var(--patina)); --mk-lit: color-mix(in srgb, var(--limestone) 84%, var(--patina)); }
.mk .t-lead { --mk-body: color-mix(in srgb, var(--limestone) 34%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 56%, var(--slate)); }
.mk .t-silver { --mk-body: color-mix(in srgb, var(--limestone) 72%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 92%, var(--patina)); }
.mk .t-stoneblade { --mk-body: color-mix(in srgb, var(--limestone) 38%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 62%, var(--slate)); }
.mk .t-quartzblade { --mk-body: color-mix(in srgb, var(--limestone) 56%, var(--slate)); --mk-lit: color-mix(in srgb, var(--limestone) 90%, var(--patina)); }
.mk .t-obsidianblade { --mk-body: color-mix(in srgb, var(--limestone) 26%, var(--charcoal)); --mk-lit: color-mix(in srgb, var(--limestone) 78%, var(--slate)); }

.mk .t-coal { --mk-body: color-mix(in srgb, var(--limestone) 22%, var(--charcoal)); }
.mk .t-char { --mk-body: color-mix(in srgb, var(--limestone) 30%, var(--charcoal)); }
.mk .t-coke { --mk-body: color-mix(in srgb, var(--limestone) 28%, var(--slate)); }

.mk .t-sand { --mk-body: color-mix(in srgb, var(--straw) 46%, var(--limestone)); }
.mk .t-salt { --mk-body: color-mix(in srgb, var(--limestone) 66%, var(--slate)); }
.mk .t-flour { --mk-body: color-mix(in srgb, var(--limestone) 62%, var(--straw)); }
.mk .t-meal { --mk-body: color-mix(in srgb, var(--straw) 58%, var(--limestone)); }
.mk .t-lime { --mk-body: color-mix(in srgb, var(--limestone) 64%, var(--patina)); }
.mk .t-mortar { --mk-body: color-mix(in srgb, var(--limestone) 50%, var(--slate)); }
.mk .t-grain { --mk-body: color-mix(in srgb, var(--straw) 64%, var(--limestone)); }
.mk .t-bean { --mk-body: color-mix(in srgb, var(--ember) 46%, var(--charcoal)); }
.mk .t-cooked { --mk-body: color-mix(in srgb, var(--ember) 56%, var(--straw)); }
.mk .t-hazel { --mk-body: color-mix(in srgb, var(--ember) 40%, var(--straw)); }
.mk .t-berry { --mk-body: color-mix(in srgb, var(--ember) 62%, var(--slate)); }
.mk .t-nutmeal { --mk-body: color-mix(in srgb, var(--straw) 44%, var(--slate)); }
.mk .mk-pip { fill: color-mix(in srgb, var(--charcoal) 42%, transparent); }

.mk .t-water { --mk-body: color-mix(in srgb, var(--patina) 48%, var(--limestone)); }
.mk .t-brine { --mk-body: color-mix(in srgb, var(--patina) 26%, var(--limestone)); }
.mk .t-ale { --mk-body: color-mix(in srgb, var(--straw) 52%, var(--ember)); }
.mk .t-oil { --mk-body: color-mix(in srgb, var(--straw) 74%, var(--patina)); }

/* A shell is the one mark with ribs, so it needs a stroke where everything else fills. */
.mk .mk-shell { fill: var(--mk-body); }
.mk .t-shell { --mk-body: color-mix(in srgb, var(--limestone) 74%, var(--straw)); }
.mk .mk-rib { stroke: color-mix(in srgb, var(--slate) 52%, transparent); stroke-width: 0.7; fill: none; }

.mk .t-flax { --mk-body: color-mix(in srgb, var(--straw) 66%, var(--limestone)); }
.mk .t-nettle { --mk-body: color-mix(in srgb, var(--patina) 66%, var(--limestone)); }
.mk .t-weed { --mk-body: color-mix(in srgb, var(--patina) 40%, var(--charcoal)); }
.mk .t-gorse { --mk-body: color-mix(in srgb, var(--straw) 72%, var(--patina)); }
.mk .mk-tie { fill: color-mix(in srgb, var(--limestone) 40%, var(--charcoal)); }

.mk .t-pottery { --mk-body: color-mix(in srgb, var(--ember) 54%, var(--straw)); --mk-lit: color-mix(in srgb, var(--ember) 38%, var(--limestone)); }
.mk .mk-mouth { fill: color-mix(in srgb, var(--charcoal) 55%, transparent); }

.mk .mk-heath-stem { fill: none; stroke: color-mix(in srgb, var(--straw) 40%, var(--slate)); stroke-width: 1.1; stroke-linecap: round; }
.mk .mk-heath-bell { fill: color-mix(in srgb, var(--ember) 34%, var(--patina)); }

.mk .mk-reed { fill: none; stroke: color-mix(in srgb, var(--patina) 54%, var(--straw)); stroke-width: 1.2; stroke-linecap: round; }
.mk .mk-reed-head { fill: none; stroke: color-mix(in srgb, var(--straw) 70%, var(--slate)); stroke-width: 1.2; stroke-linecap: round; }

.mk .t-fungus { --mk-body: color-mix(in srgb, var(--limestone) 62%, var(--straw)); }
.mk .t-fungus-cooked { --mk-body: color-mix(in srgb, var(--ember) 46%, var(--straw)); }
.mk .mk-cap, .mk .mk-stem { fill: var(--mk-body); }
.mk .mk-gill { fill: none; stroke: color-mix(in srgb, var(--charcoal) 46%, transparent); stroke-width: 0.9; }

.mk .t-root { --mk-body: color-mix(in srgb, var(--straw) 78%, var(--slate)); --mk-lit: color-mix(in srgb, var(--patina) 82%, var(--slate)); }
.mk .t-root-cooked { --mk-body: color-mix(in srgb, var(--ember) 54%, var(--straw)); --mk-lit: color-mix(in srgb, var(--ember) 44%, var(--charcoal)); }
.mk .t-root-salted { --mk-body: color-mix(in srgb, var(--limestone) 40%, var(--straw)); --mk-lit: color-mix(in srgb, var(--limestone) 46%, var(--patina)); }
.mk .t-fish { --mk-body: color-mix(in srgb, var(--patina) 44%, var(--limestone)); --mk-lit: color-mix(in srgb, var(--patina) 66%, var(--slate)); }
.mk .t-fish-cooked { --mk-body: color-mix(in srgb, var(--ember) 50%, var(--straw)); --mk-lit: color-mix(in srgb, var(--ember) 62%, var(--charcoal)); }
.mk .t-fish-salted { --mk-body: color-mix(in srgb, var(--limestone) 58%, var(--patina)); --mk-lit: color-mix(in srgb, var(--limestone) 38%, var(--slate)); }

/* Roots draw as strokes rather than fills, so they need weight as well as colour — a
   1.1 stroke in a pale tint disappeared entirely on the light panel. */
.mk .mk-root { fill: none; stroke: var(--mk-body); stroke-width: 1.4; stroke-linecap: round; }
.mk .mk-leaf { fill: none; stroke: var(--mk-lit); stroke-width: 1.4; stroke-linecap: round; }

/* ---- the bespoke ones */
.mk .mk-wood { fill: color-mix(in srgb, var(--straw) 52%, var(--limestone)); }
.mk .mk-bark { fill: color-mix(in srgb, var(--straw) 62%, var(--slate)); }
.mk .mk-ring { fill: none; stroke: color-mix(in srgb, var(--straw) 60%, var(--charcoal)); stroke-width: 0.8; opacity: 0.6; }
.mk .mk-clay { fill: color-mix(in srgb, var(--ember) 46%, var(--limestone)); }
.mk .mk-marl { fill: color-mix(in srgb, var(--limestone) 66%, var(--straw)); }
.mk .mk-grit { fill: color-mix(in srgb, var(--charcoal) 40%, var(--limestone)); }
.mk .mk-silt { fill: color-mix(in srgb, var(--ember) 36%, var(--slate)); }
.mk .mk-dent { fill: color-mix(in srgb, var(--ember) 54%, var(--charcoal)); opacity: 0.35; }
.mk .mk-haft { fill: color-mix(in srgb, var(--straw) 50%, var(--charcoal)); }
.mk .mk-cord, .mk .mk-cord-end { fill: none; stroke: color-mix(in srgb, var(--straw) 62%, var(--limestone)); stroke-width: 1.5; stroke-linecap: round; }
.mk .mk-eye { fill: color-mix(in srgb, var(--charcoal) 76%, var(--limestone)); }
.mk .mk-bread { fill: color-mix(in srgb, var(--straw) 58%, var(--ember)); }
.mk .mk-dough { fill: color-mix(in srgb, var(--limestone) 58%, var(--straw)); }
.mk .mk-slash { fill: none; stroke: color-mix(in srgb, var(--charcoal) 42%, transparent); stroke-width: 0.9; stroke-linecap: round; }
/* Dull and slightly green, which is what says *gone off* without drawing anything
   unpleasant. The bloom is lighter than the heap so it reads on both themes. */
.mk .mk-spoiled { fill: color-mix(in srgb, var(--patina) 30%, var(--slate)); }
.mk .mk-bloom { fill: color-mix(in srgb, var(--limestone) 62%, var(--patina)); opacity: 0.75; }
.mk .mk-resin { fill: color-mix(in srgb, var(--straw) 70%, var(--ember)); opacity: 0.92; }
.mk .mk-peat { fill: color-mix(in srgb, var(--ember) 34%, var(--charcoal)); }
.mk .mk-peat-side { fill: color-mix(in srgb, var(--ember) 22%, var(--charcoal)); }
.mk .mk-flint { fill: color-mix(in srgb, var(--limestone) 34%, var(--slate)); }
.mk .mk-flake { fill: color-mix(in srgb, var(--limestone) 54%, var(--slate)); }
.mk .mk-quartz { fill: color-mix(in srgb, var(--limestone) 66%, var(--slate)); }
.mk .mk-quartz-face { fill: color-mix(in srgb, var(--limestone) 78%, var(--patina)); }
.mk .mk-glass { fill: color-mix(in srgb, var(--patina) 34%, var(--limestone)); opacity: 0.72; }
.mk .mk-glass-lit { fill: color-mix(in srgb, var(--limestone) 88%, var(--patina)); opacity: 0.6; }
.mk .mk-sheen { fill: color-mix(in srgb, var(--limestone) 88%, transparent); opacity: 0.5; }
.held { font-family: var(--mono); font-size: var(--t-small); color: var(--known); margin-left: 0.4rem; }
.life { font-family: var(--mono); font-size: var(--t-tiny); color: var(--hot); margin-left: 0.4rem; }
/* Days before it turns, in the same slot and the same size as a blade's remaining
   life. Quiet by default — a date on a root is news, not a fault — and ember only in
   the last day, which is the same rule `Fed for` and `Carrying` already keep.
   `--ink-faint` and not `--ink-soft`: a ledger row stands on the panel, and the soft
   ink is for the darker surfaces (the well, the transcript, the command box). */
.turns { font-family: var(--mono); font-size: var(--t-tiny); color: var(--ink-faint); margin-left: 0.4rem; }
.turns.soon { color: var(--hot); }

/* ------------------------------------------------------------- the detail
   A table can carry four readings; a substance has twenty, and the readings are the
   vocabulary the whole game is reasoned in. Showing four of them was showing a
   fifth of the game. */
/*
  Pinned to the bottom of the panel. In the three-column layout the table above grows
  into every spare pixel, so this margin does nothing; in the stacked one the panel is
  stretched to match its neighbour, and without it the reading floats in the middle of
  a field of nothing.
*/
/*
  It takes whatever the table above it does not, and its own size never moves anything.

  This used to size to its content, between a floor and a share of the panel — so
  pointing at a row rewrote the box, the box changed height, the table above was resized
  to fit, the rows moved, and the row under the cursor became a *different* row, which
  rewrote the box again. Reading down a column set the whole panel oscillating.
*/
.detail {
  border-top: 1px solid var(--rule-strong);
  padding-top: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 1 1 0;
  min-height: 8rem;
  overflow-y: auto;
}
.detail-hint { margin: 0; font-style: italic; color: var(--ink-faint); font-size: var(--t-small); }
/* Named container, so the readings can lay themselves out by the width they actually
   have rather than by the width of the window — this panel is a third of the page on a
   desktop and the whole of it when the columns stack. */
.detail { container: detail / inline-size; }
.detail-name { margin: 0; font-size: var(--t-lead); font-weight: 600; display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5rem; }
.detail-name .count { font-family: var(--mono); font-size: var(--t-small); color: var(--known); font-weight: 400; }
/* Setting a thing down is the reciprocal of picking it up, so it lives beside the count
   of what you are carrying. Quiet, because it is not the reason anybody came here — it
   is the reason somebody with a full back can carry on (docs/attention.md §11). */
.put-down { margin-left: auto; }
.detail-said { margin: 0; font-style: italic; color: var(--ink-soft); font-size: var(--t-small); }
.detail-traits { display: flex; flex-wrap: wrap; gap: 0.25rem 0.4rem; }
.trait {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.03em;
  color: var(--ink-soft);
  border: 1px solid var(--rule);
  padding: 0.1rem 0.4rem;
}
/*
  Two pairs to a row where there is room for them.
  
  A substance has up to thirteen readings and they were one per line, so the reading was
  the tallest thing in the middle column and the one that pushed everything under it away.
  The label is short and the value is shorter — `HARDNESS 7`, `RENEWS 0.2` — so a single
  column was spending two thirds of the width on nothing.
  
  `auto 1fr` twice rather than four equal columns: each label takes what it needs and its
  own value takes the slack, so a long one (`GRINDS INTO`) does not set the width for a
  short one two rows down. Below 30rem it falls back to a single pair, because two pairs
  in a stacked column wrap into something worse than a list.
*/
.detail-readings {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 0.8rem;
  margin: 0;
  line-height: 1.4;
}
@container detail (min-width: 20rem) {
  .detail-readings { grid-template-columns: auto 1fr auto 1fr; column-gap: 0.8rem; }
}
.detail-readings dt {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}
.detail-readings dd {
  margin: 0;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  color: var(--ink);
}
/* An answer nobody here has watched given. Quieter than a reading and not an error —
   there is nothing wrong, there is something to find out (docs/readings.md). */
.detail-readings dd.unread { color: var(--ink-faint); }
.detail-first { margin: 0; font-family: var(--mono); font-size: var(--t-tiny); color: var(--first); }

/* -------------------------------------------------------- what is to hand
   The verbs this place and this moment afford. Zero-argument ones do the thing;
   ones that need naming somebody or something put the line in the box instead, so
   the interface teaches the command rather than replacing it. */
.doings { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.25rem; }
.doing {
  font-family: var(--mono);
  font-size: var(--t-small);
  letter-spacing: 0.04em;
  background: transparent;
  border: 1px solid var(--edge);
  color: var(--ink-soft);
  padding: 0.25rem 0.55rem;
  cursor: pointer;
}
.doing:hover { border-color: var(--known); color: var(--known); }
.doing.teach { border-style: dashed; }
/* A face, a name and what they said, on one line; the busy note and the verbs wrap
   under them. `row wrap` rather than the column this was, because a picture beside a
   name is one thing being named rather than two things stacked. */
.person-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.45rem; }
.person-row > .credit, .person-row > .doings { flex-basis: 100%; }
.person-name { font-size: var(--t-body); }
/* Somebody else out at work, in the register the vitals already say it in about you —
   mono, tiny, soft. Quiet on purpose: it is a fact about where they are standing, not
   a refusal and not a thing they need told about. */
.person-row .credit { color: var(--ink-faint); font-size: var(--t-tiny); font-family: var(--mono); }

/* ---------------------------------------------------------------- the place */
.place-name { font-size: var(--t-lead); margin: 0; text-wrap: balance; font-weight: 600; }
.place-desc { margin: 0; font-style: italic; color: var(--ink-soft); font-size: var(--t-small); }
.land { font-family: var(--mono); font-size: var(--t-tiny); color: var(--ink-soft); letter-spacing: 0.02em; }

.stack { display: flex; flex-direction: column; gap: 0.55rem; }
.stack.tight { gap: 0.35rem; }

/*
  Section headings hold at the top of the place column while it is scrolled past.

  That column is 556 px of window against 1889 px of content — three and a half screens
  of ground, buildings, ways, notes, people and what could go up — and every one of them
  is a heading you have already scrolled off by the time you reach the rows under it.
  Which list a row belongs to is not a thing anybody should have to scroll back to find.

  It hides nothing and decides nothing: the heading was on the page, it stays on the
  page, it simply stops leaving. The `+ *` shadow only appears once the heading has
  something scrolling underneath it. Sticky needs the label to have a background of its
  own, or the rows would read straight through it.
*/
/*
  Every section heading, not only the six that happened to carry `.label`. The chart and
  the chronicle are sections of this column exactly as *Ways on* is, and they were the
  two that did not stick — so scrolling the longest part of the page lost its heading
  while the shorter ones kept theirs.
*/
/* One gutter width, used by the marker column and by the indent of everything a section
   contains. They have to be the same number or the outline does not line up. */
.place-panel { --gutter: 0.9rem; }

.place-panel > details > summary {
  position: sticky;
  top: -0.1rem;
  z-index: 2;
  background: var(--panel);
  padding: 0.25rem 0 0.2rem;
  /* Bottom margin binds the heading to its own rows: see the note below on why it has
     to be smaller than the column's own 0.9rem gap. */
  margin: -0.25rem 0 0.15rem;
}

/*
  Every section in this column folds, and it is the player who folds it.

  Three and a half screens of content in 556 px of window, and the answer is not for the
  page to decide which parts matter — relevance is a rule and the client knows none. It
  is for the reader to put away what they are not using, and for that to be remembered.

  A `<summary>` in the register of the label it replaces: the same mono small-caps, a
  caret that turns, and no browser triangle. Open is the default and nothing on this page
  starts folded.
*/
/*
  ---- One idiom for folding, and rank by depth.

  There were two, and they lined up with nothing. Six sections carried a custom caret
  and no rule; four carried the browser's own triangle and a rule above — and the four
  were not a group: two of them were *nested inside* another section (`6 more, once you
  have the materials`) and two were top-level sections in their own right. So the marks
  that read as the strongest divisions on the page were sitting on the two least
  important things in it, and the actual sections had no divisions at all.

  Measured, before: ten headings in this column, every one of them 12px, the same
  tracking, the same case and the same colour — `rgb(133,144,141)` on all ten. Nothing
  in the panel said what contained what, and two different triangles said "this folds"
  in two different ways. Reported as not being able to tell the sections apart or work
  out what belongs to what, which is exactly what that adds up to.

  So: one caret everywhere, and the *only* thing that changes between headings is how
  deep they are. A section of this column is a direct child of it and looks like a
  heading — brighter, a hairline above, and room before it. A fold *inside* a section is
  indented under the heading it belongs to and stays quiet. Depth is a fact about the
  markup, so this cannot drift the way a hand-applied class did.
*/
/*
  A heading is a row with three parts, and the marker lives in a gutter of its own.
  
  It was a flex line, so the caret sat *in* the text flow and pushed the label right by
  however wide a caret is. Put the marker in a fixed column instead and every label in
  the panel starts on one edge — and the content below can be indented to meet that same
  edge, which is what finally makes containment visible. An outline, rather than eight
  labels and some things underneath them.

  The third column takes whatever a heading carries on its right: the gathering
  day-count, the chronicle's "N new". Both used to need their own flex nudge.
*/
.fold > summary,
.folded > summary {
  cursor: pointer;
  list-style: none;
  display: grid;
  /* marker · label · control · count. Four, because one heading carries both a count
     and the gathering day-select. The count is pinned to the *last* column whatever
     else is on the row, so every figure in the panel shares one right edge — with it in
     source order the one heading that also has a control pushed its number inboard and
     the column of figures broke. Ordered in the markup rather than placed by hand:
     grid auto-placement is *sparse* and its cursor never goes backwards, so a count
     written before a control but asked to sit after it drops the control to a second
     row. Source order is the fix; an explicit `grid-column` on top of the wrong order
     is how that bug hides. */
  grid-template-columns: var(--gutter) 1fr auto auto;
  align-items: baseline;
  column-gap: 0.5rem;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  /* 0.14em over eight stacked labels reads as a readout rather than as headings. */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.fold > summary::-webkit-details-marker,
.folded > summary::-webkit-details-marker { display: none; }
.fold > summary::before,
.folded > summary::before {
  content: "▾";
  font-size: 0.7em;
  line-height: 1;
  /* One step quieter than a section label (`--ink-soft`), so the words lead and the
     marker only says it folds. Not `--rule-strong`: that is a hairline colour, and a
     marker drawn in it disappears — which takes the affordance with it. */
  color: var(--ink-faint);
  transition: transform 120ms ease;
  display: inline-block;
  text-align: center;
}
.fold:not([open]) > summary::before,
.folded:not([open]) > summary::before { transform: rotate(-90deg); }
.fold > summary:hover,
.folded > summary:hover { color: var(--ink); }
.fold > summary:hover::before,
.folded > summary:hover::before { color: var(--ink); }
.fold > summary:focus-visible,
.folded > summary:focus-visible { outline: 2px solid var(--known); outline-offset: 2px; }

/*
  A section of the column: brighter than its contents, with a hairline above it and
  space before it. The rule is on the *second* section onward — a line above the first
  one would sit under the place's own description and read as ending that instead.
*/
.place-panel > details > summary { color: var(--ink-soft); }
.place-panel > details + details {
  border-top: 1px solid var(--rule);
  padding-top: 0.85rem;
}
/* Closed, a heading has nothing under it to bind to, and the margin that does the
   binding becomes a band of dead space between two rules. Put away three sections in a
   row and the column reads as gaps with words in them. */
.place-panel > details:not([open]) > summary { margin-bottom: 0; }

/* A heading binds to what is under it by being nearer to it than to the next heading:
   the column's own gap is 0.9rem, so the margin under a heading (set with the sticky
   rule above) has to be smaller than that, or a label floats equidistant between its
   own rows and the section before it. */

/*
  Everything a section holds sits under its label rather than beside it.

  This is the change that made the column read as structured. A heading and its rows
  shared one left edge, so nothing on the page said what belonged to what — the hairline
  above a heading was the only separation, and eight sections of heterogeneous content
  ran together into one ribbon. Indented by exactly the marker gutter, the caret hangs
  in the margin and label, rows and nested folds all meet on one strong edge.

  Applied to the children rather than as padding on the section, so the hairline between
  sections still spans the full width — an indented rule reads as broken.
*/
.place-panel > details > :not(summary) { margin-left: var(--gutter); }
/*
  A fold inside a section is a drawer, and a drawer has to look like one.

  These were the quietest thing in the column — twelve-pixel uppercase on the panel's
  own background, no border, no weight — and they are the only *pressable* things in
  it. The most interactive element on the page was the least visible one, and nothing
  about a closed one said there was anything behind it.

  So it borrows the house language for a thing you can press (`.doing`: mono, a thin
  square border, no radius) and adds the one thing a drawer needs that a button does
  not — a recessed surface, so a shut one reads as a shut one. `--well` is the token
  that already means *lower than the panel*: the transcript and the command box sit on
  it, and it is a shade darker in both themes rather than a tint that might read as
  selected.
*/
.place-panel details details > summary {
  color: var(--ink-soft);
  background: var(--well);
  border: 1px solid var(--edge);
  padding: 0.45rem 0.6rem;
  margin-top: 0.35rem;
}
/* Open, it stays as the head of the group it belongs to — the same bar, so the eye can
   see where a run of rows starts and where it ends. */
.place-panel details details[open] > summary { margin-bottom: 0.35rem; }
/*
  On the well the marker takes `--ink-soft`, not the `--ink-faint` it has on the panel.
  That is the trap this stylesheet has already been caught by once: the well is a darker
  surface, and faint ink on it is 3.86 in the light theme — under AA. Here the marker
  recedes by *size* instead (0.7em), which costs nothing legible.
*/
.place-panel details details > summary::before { color: var(--ink-soft); }
.place-panel details details > summary:hover,
.place-panel details details > summary:focus-visible { border-color: var(--known); color: var(--known); }
.place-panel details details > summary:hover::before { color: var(--known); }

/* How many are behind it: the whole reason a shut drawer is worth pressing. Tabular so
   two stacked drawers line their figures up, and a step brighter than the words because
   the number is the thing being asked for. */
.fold-count {
  /* Always the last column, so a heading that also carries a control lines its figure
     up with one that does not — an empty track still takes the column gap, which was an
     eight-pixel stagger down the panel. Safe to pin *because* it is last in the markup:
     placing an item before its neighbours is what pushed the day-select on to a second
     row (see the note on sparse auto-placement above). */
  grid-column: 4;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: 0;
}
/* On a bare section heading the figure is informational rather than the reason to
   press, and full ink beside a faint uppercase label shouts. A drawer's own count keeps
   the brighter ink: there, the number *is* the reason to open it. */
.place-panel > details > summary .fold-count { color: var(--ink-soft); }
.place-panel > details > summary .fold-count::before { content: "· "; color: var(--ink-faint); font-weight: 400; }
/* The day-count rides in the third column now, so the flex nudges it used to need are
   gone. Its clicks and keys are still stopped from reaching the fold — a `<select>`
   opens on Space and Enter, which are also how a summary is pressed. */
@media (prefers-reduced-motion: reduce) { .fold > summary::before { transition: none; } }

/*
  A seam is a button with a bar attached, because gathering is the one thing in this
  column somebody does over and over.
*/
.seam { display: grid; grid-template-columns: minmax(5.5rem, auto) 1fr auto; gap: 0.6rem; align-items: center; }
.seam-name {
  font-family: var(--mono);
  font-size: var(--t-body);
  cursor: pointer;
  border: 1px solid var(--edge);
  padding: 0.35rem 0.5rem;
  text-align: left;
  background: transparent;
  color: var(--ink);
}
.seam-name:hover { color: var(--known); border-color: var(--known); background: var(--tab); }
.seam-name:focus-visible { outline: 2px solid var(--known); outline-offset: 2px; }
.seam-track { height: 7px; background: var(--bar); position: relative; overflow: hidden; }
.seam-fill { position: absolute; inset: 0 auto 0 0; background: var(--ink-soft); }
.seam[data-state="thin"] .seam-fill { background: var(--hot); }
.seam[data-state="rich"] .seam-fill { background: var(--known); }
.seam-figures { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--t-tiny); color: var(--ink-faint); white-space: nowrap; }

/* A store row is a seam row with the middle taken out — the ground has a bar because it
   runs down, and a store does not. Same first column so the two sections line up under
   each other, and the count sits where a seam's figures do.

   `.store-row` and not `.held`, which is what it was called for as long as the panel has
   existed. `.held` was already the ledger's inline `×2` badge 386 lines above, so this
   rule won on source order and turned that badge into a grid: block-level, on its own
   line, stretched to 157px. Every carried row in the ledger was 61.9px against a plain
   row's 40.2 — a whole wasted line per thing you are carrying, growing with the game.
   Same fault as `data-folio` meaning both "this is the room" and "this opens the room",
   and as a `text-anchor` attribute losing to a stylesheet rule: one name doing two jobs,
   and the second one silently winning. */
.store-row { display: grid; grid-template-columns: minmax(5.5rem, auto) 1fr; gap: 0.6rem; align-items: center; }
.store-row .seam-figures { text-align: right; }

/* One rhythm for the three list sections.
   `.entry` (the chronicle) already read well — name on the left, a quiet mono qualifier
   on the right, hairline between rows — and `.building` and `.raiseable` each invented
   their own shape, so the column scanned as three unrelated things stacked up. Same grid
   now, so a reader learns the pattern once. The colour bar stays: it is the one place
   state is said without words. */
.building {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.15rem 0.6rem;
  align-items: baseline;
  padding: 0.35rem 0 0.35rem 0.7rem;
  border-left: 3px solid var(--rule-strong);
}
.building[data-state="running"] { border-left-color: var(--known); }
.building[data-state="worn"] { border-left-color: var(--hot); }
/* Tended and unable to start. `--first` rather than `--hot`: it is a thing to see to,
   not a thing gone wrong — the machine is sound and the store is empty. */
.building[data-state="waiting"] { border-left-color: var(--first); }
.building[data-state="unfinished"] { border-left-color: var(--first); }
.building-name { font-size: var(--t-body); font-weight: 600; }
.building-by { color: var(--ink-faint); }
/* Right-hand column, like `.entry-who`: who put it up and what state it is in are both
   qualifiers of the name, and they used to take a line each. `sound` on its own line
   was a bare word nobody could place; beside a name it is plainly a condition. */
.building-state {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-soft);
  text-align: right;
}
.building-improves, .building .doings { grid-column: 1 / -1; }
.building[data-state="worn"] .building-state { color: var(--hot); }
.building[data-state="unfinished"] .building-state { color: var(--first); }
.building[data-state="waiting"] .building-state { color: var(--first); }
.building-improves { font-size: var(--t-small); color: var(--known); }

.ways { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.way { display: flex; gap: 0.4rem; align-items: baseline; font-size: var(--t-small); }
.way button {
  font-family: var(--mono);
  font-size: var(--t-small);
  background: transparent;
  border: 1px solid var(--edge);
  color: var(--ink);
  padding: 0.35rem 0.6rem;
  cursor: pointer;
}
.way button:hover { border-color: var(--known); color: var(--known); background: var(--tab); }
.way.unwalked button { border-style: dashed; border-color: var(--first); color: var(--first); }
.way .credit { color: var(--ink-faint); font-size: var(--t-tiny); font-family: var(--mono); }
/* One group, so a narrow column can move all of them together. Empty for most ways —
   a walked way at the usual price that nobody is credited with says nothing at all —
   and an empty flex box takes no room, so there is nothing to hide. */
.way-notes { display: flex; flex-wrap: wrap; gap: 0.15rem 0.6rem; align-items: baseline; }

/* A caption over a list, for a section holding two kinds of thing.
   Deliberately *not* given the indent a nested `<details>` gets: that indent goes with a
   caret, and a caret means the thing folds. These do not fold, so they take the quiet
   colour and stay on the rows' own left edge — a caption, not a closed door. */
.label.sub { color: var(--ink-faint); margin: 0.55rem 0 0.15rem; }
.label.sub:first-child { margin-top: 0; }

.entry { display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; padding: 0.4rem 0; border-top: 1px solid var(--rule); align-items: baseline; }
.entry:first-child { border-top: none; }
.entry-what { font-size: var(--t-small); }
.entry-what .mark { color: var(--first); }
.entry-who { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--t-tiny); color: var(--ink-faint); white-space: nowrap; }

/*
  Sections that are worth having and not worth staring at. A relief map and a list of
  what other people have discovered are both things somebody consults; neither should
  cost a screenful before the ways on.
*/
/* Everything `.folded` used to say about its own summary is said above, for both kinds
   at once. What is left is the one thing that is genuinely its own: the space under the
   heading before its content starts. */
.folded > *:not(summary) { margin-top: 0.6rem; }

/* What could go up here, and whether it could go up now. */
/* No gap: the rows carry their own padding and a hairline between them, exactly as
   `.entry` and `.building` do. A flex gap *and* padding *and* a rule meant the hairline
   floated in the middle of the space instead of belonging to the rows on either side. */
.raise { display: flex; flex-direction: column; }
.raiseable {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.2rem 0.6rem;
  align-items: baseline;
  padding: 0.45rem 0;
  border-top: 1px solid var(--rule);
}
.raiseable:first-child { border-top: none; }
/* The name leads, and the cost is the quiet part.
   Both used to be `--ink-faint` — subject and qualifier in one colour at two sizes, so
   nothing led and six rows read as mush. Being short of stone is quietened by the *fold*
   these sit behind; once somebody has opened it they mean to read the list, and dimming
   what they came for is the refusal instinct applied one place too far. */
.raiseable .what { font-size: var(--t-body); color: var(--ink); }
.raiseable .cost {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
  grid-column: 1 / -1;
  margin-top: 0.1rem;
}

/*
  What it is for, between the name and what it costs — which is the order somebody reads
  in: what is it, what does it do, what will it take.

  Prose rather than mono, because these are sentences the world wrote and the costs are
  figures. `--ink-soft` and not `--ink-faint`: this is the reason to build the thing, and
  faint is for the qualifier under it. The two were one colour once already and six rows
  read as mush.
*/
.raiseable .does {
  grid-column: 1 / -1;
  font-family: var(--serif);
  font-size: var(--t-small);
  color: var(--ink-soft);
  line-height: 1.4;
}

/*
  A project's instalments, shut by default.

  The column is long enough already, and the stage list is what somebody reads *once*,
  when they are deciding whether to start — not every time they pass the panel. Same
  caret and the same manners as every other fold in this column.
*/
.raiseable .stages { grid-column: 1 / -1; margin-top: 0.2rem; }
/*
  Not the bar a nested *section* wears.

  `.place-panel details details > summary` gives a fold inside a fold the well's
  background and an edge, so it reads as the head of a group — which is right for
  `waiting on materials` and wrong here, where this is one quiet line belonging to one
  row. Inherited it looked like a button offering to do something, in a panel where the
  things that look like buttons put buildings up.
*/
.place-panel .raiseable .stages > summary {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  /* The touch floor, kept as a *target* rather than as a box: the rule this panel keeps
     is that anything a finger is meant to hit is 44px, and a bare line grows by its
     line-height rather than by drawing itself a border. */
  min-height: 44px;
}
.raiseable .stages > summary::-webkit-details-marker { display: none; }
.raiseable .stages > summary::before { content: "▸"; font-size: 0.8em; }
.raiseable .stages[open] > summary::before { content: "▾"; }
.raiseable .stages > summary:hover,
.raiseable .stages > summary:focus-visible { color: var(--known); }
.raiseable .stages ol {
  margin: 0.2rem 0 0.3rem;
  padding: 0 0 0 1.1rem;
  display: grid;
  gap: 0.3rem;
}
.raiseable .stages li { display: grid; gap: 0.05rem; }
.raiseable .stage-what { font-size: var(--t-small); color: var(--ink); }
.raiseable .stage-cost,
.raiseable .stage-more {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
}
.raiseable button {
  font-family: var(--mono);
  font-size: var(--t-small);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.35rem 0.7rem;
  background: var(--tab);
  border: 1px solid var(--known);
  color: var(--known);
  cursor: pointer;
}
.raiseable button:hover { background: var(--tab-strong); }
.raiseable[data-can="yes"] .what { font-weight: 600; }
/*
  Out of reach is quiet, not alarming. Nine lines of ember for things nobody could have
  built yet was the loudest thing on the page, and not one of them was a mistake anybody
  had made — being short of stone is a fact about a satchel, not an error.
*/
/* Whether it can go up today shows as *weight*, never as legibility: the one above is
   semibold, this one is not, and both are readable. */
.raiseable[data-can="no"] .what { font-weight: 400; }
.raiseable .cost { color: var(--ink-faint); }

/* ------------------------------------------------------------------- gate */
.gate {
  position: fixed;
  inset: 0;
  background: var(--ground);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 10;
  overflow-y: auto;
}
.gate form {
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  border-top: 3px solid var(--known);
  padding: clamp(1.6rem, 4vw, 2.6rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 38rem;
}
.gate h1 { margin: 0; font-size: var(--t-hero); line-height: 1.2; font-weight: 600; text-wrap: balance; }
.gate .lede { margin: 0; color: var(--ink-soft); font-size: var(--t-body); }
.gate p.quiet { color: var(--ink-faint); font-size: var(--t-small); font-style: italic; margin: 0; }
.gate .verdict { font-family: var(--mono); font-size: var(--t-small); min-height: 1.3em; margin: 0; }
.gate .verdict.back { color: var(--known); }
.gate .verdict.fresh { color: var(--first); }
.field { display: flex; flex-direction: column; gap: 0.35rem; }
.field span {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.gate input {
  font-family: var(--mono);
  font-size: var(--t-lead);
  background: var(--ground);
  color: var(--ink);
  border: 1px solid var(--edge);
  padding: 0.75rem 0.85rem;
}
.gate input:focus-visible { outline: 2px solid var(--known); outline-offset: -1px; }
.gate button { align-self: flex-start; }

/* ---------------------------------------------------------------- welcome
   Paged, because three short screens are read and one long one is skipped. */
.welcome {
  position: fixed;
  inset: 0;
  background: var(--shade);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 20;
  overflow-y: auto;
}
.welcome-card {
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  border-top: 3px solid var(--first);
  padding: clamp(1.5rem, 3.5vw, 2.4rem);
  max-width: 44rem;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 1.2rem;
}
.leaf { display: flex; flex-direction: column; gap: 0.9rem; }
.leaf h2 { margin: 0; font-size: var(--t-hero); line-height: 1.2; font-weight: 600; text-wrap: balance; }
.points { margin: 0; padding-left: 1.3rem; display: flex; flex-direction: column; gap: 0.7rem; }
.points li { font-size: var(--t-body); color: var(--ink-soft); }
.points li b { color: var(--ink); font-weight: 600; }
.points.steps li { color: var(--ink); }
.on-ledger, .on-bench, .on-place { border-bottom: 2px solid var(--rule-strong); }
.on-bench { border-bottom-color: var(--known); }
.leaf .aside { margin: 0; font-size: var(--t-small); font-style: italic; color: var(--ink-faint); }

.welcome-foot {
  display: flex;
  align-items: center;
  gap: 1rem;
  border-top: 1px solid var(--rule);
  padding-top: 1.1rem;
}
.welcome-buttons { margin-left: auto; display: flex; gap: 0.6rem; }
.dots { display: flex; gap: 0.4rem; }
.dots i { width: 8px; height: 8px; border: 1px solid var(--rule-strong); display: block; }
.dots i.on { background: var(--first); border-color: var(--first); }

/* ------------------------------------------------------------------- the map
   Drawn from the same heights and coordinates the land was generated from, so it is
   a reading of the world rather than a picture of it. Only what somebody has walked
   appears — a map that showed everywhere would hand over the thing exploring exists
   to earn. */
.chart {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
  margin: 0.2rem 0;
  /* Dragging across a map should not select its place names. */
  user-select: none;
}
.chart .way { stroke: var(--rule-strong); stroke-width: 1.4; }
.chart .way.found { stroke: var(--known); stroke-opacity: 0.55; }
.chart .frontier { stroke: var(--first); stroke-width: 1.4; stroke-dasharray: 3 3; }
.chart .place { cursor: pointer; }
/*
  The browser's own focus ring draws a rounded rectangle around a `<g>`'s whole bounding
  box — label and all — which on a map reads as a large blue blob over the country next
  door. The indicator belongs on the mark itself.
*/
.chart .place:focus { outline: none; }
.chart .place circle.mark {
  stroke: var(--ground);
  stroke-width: 1.2;
  transition: stroke 120ms ease;
}
.chart .place circle.halo { fill: none; stroke: none; }
.chart .place:hover circle.mark { stroke: var(--known); stroke-width: 1.6; }
.chart .place:focus-visible circle.halo { stroke: var(--known); stroke-width: 1.2; }
.chart .place.here circle.halo { stroke: var(--known); stroke-width: 1.1; stroke-opacity: 0.75; }
.chart .place text {
  font-family: var(--mono);
  font-size: 3.6px;
  letter-spacing: 0.02em;
  fill: var(--ink-soft);
  text-anchor: middle;
  pointer-events: none;
}
.chart .place.here text { fill: var(--ink); }
/* A place at the end of the world turns its label inwards, or half of it hangs off the
   panel — and the place at the end of the world is the one just walked to. `client.js`,
   `facingOf`. */
.chart .place.to-right text { text-anchor: start; }
.chart .place.to-left text { text-anchor: end; }
.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.9rem;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
}
.chart-key b { font-weight: 400; color: var(--ink-soft); }
.relief { display: inline-block; width: 2.6rem; height: 6px; vertical-align: middle;
  background: linear-gradient(to right, #2b3a44, #6d7f74, #b8b089, #e8dcc0); }

/* ----------------------------------------------------------------- the scene
   The place you stand, drawn. Everything below derives from the palette above —
   nothing in the scene uses a raw colour literal, so both themes come for free and a
   new sprite gets no seventh colour. Two accents, both semantic: fire is the only warm
   thing, and the player's own figure is the only thing in the known colour. */
.scene {
  /* Sky, interpolated by altitude: `--sc-alt` is set inline by scene.js, 0 at the
     sea and 1 at the summits, and the wash cools as the ground climbs. Everything is
     mixed from the five palette stones — slate, limestone, patina, ember, straw — not
     from the theme variables, so a log stays a log's colour at night. */
  --sc-alt: 0.3;
  --sc-sky-low: light-dark(color-mix(in srgb, var(--straw) 24%, var(--limestone)), color-mix(in srgb, var(--straw) 22%, var(--slate-lift)));
  --sc-sky-high: light-dark(color-mix(in srgb, var(--patina) 30%, var(--limestone)), color-mix(in srgb, var(--patina) 26%, var(--charcoal)));
  --sc-haze: light-dark(color-mix(in srgb, var(--limestone) 70%, transparent), color-mix(in srgb, var(--limestone) 14%, transparent));

  /* Ground, one per `land.rock`. */
  --sc-rock-granite: light-dark(color-mix(in srgb, var(--slate) 52%, var(--limestone)), color-mix(in srgb, var(--limestone) 17%, var(--slate)));
  --sc-rock-limestone: light-dark(color-mix(in srgb, var(--straw) 22%, var(--limestone)), color-mix(in srgb, var(--limestone) 27%, var(--slate)));
  --sc-rock-sandstone: light-dark(color-mix(in srgb, var(--ember) 30%, color-mix(in srgb, var(--straw) 32%, var(--limestone))), color-mix(in srgb, var(--ember) 19%, var(--slate)));
  --sc-rock-measures: light-dark(color-mix(in srgb, var(--charcoal) 52%, var(--limestone)), color-mix(in srgb, var(--limestone) 7%, var(--charcoal)));
  /* Added with the rock and missed here, so the black country drew as sandstone for as
     long as it has existed — a fifth of the places in a world. Darker than measures and
     cooler, because coal is brown-black and basalt is grey-black. */
  --sc-rock-basalt: light-dark(color-mix(in srgb, var(--slate) 62%, var(--charcoal)), color-mix(in srgb, var(--limestone) 11%, var(--slate)));

  --sc-water: light-dark(color-mix(in srgb, var(--patina) 52%, var(--limestone)), color-mix(in srgb, var(--patina) 45%, var(--slate)));
  --sc-water-deep: light-dark(color-mix(in srgb, var(--patina) 55%, var(--slate)), color-mix(in srgb, var(--patina) 32%, var(--charcoal)));
  --sc-green: light-dark(color-mix(in srgb, var(--patina) 62%, var(--straw)), color-mix(in srgb, var(--slate) 30%, color-mix(in srgb, var(--patina) 62%, var(--straw))));

  /*
    The heavens. `--sc-night` runs 0 at noon to 1 at midnight and `--sc-dawn` peaks
    where a sun or moon stands at the horizon — both are set on the element by
    `turnHeavens`, once a second, from the same day fraction the vitals count with.
    Mixed from the palette stones like everything else in the scene: night is what
    falls on the place, not a different place.
  */
  --sc-night: 0;
  --sc-dawn: 0;
  --sc-sky-night: color-mix(in srgb, var(--patina) 14%, var(--charcoal));
  --sc-sky-night-foot: color-mix(in srgb, var(--straw) 10%, var(--slate));
  --sc-dawnlight: color-mix(in srgb, var(--ember) 55%, var(--straw));
  /* Paler and cooler than the dawn light: this is distance, not a time of day. */
  --sc-airlight: color-mix(in srgb, var(--limestone) 62%, var(--straw));

  /* The six material fills. The whole vocabulary; a new sprite gets no seventh. */
  --sc-stone: light-dark(color-mix(in srgb, var(--slate) 40%, var(--limestone)), color-mix(in srgb, var(--limestone) 30%, var(--slate)));
  --sc-timber: light-dark(color-mix(in srgb, var(--ember) 48%, var(--slate)), color-mix(in srgb, var(--ember) 38%, var(--charcoal)));
  --sc-thatch: light-dark(color-mix(in srgb, var(--straw) 62%, var(--limestone)), color-mix(in srgb, var(--straw) 42%, var(--slate)));
  --sc-metal: light-dark(color-mix(in srgb, var(--patina) 16%, var(--slate)), color-mix(in srgb, var(--limestone) 16%, var(--charcoal)));
  --sc-smoke: light-dark(color-mix(in srgb, var(--charcoal) 60%, transparent), color-mix(in srgb, var(--limestone) 55%, transparent));
  --sc-ink: var(--charcoal);
  --sc-fire: var(--hot);
  --sc-you: var(--known);

  width: 100%;
  aspect-ratio: 2 / 1;
  flex: none;
  display: block;
  background: var(--sc-sky-low);
  border: 1px solid var(--rule);
  user-select: none;
}

/* Dusk, not mud: the dark theme dims the light falling on the scene, not the things
   standing in it. Sky, rock and water take the night; timber and thatch keep their
   hue and lose a little of it. */

/* ---- sky, ridges, ground, water */
/* Night takes the sky by degrees: each stop mixes toward the night stones by
   `--sc-night`, over the same altitude blend the day always had. The first,
   unmixed declaration is the floor for a browser without color-mix-in-calc. */
.scene .sc-sky-top {
  stop-color: var(--sc-sky-high);
  stop-color: color-mix(in srgb, var(--sc-sky-night) calc(var(--sc-night) * 88%),
    color-mix(in srgb, var(--sc-sky-high) calc(30% + var(--sc-alt) * 70%), var(--sc-sky-low)));
}
.scene .sc-sky-foot {
  stop-color: var(--sc-sky-low);
  stop-color: color-mix(in srgb, var(--sc-sky-night-foot) calc(var(--sc-night) * 82%), var(--sc-sky-low));
}
/* Vapour, not stones. At full haze the overhead ones read as grey lozenges laid on the
   sky rather than as part of it — and there are seven now rather than three, so the
   weight of them matters more than it did. The per-cloud opacity in `scene.js` carries
   the distance falloff on top of this. */
.scene .sc-cloud { fill: var(--sc-haze); opacity: calc(0.52 - var(--sc-night) * 0.34); }
.scene .sc-haze-stop { stop-color: var(--sc-haze); }
/* Ridges darken with the sky they stand against, into silhouettes by midnight.

   Three ranges, and the tonal ladder is the whole of what says which is which — so it
   had better be a ladder. It was not: composited off the rendered pixels, the sky stood
   at **1.05** against the furthest range, 1.06 furthest to far, 1.06 far to near, and
   **1.18 from the sky to the nearest hill in the world**. Five percent a rung is under
   the threshold at which an area reads as a different tone at all, which is why the
   skyline read as one band whatever shape was cut into it.

   The ceiling was the pigment. `--slate` is 0.013 luminance against `--sc-sky-low` at
   0.042, so even a ridge painted *entirely* in slate could only reach 1.43 — the ladder
   had no room left in it at any mix. `--sc-ink` is 0.006, and mixing depth from ink
   while keeping the hue from slate takes the rungs to 1.13 / 1.16 / 1.13 and the whole
   skyline to **1.48**. Check this with a ratio and never by eye: at these levels the
   difference between a step that reads and one that does not is a few units of green.

   The night pole is `--sc-ink` and not `--sc-sky-night`, which is a correction that only
   showed up once the day colours were right. Both the far and near ranges are now darker
   than the night sky, so mixing *toward* it made a hill get lighter as the sun went
   down. Night turns a hill into a silhouette; ink is what a silhouette is made of — and
   the *most* ink goes on the nearest range, not the furthest, or the ladder inverts
   after dark. Measured at full night: 1.04 / 1.14 / 1.07, and every range darker than
   its own daylight self. Ranges converging after dark is what ranges do. */
.scene .sc-ridge.furthest {
  fill: color-mix(in srgb, var(--sc-ink) calc(var(--sc-night) * 30%),
    color-mix(in srgb, var(--sc-ink) 14%,
      color-mix(in srgb, var(--slate) 12%, var(--sc-sky-low))));
}
.scene .sc-ridge.far {
  fill: color-mix(in srgb, var(--sc-ink) calc(var(--sc-night) * 42%),
    color-mix(in srgb, var(--sc-ink) 38%,
      color-mix(in srgb, var(--slate) 24%, var(--sc-sky-low))));
}
.scene .sc-ridge.near {
  fill: color-mix(in srgb, var(--sc-ink) calc(var(--sc-night) * 52%),
    color-mix(in srgb, var(--sc-ink) 66%,
      color-mix(in srgb, var(--slate) 34%, var(--sc-sky-low))));
}
.scene .sc-ground { fill: var(--sc-rock-sandstone); }
.scene .sc-ground.rock-granite { fill: var(--sc-rock-granite); }
.scene .sc-ground.rock-limestone { fill: var(--sc-rock-limestone); }
.scene .sc-ground.rock-sandstone { fill: var(--sc-rock-sandstone); }
.scene .sc-ground.rock-measures { fill: var(--sc-rock-measures); }
.scene .sc-ground.rock-basalt { fill: var(--sc-rock-basalt); }
/* The ground catches the same air the sky does.

   Below the horizon it was one flat fill with three darkening bands at 4, 7 and 12
   percent — the near-is-darker half of aerial perspective and none of the far-is-hazier
   half, so the largest surface in the picture had no distance in it at all. This is the
   sky's own `--sc-airlight` mirrored downward, which is why the two halves now meet
   instead of butting: the same air is in front of both. */
.scene .sc-groundair { pointer-events: none; opacity: calc(0.30 - var(--sc-night) * 0.18); }
.scene .sc-depth { fill: var(--sc-ink); }
.scene .sc-depth.far { opacity: 0.04; }
.scene .sc-depth.mid { opacity: 0.07; }
.scene .sc-depth.near { opacity: 0.12; }
.scene .sc-tuft-mark { stroke: var(--sc-green); stroke-width: 0.7; fill: none; opacity: 0.55; }
.scene .sc-scree { fill: var(--sc-ink); opacity: 0.12; }
/* The near ground. `pointer-events: none` on the group is not a nicety — it lies over
   the bottom of the frame, and anything in front of the picture that could take a tap
   would be taking it away from whatever it covers. */
.scene .sc-foreground { pointer-events: none; }
.scene .sc-fore-mass { fill: var(--sc-ink); opacity: 0.3; }
.scene .sc-fore-tuft { stroke: var(--sc-ink); stroke-width: 0.7; fill: none; opacity: 0.42; }
.scene .sc-fore-rock { fill: var(--sc-ink); opacity: 0.38; }
.scene .sc-sea-top { stop-color: var(--sc-water); }
.scene .sc-sea-foot { stop-color: var(--sc-water-deep); }
.scene .sc-foam { fill: var(--limestone); opacity: 0.5; }
.scene .sc-flow { fill: var(--sc-water); }
.scene .sc-flow-deep { fill: var(--sc-water-deep); opacity: 0.45; }
/* The wet ground either side. Drawn from the same ribbon a little wider, so the margin
   follows every wander of the bank instead of being a second straight line near it. */
.scene .sc-bank { fill: var(--sc-ink); opacity: 0.22; }
.scene .sc-shimmer { fill: var(--limestone); opacity: 0.28; }
/* A river's light is a broken thing, not a painted stripe. */
.scene .sc-glint { fill: var(--limestone); opacity: 0.16; pointer-events: none; }

/* ---- one contact shadow, one outline weight */
/* Every figure, building and seam already had a shadow — at 12% of ink on ground that is
   itself dark, which is very close to none, and it is why everything read as floating a
   little above the picture rather than standing in it. A shadow is the cheapest thing
   there is for saying *this object is on that surface*, and it was being paid for and
   not collected. */
.scene .sc-shadow { fill: var(--sc-ink); opacity: 0.26; }
.scene .l-ink { stroke: var(--sc-ink); stroke-opacity: 0.15; stroke-width: 1; fill: none; stroke-linecap: round; }

/* ---- material fills */
.scene .m-stone { fill: var(--sc-stone); }
.scene .m-timber { fill: var(--sc-timber); }
.scene .m-timber-end { fill: color-mix(in srgb, var(--panel) 35%, var(--sc-timber)); }
.scene .m-thatch { fill: var(--sc-thatch); }
/* Crocks outside the salting house. Fired clay, mixed from the scene's own earth-ish
   token rather than a new brown. */
.scene .m-pot { fill: color-mix(in srgb, var(--sc-timber) 76%, var(--sc-stone)); }
.scene .m-metal { fill: var(--sc-metal); }
.scene .m-green { fill: var(--sc-green); }
/* Woodland, standing behind everything that happens. Duller and colder than the
   undergrowth in front of it, because it is further away — the same reason it shrinks. */
.scene .sc-tree-canopy { fill: var(--sc-green); opacity: 0.55; }
.scene .sc-tree-trunk { fill: var(--sc-ink); opacity: 0.45; }

/* Ground nobody has looked under: a scar and a few clods, the colour of the earth it
   came out of. It says there is something, never what. */
.scene .sc-unturned-scar { fill: var(--sc-ink); opacity: 0.16; }
.scene .sc-unturned ellipse:not(.sc-unturned-scar) { fill: var(--sc-ink); opacity: 0.3; }
.scene .sc-unturned:hover .sc-unturned-scar, .scene .sc-unturned:focus-visible .sc-unturned-scar { opacity: 0.26; }

/* A post with a note on it. Somebody was here and said something. */
.scene .sc-word-post { fill: var(--sc-ink); opacity: 0.7; }
.scene .sc-word-note { fill: var(--known); opacity: 0.85; }
.scene .sc-word[data-colour="bone"] .sc-word-note { fill: var(--who-bone); }
.scene .sc-word[data-colour="moss"] .sc-word-note { fill: var(--who-moss); }
.scene .sc-word[data-colour="teal"] .sc-word-note { fill: var(--who-teal); }
.scene .sc-word[data-colour="amber"] .sc-word-note { fill: var(--who-amber); }
.scene .sc-word[data-colour="rust"] .sc-word-note { fill: var(--who-rust); }
.scene .sc-word[data-colour="rose"] .sc-word-note { fill: var(--who-rose); }
.scene .sc-word[data-colour="plum"] .sc-word-note { fill: var(--who-plum); }
.scene .sc-word[data-colour="sky"] .sc-word-note { fill: var(--who-sky); }

.scene .m-greenline { stroke: var(--sc-green); stroke-width: 0.9; fill: none; stroke-linecap: round; }
/* A sheaf is the same green line as a tuft, tied — a plant grown to be broken down
   rather than burnt. Coal is the one fuel that came out of the ground, so it is the
   one drawn as broken lumps with a dull shine on them. */
.scene .m-tie { stroke: var(--sc-green); stroke-width: 0.7; fill: none; opacity: 0.75; }
.scene .m-coal { fill: #2b2b30; stroke: #14141a; stroke-width: 0.4; }
.scene .m-coal-sheen { stroke: #6f7480; stroke-width: 0.5; fill: none; stroke-linecap: round; opacity: 0.7; }
.scene .m-water { fill: var(--sc-water); }
/* The pond: a puddled clay lip and the stems standing out of it. Both mixed from tokens
   the scene already has — `--sc-timber` for the earth of the bank and `--sc-green` for
   what grows in it — rather than inventing a second brown and a second green, which is
   how a palette stops being one. */
.scene .m-clay { fill: color-mix(in srgb, var(--sc-timber) 62%, var(--sc-ink)); }
.scene .m-stalk { fill: none; stroke: var(--sc-green); stroke-width: 1.1; stroke-linecap: round; }
.scene .m-sheen { fill: var(--limestone); opacity: 0.4; }
.scene .m-soil { fill: color-mix(in srgb, var(--sc-timber) 60%, var(--limestone)); }
.scene .m-grain { fill: color-mix(in srgb, var(--sc-ink) 30%, var(--sc-thatch)); }
.scene .m-mound { fill: var(--sc-stone); opacity: 0.8; }
.scene .m-char { fill: var(--sc-ink); opacity: 0.5; }
.scene .m-hollow { fill: var(--sc-ink); opacity: 0.45; }
.scene .m-hole { fill: var(--sc-ink); opacity: 0.3; }
.scene .sc-scuff { stroke: var(--sc-ink); stroke-opacity: 0.25; stroke-width: 0.8; fill: none; }

/* ---- structure states */
.scene .sc-built[data-state="unfinished"] .sc-sprite { opacity: 0.35; }
.scene .sc-scaffold path { stroke: var(--sc-timber); stroke-width: 1.1; fill: none; stroke-linecap: round; }
.scene .sc-progress-track { fill: var(--sc-ink); opacity: 0.15; }
.scene .sc-progress { fill: var(--first); }
.scene .sc-built[data-state="worn"] .sc-sprite { filter: saturate(0.3); opacity: 0.8; }
.scene .sc-crack { stroke: var(--sc-ink); stroke-opacity: 0.55; stroke-width: 0.9; fill: none; }
.scene .sc-glow { fill: var(--sc-fire); opacity: 0.55; }
.scene .sc-plume circle { fill: var(--sc-smoke); opacity: 0; }
.scene .sc-wear-track { fill: var(--sc-ink); opacity: 0.15; }
.scene .sc-wear { fill: var(--sc-ink); opacity: 0.45; }
.scene .sc-wear.low { fill: var(--sc-fire); opacity: 0.8; }
.scene .sc-improves { fill: var(--sc-green); }
.scene .w-rim { stroke: var(--sc-timber); stroke-width: 2.4; fill: none; }
.scene .w-spokes { stroke: var(--sc-timber); stroke-width: 1.3; fill: none; }
.scene .sc-wheel { transform-box: fill-box; transform-origin: center; }

/* ---- the heavens: the sky as a clock face, where the world keeps real time
   Sun through the first half of the day, moon through the second, stars only in
   real dark (squared, so dusk does not salt the sky), and one nightfall wash over
   the ground — the sky gradient above already takes the night, and darkening it
   twice buried the stars. Positions move by transition between one-second ticks,
   which is what makes a sixty-second demo day glide instead of step. */
.scene .sc-star { fill: var(--limestone); opacity: calc(var(--sc-night) * var(--sc-night) * 0.85); }
/* Scattering, at every hour rather than only at dawn. Dimmed at night but never gone —
   a night sky is still palest at the horizon. */
.scene .sc-air-stop { stop-color: var(--sc-airlight); }
.scene .sc-airlight { opacity: calc(0.42 - var(--sc-night) * 0.26); pointer-events: none; }
.scene .sc-dawn-stop { stop-color: var(--sc-dawnlight); }
.scene .sc-dawnglow { opacity: calc(var(--sc-dawn) * 0.38); pointer-events: none; }
.scene .sc-sun-disc { fill: color-mix(in srgb, var(--straw) 48%, var(--limestone)); }
.scene .sc-sun-halo { fill: color-mix(in srgb, var(--straw) 60%, var(--limestone)); opacity: 0.3; }
.scene .sc-moon-disc { fill: color-mix(in srgb, var(--limestone) 82%, var(--patina)); }
/* Earthshine — the unlit part of the moon, lit by the earth. Faint enough to read as
   the *shape* of a moon that is mostly dark rather than as a second, dimmer moon, and
   it fades out with the night like everything else up there. */
.scene .sc-moon-dark {
  fill: color-mix(in srgb, var(--limestone) 82%, var(--patina));
  opacity: calc(var(--sc-night) * 0.16);
}
.scene .sc-moon-sea { fill: var(--sc-ink); opacity: 0.14; }
.scene .sc-sun, .scene .sc-moon {
  opacity: 0;
  /* Until the ticker has run once, neither body is anywhere and neither may be touched;
     `turnHeavens` hands the pointer to whichever one is actually in the sky. */
  pointer-events: none;
  cursor: pointer;
  transition: transform 1.1s linear, opacity 1.2s ease;
}
/* Under the ground, waiting its turn: it fades, it does not travel. The transition on
   the line above is what makes a once-a-second tick read as a sun crossing a sky, and
   it is what made the setting sun glide back across the whole frame to its rising
   point. The duration stays here rather than being restated in `turnHeavens`, which
   only adds and removes the class. */
.scene .sc-sun.sc-below, .scene .sc-moon.sc-below { transition: opacity 1.2s ease; }
/* Struck: a ring going out, a crown of rays with it on the sun, and the stars catching
   the moon's. Every part is authored invisible and animated up and away again, so with
   motion turned off — where all of this goes — nothing appears rather than a burst
   stopped at full stretch. */
.scene .sc-strike { pointer-events: none; }
.scene .sc-strike .sc-ring, .scene .sc-strike .sc-rays {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}
.scene .sc-strike .sc-ring { fill: none; stroke-width: 1.1; }
.scene .sc-strike.sun .sc-ring { stroke: color-mix(in srgb, var(--straw) 60%, var(--limestone)); }
.scene .sc-strike.moon .sc-ring { stroke: color-mix(in srgb, var(--limestone) 82%, var(--patina)); }
.scene .sc-strike .sc-ray {
  stroke: color-mix(in srgb, var(--straw) 48%, var(--limestone));
  stroke-width: 1.1;
  stroke-linecap: round;
}
@keyframes sc-ring-out {
  0% { opacity: 0.6; transform: scale(0.45); }
  100% { opacity: 0; transform: scale(3.1); }
}
@keyframes sc-rays-out {
  0% { opacity: 0; transform: scale(0.55); }
  25% { opacity: 0.8; }
  100% { opacity: 0; transform: scale(1.4); }
}
@keyframes sc-star-catch {
  0%, 100% { opacity: calc(var(--sc-night) * var(--sc-night) * 0.85); }
  35% { opacity: calc(0.3 + var(--sc-night) * 0.6); }
}
.scene .sc-strike .sc-ring { animation: sc-ring-out 900ms ease-out forwards; }
.scene .sc-strike .sc-ring.late { animation-delay: 240ms; }
.scene .sc-strike .sc-rays { animation: sc-rays-out 900ms ease-out forwards; }
.scene .sc-stars.sc-caught .sc-star { animation: sc-star-catch 1.4s ease-in-out; }
.scene .sc-stars.sc-caught .sc-star:nth-child(3n) { animation-delay: 170ms; }
.scene .sc-stars.sc-caught .sc-star:nth-child(3n + 1) { animation-delay: 340ms; }
/* The depth is a token because it is the one theme value that is a number rather than a
   colour, so `light-dark()` cannot hold it — the dark theme already dims the light on
   the scene, and night on top of dusk needs a lighter hand or the ground goes to pitch.
   Both halves are declared with the palette, where the switch can reach them. */
.scene .sc-nightfall {
  fill: var(--charcoal);
  opacity: calc(var(--sc-night) * var(--sc-nightfall-depth));
  pointer-events: none;
}

/* ---- seams by depletion: the picture and the bar beneath are the same two numbers */
.scene .sc-seam[data-bucket="worked"] .sc-cluster { opacity: 0.85; }
.scene .sc-seam[data-bucket="thin"] .sc-cluster { opacity: 0.6; }

/* ---- figures */
/*
  A figure wears its owner's colour, and the colour is the part that reads at scene
  scale (docs/faces.md §3).

  Measured before it was built: the head is radius 2.7 in a 160-unit viewBox, so at the
  collapsed scene — 384 × 192 — that is 1.2 px per unit and a head **6.5 px across**. A
  face there is three grey pixels, and *draw nothing rather than a placeholder* has been
  the rule since the marks arrived. A colour at 6.5 px is perfectly legible, which is the
  whole reason a colour is the thing that goes here and the face is the thing that goes
  in the panels.

  `--who-teal` is the colour the player's figure has always been drawn in — exactly
  `--known` in the dark theme, and a little darker than it in the light one, where the
  ground measurement above moved every colour down. So a world where nobody chooses looks
  like the game already looked, and reads better on the ground it starts on.
*/
.scene .f-you,
.scene .f-other { fill: var(--who-teal); }
.scene .sc-figure[data-colour="bone"] .f-you,
.scene .sc-figure[data-colour="bone"] .f-other { fill: var(--who-bone); }
.scene .sc-figure[data-colour="moss"] .f-you,
.scene .sc-figure[data-colour="moss"] .f-other { fill: var(--who-moss); }
.scene .sc-figure[data-colour="teal"] .f-you,
.scene .sc-figure[data-colour="teal"] .f-other { fill: var(--who-teal); }
.scene .sc-figure[data-colour="amber"] .f-you,
.scene .sc-figure[data-colour="amber"] .f-other { fill: var(--who-amber); }
.scene .sc-figure[data-colour="rust"] .f-you,
.scene .sc-figure[data-colour="rust"] .f-other { fill: var(--who-rust); }
.scene .sc-figure[data-colour="rose"] .f-you,
.scene .sc-figure[data-colour="rose"] .f-other { fill: var(--who-rose); }
.scene .sc-figure[data-colour="plum"] .f-you,
.scene .sc-figure[data-colour="plum"] .f-other { fill: var(--who-plum); }
.scene .sc-figure[data-colour="sky"] .f-you,
.scene .sc-figure[data-colour="sky"] .f-other { fill: var(--who-sky); }

/*
  The halo, and why every figure gets one now.

  `.f-other` used to be theme ink at 0.7 — dark on light, pale on dark — which reads
  against most ground and not against all of it: the scene's rock runs from pale
  limestone to near-black basalt *inside one theme*, so no fill can clear a ratio against
  every place in the world. The outline is the same device `.sc-exit-name` already uses,
  and it is what makes eight arbitrary colours safe where one careful one was not.

  `paint-order: stroke` so the outline sits behind the fill and the shape keeps its size.
*/
.scene .f-you,
.scene .f-other {
  paint-order: stroke;
  stroke: var(--who-halo);
  stroke-width: 0.6;
  stroke-opacity: 0.55;
}
/* Somebody else is still a shade quieter than you are. Opacity on the group, so the
   halo dims with the figure rather than showing through it. */
.scene .sc-other { opacity: 0.85; }
.scene .sc-name { font-family: var(--mono); font-size: 4.4px; fill: var(--ink-soft); text-anchor: middle; }

/*
  The face, on every figure at every size — and `non-scaling-stroke` is what makes that
  possible rather than a matter of taste.

  This was `stroke-width: 0.34` in user units, and `scene.js` refused to draw a face at
  all below 2.2 px per unit on the grounds that a 6.5 px head cannot hold one. The head
  measurement was right and the diagnosis was wrong: 0.34 user units at the collapsed
  scene is **0.4 CSS px**, so every line of every face was a sub-pixel smear. What looked
  like *too small for a face* was *too thin to draw*.

  `non-scaling-stroke` takes the width in CSS pixels whatever the viewBox does, so one
  declaration is crisp at 6 px and at 16 px and the threshold could go. Strokes rather
  than fills throughout, because a mouth is a line and two of the nine moods are a brow.

  **On the shapes, not on the group, and getting that wrong is silent and ugly.**
  `vector-effect` is **not an inherited property** — `stroke-width` is — so declaring both
  on the `<g>` handed the paths the width and none of the effect. `stroke-width: 1px`
  inside an SVG is *one user unit*, which the viewBox then scales: the picker's mug went
  to **6.00 px against the 1.46 px it was designed at**, and the faces turned into blobs.
  Reported as *the faces are too thick*, and it is the same shape of fault as a
  presentation attribute losing to a stylesheet rule — a declaration that looks right in
  the source and never reaches the thing it names.
*/
.scene .sc-face {
  fill: none;
  stroke: var(--sc-ink);
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.82;
}
.scene .sc-face > * { vector-effect: non-scaling-stroke; stroke-width: 1px; }
.scene .sc-face .eye { stroke-width: 1.4px; }
/* The one filled shape in the set — `awed`'s open mouth — which a stroke-only rule would
   draw as a ring. */
.scene .sc-face ellipse { fill: var(--sc-ink); stroke: none; }

/* ---- exits: solid for walked, dashed for unwalked — the map's own vocabulary */
.scene .sc-arrow { stroke: var(--ink-soft); stroke-width: 1.2; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.scene .sc-exit.unwalked .sc-arrow { stroke: var(--first); stroke-dasharray: 2.4 2; }
/* The label carries the page's own colour as a halo. It folds into the margin and keeps
   off the seams (`scene.js`, MARGIN), but a long place name can still put a letter over
   the ground, and a word half-legible against a bank of material is worse than a word
   standing slightly proud of it. Hover changes the fill alone, so the halo survives. */
.scene .sc-exit-name {
  font-family: var(--mono);
  font-size: 5px;
  fill: var(--ink-soft);
  paint-order: stroke;
  stroke: var(--panel);
  stroke-width: 1.5px;
  stroke-linejoin: round;
}
.scene .sc-exit.unwalked .sc-exit-name { fill: var(--first); }
.scene .sc-exit:hover .sc-arrow { stroke: var(--known); }
.scene .sc-exit:hover .sc-exit-name { fill: var(--known); }

/* ---- the +N chip: overflow folds away rather than shrinking anything */
.scene .sc-more rect { fill: var(--panel); stroke: var(--rule-strong); stroke-width: 0.7; opacity: 0.9; }
.scene .sc-more text { font-family: var(--mono); font-size: 5.5px; fill: var(--ink-soft); text-anchor: middle; }

/* ---- interaction: colour and outline only, never geometry */
.scene .sc-seam, .scene .sc-built[data-do], .scene .sc-other, .scene .sc-exit { cursor: pointer; }
.scene .sc-hit { fill: transparent; stroke: none; }
.scene g:focus { outline: none; }
.scene .sc-seam:hover .sc-hit, .scene .sc-built[data-do]:hover .sc-hit, .scene .sc-other:hover .sc-hit,
.scene .sc-seam:focus-visible .sc-hit, .scene .sc-built:focus-visible .sc-hit,
.scene .sc-other:focus-visible .sc-hit, .scene .sc-exit:focus-visible .sc-hit {
  fill: color-mix(in srgb, var(--known) 9%, transparent);
  stroke: var(--known);
  stroke-width: 0.8;
}

/* ---- motion: three animations, no fourth, all decorative, all optional */
@keyframes sc-plume-rise {
  0% { opacity: 0; transform: translate(0, 2px); }
  18% { opacity: 0.5; }
  100% { opacity: 0; transform: translate(2.5px, -11px); }
}
.scene .sc-plume circle { animation: sc-plume-rise 8s linear infinite; }
.scene .sc-plume circle:nth-child(2) { animation-delay: 2.7s; }
.scene .sc-plume circle:nth-child(3) { animation-delay: 5.4s; }
@keyframes sc-shimmer-drift {
  0% { transform: translateX(0); }
  50% { transform: translateX(26px); }
  100% { transform: translateX(0); }
}
.scene .sc-shimmer { animation: sc-shimmer-drift 12s ease-in-out infinite; }
.scene .sc-shimmer.late { animation-delay: 6s; }
@keyframes sc-turn-wheel { to { transform: rotate(360deg); } }
.scene .sc-built[data-state="running"] .sc-wheel { animation: sc-turn-wheel 6s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .scene * { animation: none !important; transition: none !important; }
  .dayline-fill { transition: none; }
}

/* ------------------------------------------------------------------- the tip
   One tooltip for the whole page, replacing the browser's own. A quiet reading card:
   what the thing is, what state it is in, and — where clicking would compose a
   command — the command itself, in the transcript's own `>` voice, so pointing at a
   thing teaches the line that operates it. Pointer-transparent and read-only: it
   changes nothing and its size moves nothing. */
.tip {
  position: fixed;
  /* Above everything, including the expanded scene (60) and the command line a narrow
     screen fixes to the bottom (70). A tooltip that renders behind the thing it is
     describing is worse than no tooltip: expanded, every reading in the drawing simply
     stopped appearing. */
  z-index: 90;
  pointer-events: none;
  max-width: 18rem;
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  border-left: 3px solid var(--known);
  padding: 0.45rem 0.7rem 0.5rem;
  box-shadow: 0 6px 18px var(--shade);
  display: flex;
  flex-direction: column;
  gap: 0.12rem;
  animation: tip-settle 120ms ease-out;
}
.tip-name { margin: 0; font-size: var(--t-small); font-weight: 600; line-height: 1.3; }
.tip-more { margin: 0; font-family: var(--mono); font-size: var(--t-tiny); color: var(--ink-soft); }
.tip-do { margin: 0; font-family: var(--mono); font-size: var(--t-tiny); color: var(--known); }
@keyframes tip-settle {
  from { opacity: 0; transform: translateY(3px); }
}
.tip.below { animation-name: tip-settle-down; }
@keyframes tip-settle-down {
  from { opacity: 0; transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) { .tip { animation: none; } }

/* ---------------------------------------------------------------- the answer
   The same card, carrying what the world just said instead of what a thing is.

   It exists because the transcript is 72px tall against 1186px of content — the
   smallest scrolling box on the page, and smallest exactly while the first-steps guide
   is up — and a thousand pixels from the gather button whose refusal it prints. A rule
   you cannot see is indistinguishable from a broken page (docs/friction.md §4); so is a
   consequence (docs/attention.md).

   Louder than a reading and quieter than the transcript: the tone rides the edge and
   the headline, and the detail under it is the same dimmed mono the reading uses. This
   does not reopen "refusals are quiet" — that is about *standing* refusals, a list of
   ten things a satchel cannot afford. This is the answer to something just done, which
   is the one thing on the page that has to be seen. */
.tip.answer { border-left-width: 3px; }
.tip.answer .tip-name { font-family: var(--mono); font-size: var(--t-tiny); font-weight: 500; }
.tip.answer.said { border-left-color: var(--rule-strong); }
.tip.answer.good { border-left-color: var(--known); }
.tip.answer.good .tip-name { color: var(--known); }
.tip.answer.bad { border-left-color: var(--hot); }
.tip.answer.bad .tip-name { color: var(--hot); }
.tip.answer.first { border-left-color: var(--first); }
.tip.answer.first .tip-name { color: var(--first); font-size: var(--t-small); }

/* The control wears what came back, so a click that changed nothing still visibly
   landed. An outline rather than a fill, because half of these are groups in a drawing
   and the other half are buttons whose colour already means something. The pulse is
   the arrival; the outline is what remains, so with motion off it is only ever the
   outline and nothing is lost. */
.shell [data-answered] {
  outline: 1.5px solid var(--rule-strong);
  outline-offset: 2px;
  border-radius: 3px;
  animation: answered-land 300ms ease-out;
}
.shell [data-answered="good"] { outline-color: var(--known); }
.shell [data-answered="bad"] { outline-color: var(--hot); }
.shell [data-answered="first"] { outline-color: var(--first); }
@keyframes answered-land {
  from { outline-width: 3.5px; outline-offset: 0; }
}
@media (prefers-reduced-motion: reduce) { .shell [data-answered] { animation: none; } }

/* ------------------------------------------------------- what just changed
   Everything is rewritten at once, so nothing reads as the thing that moved. A wash
   that fades says which number it was.

   No colour semantics and no ember: a satchel emptying and a satchel filling are the
   same event to this — *you did that* — and painting the ordinary spending of days in
   the colour of a refusal is the mistake `toneOf` already had to unlearn once, when it
   drew the opening banner as something the player had got wrong. Direction rides on a
   three-pixel nudge instead, which is free and says nothing it should not.

   A background rather than a colour on the vitals, so `Fed for` and `Carrying` keep
   their own ember when they are low. That state is a warning; this is only news. */
.num[data-moved] { animation: moved-num 1.3s ease-out; border-radius: 3px; }
.seam[data-moved], .building[data-moved] { animation: moved-row 1.3s ease-out; }
@keyframes moved-num {
  from { background: color-mix(in srgb, var(--known) 26%, transparent); transform: translateY(3px); }
  55% { background: color-mix(in srgb, var(--known) 14%, transparent); transform: translateY(0); }
}
.num[data-moved="up"] { animation-name: moved-num-up; }
@keyframes moved-num-up {
  from { background: color-mix(in srgb, var(--known) 26%, transparent); transform: translateY(-3px); }
  55% { background: color-mix(in srgb, var(--known) 14%, transparent); transform: translateY(0); }
}
@keyframes moved-row {
  from { background: color-mix(in srgb, var(--known) 18%, transparent); }
  55% { background: color-mix(in srgb, var(--known) 10%, transparent); }
}
/* With motion off the wash is a state rather than an animation, and the same timer
   takes it away — so it is quieter, never absent. */
@media (prefers-reduced-motion: reduce) {
  .num[data-moved], .seam[data-moved], .building[data-moved] { animation: none; }
  .num[data-moved], .seam[data-moved], .building[data-moved] {
    background: color-mix(in srgb, var(--known) 14%, transparent);
  }
}

/* ------------------------------------------------------------------ folios */
/*
  The reading rooms (docs/meanwhile.md): one overlay vocabulary, many rooms, all of
  them quiet. The row in the header is set in the register of `not you?` — present,
  findable, and never competing with the bench. The card is the welcome card grown
  up: a fixed frame whose body scrolls, so a long chronicle cannot push the close
  button off the page.
*/
.folios { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; }
.folio-btn {
  font-family: var(--mono);
  font-size: var(--t-small);
  letter-spacing: 0.08em;
  background: none;
  border: none;
  border-bottom: 1px dotted var(--edge);
  color: var(--ink-soft);
  cursor: pointer;
  padding: 0 0 0.1rem;
}
.folio-btn:hover { color: var(--known); border-bottom-color: var(--known); }

.folio-shade {
  position: fixed;
  inset: 0;
  background: var(--shade);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 18;
  overflow-y: auto;
}
/*
  A reading room is a document, so it gets a document's measure.

  Measured in the handbook: the text column was 760 px at 14 px, which is **112
  characters a line**, against the 45–75 every typographer since Tschichold has given
  and the 66 most of them recommend. Over three screens of prose that is not a nicety —
  past about 80 characters the eye starts missing the line it was returning to, and the
  handbook is the one page in the game somebody reads rather than scans.

  36 rem is 509 px of text here, which is **75** — the top of the band rather than the
  middle, because the two-column rows in the chronicle and the market (*Firing* … *wren ·
  day 36*) want the span, and because a dialog much narrower than this reads as a prompt
  rather than a page.

  The atlas keeps the old width and is the only one that does: it holds a drawn map,
  which is the one thing here that is not read a line at a time.
*/
.folio-card {
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  border-top: 3px solid var(--known);
  padding: clamp(1.3rem, 3vw, 2.1rem);
  width: 100%;
  max-width: 36rem;
  height: min(46rem, 88vh);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
/* `data-room` rather than `data-folio`: `[data-folio]` is what the click delegate reads
   as *a control that opens this room*, so the shade wearing it made everything inside a
   room reopen it. See `openFolio`. */
.folio-shade[data-room="atlas"] .folio-card { max-width: 52rem; }
/*
  The card takes focus when a room opens, and must not draw a ring for it.

  A ring is how a *control* says the keyboard is on it; the card is a container that
  holds focus so the next Tab starts inside the room and a screen reader reads the title
  it is labelled by. Drawing 832 px of green around the whole dialog says nothing except
  that something has gone wrong — and it appeared only sometimes, because whether
  `:focus-visible` matches a programmatic focus depends on what the last input was, so
  the same room ringed itself when opened one way and not the other. Suppressed here and
  nowhere else: every control inside it keeps the base rule.
*/
.folio-card:focus-visible { outline: none; }
.folio-head { display: flex; align-items: baseline; gap: 0.6rem; }
.folio-title { margin: 0; font-size: var(--t-title); font-weight: 600; line-height: 1.15; flex: 1; }
.folio-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  padding-right: 0.2rem;
}
.folio-sub { margin: 0; font-size: var(--t-small); font-style: italic; color: var(--ink-soft); }
.folio-group { display: flex; flex-direction: column; gap: 0.35rem; }
.folio-list { display: flex; flex-direction: column; gap: 0.35rem; }
.folio-body input[type="search"] {
  font-family: var(--mono);
  font-size: var(--t-small);
  background: var(--ground);
  color: var(--ink);
  border: 1px solid var(--edge);
  padding: 0.5rem 0.65rem;
  min-width: 0;
}
.folio-body .entry .doings { margin: 0; }

/* The field book. The composer sits in the transcript's register — it is the one
   place on the page where the player is the author. */
.note-form { display: flex; flex-direction: column; gap: 0.5rem; }
.note-aboutline { display: flex; align-items: baseline; gap: 0.6rem; }
.note-form textarea {
  font-family: var(--serif);
  font-size: var(--t-body);
  background: var(--well);
  color: var(--ink);
  border: 1px solid var(--edge);
  padding: 0.65rem 0.75rem;
  resize: vertical;
  min-height: 3.5rem;
}
.note-form textarea:focus-visible { outline: 2px solid var(--known); outline-offset: -1px; }
.note-actions { display: flex; gap: 0.6rem; align-items: stretch; }
.note-actions input { flex: 1; }
.note {
  border-left: 3px solid var(--rule-strong);
  padding: 0.1rem 0 0.2rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.note[data-editing="yes"] { border-left-color: var(--first); }
.note-meta { display: flex; align-items: baseline; gap: 0.6rem; }
.note-meta .notyou { margin-left: auto; }
.note-text { margin: 0; font-size: var(--t-body); white-space: pre-wrap; }
/* The mark a written-about thing carries in the ledger. */
.noted { font-size: var(--t-tiny); color: var(--known); margin-left: 0.4rem; }

/* The book of knowing: what the world would say about you, said in one row. */
.standing-card {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.4rem;
  border: 1px dashed var(--rule-strong);
  padding: 0.6rem 0.8rem;
}
.stand { font-family: var(--mono); font-size: var(--t-tiny); color: var(--ink-soft); }
.stand b { color: var(--ink); font-weight: 600; }
/* Working something out unaided is real and is not a world first. Said, and said
   more quietly, so recognising it never starts to compete with being first. */
.stand.quiet { opacity: 0.7; }
.stand.quiet b { color: var(--ink-soft); }

/* The atlas: one card per walked place, distances in the world's own days. */
.atlas-place {
  border-left: 3px solid var(--rule-strong);
  padding: 0.15rem 0 0.25rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.atlas-place.here { border-left-color: var(--known); }
.atlas-head { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.atlas-name { font-size: var(--t-body); font-weight: 600; }
.atlas-head .doings { margin: 0 0 0 auto; }
.atlas-land { margin: 0; font-family: var(--mono); font-size: var(--t-tiny); color: var(--ink-soft); letter-spacing: 0.02em; }
.atlas-row { margin: 0; font-size: var(--t-small); color: var(--ink); }
.atlas-key {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-right: 0.5rem;
}

/* What arrived while you were out: said once, quietly, in the straw of a first. */
.new-say { color: var(--first); font-weight: 600; font-style: normal; }
.entry.unseen .mark { text-shadow: 0 0 6px color-mix(in srgb, var(--first) 55%, transparent); }

/* The handbook: a term and its gloss, the shape `help` already speaks in. */
.hb-said { margin: 0; font-size: var(--t-small); color: var(--ink); }
.hb-verbs {
  display: grid;
  grid-template-columns: minmax(11rem, auto) 1fr;
  gap: 0.15rem 1rem;
  align-items: baseline;
}
.hb-term { font-family: var(--mono); font-size: var(--t-tiny); color: var(--known); white-space: nowrap; }
.hb-gloss { font-size: var(--t-small); color: var(--ink-soft); }

@media (max-width: 900px) {
  .folios { order: 5; width: 100%; }
  .hb-verbs { grid-template-columns: 1fr; gap: 0 0; }
  .hb-term { margin-top: 0.3rem; }
}

/*
  The day-count above the gatherable list, and the number it puts on each button.

  Deliberately plain: it is a setting on an act, not an act, so it should read as the
  quietest thing in the panel. The `×3` on the button is where the weight belongs,
  because that is the part that says what pressing will cost.
*/
.ground-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.days-each {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
}
.days-each select {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  background: var(--panel);
  color: var(--ink-soft);
  border: 1px solid var(--edge);
  padding: 0.15rem 0.3rem;
}
.days-each select:hover { border-color: var(--known); color: var(--known); }
/*
  Spaced by margin, not by the space in the markup: the seam button becomes a flex
  container at phone widths (for the 44px floor), and flex drops whitespace between
  its items — so `Wood ×3` silently became `Wood×3` on exactly the screens this was
  built for.
*/
.seam-days { color: var(--known); font-weight: 600; margin-left: 0.35em; }

/*
  Word left at a place: quoted, and set apart from anything the world says.

  The typographic job is the same as the design's: a note is what a player claims, not
  what the world asserts, so it must not read like the rest of the panel. Italic and
  indented behind a rule does that without a speech bubble.
*/
/* The credit sits beside the quote rather than under it, which is the rhythm every other
   list in this column now keeps: the thing on the left, a quiet mono qualifier on the
   right. It also stops a two-line note taking four lines. */
.left-word {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.15rem 0.7rem;
  align-items: baseline;
  border-left: 2px solid var(--rule-strong);
  padding-left: 0.65rem;
}
.left-said {
  margin: 0;
  font-style: italic;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.left-word .credit {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
  white-space: nowrap;
}

/* ------------------------------------------------------------- on a phone */
/*
  Everything above already stacks at 900px, and it has always stacked correctly — the
  page has never scrolled sideways at 390px. What it did not do was stay *operable*.
  Three panels stacked make a page around 3300px tall, and two things went wrong in it:

  **The command line went below the fold and stayed there.** It sits at the foot of the
  bench, roughly 870px down, so on a phone the one control that can reach any part of
  this game was off screen from the first moment and almost always. The interface is a
  lens over `execute(line)`, and the box is what stops it holding the game back — that
  claim was simply false on a phone. So the box is fixed to the bottom here. It is the
  same principle the three-column layout satisfies by having room; a phone has no room,
  so it has to be said in CSS instead.

  **Everything was too small to hit.** The folio links were 17px tall, the guide's close
  button 17×14. Those are pointer sizes on a surface that has no pointer. 44px is the
  floor throughout — Apple's number, and Android's 48 is close enough that one figure
  serves both.

  Both are width-gated rather than `pointer: coarse`. A narrow window on a laptop is
  rare, gets the roomier version harmlessly, and one query that can actually be tested
  beats two that describe the situation more precisely.
*/
@media (max-width: 900px) {
  /*
    Fixed rather than sticky: sticky would only hold while the bench itself is in view,
    which is the first quarter of the page and not the part where you need it.
  */
  .commandline {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    gap: 0.5rem;
    padding: 0.65rem clamp(0.9rem, 4vw, 1.5rem);
    /* Above the home indicator on a notched phone, flush to the edge on anything else. */
    padding-bottom: calc(0.65rem + env(safe-area-inset-bottom, 0px));
    background: var(--panel);
    border-top: 1px solid var(--rule-strong);
  }
  .commandline input { flex: 1 1 auto; min-width: 0; }
  .commandline .verb.big { flex: none; padding-inline: 1rem; }
  /* The panel must not clip a child that has left it. */
  .bench-panel { overflow: visible; }
  /*
    Room under everything for the bar, so the last line of the last panel is still
    readable, and so focusing an input never scrolls it to where the bar covers it.
  */
  .shell { padding-bottom: 5.5rem; }
  html { scroll-padding-bottom: 6rem; }

  /* ---- 44px everywhere something is meant to be touched. */
  .verb,
  .chip,
  .chipbtn,
  .doing,
  .way button,
  .raiseable button {
    min-height: 44px;
  }
  /*
    Both page buttons drawn small and *touched* big — the ring shrinks, the box does not.

    The 44px floor is about the target, not about the ink, and this file already makes
    that distinction for `.notyou` and the folio words: they keep small type and grow a
    box around themselves. A 44px circle on a 350px header is a lot of furniture for two
    controls that change nothing in the world, so the ring is 34px inside a box that
    stays 44.

    **Drawn inward rather than expanded outward, and that is the whole finding.** The
    first attempt kept the button 34px and pushed a transparent `::after` out to 44,
    which `elementFromPoint` showed failing in three separate ways: a *round* 44px target
    does not cover the corners of its own box; two of them 8px apart with 5px of
    expansion each **overlap by 10px**, so the theme button's right edge answered as `?`;
    and the parts that reached past the header were painted over by `#shell`, which is
    simply the next element in the document. An overflowing hit area is at the mercy of
    everything it overflows into. A box that is honestly 44px has none of those problems,
    and the pseudo-element does the one job it is good at — drawing.
  */
  .theme,
  .help {
    position: relative;
    width: 44px;
    height: 44px;
    border: none;
    background: none;
    border-radius: 0;
    font-size: var(--t-body);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .theme::before,
  .help::before {
    content: "";
    position: absolute;
    inset: 5px;
    border: 1px solid var(--rule-strong);
    border-radius: 50%;
  }
  .help::before { border-color: var(--edge); }
  .theme:hover::before,
  .theme:focus-visible::before,
  .help:hover::before,
  .help:focus-visible::before { border-color: var(--known); }

  /*
    The rooms, folded behind one word.

    Six of them are two rows of a phone spent on a menu, on the surface where the header
    was already the largest thing that is not the game. A sheet rather than a drawer that
    pushes: opening a menu should not move the thing you were reading.

    `header` takes `position: relative` for it, and no `z-index` — so it creates no
    stacking context and the sheet sits in the page's own ladder, under the folio it
    opens (18) and under everything above that.
  */
  header { position: relative; }
  .rooms {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 0.7rem;
    font-family: var(--mono);
    font-size: var(--t-small);
    background: none;
    border: 1px solid var(--rule-strong);
    border-radius: 0.3rem;
    color: var(--ink-soft);
    cursor: pointer;
  }
  .rooms[aria-expanded="true"] { border-color: var(--known); color: var(--known); }
  .folios {
    display: none;
    position: absolute;
    z-index: 16;
    top: 100%;
    right: 0;
    left: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0.3rem 0.6rem;
    background: var(--panel);
    border: 1px solid var(--rule-strong);
    box-shadow: 0 8px 28px var(--shade);
  }
  header.rooms-open .folios { display: flex; }
  /*
    These are underlined words rather than boxes, so growing the box to 44px would leave
    the rule stranded at the bottom of it, an eighth of an inch under the word it
    belongs to. The underline moves onto the text itself and the box grows around it.
  */
  /*
    Colour swatches are a control and were 30px, which is under the floor and was missed
    when they were built — eight of them at 44px do not fit a 350px row, and that is the
    reason rather than an excuse, so they go four to a row instead of eight. The door's
    row wrapped 7 + 1 at this width, which is the lone-item fault the mood grid already
    had; a four-column grid answers both.
  */
  .swatch { width: 2.75rem; height: 2.75rem; }
  .swatch-row,
  .mugs-colours {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    justify-items: center;
    gap: 0.5rem;
  }
  /* The sheet was sized for eight swatches in a row; four to a row does not need it,
     and 20.5rem is wider than a small phone. */
  .mugs { min-width: 0; }

  .notyou,
  .folio-btn,
  .seam-name {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    font-size: var(--t-small);
    border-bottom: none;
    text-decoration: underline dotted var(--rule-strong);
    text-underline-offset: 0.32em;
  }
  .notyou:hover,
  .folio-btn:hover,
  .seam-name:hover {
    text-decoration-color: var(--known);
  }
  /*
    ---- The header, which was taking 44 % of a phone before the game started.

    Measured at 390 × 844: **371 px** of header above the bench — the wordmark on its
    own line, the vitals wrapping three-and-one so that *Carrying* sat alone beside an
    empty half-row, the `?` orphaned onto a third line by `margin-left: auto`, and the
    rooms on a fourth. None of that is information; it is the desktop's arrangement
    surviving into a column a quarter of its width.

    Nothing is removed and nothing is hidden behind a control. The rooms all still
    stand in a row, every vital still shows, and every touch target keeps its 44 px.
    What changes is that each thing is laid out for the space it is actually in.
  */

  /* The wordmark is decoration, and the only place it was load-bearing — the door —
     draws its own. The tab says it, and the player's name is what a returning player
     is actually checking. */
  .wordmark { display: none; }

  /* One row: who you are, and the way back to how this works. The `?` was pushed onto
     a line of its own by `margin-left: auto` on a wrapping flex header; it belongs in
     the corner *of the first row*, which is what a corner control means. */
  header { gap: 0.35rem 1rem; padding-bottom: 0.6rem; }
  .who { flex: 1 1 auto; }
  .help { order: 1; margin-left: auto; align-self: flex-start; }
  .vitals { order: 2; flex-basis: 100%; }
  .folios { order: 3; flex-basis: 100%; }

  /*
    Vitals read across rather than down.

    A label stacked over a 2 rem figure is 59 px, and four of those wrap to 118 px with
    a hole in it. Read across — `DAY 4` — a vital is one line, they wrap as whole pairs,
    and the pair that does not fit goes to a second row that is full rather than empty.
    The figure stays a figure (the size difference against the label is what makes it
    glanceable) and keeps `low`, `busy` and the changed-wash untouched: this moves them,
    it does not restyle them.
  */
  .vitals { margin: 0; gap: 0.1rem 1.1rem; align-items: baseline; }
  .vital { flex-direction: row; align-items: baseline; gap: 0.4rem; min-height: 26px; }
  .vital dd { font-size: 1.25rem; }
  /* Both of these were stacked under the figure and would now break the row. The day's
     bar keeps its meaning lying beside the number; the countdown keeps its own. */
  .vital .dayline { margin-top: 0; width: 2.2rem; align-self: center; }
  .vital .soon { margin-top: 0; }
  /* It wraps to as many lines as it needs, and it is the one vital that can, so it
     takes the whole row when it is showing at all. */
  #v-busy-wrap { flex-wrap: wrap; flex-basis: 100%; }
  .guide-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    margin: -0.6rem -0.6rem -0.6rem 0;
    font-size: var(--t-body);
  }
  /*
    Five things this block was written to catch and did not, found by measuring every
    interactive element in a real 388 px viewport rather than by reading the list back.

    **`.fold > summary` was 26 px.** `.folded` got the rule and `.fold` — its sibling,
    same file, five sections of it — never did. These are the section headings of the
    place column, which is the *navigation* of a 4457 px page on a phone.

    **`.expand` was 24 × 24**, and is not a nicety here: stacked, the drawing is a
    330 × 110 strip, so the control that fills the screen with it is the one that makes
    the picture worth having. It was never in the list at all.

    **The `✎` beside a place's name was 8 px wide.** It is a `.notyou`, so it took the
    44 px *height* from the rule below and no width, because that rule was written for
    words — "not you?", "notes", a seam's name — and a rule that grows a box around text
    gives a single glyph nothing to grow around.

    **A `<th>` was 29 px.** They sort the ledger, and four of the five are narrow numeric
    columns, so they were the smallest deliberate targets on the page.

    **And a `.verb` that is disabled is not a target at all.** The 44 px floor below
    applies to `.verb`, which is right for the row of techniques you can press and wrong
    for the quiet line of the ones you cannot: three items at 44 px each made that line
    137 px tall on a phone — taller than the boxed row it replaced, which was the whole
    point of replacing it.
  */
  .fold > summary,
  .folded > summary { min-height: 44px; display: flex; align-items: center; }
  .expand {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }
  .place-name .notyou { min-width: 44px; justify-content: center; }
  table.ledger thead th { min-height: 44px; height: 44px; }
  .verbs-shut .verb { min-height: 0; }
  /* A ledger row is the most-tapped thing on the page now that a tap reads it. */
  tr.thing td { padding-top: 0.7rem; padding-bottom: 0.7rem; }

  /*
    Under 16px, iOS zooms the whole page when the field takes focus and does not zoom
    back out. The command box was already 16px; the ledger's filter was not, and a
    `select` opens a picker on the same rule.
  */
  #find { font-size: 16px; min-height: 44px; }
  .days-each select { font-size: 16px; min-height: 44px; }


  /*
    A way's note goes under its button rather than into the gutter beside it.

    `explore west` is a 44px dashed box here, and `falls` sat outside it, baseline-aligned
    against a control four times the height of the word — a fact about the ground with
    nothing attaching it to the way it is about. Under the button it reads as that
    button's note, which is the register the *out at work* line already uses under a
    person's name. Grouped in `.way-notes`, so *2 days* and *found by ada* move together
    and still share one line instead of taking one each.

    Only here. Where the column is wide the note sits beside the control and reads as
    attached already, and stacking every way would make the list twice as long for
    nothing (docs/mobile.md §7.3).
  */
  .way { flex-direction: column; align-items: flex-start; gap: 0.1rem; }
  /* Indented to the button's own text, so it reads as hanging off it rather than as the
     next item in the list. The wrapper is not emitted at all when there are no notes,
     so this never adds a line to a way that has nothing to say. */
  .way-notes { padding-left: 0.6rem; }
}

/*
  A tap should say it landed, and say it in this world's colours rather than in the
  browser's default translucent blue rectangle.
*/
@media (hover: none) {
  * { -webkit-tap-highlight-color: color-mix(in srgb, var(--known) 22%, transparent); }
}

[hidden] { display: none !important; }

/* ------------------------------------------------------------ the scene, expanded
   Opt-in and temporary, and the default layout is untouched. The bench is the widest
   thing on the page for a recorded reason — three columns of equal weight read as a
   dashboard where nothing said what to touch first — so this borrows the screen while
   somebody is looking at it and gives it straight back.

   The command line comes with it. Anything the interface has no picture for has to stay
   playable, which is the rule that keeps the drawing a lens rather than a second
   implementation of the game. */

.expand {
  background: none; border: 1px solid var(--edge); color: var(--ink-soft);
  border-radius: 0.3rem; cursor: pointer; font-size: 0.9rem; line-height: 1;
  padding: 0.25rem 0.45rem; margin-left: auto;
}
.expand:hover, .expand:focus-visible { color: var(--ink); border-color: var(--accent); }

body.scene-full { overflow: hidden; }
body.scene-full .columns > .panel:not(.place-panel),
body.scene-full .place-panel > *:not(.scene):not(.panel-head):not(.scene-dock) { display: none; }

body.scene-full .place-panel {
  position: fixed; inset: 0; z-index: 60; margin: 0; border: 0; border-radius: 0;
  /*
     And no padding, which was the *second* source of the same edge artifact.

     `margin`, `border` and `border-radius` were all reset here and the panel's own
     16px of horizontal padding was not — so the drawing sat 16px in from each side and
     that strip was painted by the bar gradient rather than by the picture. Identical
     symptom to the quantised width (`scene.js`): a vertical seam with the ridges
     stopping against it, and a strip whose colour is an approximation of the one beside
     it. Both had to go for the edge to be clean.

     Safe because nothing here relies on it: the heading and the dock each carry their
     own padding, and the drawing is the only other child.
  */
  padding: 0;
  /* The bars beside a 2:1 drawing in a wider frame are more of the same world, not a
     border round it. Sky above the horizon and ground below, at the fraction the drawing
     itself puts the horizon at — the three colours are read back off the drawing by
     `drawScene`, so there is no second statement of a gradient that already mixes in the
     night and the altitude. Falls back to the page while nothing has been drawn yet. */
  background: linear-gradient(
    to bottom,
    var(--sc-edge-sky, var(--bg)) 0%,
    var(--sc-edge-air, var(--bg)) var(--sc-edge-at, 57%),
    /* The ground carries its haze into the bars too, or the bar meets a pale skyline
       with an unhazed colour and puts a step at the very join it exists to hide. */
    color-mix(in srgb, var(--sc-edge-haze, transparent) 30%, var(--sc-edge-ground, var(--bg)))
      var(--sc-edge-at, 57%),
    var(--sc-edge-ground, var(--bg)) var(--sc-edge-clear, 82%),
    var(--sc-edge-ground, var(--bg)) 100%
  );
  display: flex; flex-direction: column;
}
body.scene-full .place-panel .panel-head {
  padding: 0.6rem 1rem; flex: 0 0 auto;
  /* `.panel-head` stacks, which put the collapse button on a line of its own. */
  flex-direction: row; align-items: center; gap: 1rem;
}
body.scene-full .scene {
  flex: 1 1 auto; width: 100%; height: auto; min-height: 0; max-height: none;
}

/* It fits expanded the same way it fits small — `xMidYMid meet`, nothing switched.

   This used to `slice` and anchor to the bottom, to fill the frame instead of
   letterboxing it, on the stated grounds that what a short frame gives up is *empty
   sky*. That was true when it was written and stopped being true the day the sky got a
   sun and a moon. `slice` scales to cover, so a frame wider than 2:1 loses the top, and
   full screen is much wider than 2:1: measured, 30 units of sky gone at 1920 × 1080,
   54 at 3440 × 1440, 63 at 2560 × 1080 — against a sun whose arc peaks at y = 24. The
   sun and the moon were absent for most of the night and most of the day.

   A wide screen gets bars at the sides now. That is the honest trade: the drawing is
   2:1 and a screen is not, and the alternative is cutting the top off a picture whose
   top half is the part that moves. */

/* Below the header rather than over it: the day, the days spent and how long the food
   lasts are worth keeping on screen, and "Where you stand" landing on top of the
   player's own name was the overlay covering the one line that says who they are. */
body.scene-full .place-panel { top: var(--header-height, 5.2rem); }

body.scene-full .scene-dock {
  flex: 0 0 auto; padding: 0.6rem 1rem 1rem; border-top: 1px solid var(--rule);
  background: var(--bg);
}
.scene-dock { display: none; }
body.scene-full .scene-dock { display: block; }
body.scene-full .scene-dock .commandline { margin: 0; }

/*
   An overlay opened *from* this mode has to be over it.

   The expanded scene is `position: fixed; z-index: 60` and the two overlays are 18 and
   20, so expanding the scene and then opening a reading room drew the room **behind the
   drawing** — the page went dark at the edges and nothing else happened. Same for `?`.
   Both are things somebody has just this moment asked for, and the one rule an overlay
   has is that it is on top.

   Raised only in this mode rather than globally, because the ordinary ladder is
   deliberate: under 900px the command line is fixed at 40, *above* the folio, so a room
   never takes away the one control everything is a shortcut for. Lifting the folio over
   60 everywhere would have quietly taken that away. Still under `.tip` at 90, which
   says so where it is defined. */
body.scene-full .folio-shade,
body.scene-full .welcome { z-index: 80; }

/*
   The same fault, one control over — and the header is where it was always going to bite.

   Expanded, the header stays on screen and the panel is fixed *below* it, so the mugshot
   is still there to press and the sheet it drops still opened at 40 underneath a drawing
   at 60. Measured: the sheet ran 85 to 418 and the panel began at 92, so **seven pixels
   of it were visible and the other 326 were behind the picture** — and `elementFromPoint`
   at the middle of the sheet returned the scene, which is the only test that settles it.

   65 rather than 80, and the number is the argument. It has to beat the panel at 60. It
   must *not* beat the command line at 70, because under 900px that is fixed to the bottom
   of the screen and a tall sheet can reach it — and the one rule this ladder keeps is
   that nothing takes away the control everything else is a shortcut for. A reading room
   at 80 still wins, which is right: a room is a full overlay and this is a popover
   hanging off a header.

   So the whole ladder in this mode reads: drawing 60, this 65, command line 70, rooms 80,
   the tip card 90.
*/
body.scene-full .mugs { z-index: 65; }

/* Under 900px the command line is *already* fixed to the bottom of the screen, which is
   exactly where expanded mode wants it — so it stays put and is not moved into the dock
   at all (see `client.js`). All it needs is to sit above the expanded panel rather than
   behind it. */
@media (max-width: 900px) {
  body.scene-full .scene-dock { display: none; }
  body.scene-full .commandline { z-index: 70; }
  body.scene-full .place-panel { bottom: 4.4rem; }
}

/* --------------------------------------- an overlay stops where the bar begins

   The ladder above is deliberate and stays: under 900px the command line is fixed at 40,
   *above* the folio at 18 and the welcome at 20, so a reading room never takes away the
   one control everything else is a shortcut for.

   But "above" must not mean "over the top of". `.folio-card` is `88vh` and centred, so
   its bottom lands at 0.94vh and the clearance under it is 0.06vh — about 51px on a
   844px phone, against a bar that is 59px plain and ~93px once a notched phone's
   safe-area padding is added. So the last 8–42px of every reading room sat under the bar,
   and `.folio-body` scrolls *inside* the card, which means content could be scrolled into
   a region that cannot be seen. The same fault as the ledger's put-down button, one
   overlay up.

   The welcome is worse rather than better: `.welcome-card` has no height cap at all, and
   its NEXT and BEGIN buttons are the last thing in it.

   4.4rem is the same reservation `body.scene-full .place-panel` above already makes for
   the same bar, and it is one number for that reason. */
@media (max-width: 900px) {
  .folio-shade,
  .welcome {
    padding-bottom: calc(1.5rem + 4.4rem + env(safe-area-inset-bottom, 0px));
  }
  /* Capped as well as padded: the shade centres its card, and a card taller than what is
     left would simply overflow back under the bar. */
  .folio-card {
    height: min(46rem, calc(88vh - 4.4rem - env(safe-area-inset-bottom, 0px)));
  }
}

/* A card taller than the shade must overflow downwards, never upwards.

   `align-items: center` on a container that scrolls is the classic one: when the content
   is taller than the box, centring pushes the top out past the scroll origin and there is
   no way to scroll back to it. It bites in landscape on a phone — 390px of height against
   a welcome card that has no height cap at all — where the thing put out of reach is the
   first line of the opening. `safe` keeps the centring whenever it fits and falls back to
   flex-start when it does not, and an older engine that does not know the word ignores
   the whole declaration and keeps today's behaviour. */
.folio-shade,
.welcome { align-items: safe center; }

/* ------------------------------------------------- a short window, three columns
   Wide enough for three columns and not tall enough to hold them — and it took the
   command box with it.

   Measured on 1512 × 688: the bench's content came to 577 px against 511 px of panel,
   so **46 px of the command line was outside a panel that is `overflow: hidden`** — the
   one control everything else on the page is a shortcut for, cut off with no scrollbar
   to say so. Under the eager clock the same window cleared it by three pixels, so the
   bench fitted by a hair and stopped fitting the moment anything above it grew: a busy
   header, a longer name, one more line of vitals (docs/attention.md §6).

   Below 1420 px **wide, or 720 px tall**, none of this applies: the columns have stacked
   and the page simply scrolls, which is the honest answer either way. So what is left
   here is one tier of pure tightening, for windows short enough to be tight and tall
   enough to still divide a fixed height.

   **Last in the file on purpose.** The first attempt sat beside the other responsive
   blocks near the top, ahead of `.panel`, `.guide` and `.well` — same specificity,
   earlier in source, so every line of it lost and the only rule that appeared to work
   was the one nothing else contradicted. A tightening that must beat the defaults has
   to come after them.

   The frame gives first: gaps, padding, and the two floors that cannot otherwise move.
   At a normal height it changes nothing, and it is enough on every window that still
   has three columns to divide.

   **There used to be a second tier here, and it is gone.** Past what tightening could do
   it let the *bench* scroll, with the command line `sticky; bottom: 0` so the one control
   everything else is a shortcut for stayed reachable. It was right about the danger and
   wrong about the remedy: the transcript always yielded before the bench ever needed to
   scroll (54 px, nine tenths of a line at 1512 × 621), and raising its floor so that the
   bench *did* scroll put the sticky command line over the transcript's own foot — 65 px
   covered, including the newest line. A scrollbar useless until used and harmful once
   used. A short window stacks now, exactly as a narrow one does, and the whole tier
   dissolved into the `max-height: 720px` arm of the stacked block far above
   (docs/attention.md §12.2). */
/* One tier, and it is bounded on both sides. Above 900 px there is room to spare;
   at 720 px and below the columns have stacked and the page scrolls, so tightening a
   fixed height it no longer has would do nothing. */
@media (min-width: 1421px) and (max-height: 900px) and (min-height: 721px) {
  .panel { gap: 0.5rem; padding: 0.8rem 1rem 0.9rem; }
  .guide { padding: 0.35rem 0.7rem 0.45rem; }
  .well { min-height: 4rem; padding: 0.65rem; }
  .verbs { gap: 0.4rem; }
  .bench-panel > .transcript { min-height: 3.2rem; }
}

/* ------------------------------------------------- the transcript, stacked
   Stacked, the transcript has to be *given* a height, because there is no longer a
   remainder for it to take.

   `flex: 1 1 0` divides what is left over, and once the shell is `height: auto` there
   is nothing left over to divide: the bench is exactly its own `min-height`, this box's
   basis is zero, and it lands on its floor and stays there. Measured at 1100 × 800 and
   at 756 × 900 it was **72 px either way — 1.6 lines** — and, worse than the number,
   *the window height made no difference at all*. Nine hundred pixels of screen bought
   the world's voice nothing, and two browsers side by side on a laptop is exactly this
   layout.

   12 rem, six and a half lines — the floor the base `.transcript` rule already authored
   before `.bench-panel > .transcript` overrode it — and the bench grows to fit. That
   costs nothing here: this is the layout where the panel does not clip (`max-height:
   none; overflow: visible`) and the page scrolls as a whole, which is the honest answer
   once the columns have stacked.

   **Last in the file, beside the short-window tiers, for the reason written up there.**
   It was first written inside the `max-width: 1420px` block near the top, where it lost
   to `.bench-panel > .transcript` on source order and changed nothing at all — same
   specificity, earlier in the file. Measured before and after and it was 72 px both
   times, which is the only reason it was caught. A rule that must beat the defaults has
   to come after them; this file has now taught that twice.

   The query matches the stacked block's exactly — narrow **or** short. Keep them in
   step: a stacked layout whose transcript floor did not follow it is the 72 px fault
   again, in the other axis.

   ---- And it is not alone. Four more rules were written into the stacked block near the
   top of this file and **none of them has ever taken effect**, each losing on source
   order to the base rule it was written to override:

   | intended | beaten by | what that did |
   |---|---|---|
   | `.panel { overflow: visible }` | `.panel { overflow-y: auto }` (~429) | every panel stayed a scroll container |
   | `.panel { max-height: none }` | — (no base `max-height`) | harmless, but it was never the lever |
   | `.ledger-panel { overflow: visible }` | `.ledger-panel { overflow: hidden }` (~753) | the ledger clipped instead of growing |
   | `.ledger-scroll { height: auto }` | `.ledger-scroll { height: … }` (~755) | the table kept a divided height with nothing to divide |
   | `.detail { flex: none; overflow: visible }` | `.detail { flex: 1; overflow: auto }` (~825) | the reading kept its own scrollbar inside a page that scrolls |

   The first one is the one that bites hardest, and it is invisible until you look for
   it: **a `.panel` that is still a scroll container is the containing block for
   `position: sticky`**, so every section label in *Where you stand* stopped sticking the
   moment the columns stacked. Measured on a phone, the *In the ground* label sat at
   y = −190 px with its own rows still on screen at −130 to +118. The labels exist
   because that column is 556 px of window against 1889 px of content and which list a
   row belongs to should not want scrolling back for — which is exactly the question a
   phone asks hardest, and the one layout where the answer was switched off.

   So they live here, after everything they override, and the comment above them says
   what the stacked layout is *for*: panels grow downward and the page scrolls, instead
   of dividing a fixed height that no longer exists. */
@media (max-width: 1420px), (max-height: 720px) {
  .bench-panel > .transcript { min-height: 12rem; }
  .panel { max-height: none; overflow: visible; }
  /* The table's top still cannot move — the reading sits under it, and a reading that
     resizes the table above it puts the pointer in a control loop (docs/decisions.md). */
  .ledger-panel { overflow: visible; }
  .ledger-scroll { height: auto; max-height: 32rem; }
  .detail { flex: none; overflow: visible; }
}

/* ------------------------------------------------------ one thing, in full

   The reading under the ledger is a fixed height on purpose — see `.ledger-scroll` —
   so everything it knows arrives as one grid with no room to say which part is which.
   This is the same content with the room to group it: what a thing *is*, how it
   *behaves*, and what it *becomes*, which is the distinction docs/readings.md is about. */
.detail-open {
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: inherit; letter-spacing: inherit;
  border-bottom: 1px dotted var(--ink-faint);
}
.detail-open:hover, .detail-open:focus-visible { color: var(--known); border-bottom-color: var(--known); }
.detail-where { margin: 0.2rem 0 0; font-size: 0.78rem; color: var(--ink-soft); font-style: italic; }

.thing-head { display: flex; gap: 0.8rem; align-items: flex-start; margin-bottom: 1rem; }
.thing-mk { flex: none; width: 2.4rem; height: 2.4rem; }
.thing-mk svg { width: 100%; height: 100%; }
.thing-said { margin: 0; font-family: var(--serif); font-size: 1rem; line-height: 1.5; }
.thing-note { margin: 0.35rem 0 0; font-size: 0.82rem; color: var(--ink-soft); }
.thing-part { margin: 0 0 1rem; }
.thing-part h4 {
  margin: 0 0 0.45rem; font-size: 0.7rem; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600;
  border-top: 1px solid var(--rule); padding-top: 0.5rem;
}
.thing-rows { display: grid; grid-template-columns: auto 1fr; gap: 0.2rem 0.9rem; margin: 0; }
.thing-rows dt { font-size: 0.74rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
.thing-rows dd { margin: 0; font-family: var(--mono); font-size: 0.82rem; }
.thing-aside { margin: 0.5rem 0 0; font-size: 0.76rem; color: var(--ink-soft); font-style: italic; }
.thing-where { margin: 0; font-family: var(--mono); font-size: 0.82rem; }
.thing-first { margin: 0.6rem 0 0; font-size: 0.8rem; color: var(--first); }

/* ----------------------------------------------- the same chart, with room

   `.chart` in the place column is a thumbnail: 556px of window against 1889px of
   content, folded, and sharing that column with nine other sections. Right for
   glancing, wrong for reading — at sixty-six places the labels collide and the shape,
   which is the only thing a map is for, is the thing you cannot make out.

   In the atlas it gets the full width of a dialog and room to breathe under it. Same
   drawing, same code, one implementation. */
.atlas-chart {
  margin: 0.2rem 0 0;
  /* Bounded, because the folio body scrolls and a map that fills it would push the
     list — which holds every detail the map deliberately leaves out — off the bottom. */
  max-height: min(26rem, 52vh);
}
.atlas-key { margin-bottom: 0.8rem; }

/* Sixty-six labels is 219 overlapping pairs, and an SVG scales so a bigger drawing does
   not help — measured identical in the column and in the atlas. Only the marks that
   carry the shape are labelled without being asked: where you are, and anywhere anybody
   has built. The rest name themselves when reached for, into the strip below, which is a
   fixed line so that naming one never moves the thing being pointed at. */
.chart .place text { opacity: 0; transition: opacity 0.12s ease; }
.chart .place.anchored text,
.chart .place.named text,
.chart .place:hover text,
.chart .place:focus-visible text { opacity: 1; }
.chart .place.named circle.halo { stroke: var(--known); stroke-width: 1.2; }
.chart-said {
  margin: 0.1rem 0 0.35rem;
  min-height: 1.1em;
  font-size: 0.76rem;
  color: var(--ink-soft);
  font-style: italic;
}

/* --------------------------------------------------- rock a body of ore went through

   The `here` line has said this in words since the veining bands arrived, and it is the
   most useful thing a prospector reads. Looking at rock is the act being modelled, so a
   picture is where it belongs.

   Ochre, and never green: green is copper, and 59% of veined ground carries nothing at
   all. The drawing may say *something has been through this rock*; it may not say what,
   or the map becomes a treasure map and prospecting stops being a decision. */
.sc-vein-body { fill: color-mix(in srgb, var(--sc-stone) 78%, var(--charcoal)); }
.sc-vein-seam {
  fill: none;
  stroke: color-mix(in srgb, var(--ember) 58%, var(--straw));
  stroke-width: 0.9;
  stroke-linecap: round;
  opacity: 0.85;
}

/* ------------------------------------------------------- the light of a country

   `warmth` moves the treeline 457 m to 768 m inside one world, and the drawing has been
   showing the consequence — trees, or bare scree — without ever showing the cause. Thin
   and blue in the cold north, low and golden in the warm south, and nothing at all in the
   temperate middle, which is most of the world.

   Weather in the sense docs/clock.md means it: nothing reads it, nothing is gated on it,
   no verb touches it. It is there so that a bare hillside at 500 m stops being a puzzle
   and becomes a place with cold light on it. */
.sc-light { pointer-events: none; }
.sc-light.cold { fill: color-mix(in srgb, var(--patina) 30%, var(--slate)); opacity: 0.16; }
.sc-light.warm { fill: color-mix(in srgb, var(--straw) 62%, var(--ember)); opacity: 0.12; }


/* ------------------------------------------------------- a rule you worked out
   The knowing room said a name and how it was come by. It says the rule now, to the
   one person entitled to read it — see `knownRow` for why that gives nothing away.

   Laid out as the sentence it is: the technique, then what it asks for, then what it
   gives. The clauses are mono because they are the rule's own vocabulary and the player
   types in that vocabulary; `plus` is prose between them, because that word is the
   game's and not the rule's. */
.known-rule .rule {
  grid-column: 1 / -1;
  display: grid;
  gap: 0.15rem;
  margin-top: 0.3rem;
  padding-left: 0.7rem;
  border-left: 2px solid var(--rule);
}
.known-rule .rule-how {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--known);
  text-transform: lowercase;
}
.known-rule .rule-wants {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-soft);
  line-height: 1.5;
}
/* The one word in the line that is the game speaking rather than the rule. */
.known-rule .rule-wants i { font-family: var(--serif); font-style: italic; color: var(--ink-faint); }
.known-rule .rule-gives {
  font-family: var(--mono);
  font-size: var(--t-tiny);
  color: var(--ink-faint);
}


/* ------------------------------------------------------------------ emblems
   The same sprite the scene draws, beside the name that names it — so the shapes in the
   picture above become nameable rather than being learned by putting each one up. See
   `emblemFor`; it leaks nothing, because `buildable` names every structure to everybody
   already.

   Sized in `em` so it follows the type, and `vertical-align` rather than a flex box
   because these sit inside a line of text and a line is what they belong to. The scene's
   own drawings are its ground; here there is none, so nothing is anchored to a baseline
   but the text itself. */
.emblem {
  /*
     It wears `.scene` to inherit the material palette and every `.scene .m-*` fill, which
     is sixty selectors nobody should have to duplicate — but `.scene` is also the drawing
     *frame*: a 2:1 aspect, the sky as a background and a rule around it. An emblem is a
     fragment of that drawing, not a framed one, so the frame is put back off. Without
     this each building was a filled olive rectangle with a few marks along the bottom.
  */
  background: none;
  border: none;
  aspect-ratio: auto;
  /* `.scene` is `display: block`, which put every building on a line of its own above
     its own name. These belong *in* the line, beside the word they illustrate. */
  display: inline-block;
  width: 1.9em;
  height: 1.9em;
  vertical-align: -0.55em;
  margin-right: 0.3em;
  flex: none;
  /* The scene sets this inline per place, and there is no place here — the mid altitude
     is the one that makes stone read as stone rather than as a summit. */
  --sc-alt: 0.3;
}
/* A thing that cannot go up yet is quiet, and its picture should be as quiet as its
   name — the same instinct the raise rows already follow, applied to the drawing. */
.raiseable[data-can="no"] .emblem { opacity: 0.55; }
