/* 1bit — the whole stylesheet.
 *
 * docs/06-website.md ▸ Part 2 is the source of truth. The short version:
 *
 *   Two colours. One unit. Bitmap type at whole multiples of its own cell.
 *   Every shade is a checkerboard of the two colours, never a grey.
 *   A radius only ever makes a circle. A shadow never has a blur.
 *   Nothing moves except a marquee, a cursor, and the invert on touch.
 *
 * The page is still mostly paper, and the second draft added the thing the
 * first one was missing: ink that is not type. A page whose every device is a
 * hairline reads as a wireframe of itself, so the argued claims sit in filled
 * slabs, every picture is framed and captioned in an ink strip, and each
 * section is announced by a kicker above its title. Same two colours, same
 * grid, more of the paper spent.
 *
 * `build.py check` enforces the mechanical half and fails the build on a grey.
 */

/* ── the faces ──────────────────────────────────────────────────────────
 * Subset to what the site actually sets, which is a couple of kilobytes.
 * `font-display: block` rather than `swap`: a bitmap page briefly set in
 * Helvetica reflows every measure, which is a worse first impression than a
 * beat of blank paper.
 */
@font-face {
  font-family: "unscii";
  src: url("../fonts/unscii-16.woff2") format("woff2");
  font-weight: normal; font-style: normal; font-display: block;
}

/* ── the constitution ───────────────────────────────────────────────────*/
:root {
  --ink: #000000;
  --paper: #ffffff;

  /* The virtual pixel. Every border, gap, offset and size below is a whole
     multiple of it, and there are no fractions anywhere in this file. */
  --px: 2px;

  /* Type. 16px is unscii-16's native cell, and every size on this page is a
     whole multiple of it except one: the body, which is one and a half.

     That exception was refused for two drafts on the rule that a bitmap face at
     1.5x is a bitmap face with grey edges. The rule is right about a dithered
     picture, where 1.5x is moire, and it turns out not to be about this face.
     unscii's glyphs are axis-aligned rectangles on a four-unit grid in a
     64-unit em, so at 24px one glyph pixel is exactly 1.5px and every edge
     lands on a whole or a half: a one-pixel stem comes out alternately one and
     two device pixels, which is a rhythm and not a blur. On a 2x screen it is
     three device pixels to the glyph pixel and dead crisp, and on a 3x screen
     the half is a third of a CSS pixel. Rendered at 16, 24 and 32 side by side
     in the same engine that draws the site, 24 is clean.

     What 16 actually was, was small. unscii keeps a low x-height in a tall
     cell, so it reads a size or two under whatever number is set, and the site
     was asking people to read paragraphs at an apparent 12. 32 is the other
     end and reads as shouting: a monospace at 32 fills a screen with about
     forty words and nothing else. 24 is the one step in between that the face
     can hold, and the headings stay on the cell either side of it. */
  --body: 24px;
  --lead: 36px;

  /* The vertical rhythm, in whole leads. Everything vertical is one of these.
     `--gap-l` came down from 64 when the sections grew frames and slabs of
     their own: air between two hairlines is the only thing separating them,
     and air between two framed blocks is air on top of a boundary that is
     already drawn. */
  --gap-s: 16px;
  --gap-m: 32px;
  --gap-l: 48px;

  /* Exact rather than approximate, because the face is monospace: 1ch is 8px
     at the body size, so this is 640 real pixels.

     It was 58ch, which is 464, and on a monitor that is a ribbon of words down
     the middle of a very large empty page. The tell was the reader's own type
     control: the two larger steps set 40ch and 28ch, which at 32px and 48px are
     640 and 672 real pixels, so the *smallest* setting was the only one drawing
     a narrow column. All three are one physical measure now, and the column
     stopped being the thing you notice first. */
  /* Stated in `ch` so it follows the face, which is what keeps it 672 real
     pixels through a change of body size: 84 characters at 16px and 56 at 24px
     are the same column, and 56 is the better line anyway. */
  --measure: 56ch;
  /* Wide enough for three captures at their own point size with a gap between
     them, which is what the picture cards are: 1270. They were half that size
     for a draft and read as thumbnails of themselves, under a caption strip
     nearly as tall as the picture was wide. The masthead spreads to the same
     edges, so the page has one wide measure and one reading measure and
     nothing in between. */
  --wide: 1280px;
  --h1: 48px;

  /* A section title is a whole cell above the body it introduces, and it moves
     when the body moves. It used to be `--chrome`, which is fixed at 16px, and
     the first reader to press A++ got 48px prose under 16px section names: the
     titles vanished, and a page of long sections with nothing announcing them
     reads as one undifferentiated run of words. A heading that does not scale
     with the text it heads is not a heading at the sizes that need one most. */
  --h2: 32px;

  /* Content set small by choice, and furniture. See the note below the
     palette for why neither of them steps any more. */
  --small: 16px;
  --chrome: 16px;

  /* One height for everything in the top bar, which is the app icon's own 32.
     Every cell up there is this tall and vertically centred, so the mark, the
     page names and the tools sit on one line instead of three heights that
     nearly agree. */
  --bar: 32px;

  /* The one softened corner on this site, and it is a measurement rather than
     a taste: a capture is drawn at the phone's own point grid, so 56 is the
     display corner of the phone the picture was taken on. It is stated once,
     as a token, so the checker can allow exactly this radius and go on failing
     the build for every other one. */
  --phone: 56px;
}

