/* Fallow — the web app, as a preview.
 *
 * **No JavaScript, and that is a constraint rather than a shortcut.**
 * `site/_headers` sets `default-src 'none'` with no `script-src`, so the
 * published policy refuses every script — which is the stronger claim for a
 * product whose whole pitch is that it does not watch you. Everything that
 * changes here changes through a link or `:target`.
 *
 * Colours come from `../tokens.css`, generated from `src/theme.ts`. Anything
 * added here that needs a colour takes a token; a literal drifts.
 */

*, *::before, *::after { box-sizing: border-box; }


/* ---------- the ground, and the sheet on it ----------
 *
 * **A site value, not an app token**, and there is precedent one directory up:
 * the handbook defines its own `--d-*` for the drawings because the app's
 * palette is tuned for a phone held at arm's length rather than for a page.
 * `--bg` is a whole screen in the app; on a 720px column in the middle of a
 * browser window it is a lot of flat tan.
 *
 * **Dark flips which way the separation goes, and that is not a compromise.**
 * The handbook's own note says the same thing about its drawings. On paper the
 * ground goes *down* from the sheet — `#E2D7C9` under `--bg`, sitting between
 * it and `--bgSelected` without being either, since the selected tint means
 * *this one is chosen* and a ground borrowing it would put a selection colour
 * behind every page.
 *
 * In the dark palette there is nowhere down to go: `--bg` is `#1A1613` and even
 * pure black only reaches 1.17 against it. So the **sheet goes up** instead —
 * `--bgRaised` on a `--bg` ground — which is `theme.ts`'s own model read
 * straight, and gives real separation rather than a darker shade of nothing.
 */
/* **The tan margin is gone — 2026-10-15.** The ground was a darker band either
   side of the sheet, so a wide page read as a column floating on a mat. Asked
   for by eye: *get rid of the darker side bars.* One surface now, and `--ground`
   stays as a name so every rule that reaches for it keeps working. */
:root { --ground: var(--bg); --sheet: var(--bg); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --ground: var(--bg); --sheet: var(--bgRaised); }
}
/* And again for an explicit choice, which is not the same condition.
 *
 * `tokens.css` already carries all three states -- bare `:root`, the media
 * query, and `[data-theme='dark']` -- and this file carried only two. So a
 * reader who had picked dark rather than inherited it got dark tokens inside a
 * **tan margin**: `--sheet` followed, because it is `var(--bg)` and resolves
 * late, and `--ground` did not, because it is a literal.
 *
 * Found 2026-09-02 by forcing the attribute in a test rig, which is the only
 * way anybody would meet it -- nothing in the app sets it today. Same shape as
 * the CSP that had been refusing `tokens.css` for sixteen days: a rule written
 * when there was one way to be dark, left behind when a second arrived. */
:root[data-theme="dark"] { --ground: var(--bg); --sheet: var(--bgRaised); }

body {
  margin: 0;
  background: var(--ground);
  color: var(--text);
  font: 17px/26px var(--read);
  -webkit-text-size-adjust: 100%;
}

/* ---------- the bar ---------- */

/* **The development notice.** A band above everything, in `--warm` — hue 20,
   chosen 2026-10-15 by looking and generated from `src/theme.ts`.

   **The whole band is the link**, asked for 2026-10-15 after it carried a
   button: a bar with one destination does not need a control inside it, and a
   44pt target is easier to hit when it is the full width of the window.

   **It sticks, like the bar under it**, which is why the height is a variable:
   `.bar` has to know how far down to sit, and a number written twice is a
   number that goes wrong the first time this is retuned.

   **It holds still.** Built sliding and stopped within the hour — a line of
   text that will not hold still is what `prefers-reduced-motion` exists to
   remove, and a rule undoing your own animation for some readers says it was
   not worth having for any. */
/* The development banner lives in `/dev-note.css`, because the marketing home
   page draws it too and does not load this file. */


.bar {
  display: flex; align-items: center; gap: var(--space-md);
  /* **The band runs edge to edge; what is in it does not.** Asked 2026-10-15:
     *the bar itself can expand full width but the content inside should follow
     the body's width.* Done with padding rather than an inner wrapper, so no
     page's markup has to change -- the padding grows to whatever is left either
     side of the measure, and never drops below the normal gutter. 1200 rather
     than 720, because the bar sits above both and the wider pages are the ones
     where a narrow bar looks wrong. */
  padding: 12px max(var(--space-md), calc((100% - 1200px) / 2));
  border-bottom: 1px solid var(--line);
  background: var(--bgRaised);
  position: sticky; top: var(--dev-note-h); z-index: 10;
}
.brand { font: 700 21px/26px var(--read); color: var(--text); text-decoration: none; }
.bar nav { display: flex; gap: var(--space-md); }
.bar nav a {
  font: 13px/20px var(--meta); color: var(--textSoft); text-decoration: none;
  /* **Eight above, six below, plus a 2px border.** Reported 2026-10-15 as not
     lining up with the wordmark, and the cause is the border rather than the
     padding: it is part of the box, so `6px 0` with a 2px rule underneath is 6
     above and 8 below, and the text rides high by a pixel. Taking the
     difference out of the top is what centres it against a 26px wordmark. */
  padding: 8px 2px 6px; border-bottom: 2px solid transparent;
}
.bar nav a:hover { color: var(--text); }
.bar nav a[aria-current] { color: var(--text); border-bottom-color: var(--text); }
.me { margin-left: auto; font: 13px/20px var(--meta); color: var(--textSoft); }

/* ---------- the page ---------- */

main {
  max-width: 720px; margin: 0 auto; padding: var(--space-lg) var(--space-md) 72px;
  background: var(--sheet); min-height: 78vh;
}
/* The video pages and the circle: a grid of stills, or a player beside its
   marks, is not a reading column. 720 is right for prose and this is not
   prose. */
main.wide { max-width: 1200px; }
/* Edge to edge on a phone, where a sheet with a margin either side is just a
   narrower page and two hairlines nobody asked for. */
@media (max-width: 780px) {
  main { border-left: 0; border-right: 0; max-width: none; }
}

h1 { font: 600 26px/33px var(--read); margin: 0 0 var(--space-xs); }
h2 { font: 11px/20px var(--meta); letter-spacing: .14em; text-transform: uppercase;
     color: var(--textSoft); margin: var(--space-xl) 0 var(--space-sm); font-weight: 400; }
h2:first-of-type { margin-top: var(--space-lg); }
p { margin: 0 0 var(--space-md); }

.lede { color: var(--textSoft); font-size: 15px; line-height: 23px; margin-bottom: var(--space-lg); }

/* **The preview notice, and it is not decoration.** Everybody on these pages is
   invented, per the content policy — real seed content comes from Standard
   Ebooks or Project Gutenberg, and there is no such source for a person. A
   stranger has to be able to tell a drawing from a service. */
.preview {
  border: 1px solid var(--lineStrong); border-radius: var(--radius-md);
  background: var(--bgRaised); padding: 12px var(--space-md);
  font: 13px/21px var(--meta); color: var(--textSoft);
  margin: 0 0 var(--space-lg);
}
.preview b { color: var(--text); font-weight: 400; }

/* ---------- an article ---------- */

.card { border-top: 1px solid var(--line); padding: var(--space-md) 0; }
.card:last-of-type { border-bottom: 1px solid var(--line); }
.card .who { font: 12px/20px var(--meta); color: var(--textSoft); margin: 0 0 var(--space-sm); }
.card .who a { color: var(--textSoft); }
.card .body { display: flex; gap: var(--space-md); align-items: flex-start; }
.card .words { flex: 1; min-width: 0; }
.card .src {
  font: 10px/16px var(--meta); letter-spacing: .1em; text-transform: uppercase;
  color: var(--textSoft); margin: 0 0 3px;
}
/* **19 rather than 21**, with the picture at 184: the two were competing and
   the headline was winning, which is right on a page of four and wrong on a
   feed. `DESIGN.md`'s rule holds either way — a title is the biggest thing in
   a list because it is what you are choosing between, not because it is the
   most important. */
.card h3 { font: 600 19px/25px var(--read); margin: 0 0 var(--space-xs); }
.card h3 a { color: var(--text); text-decoration: none; }
.card h3 a:hover { text-decoration: underline; }
.card .dek { font-size: 15px; line-height: 23px; color: var(--textSoft); margin: 0; }