/* Day, night, auto. Auto follows the OS; the switch in the top bar has to beat
   it in both directions, which is why "light" is stamped as deliberately as
   "dark". Night is genuinely just these two values swapping. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --ink: #ffffff; --paper: #000000; }
}
:root[data-theme="dark"]  { --ink: #ffffff; --paper: #000000; }
:root[data-theme="light"] { --ink: #000000; --paper: #ffffff; }

/* The one thing that steps at a breakpoint is the headline, and it steps by a
   whole cell. No fluid type, no clamp(), no vw units anywhere: a monospace grid
   that reflows continuously is a monospace grid you have broken. */
@media (min-width: 720px) { :root { --h1: 64px; --h2: 48px; } }

/* There is no type-size control any more. It offered 32px and 48px, and at
   those steps `--small` went to 32 as well, which put a caption strip taller
   than the picture it labelled under every screenshot on the site and turned
   every heading row into a column of single words. A control whose larger
   settings are broken is a control that hands the reader the broken version of
   the page and calls it accessibility; the browser's own zoom does the same
   job and scales the pictures with it.

   The two smalls stay as constants, and they stay separate: `--small` is
   content set small by choice (a caption, an aside, a table) and `--chrome` is
   furniture (the bar, the foot). They happen to be the same number now, and
   they are still two names because they answer to different arguments. */

/* ── reset ──────────────────────────────────────────────────────────────
 * border-radius is zeroed on everything and only ever put back as 50%, on
 * something that genuinely is a disc, or as `--phone`, on a capture that is
 * being drawn inside the phone it was taken on. There is no softened
 * rectangle here that is not a picture of a real corner.
 */
*, *::before, *::after { box-sizing: border-box; border-radius: 0; }

html { background: var(--paper); }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "unscii", ui-monospace, "SF Mono", Menlo, monospace;
  font-size: var(--body);
  line-height: var(--lead);
  -webkit-font-smoothing: none;
  padding-bottom: var(--gap-m);
  text-rendering: optimizeSpeed;
}

/* The 50% shade, and the only one on the site: a checkerboard of the same two
   colours at whole virtual pixels. Zoom in on it; it is ink and paper. */
.dither {
  background-image: conic-gradient(
    var(--ink) 0 25%, var(--paper) 0 50%, var(--ink) 0 75%, var(--paper) 0);
  background-size: calc(var(--px) * 2) calc(var(--px) * 2);
}

.skip {
  position: absolute; left: var(--gap-s); top: -100px;
  background: var(--paper); color: var(--ink);
  border: var(--px) solid var(--ink);
  padding: calc(var(--px) * 2) calc(var(--px) * 3);
  font-size: var(--chrome); line-height: var(--chrome);
  text-transform: uppercase; z-index: 20;
}
.skip:focus { top: calc(var(--px) * 2); }

/* An inverted hover state with no focus state leaves a keyboard user with
   nowhere to be. An ink outline offset by one virtual pixel becomes a paper
   outline on an inverted row, so it stays visible either way. */
:where(a, button, summary, input, [tabindex]):focus-visible {
  outline: calc(var(--px) * 2) solid var(--ink);
  outline-offset: var(--px);
}

/* ── the top bar ────────────────────────────────────────────────────────
 * The app's own furniture, and there is exactly one of it. It used to be two:
 * a bar with the mark and the clock, and a second sticky strip under it
 * holding the page names, each with its own hairline. Two rules a few pixels
 * apart across the whole width is a lot of furniture to look at before the
 * first word of the page, and the second one bought nothing the first could
 * not hold. The names sit in the bar now, and there is one line under it.
 *
 * The clock is the visitor's, not ours, and it is the cheapest possible proof
 * that the page is alive.
 */
/* Three columns, and the outer two are the same width, so the page names land
   on the middle of the bar rather than wherever the wordmark happens to end.
   As a flex row with a spacer after them they sat jammed against the mark with
   the whole rest of the bar empty to their right, which reads as two things
   that were put down next to each other and one that was left somewhere else.
   A grid is what makes "centred" mean centred: `1fr auto 1fr` gives the names
   the middle regardless of how long the identity on the left or the controls
   on the right happen to be. */
.topbar {
  position: sticky; top: 0; z-index: 10;
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: var(--gap-s);
  padding: calc(var(--px) * 3) var(--gap-s);
  background: var(--paper);
  border-bottom: var(--px) solid var(--ink);
  font-size: var(--chrome); line-height: var(--chrome);
  text-transform: uppercase; letter-spacing: var(--px);
}
.topbar a { text-decoration: none; color: inherit; }
.wordmark { display: flex; align-items: center; gap: calc(var(--px) * 4); white-space: nowrap; }
/* The mark is a black tile with a white block on it. On black paper the tile
   and the page are the same colour, so it gets the hairline the pictures get
   and stays a square rather than becoming a floating white shape. */
.wordmark img { display: block; image-rendering: pixelated;
  outline: var(--px) solid var(--ink); }