/* A picture we do not have. Drawn rather than fetched: `img-src 'self' data:`,
   and a preview that reached out to seven publishers for thumbnails would be
   the tracking this whole product refuses, on the page explaining it. */
/* **`radius.sm` and the shelf's hairline** — `BookRow` draws a cover at
   `borderWidth: 1, borderColor: t.line`. A picture is a thing on a page rather
   than a control, so it takes an edge instead of a corner radius, and a pale
   photograph does not bleed into the paper.

   Declared here rather than overridden further down: two rules 250 lines apart
   arguing about one value is how a stylesheet stops being readable. */
/* **An article's masthead: the picture left, the words right.** Asked
   2026-10-15, matching the card — the headline is what you are choosing and the
   source is read once, so the title sits above it in both places.

   It stacks on a narrow screen, picture first, because a 148px-wide photo beside
   a wrapped headline is neither. */
.masthead { display: flex; gap: var(--space-lg); align-items: flex-start;
  margin: 0 0 var(--space-lg); }
/* **420, after a round trip through 280 and back.** It was reported too big and
   then too small the same day, which is what a drawing is for and this did not
   get one. 420 is the answer: it is a masthead rather than a thumbnail, and the
   page has the width now that it is wide. Still the card's 16:9, so it reads as
   the same object made larger. */
.masthead .lead { width: 420px; flex: none; margin: 0; }
.masthead .words { flex: 1; min-width: 0; }
.masthead h1 { margin: 0 0 var(--space-xs); }
.masthead .src { margin: 0; }
@media (max-width: 860px) {
  .masthead { flex-direction: column; gap: var(--space-md); }
  .masthead .lead { width: 100%; }
}

/* **The two controls under an article.** A heart is a mark somebody made, shown
   beside them and never totalled — `internal/circle-prd.md` §11: never
   aggregate, never sort. So this is a toggle on your own mark, not a tally. */
.acts { display: flex; gap: var(--space-sm); align-items: center;
  margin: 0 0 var(--space-lg); flex-wrap: wrap; }
.heart {
  display: inline-flex; align-items: center; gap: 7px; min-height: var(--touch-min);
  padding: 0 14px; border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
  background: transparent; color: var(--text); cursor: pointer;
  font: 500 13px/1 ui-sans-serif, -apple-system, sans-serif;
}
.heart[aria-pressed="true"] { background: var(--bgSelected); border-color: var(--text); }
.heart .g { font-size: 15px; line-height: 1; color: var(--textSoft); }
.heart[aria-pressed="true"] .g { color: var(--danger); }

.heart-mark { color: var(--danger); }
.compose { margin: 0 0 var(--space-lg); }
.compose textarea {
  width: 100%; min-height: 84px; resize: vertical; padding: 11px 12px;
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text);
  font: 15px/1.5 var(--read);
}
.compose .row { display: flex; justify-content: flex-end; margin-top: var(--space-sm); }

/* **The lead picture on an article's own page.** Full measure, and a fixed
   ratio so the page does not jump as it loads — the same reason the jacket in
   the app keeps the spinner's space. Added 2026-10-15 with the article page. */
.lead {
  width: 100%; aspect-ratio: 16 / 9; overflow: hidden; margin: 0 0 var(--space-md);
  border-radius: var(--radius-sm); background: var(--bgSelected);
}
.lead img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* **184 rather than 148**, asked for 2026-10-16 once there were real pictures
   on Home rather than four placeholders. Still 16:9, which is what every
   `og:image` arrives as — a ratio chosen by the publishers rather than by us,
   so a crop would be us second-guessing what they framed. */
.shot { width: 184px; height: 104px; border-radius: var(--radius-sm); flex: none;
        border: 1px solid var(--line); overflow: hidden;
        background: var(--bgSelected); }
.shot.sm { width: 84px; height: 48px; }

@media (max-width: 560px) {
  /* **Full width, and the blur is an asset problem rather than a layout one.**
   * Stacked, the picture is about 358 CSS px on a phone -- 1074 device pixels
   * at 3x -- and the lead images are 440 wide, so it is an upscale of about
   * 2.4 and it shows.
   *
   * Shrinking the picture fixes the sharpness and loses the thing worth
   * having, so the picture stays and the images want re-pulling at their real
   * size. They are `og:image` cards, usually around 1200px; the originals are
   * in a Fallow Reader library on a phone rather than in this repo, because
   * the copies here were downsized on the way in. Reported and reverted
   * 2026-09-02. */
  .card .body { flex-direction: column-reverse; }
  .shot { width: 100%; height: 132px; }
}

/* ---------- people ---------- */

.av {
  width: 26px; height: 26px; border-radius: var(--radius-full); flex: none;
  background: var(--primary); color: var(--primaryText);
  font: 12px/26px var(--meta); text-align: center;
}
.av.b { background: #7A5A4E; } .av.c { background: #3F5A73; }
.av.d { background: #6E5A82; } .av.e { background: #7A6B4E; }
.av.big { width: 56px; height: 56px; font-size: 22px; line-height: 56px; }

.who { display: flex; align-items: center; gap: var(--space-sm); }

.profile {
  display: flex; gap: var(--space-md); align-items: center;
  padding-bottom: var(--space-md); border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-sm);
}
.profile .name { font: 600 26px/32px var(--read); margin: 0; }
.profile .handle { font: 13px/20px var(--meta); color: var(--textSoft); margin: 0; }
.profile .now { font: 13px/20px var(--meta); color: var(--primary); margin: 2px 0 0; }

/* ---------- controls ---------- */

.btn {
  display: inline-block; font: 13px/18px var(--meta); text-decoration: none;
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
  padding: 10px 14px; color: var(--text); background: transparent;
}
.btn:hover { background: var(--bgSelected); }
.btn.fill { background: var(--primary); border-color: var(--primary); color: var(--primaryText); }
.btn.fill:hover { background: var(--primary); opacity: .9; }
/* **`danger`, and it is the only thing on these pages that gets it.**
   `DESIGN.md` reserves the token — it already means *something is wrong, tell
   us* on the report row, and a colour meaning two things means neither. This is
   the one control here that removes something permanently, so it is the one
   that earns it.

   **Filled, like `Save` and `Try again`**, which is the app's own rule: a fill
   means an action rather than a state, and this is emphatically an action. It
   also makes it the only filled control that is not moss, so it cannot be
   pressed by muscle memory for one of the others. */
.btn.danger {
  background: var(--danger); border-color: var(--danger); color: var(--bgRaised);
}
.btn.danger:hover { background: var(--danger); border-color: var(--danger); opacity: .9; }

/* **The published tab is set apart**, because it is the only one that is not a
   shelf: the other three are derived from where you are in a piece, and this is
   something you did. A rule in the layout says that without a caption. */
.shelves .apart { margin-right: var(--space-sm); position: relative; }
.shelves .apart::after {
  content: '';
  position: absolute;
  /* **The midpoint of the whole gap, not of the margin.** The list's own
     `gap` is `space-sm` and this adds another, so the space after Published is
     two of them — and half of two is one. It sat at half of one and leaned
     toward Published, reported 2026-10-16. */
  right: calc(var(--space-sm) * -1);
  top: 15%; bottom: 15%; width: 1px; background: var(--lineStrong);
}

.shelves { display: flex; gap: var(--space-sm); margin: 0 0 var(--space-sm); flex-wrap: wrap; }
/* A shelf is a link in the drawn pages and a button on the real one, because
   choosing one is an act rather than a destination. Both take the same rule:
   `appearance` and `margin` because a button carries WebKit's own metrics
   otherwise, and `font` because a button does not inherit one. */
.shelves a, .shelves button {
  font: 12px/18px var(--meta); text-decoration: none; color: var(--textSoft);
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm); padding: 8px 12px;
  appearance: none; -webkit-appearance: none; background: none; margin: 0;
  min-height: 44px; cursor: pointer;
}
.shelves a[aria-current], .shelves button[aria-current] {
  background: var(--bgSelected); border-color: var(--text); color: var(--text);
}

/* ---------- the discussion ---------- */

.said { display: flex; gap: 12px; padding: var(--space-md) 0; border-top: 1px solid var(--line); }
.said .m { flex: 1; min-width: 0; }
.said .n { font: 12px/20px var(--meta); color: var(--text); margin: 0; }
.said .n a { color: var(--text); text-decoration: none; }
.said .n .badge { color: var(--primary); }
.said p { font-size: 16px; line-height: 25px; margin: var(--space-xs) 0 0; }
.said blockquote {
  margin: var(--space-sm) 0 0; padding: 0 0 0 12px;
  border-left: 2px solid var(--accent); font-style: italic; color: var(--text);
}

.gate {
  border-top: 1px solid var(--line); background: var(--bgRaised);
  padding: var(--space-md); margin-top: 0;
}
.gate p { font-size: 16px; line-height: 25px; margin: 0 0 12px; }
.gate .say {
  border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--bg);
  padding: 12px; font: 14px/22px var(--meta); color: var(--textSoft);
}

/* **The read gate is gone — 2026-10-15.** It hid `#read` until the URL carried
   a hash, so there were two states switched by `:target` and no JavaScript.
   Decided against: *we should be able to see the comment even if I never even
   touched the article.* Removing the rule without removing this note, because
   deleting the markup alone left a page showing a picture, a headline and
   nothing else — the comments were in the file the whole time, hidden here. */

/* ---------- the sponsor ----------
 *
 * **In the margin, not in the column** — 2026-09-02, and it answers the
 * objection the concept page raised against itself. Drawn inside the feed, the
 * slot was **the same size as a person**: somebody who read the piece and had
 * something to say sat at exactly the weight of somebody who paid to be there,
 * on a page whose whole premise is *a specific person you trust*.
 *
 * Out here it is beside the reading rather than in it, and it costs the column
 * nothing. The ground was already empty; this is what the empty part is for.
 *
 * **It disappears entirely below 1300px** rather than folding into the feed,
 * which is the rule that keeps the argument true: if there is no margin, there
 * is no sponsor. It is never allowed back among the people.
 */
.rail {
  position: fixed; top: 92px; right: 20px; width: 168px; z-index: 5;
  display: none;
}
@media (min-width: 1300px) { .rail { display: block; } }

/* **Beside the feed rather than fixed to the window.** Asked 2026-10-15 after
   the fixed rail overlapped the page: it sat at `right: 20px` from 1300px wide,
   while `main.wide` is 1200 -- so from 1300 to about 1660 the two occupied the
   same pixels. Positioned in the layout it cannot overlap by construction,
   which is the better fix than moving a breakpoint.

   **And it is on the left**, which is where it was asked for and is also the
   quieter half: the eye starts at the headline, not at the sponsor. */
.cols .rail.beside {
  /* **Static, and it stays where the page put it.** Asked 2026-10-15: it was
     sticky so it would stay with you down a long feed, and what that actually
     did was follow you — a sponsor that tracks the reader down a page is the
     one thing `index.html` says it must never be, arriving as a scroll
     behaviour rather than as a placement. It sits at the top of its column and
     scrolls away like everything else. */
  position: static; display: block; width: 168px; flex: none; order: -1;
  align-self: flex-start;
}
@media (max-width: 860px) {
  /* Stacked, it goes last. A sponsor above the first article would be the one
     thing `index.html` says it must never be: among the people. */
  .cols .rail.beside { width: 100%; position: static; order: 99; }
}

.sponsor {
  border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--bgRaised); padding: 14px; margin: 0;
}
.sponsor .k { font: 9px/14px var(--meta); letter-spacing: .16em; text-transform: uppercase;
              color: var(--textSoft); margin: 0; }
/* **A profile's identity block.** Added 2026-10-16.

   **The face is a coloured initial and that is the default rather than the
   fallback** — it is free, it cannot fail to load, it needs no upload and no
   storage, and it is what every reader has on the day friends lands. A picture
   is the option on top of it, which is the right way round: B-xxx's slice 2a
   ships alone and is most of the value. */
.whois { display: flex; gap: var(--space-md); align-items: center;
         margin: 0 0 var(--space-lg); }
.whois .face {
  width: 64px; height: 64px; border-radius: var(--radius-full); flex: none;
  display: flex; align-items: center; justify-content: center;
  font: 600 26px/1 var(--read); color: var(--primaryText);
  background: var(--primary);
}
.whois .face img { width: 100%; height: 100%; border-radius: var(--radius-full);
                   object-fit: cover; display: block; }
.whois h1 { margin: 0 0 2px; }
.whois .at { font: 12px/18px var(--meta); color: var(--textSoft); margin: 0;
             display: flex; align-items: center; gap: var(--space-sm);
             flex-wrap: wrap; }
/* The visibility pill is shown only on your own profile — it is the answer to
   "who can see this" standing where the question is asked. */
.whois .vis { display: inline-block; font: 10px/1 var(--meta);
              letter-spacing: .06em; text-transform: uppercase;
              padding: 5px 8px; border-radius: var(--radius-full);
              background: var(--bgSelected); color: var(--textSoft); }

.sponsor + .sponsor { margin-top: var(--space-md); }
.sponsor .t { font: 600 16px/22px var(--read); margin: 4px 0 3px; }
/* **The name links out, and reads as a name rather than a link.** A sponsor
   that underlines itself in `link` blue would be the one thing on the page
   shouting; the destination is obvious from context and the hover says it. */
.sponsor .t a { color: var(--text); text-decoration: none; }
.sponsor .t a:hover { text-decoration: underline; }
.sponsor .d { font-size: 13px; line-height: 20px; color: var(--textSoft); margin: 0; }

/* ---------- the foot ---------- */

footer {
  border-top: 1px solid var(--line); margin-top: var(--space-xl);
  padding: var(--space-lg) var(--space-md);
  font: 12px/21px var(--meta); color: var(--textSoft); text-align: center;
}
footer a { color: var(--textSoft); }

a { color: var(--link); }
a:focus-visible, .btn:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

.locked { text-align: center; padding: var(--space-xl) var(--space-md);
          color: var(--textSoft); font-size: 16px; line-height: 25px; }

/* ---------- two columns, and what goes in the second ----------
 *
 * **Letterboxd puts lists beside the feed and ours cannot be the same list.**
 * Theirs is *popular this week* — global, ranked, a leaderboard by another
 * name, which is the same thing the vote refused. So the column holds two
 * things that are not rankings: **lists somebody made and put their name on**,
 * and the people in your circle who made them.
 */
.cols { display: flex; gap: var(--space-xl); align-items: flex-start; }
.cols .feed { flex: 1; min-width: 0; }
.cols aside { width: 268px; flex: none; }

@media (max-width: 860px) {
  .cols { flex-direction: column; gap: var(--space-lg); }
  .cols aside { width: 100%; position: static; }
}

aside h2 { margin-top: 0; }
aside h2 + p { font-size: 14px; line-height: 21px; color: var(--textSoft); }

.list {
  display: block; text-decoration: none; color: inherit;
  padding: 12px 0; border-top: 1px solid var(--line);
}
.list:last-of-type { border-bottom: 1px solid var(--line); }
.list:hover .lt { text-decoration: underline; }
/* **Under the title now, not above it.** Asked 2026-10-15, and it matches what
   the cards already do: the name is what you are choosing between, and the
   pictures are what it is made of. */
.stack { display: flex; gap: 3px; margin: var(--space-sm) 0 0; }
.stack img, .stack span {
  width: 46px; height: 30px; border-radius: var(--radius-sm); object-fit: cover; display: block;
  background: var(--bgSelected);
}
.lt { font: 600 16px/22px var(--read); margin: 0 0 2px; color: var(--text); }
.lby { font: 11px/17px var(--meta); color: var(--textSoft); margin: 0; }
.list .stack + .lby { margin-top: var(--space-sm); }
.lby .cur { color: var(--primary); }

/* A picture the app already downloaded, at the size it is drawn.
   `img-src 'self'` — nothing is fetched from a publisher when this page opens,
   which is R-1's rule about the reader pointed at the web half. */
.shot img, img.shot { width: 100%; height: 100%; object-fit: cover;
                      border-radius: 0; display: block; }