.topbar .tools {
  display: flex; align-items: center; justify-self: end;
  gap: calc(var(--px) * 4);
}
.clock { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The page names are cells, and so is every control opposite them. The draft
   before this one set them as bare words and painted only the current one,
   which gave one item a filled box and its neighbour none: two different
   shapes on one line, at two different heights, next to a 32px tile and a
   bordered chip. Nothing was aligned with anything. Every cell in the bar is
   `--bar` tall and centred in it now, the page names are butted so there is
   one hairline between them, and the current page is the one wearing ink. */
.pages ul { display: flex; list-style: none; margin: 0; padding: 0; }
.pages li { margin: 0; }
.pages li + li a { margin-left: calc(var(--px) * -1); }
.pages a {
  display: flex; align-items: center; white-space: nowrap;
  height: var(--bar); padding: 0 calc(var(--px) * 5);
  border: var(--px) solid var(--ink);
  color: inherit; text-decoration: none;
}
.pages a:hover, .pages a[aria-current="page"] { background: var(--ink); color: var(--paper); }

/* ── the column ─────────────────────────────────────────────────────────
 * One column, centred, and the space either side is paper. Not a pattern, not
 * a texture, not a dithered fill. The app is mostly paper too.
 */
main { padding: 0 var(--gap-s); }
/* margin-inline, not the `margin: 0 auto` shorthand. These classes sit on the
   same elements as `section`, they win the specificity tie, and a shorthand
   there silently zeroes the vertical rhythm for the whole page. That is exactly
   what it did: every section butted against the next one. Horizontal centring
   is all these two are for, so horizontal centring is all they set. */
.col { max-width: var(--measure); margin-inline: auto; }
.wide { max-width: var(--wide); margin-inline: auto; }

/* Only the bottom margin is set here. A `margin: 0 0 x` shorthand on a section
   would beat `.col`'s `margin: 0 auto` on specificity ties and quietly shove
   every centred column to the left, which is exactly what it did. */
section { margin-bottom: var(--gap-l); }

/* ── the masthead ───────────────────────────────────────────────────────
 * Left aligned, and it took a draft to get there. The first hero centred a
 * 64px mark over a centred wordmark over a centred headline, which is the
 * shape every app landing page has had since about 2014 and says nothing
 * about this one. Set flush left under a rule, with the mark worn as a chip
 * and the facts stated in a strip underneath, it reads as the first page of
 * something rather than as an advertisement for it.
 */
.masthead { padding-top: var(--gap-m); margin-bottom: var(--gap-l); }
/* Capped at the measure whether or not the two columns are on. Without it the
   headline and the fact strip run the full width of the wide container while
   the paragraphs under them stop at the measure, which is three different left
   edges and no right one. */
.masthead-copy { max-width: var(--measure); }
.masthead-copy > :last-child { margin-bottom: 0; }
/* Stacked, the picture is centred under the words. The two rules below have to
   come *before* the flex block and not after it: `margin-inline: auto` on a
   flex item absorbs the row's free space, so when this one won the cascade it
   shoved the picture to the far right and left a hundred and sixty pixels of
   paper between the two halves of the masthead. Same specificity, so the only
   thing deciding it is which one is written last. */
/* `.frame.masthead-shot`, and the second class is the whole point. Written as
   one class this rule is the same specificity as `.frame`, which is declared
   three hundred lines further down and therefore wins the whole shorthand: the
   top margin went to zero and the picture stood welded to whatever the copy
   ended with. Nothing showed it while that was a paragraph. It showed the
   moment it became the filled slab, because a solid slab is ink and a framed
   capture is ink on its own hairline of ink, so by day the claim and the
   screenshot were one continuous black shape with the boundary invisible
   inside it. The band of paper is the boundary, which is the same rule the
   caption strip obeys further down: nothing filled may touch a picture. */
.frame.masthead-shot { margin: var(--gap-l) auto 0; }
/* Pitch on the left, the phone on the right, once there is room for both. The
   first draft stacked them, which on a monitor is a narrow column of words
   with a narrow picture under it and a screen and a half of paper around the
   pair: the two halves of the argument were never in the eye at the same time.
   Below the breakpoint the picture goes back under the words, where it is the
   next thing you reach rather than the thing beside them. */
@media (min-width: 1160px) {
  /* Centred, not top aligned. A phone screenshot is nearly twice the height of
     the pitch beside it, so flush tops put the whole difference in one lump
     under the last line of copy, which reads as a column that failed to load.
     Split above and below it is air around a block instead of a hole under
     one. */
  /* `.split`, and the class is load-bearing. Written as plain `.masthead` this
     rule reached the masthead on every other page, where nothing is wrapped in
     a `.masthead-copy`: the kicker, the headline and the standfirst each became
     a flex item, so /get and /manual opened as three columns of one word each.
     A layout rule that assumes a child has to name the child. */
  .masthead.split { display: flex; align-items: center; gap: var(--gap-l);
                    justify-content: space-between; }
  .masthead.split .masthead-copy { flex: 0 1 var(--measure); min-width: 0; }
  .masthead.split .masthead-shot { flex: 0 0 auto; margin: 0; }
}
.masthead .brand {
  display: flex; align-items: center; gap: var(--gap-s);
  margin-bottom: var(--gap-m);
}
/* The mark stays on paper and the word beside it wears the ink, which took a
   try to get right: the first version put the mark *inside* the chip, and the
   file is a black tile with a white block on it, so by day it vanished into
   the chip and left the block floating, and by night it stood on the chip as a
   black square. One picture, two appearances, neither of them the app icon.
   Beside the chip it is the same tile the top bar draws, hairline and all. */
.masthead .brand-mark {
  display: block; image-rendering: pixelated;
  outline: var(--px) solid var(--ink);
}
.masthead .chip-mark {
  background: var(--ink); color: var(--paper);
  padding: calc(var(--px) * 2) calc(var(--px) * 4);
  font-size: var(--chrome); line-height: var(--chrome);
  text-transform: uppercase; letter-spacing: calc(var(--px) * 2);
}
.masthead h1 { margin-bottom: var(--gap-m); }

/* The strip of facts under the standfirst. Cells butted against each other so
   there is one hairline between any two, which is how a status line is drawn
   everywhere else in this design. */
.deck {
  display: flex; flex-wrap: wrap; list-style: none;
  margin: var(--gap-m) 0 0; padding: 0;
}
.deck li {
  margin: 0 calc(var(--px) * -1) calc(var(--px) * -1) 0;
  border: var(--px) solid var(--ink);
  padding: calc(var(--px) * 2) calc(var(--px) * 4);
  font-size: var(--chrome); line-height: var(--lead);
  text-transform: uppercase; letter-spacing: var(--px);
  white-space: nowrap;
}

/* ── type ───────────────────────────────────────────────────────────────*/
h1 {
  font-size: var(--h1); line-height: var(--h1);
  margin: 0 0 var(--gap-m);
  font-weight: normal; text-transform: uppercase; text-wrap: balance;
}

/* A section header is a title with a hairline under it, not a full width slab
   of ink. The app's inverted strip is right on a 390px screen and wrong across
   a monitor, where it reads as a bar of tape stuck over the page. The rule says
   the same thing and leaves the paper alone.

   Two things were wrong with the first version of this, and they were the same
   mistake twice. The title was set at `--chrome`, the top bar's size, which is
   fixed: a reader who pressed A++ got 48px prose under 16px section names,
   never saw them, and read the page as one undifferentiated run of words. And
   the hairline ran out of the *side* of the title as a flex `::after`, which is
   a fine device for a label of a few characters and falls over the moment the
   title is a sentence. "THREE · THE LIST MOVES TO YOUR HOME SCREEN" at a whole
   cell above the body is wider than the measure, so the words wrapped and the
   rule was squeezed to nothing. Underneath, it is the same one hairline and it
   is the same width whatever the title says.

   A whole cell above the body, and it steps with it. On a narrow screen that
   brings it level in size with `h1`; the two are told apart by the rule, the
   letterspacing, the kicker above, and the band of paper above the headline,
   which is the ranking a reader actually uses. */
h2 {
  font-size: var(--h2); line-height: var(--h2); font-weight: normal;
  text-transform: uppercase; letter-spacing: var(--px); text-wrap: balance;
  margin: 0 0 var(--gap-m);
  padding-bottom: calc(var(--px) * 4);
  border-bottom: var(--px) solid var(--ink);
}
h3 {
  font-size: var(--body); line-height: var(--lead); font-weight: normal;
  text-transform: uppercase; letter-spacing: var(--px);
  margin: var(--gap-m) 0 var(--gap-s);
}

/* The kicker above a title. Chrome sized and staying there: it is a label on
   the section, and a label that grows into the thing it labels stops being one.
   The mark in front is the app's own list mark, which is why it is a character
   and not a border. */
.eyebrow {
  margin: 0 0 calc(var(--px) * 3);
  font-size: var(--chrome); line-height: var(--chrome);
  text-transform: uppercase; letter-spacing: calc(var(--px) * 2);
}
.eyebrow::before { content: "\25AA\00A0\00A0"; }

p { margin: 0 0 var(--gap-s); }
p:last-child { margin-bottom: 0; }
ul, ol { margin: 0 0 var(--gap-s); padding-left: calc(var(--px) * 10); }
/* Only prose lists get air between their items. A blanket `li` margin reaches
   the nav strip, the launcher and every figure row and pads each of them by a
   line nobody asked for, which is what made the page nav twice as tall as its
   own type. */
li { margin: 0; }
ul:not([class]) > li, ol:not([class]) > li { margin-bottom: var(--gap-s); }
ul:not([class]) > li:last-child, ol:not([class]) > li:last-child { margin-bottom: 0; }

/* Lead paragraphs sit a normal line apart from each other, and the gap goes in
   front of whatever follows the last of them. Giving every .lead the big margin
   put a hole in the middle of the hero. */
.lead { margin-bottom: var(--gap-s); }

/* Emphasis inverts a few words rather than bolding them, because a bitmap face
   has no bold and a faked one is a blurred one. Used sparingly. */
strong { font-weight: normal; background: var(--ink); color: var(--paper); padding: 0 var(--px); }
em { font-style: normal; text-decoration: underline; text-underline-offset: calc(var(--px) * 2); }
a { color: inherit; text-underline-offset: calc(var(--px) * 2); }
code { font-family: inherit; border: var(--px) solid var(--ink); padding: 0 var(--px); }
hr { border: 0; border-top: var(--px) solid var(--ink); margin: var(--gap-l) 0; }

.quiet { text-transform: uppercase; font-size: var(--small); line-height: var(--lead); letter-spacing: var(--px); }
.centre { text-align: center; }
.note { margin-top: var(--gap-s); }

/* ── slabs ──────────────────────────────────────────────────────────────
 * A box for the claim a section is actually making, and a filled one for the
 * claim the page is making. This is the device the first draft did not have:
 * ink that is not type. A page of hairlines and paper is legible and reads as
 * a wireframe of itself, and one filled rectangle per screenful is the
 * difference between a specimen and a design.
 *
 * The filled variant inverts its own emphasis, or a `strong` inside it paints
 * ink on ink and the words disappear.
 */
.slab {
  border: var(--px) solid var(--ink);
  padding: var(--gap-s);
  /* Air on both sides. The shorthand is the whole declaration on purpose: a
     separate `margin-top` written above it is reset to zero by this line, which
     is how the slab in the masthead ended up welded to the fact strip over it. */
  margin: var(--gap-m) 0;
}
.slab.solid { background: var(--ink); color: var(--paper); }
.slab.solid strong { background: var(--paper); color: var(--ink); }
.slab.solid code { border-color: var(--paper); }
.slab > :last-child { margin-bottom: 0; }
.slab .eyebrow { margin-bottom: var(--gap-s); }
/* The card: an ink strip naming the block, and the block. This is the device
   that puts real weight on the page, because what a reader sees first is the
   ratio of ink to paper and a page assembled out of hairlines has almost none
   of it. A slab states a claim; a field holds a set of things and says what
   the set is. */
.field { border: var(--px) solid var(--ink); margin: 0 0 var(--gap-m); }
.field-head {
  background: var(--ink); color: var(--paper);
  padding: calc(var(--px) * 3) var(--gap-s);
  font-size: var(--chrome); line-height: var(--lead);
  text-transform: uppercase; letter-spacing: calc(var(--px) * 2);
}
.field-body { padding: var(--gap-s); }
.field-body > :last-child { margin-bottom: 0; }

.slab .big {
  font-size: var(--h2); line-height: var(--h2);
  text-transform: uppercase; letter-spacing: var(--px); text-wrap: balance;
}

/* ── the launcher list ──────────────────────────────────────────────────
 * Rockbox's inverted bar, which is free, instant and unmistakable. Rows are
 * wide rather than tall and never below 44px whatever the type is doing. One
 * hairline between rows, none around the outside.
 */
.launcher { list-style: none; margin: 0; padding: 0; }
.launcher li { margin: 0; border-top: var(--px) solid var(--ink); }
.launcher li:last-child { border-bottom: var(--px) solid var(--ink); }
.launcher a {
  display: flex; align-items: baseline; gap: var(--gap-s);
  min-height: 44px; padding: calc(var(--px) * 5) calc(var(--px) * 4);
  text-decoration: none; text-transform: uppercase; letter-spacing: var(--px);
}
.launcher a:hover, .launcher a:active { background: var(--ink); color: var(--paper); }
.row-name { flex: 1; }
.row-aux { white-space: nowrap; font-size: var(--small); }
.row-mark { white-space: nowrap; }

/* ── the marks ──────────────────────────────────────────────────────────
 * A list where each line reports a state, drawn the way the app draws one: a
 * filled box has happened, an outline has not, and the checkerboard is the
 * half of it that is true. A tick and a cross would say the same thing in two
 * characters, and would say it in whatever the reader's emoji font decided.
 */
.marks { list-style: none; margin: 0 0 var(--gap-m); padding: 0; }
.marks li {
  display: grid; grid-template-columns: 16px 1fr; gap: var(--gap-s);
  margin: 0 0 var(--gap-s);
}
.marks li:last-child { margin-bottom: 0; }
/* The box is a chrome-sized square, so it needs nudging down to sit on the
   middle of a body-sized line rather than at the top of one. */
.box {
  width: 16px; height: 16px; margin-top: calc(var(--px) * 5);
  border: var(--px) solid var(--ink);
}
.box.on { background: var(--ink); }

/* ── the step strip ────────────────────────────────────────────────────
 * Three moves drawn as three segments of one bar, which is the app's own
 * shape: the day bar on the today panel and the peak meter under the music
 * are both a run of cells, filled up to where you are. The number is an ink
 * cell at the head of each segment and there is one hairline between any two.
 *
 * The bar runs down the page rather than across it, and that is a consequence
 * of the body size rather than a preference. Three segments across the measure
 * is 224 pixels each, which at 24px type is sixteen characters to a line: the
 * instructions came out as four and five word-wrapped fragments apiece, all
 * different lengths, which is a bar made of rubble. Stood on its end each
 * segment gets the whole measure and reads as the sentence it is.
 */
.steps {
  list-style: none; margin: 0 0 var(--gap-m); padding: 0;
  border: var(--px) solid var(--ink);
}
.steps li {
  display: flex; gap: var(--gap-s); margin: 0;
  padding: var(--gap-s);
  border-top: var(--px) solid var(--ink);
}
.steps li:first-child { border-top: 0; }
.steps .step-n {
  flex: 0 0 auto;
  width: 24px; height: 24px;
  background: var(--ink); color: var(--paper);
  text-align: center; line-height: 24px;
  font-size: var(--chrome);
}

/* ── pictures ───────────────────────────────────────────────────────────
 * One file per picture, and it is the capture itself: a 3x screenshot of a
 * two-colour interface, thresholded and never resized. The page draws it at a
 * third of its pixel width, which is the phone's own point grid, so a 3x
 * screen paints one image pixel to one device pixel and a 2x screen takes two
 * out of every three. Both of those are whole ratios. `build.py` has the long
 * version and the arithmetic; what matters here is that nothing in this file
 * may state a width for one, because the markup already carries the only width
 * that is legal.
 *
 * **A picture is never turned over at night.** This is the app's own rule
 * (`CLAUDE.md` ▸ "A picture that is already ink and paper"), and the site
 * spent a draft on the other side of it: every file was stored as a negative
 * with its frame drawn in, and `mix-blend-mode: difference` over paper handed
 * back the positive by day and left the negative standing by night. It works,
 * and what it looks like is a photocopy. A light iOS sheet came out black, a
 * capture of the app's own white paper came out black beside it, and a
 * screenshot of a real screen that no screen has ever looked like reads as
 * something cheaply made. So the files are the pictures, one appearance, at
 * every hour and in print.
 */
.shot {
  display: block;
  image-rendering: pixelated;
  max-width: 100%; height: auto;
}

/* ── the phone around a capture ─────────────────────────────────────────
 * Every full-screen capture is drawn inside a phone: a band of paper, then an
 * ink outline, at the corner the phone actually has. It is the one softened
 * rectangle on this site and the exception is argued in `docs/06-website.md`
 * ▸ "A radius only ever makes a circle": what that rule bans is the softened
 * *card*, a radius spent on decoration. This one is a picture of a real
 * corner, and it is the thing the site was missing. Without it a capture is a
 * rectangle of interface butted against a hairline, and the eye reads it as a
 * diagram; inside the outline the same file reads as a phone somebody is
 * holding, which is what every one of these pictures is of.
 *
 * Both rings are `box-shadow` spread rather than a border and padding. A
 * border would have to come out of the picture's own width, since `box-sizing`
 * is `border-box` for everything here, and two pixels off a bitmap capture is
 * a resample of the whole thing. Spread follows the radius, so the outline's
 * corner comes out one band larger without anything stating it twice, and the
 * margin is only there to keep both rings inside the frame. No blur, as
 * everywhere else.
 *
 * Paper inside, ink outside, which is the argument the caption strip makes
 * further down: a capture is both colours, so whichever one it happens to be
 * at that hour, one of the two rings is the opposite of it and the picture
 * cannot bleed into the page.
 *
 * Only a whole screen gets one. The widget-gallery crops are a piece of iOS
 * cut out of its surroundings, and a phone drawn around a fragment of a phone
 * is a lie about what is being shown, so `is-phone` is written by the mill
 * from the picture's own aspect and never by hand.
 */
.shot.is-phone {
  margin: calc(var(--px) * 7);
  /* The rings sit in that margin, and a margin is outside the width, so the
     `max-width: 100%` above is the frame's whole content box *plus* twenty
     eight pixels of phone. Nothing notices above the breakpoint, where the
     frame is `max-content` and simply grows to hold both. On a phone the frame
     is clamped to the column instead and the picture went on asking for the
     full column, so a capture hung out of its own box on the right with the
     caption strip ending short of it, which reads as a box drawn too small for
     the thing in it. What has to fit the frame is the picture's margin box. */
  max-width: calc(100% - var(--px) * 14);
  border-radius: var(--phone);
  box-shadow: 0 0 0 calc(var(--px) * 5) var(--paper),
              0 0 0 calc(var(--px) * 7) var(--ink);
}

/* The frame is the job the baked-in negative border used to do: a dark plate
   on dark paper has no edge. It is a real border on the figure rather than
   anything on the picture, because `box-sizing: border-box` on the picture
   itself would eat two pixels off its width, and two pixels off a bitmap
   screenshot is a resample of the whole thing.

   The caption lives in the frame as an ink strip, which is the app's own
   modal title bar and the one place on this site where a label is guaranteed
   to be the same width as the thing it labels. Loose under the picture it was
   ambiguous the moment two pictures sat side by side. */
.frame {
  margin: 0 auto var(--gap-m);
  width: max-content; max-width: 100%;
  border: var(--px) solid var(--ink);
  display: flex; flex-direction: column;
}
/* A caption must not be what decides how wide its picture is. The frame is
   sized to its widest child, and a caption is nearly always longer than a
   phone screenshot, so three 201px pictures were each given a 350px frame and
   sat in the left half of it with paper to the right. Zero width takes the
   caption out of that measurement; the min-width puts it back to whatever the
   picture settled on. `margin-top: auto` is the other half: a two-line caption
   in a row of one-line ones would otherwise leave its own picture standing
   short of its neighbours. */
/* **The strip is paper, and this is the rule an ink strip cannot obey.** It
   was ink for a draft, which looks right for a light screenshot by day and for
   a dark one at night, and merges completely with the other two pairings: a
   white capture at night sits on a white strip with a white rule between them,
   and the caption reads as words floating inside the picture. Every other ink
   block on this site stands on paper and always will. A picture is both
   colours, so nothing filled may touch one, and the boundary is the rule,
   which is ink and is therefore always the opposite of whatever it separates. */
.frame figcaption {
  width: 0; min-width: 100%; margin-top: auto;
  border-top: var(--px) solid var(--ink);
  background: var(--paper); color: var(--ink);
  padding: calc(var(--px) * 3) calc(var(--px) * 4);
  font-size: var(--small); line-height: var(--lead);
  text-transform: uppercase; letter-spacing: var(--px);
}

/* A run of framed pictures that breaks out of the measure. Wraps rather than
   scrolls, and centres what is left over, so it works at any number of them.
   The frames are `width: max-content`, so a caption longer than its picture
   would decide how wide the picture is: three 201px shots were being given
   650px each and the row wrapped after two. Each cell is told to be exactly as
   wide as it wants and no wider. */
.cards {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: stretch;
  gap: var(--gap-m); list-style: none; margin: 0 0 var(--gap-m); padding: 0;
}
/* `min-width: 0` is the half of that which a narrow window needs. A flex item
   floors at its own min-content, and a frame declared `max-content` reports the
   whole picture there, so a 434 pixel cell on a 393 pixel phone neither wrapped
   its way out of trouble nor shrank: the row hung a capture off each side of
   the window and gave the page a sideways scroll. Wrapping was never the
   problem. The floor was. */
.cards li { margin: 0; display: flex; min-width: 0; max-width: 100%; }
.cards .frame { margin-bottom: 0; }
.cards figcaption { text-align: center; }

/* ── the App Store badge ────────────────────────────────────────────────
 * Both states are written by `build.py` out of `data/site.json`: a link when
 * there is a listing to point at, and the plate below when there is not. The
 * unshipped one stays in the file because the URL is a data value and a build
 * without one has to draw something honest.
 *
 * The front page carries two of them, which is a departure from the brief and
 * argued in `docs/06-website.md` ▸ "The front page": one under the standfirst
 * and one in GET IT. `.store` is the wrapper both sit in, and only the second
 * takes `.centre` — the masthead is flush left and a centred plate in the
 * middle of a left-aligned column is the one thing up there that would not
 * line up with anything.
 *
 * One line of type, not two stacked and centred. The two-line badge is Apple's
 * shape and it needs Apple's face to hold together: set in a bitmap at whole
 * cells, DOWNLOAD ON THE over APP STORE came out as a small line balanced on a
 * large one with no optical relationship between them, and the whole plate read
 * as a placeholder somebody had not finished. Said straight across, at the size
 * the rest of the chrome is set at, it is a plate with a sentence on it.
 *
 * The unshipped state is the same plate inverted, and the inversion is what
 * makes it read as inert: a filled plate is every other pressable thing on
 * this site. A checkerboard was tried and looked like a button somebody had
 * scribbled over, and ink type on half ink at this size cancels itself out.
 * The words already do that job: nobody presses a badge that says COMING SOON.
 *
 * Which is also why the live badge is the one pressable thing on this site
 * that does not invert under the pointer. Inverted is what the plate looks
 * like when there is nothing to press, so a hover state here would be the
 * badge reporting itself unavailable at the moment somebody reaches for it.
 * The pointer and the focus outline are the feedback.
 */
.store { margin-top: var(--gap-m); }
.store-badge {
  display: inline-flex; align-items: center; gap: calc(var(--px) * 5);
  border: var(--px) solid var(--ink);
  padding: calc(var(--px) * 5) var(--gap-m);
  background: var(--ink); color: var(--paper);
  font-size: var(--chrome); line-height: var(--lead);
  text-transform: uppercase; letter-spacing: calc(var(--px) * 2);
  text-decoration: none;
}
.store-badge.is-soon {
  background: var(--paper); color: var(--ink);
  cursor: default; pointer-events: none; user-select: none;
}
.store-apple { display: block; flex: 0 0 auto; }

button, .button {
  font: inherit; text-transform: uppercase; letter-spacing: var(--px);
  background: var(--paper); color: var(--ink);
  border: var(--px) solid var(--ink);
  padding: calc(var(--px) * 3) calc(var(--px) * 6);
  min-height: 48px; cursor: pointer; transition: none;
  text-decoration: none; display: inline-block; white-space: nowrap;
}
button:hover, button:active, .button:hover, .button:active,
button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
/* The bar's own control, cut to the same cell as a page name. */
.chip {
  display: inline-flex; align-items: center;
  height: var(--bar); min-height: 0;
  padding: 0 calc(var(--px) * 4);
  font-size: var(--chrome); line-height: var(--chrome);
}

/* ── the FAQ ────────────────────────────────────────────────────────────
 * Real <details>, so it works with the script off and search engines read the
 * answers. The marker is one of the app's own action marks.
 */
.faq { border-top: var(--px) solid var(--ink); }
.faq:last-of-type { border-bottom: var(--px) solid var(--ink); margin-bottom: var(--gap-m); }
.faq summary {
  display: flex; gap: var(--gap-s); align-items: baseline;
  list-style: none; cursor: pointer;
  padding: calc(var(--px) * 5) calc(var(--px) * 4);
  min-height: 44px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; margin-left: auto; }
.faq[open] summary::after { content: "\2212"; }
.faq summary:hover { background: var(--ink); color: var(--paper); }
.faq p { padding: 0 calc(var(--px) * 4); }
.faq p:last-child { padding-bottom: calc(var(--px) * 5); }

/* ── entries on /manual ─────────────────────────────────────────────────*/
.entry, .step { margin: 0 0 var(--gap-l); scroll-margin-top: calc(var(--gap-m) + var(--gap-s)); }
.entry-what { margin-bottom: var(--gap-s); }
.entry-built { font-size: var(--small); line-height: var(--lead); }
/* The step number was a flex item with a gap after it. The h2 is not a flex
   container any more, so the air is the span's own. */
.step-no { white-space: nowrap; margin-right: calc(var(--px) * 6); }
.step-no::after { content: " ·"; }

/* ── tables, for the reference pages ────────────────────────────────────
 * The head is an ink strip, which is what tells a table from a stack of rules
 * at a glance. That costs the head its left flush, so the whole table gets a
 * cell padding on both sides and a border around the outside; a padded head
 * over unpadded rows is a column that does not line up with its own name.
 */
.scroller { overflow-x: auto; margin: 0 0 var(--gap-m); border: var(--px) solid var(--ink); }
/* A table on this site is prose in columns (what you need, what to try when
   something is wrong), so it is set at the body size. `--small` is for the
   things that are asides by choice: a caption, the aux column on a launcher
   row, the line about how a section is built. */
table { border-collapse: collapse; width: 100%; line-height: var(--lead); }
th, td {
  text-align: left; vertical-align: top;
  padding: calc(var(--px) * 3) calc(var(--px) * 4);
  border-bottom: var(--px) solid var(--ink);
}
th { text-transform: uppercase; letter-spacing: var(--px); font-weight: normal; }
thead th { background: var(--ink); color: var(--paper); }
/* Every cell in the last row, not just the `td`s: a row-header column is
   `th`, and taking the rule off its neighbours alone leaves one short line
   hanging under the first column. */
tbody tr:last-child > * { border-bottom: 0; }
pre { margin: 0 0 var(--gap-s); padding: calc(var(--px) * 4); border: var(--px) solid var(--ink); overflow-x: auto; font-size: var(--small); line-height: var(--lead); }

/* ── the one modal shape ────────────────────────────────────────────────
 * The only permitted shadow on the site: a hard ink rectangle offset down and
 * right with no blur, which is how a two-colour desktop drew a dialog. It
 * appears on the 404 page and nowhere else.
 */
.confirm {
  border: var(--px) solid var(--ink);
  background: var(--paper);
  box-shadow: calc(var(--px) * 3) calc(var(--px) * 3) 0 0 var(--ink);
  margin: 0 auto var(--gap-l); max-width: 44ch;
}
/* The one box on the site wears its title as an ink strip, which is the app's
   own modal. It stays at chrome size and drops the section rule: a hairline
   under a band of ink is a hairline nobody can see. */
.confirm h2 {
  margin: 0; padding: calc(var(--px) * 3) calc(var(--px) * 4);
  font-size: var(--chrome); line-height: var(--chrome);
  border-bottom: 0;
  background: var(--ink); color: var(--paper);
}
.confirm-body { padding: var(--gap-s); }
.confirm .answers { display: flex; border-top: var(--px) solid var(--ink); }
.confirm .answers > * { flex: 1; border: 0; border-left: var(--px) solid var(--ink); text-align: center; }
.confirm .answers > *:first-child { border-left: 0; }

/* ── the foot ───────────────────────────────────────────────────────────
 * A band of blank paper with one round button in it, which goes home. The only
 * 50% radius in the file, on something that genuinely is a disc.
 */
.bezel {
  border-top: var(--px) solid var(--ink);
  padding: var(--gap-m) 0;
  display: flex; justify-content: center;
}
.home {
  width: 48px; height: 48px; padding: 0; min-height: 0;
  background: var(--paper);
  border: calc(var(--px) * 2) solid var(--ink);
  border-radius: 50%;
  display: grid; place-items: center;
}
.home span { display: block; width: 16px; height: 16px; border: var(--px) solid var(--ink); }
.home:hover, .home:active { background: var(--ink); }
.home:hover span, .home:active span { border-color: var(--paper); }

.foot { padding: var(--gap-m) 0; border-top: var(--px) solid var(--ink); text-align: center; }
.foot ul {
  list-style: none; margin: 0 0 var(--gap-s); padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: 0 var(--gap-s);
}
.foot li { margin: 0; }
.foot a { font-size: var(--chrome); line-height: var(--lead); text-transform: uppercase; letter-spacing: var(--px); }

/* ── motion ─────────────────────────────────────────────────────────────
 * Nearly none. The blinking cursor and the invert on hover, which is a state
 * change rather than a transition. The status-line marquee is gone: it read as
 * costume on a marketing page.
 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

@media (max-width: 600px) {
  :root { --gap-l: 32px; }
  .row-aux { display: none; }
  /* The word beside the mark goes, and the mark stays: on a 375pt phone the
     bar is a tile, two page names, two controls and a clock, and the one part
     of that which repeats something already on screen is the word "1bit"
     printed next to the picture of it. The bar wraps as a backstop, so a
     longer name than these two costs a second line inside the same rule
     instead of a scrollbar. */
  .wordmark span { display: none; }
  .topbar { gap: calc(var(--px) * 4); }
  /* The clock goes on a phone, and only there. It is the cheapest proof the
     page is alive and it is also the one thing in the bar that the operating
     system is already printing an inch above it. What it costs at this width
     is the room the page names need to stay centred. */
  .clock { display: none; }
  /* The letter-spacing goes, and the plate keeps its sentence on one line.
     Twenty five characters of unscii at the chrome size is two hundred pixels
     and fits the narrowest phone; spaced by two virtual pixels a character it
     is three hundred and does not, so the page's call to action broke into
     DOWNLOAD ON THE APP over STORE with Apple's mark centred against the pair
     of them, which is the two-line badge this one was drawn to avoid. Tracking
     is what the plate gives up at this width, and it is the cheapest thing on
     it. `nowrap` is the guarantee: the padding above already leaves the room,
     and a plate that is going to be one line should say so out loud instead of
     depending on an arithmetic nobody re-checks when the words change. */
  .store-badge {
    padding: calc(var(--px) * 4) var(--gap-s);
    letter-spacing: 0; white-space: nowrap;
  }
  /* A picture wider than the column has to be fitted, and a fitted picture is
     the one case where nearest is the wrong sampler: 402 image pixels into 358
     drops one row in nine, which on a bitmap face is a stroke missing out of
     every ninth letter. Smoothed, the same reduction is a screenshot very
     slightly softer than its own grid, which is what a phone screen shows
     anyway. Above this width the picture is drawn at a whole ratio and the
     sampler never comes up. */
  .shot { image-rendering: auto; }
}

/* Printed, this site is already black and white, which is the one format it
   was always going to be right for. The chrome is screen furniture. */
@media print {
  .topbar, .pages, .bezel { display: none; }
  body { padding: 0; }
}