/* ---------- settings ---------- */
.set { border-top: 1px solid var(--line); padding: var(--space-md) 0; }
.set:last-of-type { border-bottom: 1px solid var(--line); }
.set .k { font: 600 16px/23px var(--read); margin: 0 0 2px; }
.set .h { font-size: 14px; line-height: 21px; color: var(--textSoft); margin: 0; }
.set .row { display: flex; gap: var(--space-md); align-items: flex-start; }
.set .row > div { flex: 1; min-width: 0; }

.opts { display: flex; gap: var(--space-sm); flex-wrap: wrap; margin: 12px 0 0; }
.opt {
  font: 12px/18px var(--meta); text-decoration: none; color: var(--textSoft);
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm); padding: 9px 13px;
}
/* **A button, not a link, since 2026-10-13** — this is a setting rather than a
   destination, and the three were links to the page they were already on. The
   appearance is unchanged on purpose: the page should not look different on the
   day it starts working. */
button.opt { background: none; cursor: pointer; font-family: var(--meta); }
button.opt:disabled { opacity: 0.35; cursor: default; }
.opt[aria-current],
.opt[aria-checked="true"] { background: var(--bgSelected); border-color: var(--text); color: var(--text); }

.field {
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
  background: var(--bgRaised); padding: 10px 12px; font: 14px/22px var(--meta);
  color: var(--text); margin: 10px 0 0; max-width: 320px;
}
.field .ph { color: var(--textSoft); }

/* A name being edited — 2026-09-25. **The field keeps the shape it had when it
   was a drawing**, so the page does not change appearance on the day it starts
   working; what arrives is an input inside it and a button beside it. */
.name-form { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.name-form .field { flex: 1 1 220px; display: flex; align-items: center; gap: 2px; }
.name-form input {
  flex: 1; min-width: 0; border: 0; background: none; padding: 0;
  font: inherit; color: inherit;
}
.name-form input:focus { outline: none; }
/* The focus ring goes on the box rather than the input, because the `@` sits
   outside the input and belongs to the control. */
.name-form .field:focus-within { outline: 2px solid var(--link); outline-offset: 1px; }
.name-form button {
  font: 13px var(--meta); padding: 7px 13px; border-radius: 3px;
  border: 1px solid var(--lineStrong); background: var(--bgRaised);
  color: var(--text); cursor: pointer;
  min-height: 34px;
}
.name-form button[disabled] { opacity: .5; cursor: default; }
/* **Said in place rather than in a banner.** It belongs to the field that
   caused it, and it takes a line whether or not it speaks — DESIGN.md's rule
   that an indicator never goes in the layout. */
.says { flex-basis: 100%; margin: 4px 0 0; font: 12px var(--meta); min-height: 1.2em; }
.says.bad { color: var(--danger); }
.says.good { color: var(--textSoft); }

/* ---------- search ---------- */
.searchbar {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
  background: var(--bgRaised); padding: 12px 14px; margin: 0 0 var(--space-sm);
  font: 15px/22px var(--meta);
}
.searchbar .q { flex: 1; color: var(--text); }
.searchbar .scope { font-size: 11px; color: var(--textSoft); white-space: nowrap; }
.scopes { display: flex; gap: var(--space-sm); margin: 0 0 var(--space-lg); flex-wrap: wrap; }
.hit { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid var(--line); align-items: center; }
.hit:last-of-type { border-bottom: 1px solid var(--line); }
.hit .m { flex: 1; min-width: 0; }
.hit .t { font: 600 16px/22px var(--read); margin: 0 0 2px; }
.hit .t a { color: var(--text); text-decoration: none; }
.hit .s { font: 10px/16px var(--meta); letter-spacing: .08em; text-transform: uppercase; color: var(--textSoft); margin: 0; }
.hit .lock { font: 11px/17px var(--meta); color: var(--textSoft); }

/* **A row's menu, built on the same `<details>` the account menu is.** It opens
   and closes with no script, which was not a stylistic choice — these pages were
   written under a policy with no `script-src` in it — and is still the simpler
   thing now the constraint has gone. */
.rowmenu { position:relative; flex:none; }
.rowmenu > summary {
  list-style:none; width:22px; height:22px; display:flex; align-items:center;
  justify-content:center; cursor:pointer; border-radius:var(--radius-sm);
}
.rowmenu > summary::-webkit-details-marker { display:none; }
.rowmenu > summary::marker { content:""; }
/* 22pt of glyph, and the hit area is the row's own height — the same trade
   `MoreButton` makes, where a slop that overlapped its neighbour's would make
   which control you hit a matter of stacking order. */
.rowmenu > summary svg { width:16px; height:16px; display:block; fill:var(--textSoft); stroke:none; }
.rowmenu > summary:hover svg { fill:var(--text); }
.rowmenu > summary:focus-visible { outline:2px solid var(--text); outline-offset:2px; }
.rowmenu .menu { min-width:150px; top:calc(100% + 6px); }
.rowmenu .menu::before { right:6px; }
/* Destructive, and `danger` is reserved rather than free — the same colour the
   report row and sign-out already carry, which is the same class of thing rather
   than a second meaning for it. */
.menu .item.danger { color:var(--danger); }
.menu .item.asking { color:var(--danger); background:var(--bgSelected); }

/* ---- search, in the bar ----
 *
 * **A real field, which needed one line of the published policy.**
 * `form-action` was `'none'`, so a plain GET form was refused and the control
 * had to be a link that looked like a box — a dead control, and worse than no
 * box at all. It is `'self'` now: a form here can reach this origin and nowhere
 * else, which is what the directive is actually for. **Still no JavaScript.**
 */
.find {
  display:flex; align-items:center; gap:8px; margin-left:auto;
  border:1px solid var(--lineStrong); border-radius:var(--radius-sm);
  background:var(--bg); padding:0 4px 0 11px; min-width:0; flex:1; max-width:280px;
}
.find:focus-within { border-color:var(--text); }
.find input {
  flex:1; min-width:0; order:1; border:0; outline:0; background:transparent;
  font:13px/19px var(--meta); color:var(--text); padding:8px 0;
  -webkit-appearance:none; appearance:none;
}
.find input::placeholder { color:var(--textSoft); opacity:1; }
.find input::-webkit-search-cancel-button { -webkit-appearance:none; }
/* The glyph is a submit control and sits where one goes — after the words,
   on the right — so the field reads as something you type into and then send. */
.find button {
  order:2; flex:none; border:0; background:transparent; cursor:pointer;
  padding:8px 7px; line-height:0; border-radius:var(--radius-sm);
}
.find button:hover { background:var(--bgSelected); }
.find svg { width:13px; height:13px; stroke:var(--textSoft); fill:none; stroke-width:2; display:block; }
.find button:hover svg { stroke:var(--text); }
.bar .me { margin-left:var(--space-md); }

@media (max-width:560px) {
  .bar { flex-wrap:wrap; }
  .find { order:3; max-width:none; margin-left:0; flex-basis:100%; }
  .bar .me { margin-left:auto; }
}

/* ---- pictures ----
 *
 * **Square, with the shelf's hairline.** `BookRow` draws a cover at
 * `borderWidth: 1, borderColor: t.line` -- a picture is a thing on a page
 * rather than a control, so it takes an edge instead of a corner radius, and
 * a pale photograph does not bleed into the paper.
 */
.stack img { border:1px solid var(--line); border-radius:var(--radius-sm); }

/* ---- the account, in the bar ----
 * An avatar rather than a handle: it is the same mark that stands for a person
 * everywhere else on these pages, so the bar says "you" in the vocabulary the
 * rows already use. The name stays as the label a screen reader hears.
 */
/* **`<details>` rather than a script**, so it opens on a click and on a
   keyboard, announces its own state, and works on the pages that load no
   JavaScript at all. It does not close on an outside click, which is the one
   thing the element does not give -- and a menu that stays open until you
   dismiss it is a smaller fault than a menu that needs a script to exist. */
/* **`flex:none`, or the search field squeezes it.** `.find` is `flex:1` and a
   `<details>` is a plain block with no flex opinion, so the account was the
   thing that gave -- wrapping the handle above the avatar and crushing it. */
.me { position:relative; margin-left:var(--space-md); flex:none; }
/* **The flex goes inside the `summary`, never on it.** Safari has never laid
   out a `summary` as a flex container -- it keeps treating it as a disclosure
   box -- so `display:flex` there silently did nothing and the handle and the
   avatar stacked as inline content in a block that was too narrow. The element
   stays a `summary` and a plain span does the row. */
.me > summary {
  display:block; cursor:pointer; list-style:none;
  border-radius:var(--radius-sm); -webkit-tap-highlight-color:transparent;
}
/* **Three of them, and all three are needed.** `list-style:none` alone leaves
   the triangle in Safari, `::-webkit-details-marker` is the old WebKit hook,
   and `::marker` is the standard one Chrome uses — a browser answers to one of
   the three and nothing tells you which. */
.me > summary::-webkit-details-marker { display:none; }
.me > summary::marker { content:""; }
.me > summary::before { display:none; }
/* The avatar leads, and the handle follows it — the same order a person is
   drawn in on every row of these pages. */
.me .tag {
  display:flex; align-items:center; gap:9px; white-space:nowrap;
  color:var(--textSoft); font:12px/18px var(--meta);
}
.me .tag .av { order:0; }
.me .tag .who { order:1; }
.me > summary:hover .tag,
.me[open] > summary .tag { color:var(--text); }
.me .av { width:32px; height:32px; font-size:13px; line-height:32px; flex:none; }
/* **It floats over the page rather than pushing it down.** `position:absolute`
   against a positioned `.me`, so it leaves the flow entirely -- the bar keeps
   its height and nothing below it moves when the menu opens. `layer.panel` is
   the app's own answer to the same question; here it is one number because
   there is one thing above the page.

   The shadow is the only one on these pages, and it is spent deliberately:
   `DESIGN.md` reserves elevation for something genuinely sitting above the
   page, and a menu is the only thing here that is. */
/* **The hover bridge for the account menu.** There is a 10px gap between the
   avatar and the panel, and a cursor crossing it would leave the control before
   it reached the menu — so the gap is covered by a transparent strip belonging
   to the menu's own box. Without it the thing opens and shuts as you reach. */
.acct-slot details > .menu::after {
  content: ''; position: absolute;
  /* **Wider than the panel, and tall enough to cover the gap twice.** Reported
     as tight: a cursor travelling diagonally from the avatar leaves the box at
     the corner, and a strip exactly the panel's width does not catch it. */
  left: -16px; right: -16px; top: -16px; height: 18px;
}

.menu {
  position:absolute; right:0; top:calc(100% + 10px); z-index:40; min-width:198px;
  background:var(--bgRaised); border:1px solid var(--lineStrong);
  border-radius:var(--radius-md); padding:4px;
  box-shadow:0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.10);
}
@media (prefers-color-scheme:dark) {
  .menu { box-shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 28px rgba(0,0,0,.5); }
}
/* A small tail, so it reads as belonging to the avatar rather than as a card
   that happens to be nearby. */
.menu::before {
  content:""; position:absolute; right:12px; top:-5px; width:8px; height:8px;
  background:var(--bgRaised); border-left:1px solid var(--lineStrong);
  border-top:1px solid var(--lineStrong); transform:rotate(45deg);
}
/* **Not `.card`, which is an article row on every other page** -- that rule
   sets its own borders, padding and a flex body, and the two were fighting
   inside a 198px menu. Its own name, so neither has to out-specify the other. */
.acct {
  padding:11px 11px 12px; border-bottom:1px solid var(--line); margin:0 0 4px;
}
.acct .nm { font:600 15px/21px var(--read); margin:0; color:var(--text); }
.acct .hd { font:11px/17px var(--meta); color:var(--textSoft); margin:1px 0 0; }
/* **A row, whether it is a link or a button.** Sign out is a `<button>` and
   every rule here named `a`, so it kept the browser's own padding, border,
   background and font and sat out of line with the two above it — reported
   2026-09-04 as being offset from the rest of the popup. The reset is what a
   button needs to stop being one visually; the shared selector is what stops
   the next one drifting the same way. */
.menu a,
.menu .item,
.menu .out {
  display:block; width:100%; box-sizing:border-box; text-align:left;
  padding:9px 11px; text-decoration:none; border-radius:var(--radius-sm);
  font:12px/18px var(--meta); color:var(--text);
  background:none; border:0; cursor:pointer;
  /* **`appearance:none`, which is the half that was missing.** A `<button>` with
     the background and border taken off still carries the platform's own
     control appearance in WebKit, and that brings its own metrics and centring
     with it — so the row matched on paper and still sat wrong, which is what was
     reported twice. `margin:0` for the same reason: a UA gives a button one and
     nothing else in this menu has any. */
  appearance:none; -webkit-appearance:none; margin:0;
}
.menu a:hover,
.menu .out:hover { background:var(--bgSelected); }
/* **`danger`, and it is reserved rather than free.** Sign out is the one control
   here that ends something, and `DESIGN.md` gives that colour to the report row
   — this is the same class of thing rather than a second meaning for it.

   **This was two rules and that is why it looked wrong twice.** A second
   `.menu .out` sat two hundred lines further down with the same specificity, so
   it won on order and brought `padding: 8px 0 0` with it — no horizontal
   padding at all, which is the row sitting left of the two above it, and the
   red. Nothing about the earlier rule was wrong and nothing about editing it
   could have shown. One rule now.

   `min-height` is `touch.min`, from the rule this menu is a control in. */
.menu .out {
  color:var(--danger); border-top:1px solid var(--line);
  margin-top:4px; padding-top:11px; border-radius:0; min-height:44px;
}

/* ---- search, in the bar ----
 *
 * **A real field, which needed one line of the published policy.**
 * `form-action` was `'none'`, so a plain GET form was refused and the control
 * had to be a link that looked like a box — a dead control, and worse than no
 * box. It is `'self'` now: a form here reaches this origin and nowhere else,
 * which is what the directive is for.
 *
 * **The glyph is inside the field and does not take the click.** It marks what
 * the box is for and nothing else; Enter submits, which is what a single-input
 * form does with no button at all. A magnifier that were a button would be a
 * second way to do the same thing, sitting where a reader's eye goes first.
 */
/**
 * **The nav pushes, not the search.** Reported 2026-10-16: on a page with no
 * search form — the 404 — the icons and the avatar sat against the nav instead
 * of at the right-hand end, because `margin-left: auto` on `.find` was the only
 * thing holding them there and `.find` was not on the page.
 *
 * Moving the push to the nav means the right-hand group is right whether or not
 * anything sits between them.
 */
.bar nav { margin-right: auto; }

/* Centred for a while on 2026-10-15 and put back the same day. */
.find {
  display:flex; align-items:center; gap:9px; flex:1;
  min-width:0; max-width:300px;
  border:1px solid var(--lineStrong); border-radius:var(--radius-sm);
  background:var(--bg); padding:0 12px;
}
.find:focus-within { border-color:var(--text); }
.find > svg {
  width:14px; height:14px; flex:none; stroke:var(--textSoft);
  fill:none; stroke-width:2; stroke-linecap:round;
}
.find:focus-within > svg { stroke:var(--text); }
.find input {
  flex:1; min-width:0; border:0; outline:0; background:transparent;
  font:13px/19px var(--meta); color:var(--text); padding:9px 0;
  -webkit-appearance:none; appearance:none;
}
.find input::placeholder { color:var(--textSoft); opacity:1; }
.find input::-webkit-search-cancel-button,
.find input::-webkit-search-decoration { -webkit-appearance:none; }
/* Enter submits; this exists so the form has an explicit submitter and a
   screen reader is told what the field does. */
.find button {
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}

@media (max-width:560px) {
  .bar { flex-wrap:wrap; }
  .find { order:3; max-width:none; margin-left:0; flex-basis:100%; }
  .bar .me { margin-left:auto; }
}


/* ── the colophon ────────────────────────────────────────────────────────
 *
 * Badius Ascensius's own device: the *Prelum Ascensianum*, two men working a
 * press with a compositor at the case behind. Public domain, ~1520.
 *
 * **At the foot, because that is what a device is for.** A printer's mark was
 * cut for the end of a book -- this is the end, here is who made it, stop --
 * which answers where it goes without a rule being invented. It is never
 * behind content: a mark under somebody else's sentences is decoration on
 * their words.
 *
 * **No filter, and that is a selection rule rather than a saving.** This scan
 * arrived on white, so `multiply` drops the paper for nothing. A scan on aged
 * paper would need a levels pass to clip its white point first -- so the rule
 * is simply that a mark on white gets used and a mark on tan does not.
 *
 * **Light multiplies, dark inverts and screens**, which is the same idea
 * mirrored: multiply drops white, screen drops black, and inverting turns the
 * paper black so the second one can. Opacity then only ever fades the line,
 * because the paper is already a no-op in both. */
.colophon {
  text-align: center;
  padding: var(--space-xl) 0 var(--space-xl);
  margin-top: var(--space-lg);
  border-top: 1px solid var(--line);
}
/* **The mark goes to where it came from.** A credit that is a link is a
 * credit somebody can check, which is the difference between naming a source
 * and citing one -- and it is the same debt B-139 spent a long argument on
 * over Project Gutenberg's name. It is also the only outbound link on the
 * page that is not to a publisher, and it leaves deliberately: a colophon
 * saying who cut a woodcut in 1520 is worth following.
 *
 * `noreferrer` as well as `noopener`: the second is the security one, and the
 * first means Commons is not told which page somebody was reading when they
 * followed it. A reader clicking a credit has told us they are curious, and
 * that is not a fact to hand on. */
/* **`fit-content` and a real margin, not `inline-block` inside `text-align`.**
 * As an inline-block clamped to a percentage, the anchor could end up narrower
 * than the image inside it -- and an image overflows its box rather than
 * shrinking, so the mark hung off to one side and read as badly centred. The
 * anchor now takes its width from its content, is centred by margin, and the
 * image is bounded by it rather than by a guess. */
.colophon a {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
  text-decoration: none;
  color: inherit;
  padding: var(--space-sm);
  border-radius: var(--radius-sm);
}
.colophon a:hover span,
.colophon a:focus-visible span { color: var(--text); opacity: 1; }
.colophon a:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
.colophon img {
  /* **160, chosen on a phone against a page holding nothing but the mark.**
   * Every earlier round was judged against a whole page and, three times,
   * against a stylesheet Safari was holding from before the change -- so of
   * 108, 168, 220 and 330, several were never actually seen.
   *
   * **Five of the eleven are upscaled here, and that is the scans' ceiling
   * rather than an export choice.** Villanueva is 325px on Commons, Ramirez
   * 336, Badius 348, and asking Commons for a larger thumbnail returns the
   * same pixels enlarged. 104 was the last size at which every mark was
   * sharp; 160 spends 0.7x on the five oldest to get a mark that reads.
   *
   * The trade is smaller than the number looks. These are high-contrast line
   * cuts rather than photographs, drawn at 0.9 through `multiply` on a
   * textured ground, so an upscale softens an already-soft woodblock edge and
   * there is no detail to lose. Past roughly 240 the small ones do show it.
   *
   * `?mark=N` overrides this at runtime, which is the only honest way to judge
   * a size like this on a phone. Read in `colophon.js`; it sets an inline
   * width, so a stale one in the URL beats everything here. */
  /* No percentage max-width on the image. The anchor around it is shrink-to-
   * fit, and a percentage on a child of such a box resolves against a box
   * whose size that child is deciding. Browsers break the loop by falling back
   * to the available width, and the mark came out far smaller than it was set
   * to. The anchor takes the constraint instead, where there is a real width
   * to be a fraction of. */
  width: 160px;
  max-width: 100%;
  height: auto;
  display: block;
  margin: 0 auto;
  mix-blend-mode: multiply;
  opacity: 0.9;
}
.colophon span {
  display: block;
  font-family: var(--font-meta);
  font-size: var(--text-tag);
  line-height: var(--leading-micro);
  color: var(--textSoft);
  margin: var(--space-sm) 0 0;
  opacity: 0.75;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .colophon img {
    mix-blend-mode: screen;
    filter: invert(1);
  }
}
/* and again for an explicit choice -- see the ground above, which had this
 * exact gap and drew a tan margin around a dark page because of it. */
:root[data-theme="dark"] .colophon img {
  mix-blend-mode: screen;
  filter: invert(1);
}

/* ── signing in ─────────────────────────────────────────────────────────
 *
 * **The account slot holds its width before it knows.** Every page here is a
 * static file, so the header cannot be rendered signed-in or signed-out on the
 * server: it ships empty and `account.js` fills it. Shipping "Sign in" and
 * swapping would flicker a reader's own name in over a button telling them to
 * sign in; shipping the menu would show a signed-out reader an account.
 *
 * A reserved width matters more here than on a phone — this sits at the end of
 * a bar that runs the width of the window, so a slot that grows on arrival
 * reflows the search field beside it. `DESIGN.md`'s rule about an indicator
 * never going in the layout, applied to an answer rather than a spinner. */
/* **Notifications and friends, left of the account.** Added 2026-10-15.
   Stroked like the search glyph and the coffee cup beside them, because the
   site has no icon font and a stroked mark holds up small and in greyscale.

   **Real targets.** `CLAUDE.md`'s 44pt floor is about a thumb, and these are
   the smallest things in the bar — so the pad does the work the glyph cannot.

   **Both are inert today and say so to a screen reader**, which is the honest
   version of a control that does nothing: the label names what it will be and
   `aria-disabled` stops it being announced as something to press. Friends is
   R-12.6 and is tomorrow's; the request notice is B-279.5, drawn and unbuilt. */
/* The two of them and the avatar read as one group, so they sit together and
   the bar's gap separates the group from the search rather than its members. */
/**
 * **Three circles, one group, evenly spaced — and the arithmetic is written
 * down because it is not guessable.**
 *
 * Each button is 44pt for the thumb and holds a 26pt circle, so there is **9pt
 * of dead space on every side**. Two buttons side by side therefore read as an
 * 18pt gap, while the last button to the avatar reads as 9 + the bar's own
 * 16pt gap = 25pt. That is why they looked unevenly spaced and too far from
 * the avatar: nothing was wrong with the values, the targets are just bigger
 * than the things drawn in them.
 *
 * **Wanted: 10pt between all three.** So each button overlaps its neighbour by
 * 4 (18 − 8 = 10), and the account slot is pulled 15 closer (25 − 15 = 10).
 * The targets still do not overlap each other's centres, which is the rule
 * `DESIGN.md` actually cares about.
 */
.bar-icons { display: flex; align-items: center; gap: 0; }
.bar-icon + .bar-icon { margin-left: -8px; }
/**
 * Circles, matching the account avatar beside them — 26pt drawn, 44pt to hit.
 *
 * **The circle is the drawn size and the button is the target**, which is the
 * split `IconButton` makes in the app: a control that must *look* small takes
 * the extra as padding rather than growing. `DESIGN.md` names both halves and
 * says never neither.
 */
.bar-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--touch-min); height: var(--touch-min);
  border: 0; background: transparent; padding: 0; color: var(--textSoft);
}
/* **Filled, like the avatar beside them.** Asked for 2026-10-16: an outlined
   circle next to a solid one reads as two kinds of thing, and these three are
   one group. The fill is `bgSelected` rather than `primary` — moss is what the
   account's own mark takes, and spending it on a neighbour would stop the
   avatar being the thing your eye goes to. */
.bar-icon .ring {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: var(--radius-full);
  background: var(--bgSelected); color: var(--textSoft);
}
.bar-icon:hover .ring { background: var(--lineStrong); color: var(--bgRaised); }
.bar-icon[aria-disabled="true"] { opacity: .55; cursor: default; }
.bar-icon[aria-disabled="true"]:hover .ring { background: transparent; }
.bar-icon svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}

/* **No min-width.** It reserved 88px so a signing-in state could not shift the
   bar, and with the two circles beside it that reserve became a gap: the icons
   sat a thumb away from the avatar they belong with. The group holds itself
   together now and the bar's own gap does the separating. */
.acct-slot { display: flex; justify-content: flex-end; margin-left: -15px; }
/* The server did not answer. **Muted rather than an alarm** — nothing is wrong
   with the reader's account and nothing of theirs is at risk; the page simply
   could not ask. It holds the slot's width like everything else in it, so a
   late answer never reflows the search field beside it. */
.acct-off {
  align-self: center;
  font: 12px var(--meta);
  color: var(--textSoft);
  letter-spacing: .02em;
  cursor: default;
}

/* The form replaces its own step rather than showing both at once. A dimmed
 * second field is a control that cannot be used yet, for the whole of the first
 * step — and the six digits arrive in another app, so a reader comes back
 * having lost their place. Drawn in 2026-09-02-signing-in-on-the-web.html. */
[data-signin] label {
  display: block;
  font-family: var(--font-meta);
  font-size: var(--text-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--textSoft);
  margin: var(--space-lg) 0 var(--space-xs);
}
[data-signin] .hint {
  font-size: 14px;
  line-height: 21px;
  color: var(--textSoft);
  margin: 0 0 var(--space-sm);
}
[data-signin] input {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-meta);
  font-size: var(--text-ui);
  color: var(--text);
  background: var(--bgRaised);
  /* `lineStrong`, not `line` — DESIGN.md measured `line` at about 1.4:1, which
   * fails WCAG 1.4.11 as the boundary of a control, and a text field is the
   * case that document names as indefensible. */
  border: 1px solid var(--lineStrong);
  border-radius: var(--radius-sm);
  padding: 11px 12px;
  min-height: 44px;
}
[data-signin] input:focus-visible {
  outline: 2px solid var(--link);
  outline-offset: 2px;
}
/* The code is six digits read off another screen and typed back, so it is set
 * wide and large — this is the one field in the product where a transcription
 * error is the likely failure. */
[data-signin] #cd {
  letter-spacing: 0.4em;
  font-size: 22px;
  text-align: center;
}
[data-signin] .go {
  display: block;
  width: 100%;
  margin-top: var(--space-md);
  min-height: 44px;
  font-family: var(--font-meta);
  font-size: var(--text-ui);
  background: var(--primary);
  color: var(--primaryText);
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
[data-signin] .go:hover { opacity: 0.9; }
[data-signin] .alt {
  display: flex;
  gap: var(--space-md);
  margin: var(--space-md) 0 0;
}
[data-signin] .alt button {
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--font-meta);
  font-size: var(--text-meta);
  color: var(--link);
  text-decoration: underline;
  cursor: pointer;
}
/* One line, and it is a live region — a reader using a screen reader is told
 * what happened without having to go looking for it. */
.says {
  font-family: var(--font-meta);
  font-size: var(--text-meta);
  color: var(--textSoft);
  margin: var(--space-sm) 0 0;
  min-height: 21px;
}
.says.bad { color: var(--danger); }

/* ---- Videos, and one of them being watched ------------------------------
 *
 * **The phone's landscape page with more room**, which is the whole economy of
 * building this second: the two columns, the rail, the lane and the marks list
 * are all decided, so what is here is those decisions in CSS rather than a
 * second arrangement of the same parts.
 */

.vgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: var(--space-lg) var(--space-md);
  margin-top: var(--space-lg);
}
.vcard { display: block; text-decoration: none; color: var(--text); min-width: 0; }
/* The still carries its own edge rather than a corner, like `.shot` above and
   for the same reason: it is a picture on a page rather than a control. */
.vshot {
  display: block; position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--bgSelected);
}
.vshot img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Where you got to, on the still — completed and never remaining. */
.vshot i {
  position: absolute; left: 0; bottom: 0; height: 3px;
  background: var(--primary); display: block;
}
.vt { display: block; font: 600 17px/23px var(--read); margin-top: var(--space-sm); }
.vcard:hover .vt { text-decoration: underline; }
.vm {
  display: block; font: 11px/18px var(--meta); color: var(--textSoft);
  margin-top: 4px;
}

.watch { display: flex; gap: var(--space-lg); align-items: flex-start; }
.wmain { flex: 1; min-width: 0; }
.wside {
  width: 300px; flex: none; border-left: 1px solid var(--line);
  padding-left: var(--space-md);
}
/* **The player's own box, and nothing drawn on it.** YouTube's terms do not
   allow an overlay, which is why the lane sits under the rail rather than on
   the picture — the same rule the phone follows. */
.wstage {
  aspect-ratio: 16 / 9; background: #000; border-radius: var(--radius-sm);
  overflow: hidden;
}
.wstage iframe { width: 100%; height: 100%; border: 0; display: block; }

.wrail {
  height: 3px; background: var(--line); border-radius: 3px;
  margin: var(--space-sm) 3px 0; position: relative;
}
.wrail i {
  position: absolute; left: 0; top: 0; bottom: 0; background: var(--primary);
  border-radius: 3px; display: block;
}
/* Under the rail and never on it: the rail keeps one job — where you are —
   and what you kept lives below it. */
/* **Both are held off their own edges by half a capsule.** A mark is centred on
   its second, so one at `00:00` puts half a circle outside the lane and reads as
   a mark that has been cut in half. The inset is the fix rather than clamping,
   because a clamp would move it — a mark at the start and one two seconds in
   would draw in the same place, and the lane's whole job is to say *where*.
   The rail takes the same inset, and that is what makes it one number: the
   lane's percentages are of the rail's width. `LANE_INSET` in the app. */
.wlane { position: relative; height: 10px; margin-top: 4px; margin-left: 3px; margin-right: 3px; }
.wcap {
  position: absolute; top: 2px; height: 6px; width: 6px; min-width: 6px;
  padding: 0; border: 0; border-radius: 999px; background: var(--primary);
  cursor: pointer; transform: translateX(-3px);
}
.wcap:hover, .wcap:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* **The title and the way to mark, on one line.** The button is not a
   convenience beside the key: it is the only route that works while the player
   has focus, which is most of the time somebody is watching. */
.whead {
  display: flex; align-items: center; gap: var(--space-md);
  margin: var(--space-md) 0 0;
}
.wtitle { font: 600 24px/31px var(--read); margin: 0; flex: 1; min-width: 0; }
/* **Two 44pt squares, and both of the pair are one.** `CLAUDE.md`'s floor, met
   with real height rather than a hit area, because two adjacent controls whose
   slops would overlap is the case that rule names — which one you hit becomes a
   matter of stacking order. The pair being identical squares is what lets the
   arrangement change without either of them changing. */
.wpair { flex: none; display: flex; align-items: center; gap: var(--space-sm); }
.wmk {
  width: 44px; height: 44px; display: flex; align-items: center;
  justify-content: center; cursor: pointer; background: none;
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
  padding: 0;
}
/* A fill means an act — the same thing Save and Try again spend it on. */
.wmk.fill { background: var(--primary); border-color: var(--primary); }
/* 18px, which is the size the app draws these at. Outline-only, which is what
   Feather is — and fine here, because neither is a toggle: `DESIGN.md`'s
   filled/outline exception is for a *state* that has to survive greyscale, and
   these controls are the same before and after. */
.wmk svg {
  width: 18px; height: 18px; display: block; fill: none;
  stroke: var(--text); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.wmk.fill svg { stroke: var(--primaryText); }
.wmk:hover { border-color: var(--text); }
.wmk.fill:hover { opacity: .9; border-color: var(--primary); }
.wmk:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
/* The keys, said once and quietly. Nothing else can tell a reader that a key
   exists, and it is the smallest type on the page for the same reason. */
.wkeys { font: 11px/18px var(--meta); color: var(--textSoft); margin: 6px 0 0; }
/* An open range is a mode, so the line that names it stops being metadata and
   starts being a state. `primary` marks what is interactive everywhere else in
   the app; here it marks the thing waiting on you. */
.wkeys.open { color: var(--primary); }
.wby { font: 12px/20px var(--meta); color: var(--textSoft); margin: 6px 0 0; }
/* The channel, marked as a link and still metadata. Inherits the quiet
   colour rather than taking `--link`, because a byline that shouted would
   be the loudest thing under the video — the underline is what says it is
   a link, and hovering is what confirms it. */
.wby a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.wby a:hover { color: var(--link); }

.wsh {
  font: 11px/18px var(--meta); letter-spacing: .14em; color: var(--textSoft);
  margin: 0 0 var(--space-sm);
}
.wnone { font-size: 15px; line-height: 23px; color: var(--textSoft); margin: 0; }
/* **Where you were before you jumped**, and the two answers it has. Going back
   is the offer and staying is declining it, so the second is quieter — the
   phone's own pairing, where neither is filled and the difference is the arrow
   and the colour rather than a second emphasis competing with the first. */
.wback {
  font: 12px/20px var(--meta); color: var(--primary); background: none;
  border: 0; padding: 4px 0; margin-right: var(--space-md); cursor: pointer;
}
.wstay { color: var(--textSoft); }
.wback:hover { text-decoration: underline; text-underline-offset: 2px; }
.wback:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* **A hairline list, not a stack of cards** — `VideoMarks` in the app, where
   the list carries a rule on top and every row one below it. A column of
   bordered boxes reads as a set of separate objects; a mark is a line in a walk
   through one video, and the rule is what says so. */
.wlist { border-top: 1px solid var(--line); margin-top: 10px; }
.wmark {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--line);
}
/* The time, in the app's own measure: monospace, `primary`, and a fixed width
   so a column of them lines up whether or not any is a range. */
.wmt {
  flex: none; min-width: 86px; text-align: left; padding: 0;
  font: 13px/23px var(--meta); color: var(--primary);
  background: none; border: 0; cursor: pointer;
}
.wmt:hover { text-decoration: underline; text-underline-offset: 2px; }
.wmt:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.wmb { flex: 1; min-width: 0; }
/* **The note is set in the reading face**, as it is on the phone -- it is the
   only thing on this page a person wrote, and everything around it is metadata.
   A button, because pressing it is how you write one, and reset to look like
   the text it is. Two lines, then it stops: the row is scanned, and the whole
   note is in the card. */
.wmn {
  flex: 1; min-width: 0; text-align: left; padding: 0; margin: 0;
  background: none; border: 0; cursor: pointer;
  font: 16px/23px var(--read); color: var(--text);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* `no note`, which is the phone's word for an empty one, in its quiet italic. */
.wmn.none { color: var(--textSoft); font-style: italic; }
.wmn:hover { text-decoration: underline; text-underline-offset: 3px; }
.wmn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
/* **The card the note is written in.** At the foot of the page and with no
   backdrop, both for one reason: nothing may be displayed in front of any part
   of a YouTube embedded player, which is their terms rather than a preference.
   A scrim over the page would cover the stage as surely as the card would. */
.wsheet {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: var(--space-lg); z-index: 50; width: min(440px, calc(100vw - 32px));
  background: var(--bgRaised); border: 1px solid var(--lineStrong);
  border-radius: var(--radius-md); padding: var(--space-md);
  box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 12px 32px rgba(0,0,0,.14);
}
@media (prefers-color-scheme: dark) {
  .wsheet { box-shadow: 0 1px 2px rgba(0,0,0,.4), 0 12px 36px rgba(0,0,0,.55); }
}
.wshead { display: flex; align-items: center; gap: var(--space-sm); }
.wstime { font: 13px/22px var(--meta); color: var(--primary); }
/* The go-back, offered only here -- the phone's rule, and its reason: this is
   the one moment the cost of a control is nothing, because the reader is
   already stopped and writing about this mark. */
.wchips { display: flex; gap: 6px; margin-left: auto; }
.wchip {
  font: 11px/18px var(--meta); color: var(--textSoft); cursor: pointer;
  background: none; border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 3px 7px;
}
.wchip:hover { color: var(--text); border-color: var(--lineStrong); }
.wchip:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
/* No placeholder, which the phone refuses outright: it read as *what was it*. */
.wsnote {
  display: block; width: 100%; box-sizing: border-box; resize: none;
  margin-top: var(--space-sm); padding: var(--space-sm);
  font: 16px/24px var(--read); color: var(--text);
  background: var(--bg); border: 1px solid var(--lineStrong);
  border-radius: var(--radius-sm);
}
.wsnote:focus { outline: 2px solid var(--text); outline-offset: -1px; }
.wsact { display: flex; gap: var(--space-sm); margin-top: var(--space-sm); }
/* A fill means an act -- the same thing Save and Try again spend it on, and
   Keep is the act here. */
.wsbtn {
  flex: 1; min-height: 40px; cursor: pointer; font: 15px/20px var(--read);
  color: var(--text); background: var(--bg);
  border: 1px solid var(--lineStrong); border-radius: var(--radius-sm);
}
.wsbtn.fill { background: var(--primary); border-color: var(--primary); color: var(--primaryText); }
.wsbtn:hover { border-color: var(--text); }
.wsbtn.fill:hover { opacity: .9; border-color: var(--primary); }
.wsbtn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* **Wide: the marks scroll on their own, and the page does not follow.**
   The phone's own rule — B-286's surviving half, where a landscape iPhone was
   cutting the title off because a column could not scroll by itself.

   `sticky` rather than locking the document to the viewport. Bounding `body` to
   `100vh` and making it a flex column does put both columns in their own
   scrollers, and it re-lays out every page-level thing at once — the header,
   the sheet's own borders, `main`'s auto margins — for a fault that is entirely
   inside one column. This changes one element and leaves the page a page.

   `align-items: flex-start` on `.watch` is what makes it work at all: a stretched
   flex item is already as tall as its row and has nothing to stick within.

   `overscroll-behavior` is the second half of what was asked for — without it a
   flick that reaches the end of the marks carries on into the document. */
@media (min-width: 761px) {
  .wside {
    position: sticky;
    top: var(--space-lg);
    max-height: calc(100vh - var(--space-lg) * 2);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* Narrow: one column, and the marks go under the video rather than beside it —
   which is what the phone does in portrait, for the same reason. */
@media (max-width: 760px) {
  .watch { display: block; }
  .wside {
    width: auto; border-left: 0; padding-left: 0; margin-top: var(--space-lg);
    border-top: 1px solid var(--line); padding-top: var(--space-md);
  }
}

/* ---------- the reader ---------- */

/* **A reading measure rather than the page's.** Everything else on this site is
   a list at 1020 or a video at 1200; prose wants the column `DESIGN.md` sets on
   the phone, which is a line you can come back to the start of. 34em against a
   19px body is a little over 60 characters — the middle of the 45–75 band, and
   the same place the reader's own margins land. */
main.reader { max-width: 34em; }

.reader .back { font: 12px/18px var(--meta); margin: 0 0 var(--space-lg); }
.reader .back a { color: var(--textSoft); text-decoration: none; }
.reader .back a:hover { text-decoration: underline; }

/* The document's own type, and it is `readerText` rather than the chrome scale:
   the reading surface starts at 19 where the app starts at 17, because here the
   text is the product. */
.reader .doc h1 {
  font: 700 26px/1.35 var(--read);
  margin: 0 0 var(--space-sm);
  text-wrap: balance;
}
.reader .doc .by {
  font: 12px/20px var(--meta); color: var(--textSoft);
  margin: 0 0 var(--space-lg);
}
.reader .doc .lead {
  display: block; width: 100%; height: auto;
  border-radius: var(--radius-sm); margin: 0 0 var(--space-lg);
}
.reader .prose { font: 19px/1.66 var(--read); color: var(--text); }
.reader .prose p { margin: 0 0 1.1em; }
.reader .prose h2, .reader .prose h3 { font: 600 21px/1.35 var(--read); margin: 1.6em 0 0.5em; }
.reader .prose img { display: block; max-width: 100%; height: auto; margin: 1.4em auto; border-radius: var(--radius-sm); }
.reader .prose figure { margin: 1.4em 0; }
/* A caption is its own step rather than the byline's — 12 is right for a source
   read once, and a caption is a sentence and sometimes three. Smaller than the
   prose is the whole point: at body size it reads as a paragraph that happens
   to sit under a picture. */
.reader .prose figcaption {
  font: 15px/1.5 var(--read); color: var(--textSoft);
  text-align: center; margin-top: 0.6em;
}
.reader .prose blockquote {
  margin: 1.4em 0; padding-left: var(--space-md);
  border-left: 2px solid var(--line); color: var(--textSoft);
}
.reader .prose a { color: var(--link); }
.reader .prose table { border-collapse: collapse; width: 100%; display: block; overflow-x: auto; }
.reader .prose td, .reader .prose th { border-bottom: 1px solid var(--line); padding: 6px 12px 6px 0; text-align: left; }
