/* The write-up page: a title band across the top, a rail on the left, and the
   prose beside it.

   The rail carries what a write-up otherwise buries in a paragraph: where to go
   look at the thing, what it is made of, and, once there is enough of an article
   to need one, which sections it has. The table of contents is theodore.net's,
   narrowed to one article, and it only appears when two or more sections are
   written: a contents list for a single paragraph advertises an article that is
   not there. Everything in the rail is navigation or metadata, so it sits in the
   sans at the 14px interface floor; the prose beside it is the only thing set
   for reading.

   The type split here runs the other way round from the rest of the site.
   Everywhere else is serif display over sans text. This is the one page with
   running prose on it, and prose is what a serif is for, so the article is
   serif and the title on top of it is sans. Captions, the rail, and the back
   link stay sans: none of them is the article.

   Widths: the rail is 200px and the column is 64 characters, which together
   come to about 850px inside the 900px `.wrap`. The column is the constraint,
   not the wrap. */

/* The band. Title and dek sit above both columns so the measure of the dek is
   set by the sentence rather than by the column under it.

   The dek is serif and stays serif. It used to be following the title; now it
   is following the prose, which is the better answer to the same question: the
   dek is the article's first sentence pulled above the fold, so it takes the
   article's face. */
/* Centered, all three lines. The band sits above both columns and is the only
   part of the page that is not a column of something, so it is the one place
   centering costs nothing: an eyebrow, a title, and one sentence are three short
   lines whose left edges were lining up with nothing in particular. The article
   below stays left-aligned, which is where the rule about centered text is
   actually pointed. */
.note-head { margin-bottom: var(--s8); text-align: center; }

/* The eyebrow. Says which section of the site this page belongs to and what
   kind of page it is, neither of which a write-up otherwise states: the nav
   marks `projects` as current, and nothing here says the write-ups have a name
   of their own.

   Sans, because it is a label rather than a line of the article. `--text-micro`
   since 2026-09-18, which is the third use of that token and so owes the same
   argument the other two made: this is the smallest thing in the band on
   purpose, and nothing depends on reading it. The section it names is already
   marked in the nav, and the `projects` link is the third route to the board
   after the nav and the foot link. It held the 14px floor until then, on the
   grounds that an exception to the eyebrow rule does not buy an exception to
   the readability one; what changed is that the band grew a title, tags and two
   buttons above the article, and at 14px the outermost label was competing with
   them.

   The caps are in CSS, not in the markup, so what a screen reader announces and
   what a reader copies is still `nhi's notes / projects` in ordinary case.
   Tracking is the one place on the site letters are pushed apart, and it is
   here because caps set solid close up on themselves. */
.note-eyebrow {
  font-family: var(--sans);
  font-size: var(--text-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
  /* 12px down to the title and 8px from the title to the dek. The three lines
     are one band, and the eyebrow is the outermost of them, so it sits the
     furthest off. */
  margin: 0 0 var(--s3);
}

/* Real spaces sit either side of the dot in the markup, so text pulled off the
   page reads `nhi's notes / project` rather than running the two together; the
   padding only opens that space up to where caps want it. */
.note-eyebrow__sep { padding: 0 .28em; }

/* `project` is the way back to the board. No underline: the eyebrow is a
   label set in tracked caps, and a rule under one of its two halves reads as
   the label being broken rather than as an affordance. It takes the same
   treatment as `back to projects` at the foot instead, muted at rest and
   `--ink` under the pointer, which is the site's pattern for a line that is
   navigation rather than a link inside prose.

   Quiet is affordable here because it is the third route to the same page,
   after the nav and the foot link. Focus is the exception and draws a real
   ring: a keyboard reader gets no hover to discover it with. */
.note-eyebrow a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.note-eyebrow a:hover { color: var(--ink); }
.note-eyebrow a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 2px; }

.note-head h1 {
  /* Sans, against a serif article. 600 rather than the 400 the serif was set
     in: Public Sans at 400 across 46px is a thin line holding up a page, and
     weight is what a sans has instead of a serif's contrast. It went 400, then
     500, then here: at 500 the title still read as a larger version of the
     page, not as the thing the page is about, and a title has to be the
     heaviest thing on the screen or the first section heading competes with it.
     600 is a face the write-ups now request; without it the browser would fake
     the weight, which is the thing that got a previous bump reverted. */
  font-family: var(--sans);
  font-weight: 600;
  color: var(--ink-display);
  /* Under `--text-display`, because a write-up's title is a second-level page
     title: the board's `projects` is the site's heading and this sits inside
     it. Still the largest thing on the page by a wide margin. */
  font-size: clamp(34px, 4.6vw, 46px);
  line-height: 1.1;
  letter-spacing: -.015em;
  /* 8px, the same as the board's reader panel puts between a project's name
     and its one-liner. The two are one unit, and 16px read as two. */
  margin: 0 auto var(--s2);
  max-width: 18ch;
}

.note-head .note-dek {
  font-family: var(--serif);
  font-size: var(--text-body);
  line-height: 1.55;
  margin: 0 auto;
  max-width: 52ch;
}

/* One column. The rail is gone: it was picked for a numbered table of contents
   that no write-up has ever had enough sections to draw, so what it held in
   practice was a button, two facts and three pills in 200px of otherwise empty
   page. All of that is now under the title, where the reader is already
   looking, and the article gets the whole width to be centered in. */
.note-grid {
  display: grid;
  grid-template-columns: minmax(0, 64ch);
  justify-content: center;
}

/* ---- the contents rail ---- */

/* Absent by default, and on most screens that is the end of it. A contents list
   is worth a column only where there is a column to spare: at 963px the gutter
   beside a 64ch article is 88px, which is not a list, it is a list crushed. */
.note-rail { display: none; }

/* Wide enough to have gutters worth using. The grid steps outside `.wrap` by an
   equal amount on both sides rather than taking width from the article, so the
   prose column does not move a pixel when the rail appears and still lines up
   under the centered title band above it. A symmetric negative margin is what
   keeps that true: the middle track is centered on the same axis it was before.

   1180px, because the grid is then 1084px wide and still has 48px of air either
   side of it. */
@media (min-width: 1180px) {
  .note-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 64ch) minmax(0, 1fr);
    margin-inline: -140px;
  }

  /* Placed rather than flowed. The other four write-ups have no rail, so their
     `main` is the grid's first child and auto-placement would drop it in the
     left gutter track and squeeze it to 228px. Naming both columns means the
     article sits in the middle one whether or not anything sits beside it. */
  .note-grid > .note-body { grid-column: 2; }

  .note-rail {
    grid-column: 1;
    display: block;
    /* Right-aligned against the article with a clear gap, so it reads as
       belonging to this page rather than floating at the window's edge. */
    justify-self: end;
    width: 180px;
    padding-right: var(--s7);
    /* It follows the reading rather than scrolling away from it. `--s8` down
       from the top so it starts level with the first section rather than level
       with the title. */
    position: sticky;
    top: var(--s5);
    align-self: start;
  }
}

.note-rail__toc {
  list-style: none;
  margin: 0;
  padding: 0;
}
.note-rail__toc li + li { margin-top: var(--s3); }

/* Sans and small: this is the way around the page, not a line of it, the same
   distinction the foot link makes. */
.note-rail__toc a {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  font-family: var(--sans);
  font-size: var(--text-label);
  line-height: 1.4;
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.note-rail__toc a:hover,
.note-rail__toc a:focus-visible { color: var(--ink); }
/* Drawn the way the header's nav draws it, since it is the same kind of thing:
   a list of places, tabbed through. */
.note-rail__toc a:focus-visible {
  outline: 2px solid var(--ink-display);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

/* The entry you are in is the readout and the control at once: no separate
   marker beside it to say the same thing twice. Color is all that changes,
   and `js/notes.js` changes nothing else, so the list works unstyled and
   unscripted. */
.note-rail__toc a[aria-current="true"] { color: var(--ink-display); }

.note-rail__num {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ---- the meta block, under the title ---- */

/* Centered like the rest of the band, and laid out as a stack rather than a row:
   the tags, then the action, then the facts. A row would have put a filled
   button beside a line of gray metadata and asked the reader to weigh them.

   The tags used to sit at the bottom and moved above the button on 2026-09-18.
   They are what the page is filed under, which belongs with the title; the
   button is what to do about it, which belongs with the decision. Read down,
   that is subject then action, and the last thing before the article is the
   thing the reader is most likely to press. It also stops the tags from
   separating the button from the facts printed under it. */
.note-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
  margin-top: var(--s5);
}

.note-meta:empty { display: none; }

/* The one primary button on the page, and the second of the three jobs the
   orange is rationed to. A project with nowhere to go does not draw it, the
   same way the board draws no action it has no address for. */
.note-try {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  /* Sans, said out loud. Under the title it inherited the page's sans and this
     was silent; at the foot of the article it sits inside the serif body, and a
     button set in the prose face reads as a pull quote in a box. */
  font-family: var(--sans);
  background: var(--accent);
  color: var(--on-accent);
  text-decoration: none;
  font-size: var(--text-sm);
  padding: var(--s2) var(--s4);
  /* Square. A pill is the shape the tag row under it uses, and those are
     labels; this is the one thing on the page you press to leave it. */
  border-radius: 0;
  margin: 0;
}
.note-try svg { width: 13px; height: 13px; }
.note-try:hover { background: var(--accent-hover); }
.note-try:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* A second action beside the first, for the one page that has two. The row is
   the exception the stack above describes and not a reversal of it: what that
   rule refuses is a filled button weighed against a line of gray metadata, and
   these two are both actions, so putting them on one line is what says they
   are the same kind of thing. They are still not equal weight, which is the
   next rule's job. It wraps because two labels of this length do not fit a
   phone, and the gap is the one the stack uses so a wrapped row and a stacked
   one measure the same. */
.note-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* Top, not stretch. It held one filled download and one hairline source
     link, at different heights; left to stretch, the shorter grew to match and
     its label drifted off the other's baseline. Kept now that the row is one
     action, because the next second action will have the same problem. */
  align-items: flex-start;
  gap: var(--s3);
}

/* The same action at the end of the article. It borrows `.note-meta`'s layout
   rather than its meaning: a centered column with the same 16px gap, so the row
   sits the same distance from what precedes it in both places and one rule
   describes both. The space above it is a section's worth, because that is what
   it follows, and it sets the prose measure aside: a button is not a paragraph
   and centers on the column rather than on the text. */
.note-get {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
  margin-top: var(--s8);
}

/* The install animation, under the action in the head. It takes `.note-fig`'s
   terms rather than its class: the same 8px radius and the same `--paper`
   ground while it loads, capped by the column instead of by height, because
   this one is wide and short and the height ceiling would never bite. The gap
   above it is the head's own 24px, not a figure's, since it belongs to the
   button rather than to the prose. */
.note-head__demo {
  margin: var(--s5) 0 0;
}
.note-head__demo img {
  display: block;
  width: 100%;
  max-width: 560px;
  height: auto;
  margin-inline: auto;
  /* No radius and no ground of its own. The animation is drawn on `--ground`
     and reaches the edge of its own canvas, so it has no edge on this page and
     nothing to round: a corner radius here would cut a curve out of the page
     and show what is behind it. The GIF ships white for GitHub's README, which
     is the ground it was drawn for; this copy is rendered on the site's. */
}

/* The board's tags, drawn as the board's pills. These are `<li>`, not
   `<button>`: on the board a pill is a toggle, and a thing that looks pressable
   and is not is worse than no affordance at all. `filed under` used to label
   them and now survives only as the group's accessible name, because a row of
   pills already announces itself as a list of tags to anyone who can see it. */
.note-meta__tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s2);
  margin: 0;
  padding: 0;
}

/* ---- the prose ---- */

.note-body {
  font-family: var(--serif);
  font-size: var(--text-body);
  line-height: 1.65;
}

.note-body > section + section { margin-top: var(--s8); }

/* 500, not the 400 it was set in. The heading used to be a serif over sans
   prose, where the face alone said "heading"; now it is a serif over a serif,
   and size on its own is one signal where there had been three. Weight and the
   display brown are the other two. */
.note-body h2 {
  font-family: var(--serif);
  font-weight: 500;
  color: var(--ink-display);
  font-size: var(--text-h2);
  line-height: 1.2;
  margin: 0 0 var(--s4);
  scroll-margin-top: var(--s5);
}

/* The number hangs in the gutter beside the heading rather than sitting in it,
   so the headings still line up with the paragraphs under them. Written this
   way round rather than as a `:not()`, so a browser without `:has()` gets the
   unnumbered heading, which is what every write-up currently has. */
.note-body h2:has(.note-body__num) {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
}

.note-body h2 .note-body__num {
  font-family: var(--sans);
  font-size: var(--text-label);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  /* Sits on the heading's first baseline rather than on its own. */
  align-self: baseline;
  padding-top: .45em;
}

/* A subsection inside a section. It takes the heading's face, weight and
   display brown, one size down, and it is not numbered: the number in the
   gutter says which of the write-up's parts you are in, and a subsection is
   not one of those. More space above than below, so it attaches to the prose
   that answers it rather than to the paragraph it followed. */
.note-body h3 {
  font-family: var(--serif);
  font-weight: 500;
  color: var(--ink-display);
  font-size: var(--text-h3);
  line-height: 1.3;
  margin: var(--s6) 0 var(--s3);
  scroll-margin-top: var(--s5);
}

/* A crossing-out, which on this site is always a joke: the sentence is meant to
   be read and then read as withdrawn. It goes muted so the line the reader is
   supposed to keep is the darker one, and the rule is `--muted` rather than the
   text color so the strike reads as drawn over the words rather than as part
   of them. */
.note-body s {
  color: var(--muted);
  text-decoration-color: var(--muted);
  text-decoration-thickness: 1px;
}

/* An identifier named in running prose. Mono at 15px, not 18: Plex Mono runs
   larger on the body than Newsreader does at the same size, and matching the
   nominal size would put a heavier word in the middle of a serif line. The
   color is the prose's own, so the face is the only thing that changes. */
.note-body code {
  font-family: var(--mono);
  font-size: 15px;
}

/* Paragraphs of the article, and only those. The exclusions are for the same
   reason the link rule below excludes `.note-try`: those lines sit inside the
   body and are not prose, and this rule is a type selector on top of a class,
   which beats the one-class rule each of them sets its own margin in. It wins
   silently, which is the whole problem: the fine print's margin went to zero and
   the line sat on the button's edge, and `More coming soon` lost the space that
   was meant to detach it from the picture above. Narrowing the rule is the fix,
   because the rule is the thing making the wrong claim.

   Two exclusions is the point to notice it and a third is the point to act: at
   three, these lines want a shared class saying what they are, a line about the
   page rather than a line of it, and this rule should exclude that one name. */
.note-body p:not(.note-soon) { margin: 0 0 var(--s5); }
.note-body p:last-child { margin-bottom: 0; }

.note-body ul { margin: 0 0 var(--s5); padding-left: 1.1em; }
.note-body li { margin: 0 0 var(--s2); }

/* Inline links in running prose: one of the three jobs the orange is rationed
   to. Underlined, because color alone is not a link.

   `:not(.note-try)` because the pair of actions at the foot of the article is
   inside the body and is not prose. This rule is two classes and `.note-try` is
   one, so without the exclusion the button took the prose link's orange text on
   its own orange fill and the label was barely there. Narrowing the rule is the
   fix rather than out-weighting it at the button, because the rule was the one
   making the wrong claim: it is about links in sentences. */
.note-body a:not(.note-try) {
  color: var(--accent-ink);
  text-underline-offset: .18em;
  text-decoration-thickness: 1px;
}

/* Five small boards in one row, one per target, each carrying the path that
   reaches it and the sum that path makes. The sentence this replaced read "on a
   board reading 5 2 9 over 1 7 8 over 3 6 4", which asks the reader to build a
   grid in their head to follow a paragraph about grids, and it showed no path
   at all. Five side by side is the argument itself: same nine squares every
   time, a different route through them each time.

   One outer rect and four internal lines rather than nine rects, so no edge is
   drawn twice and there is no doubled hairline down the middle. The route is
   the display brown, not the accent, which is rationed elsewhere. It runs
   through the digits, so each digit carries a `--ground` halo by way of
   `paint-order: stroke`: the line passes behind the number instead of through
   it, and no second fill is needed under each cell. */
.note-board { display: block; margin-inline: auto; }
.note-board .cell { fill: none; stroke: var(--line); stroke-width: 1; }
.note-board .route {
  fill: none;
  stroke: var(--ink-display);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Where the path begins. It was a filled dot at the center of the first digit,
   which that digit's own halo then painted over: the knockout that keeps the
   line from crossing the numerals does not know the difference between the line
   and the mark. A ring around the digit instead, drawn last, so it sits over the
   halo rather than under it and takes nothing away from the numeral. */
.note-board .start {
  fill: none;
  stroke: var(--ink-display);
  stroke-width: 1.2;
}
.note-board .digit {
  font-family: var(--mono);
  /* The 14px interface floor, which is also what sets the board's size: the
     cell is built around a legible numeral rather than the numeral shrunk to
     fit five boards across the column. */
  font-size: 14px;
  fill: var(--ink);
  text-anchor: middle;
  dominant-baseline: central;
  paint-order: stroke;
  stroke: var(--ground);
  stroke-width: 3.5px;
  stroke-linejoin: round;
}

/* Two rows rather than one, three boards then two. Five across left each
   caption 116px, which needs 5px type to hold its sum on one line against a
   14px floor. Three across fits at 14 with the spaces closed up, around the
   equals sign as well as around the operators: spaced, the longest caption is
   190px wide and three of those plus their gaps need 618, which is more than
   this column has at most window widths. Closed up it is 170, and three fit in
   anything down to a 542px column. The target keeps `--ink` against the sum's
   `--muted`, which is what marks the boundary now that the space is gone. The
   boards are drawn at 112 and displayed at 140, so the numerals land near 17px:
   the column is sized by the sentence under it, not by the picture. */
.note-body .note-boards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s5) var(--s4);
}
.note-boards li { flex: 0 1 170px; margin: 0; }
/* The five boards are 1, 2, 3, then 25, then 99, so the second row opens with
   an elision and carries another before the 99: what is on the page is five of
   a hundred, and without the marks it reads as five targets that happen to be
   those five. The break is a zero-height full-width item, because the rows have
   to be 1 2 3 and then the rest: left to wrap on its own the first row would
   pull the first elision up beside the 3, which puts the mark on the wrong side
   of the gap it describes.

   Pulled up by the caption's own height so it sits on the boards' middle rather
   than the row's, and it says what it stands for to a screen reader, which
   cannot infer an ellipsis from a glyph. */
.note-boards .note-boards__break { flex: 0 0 100%; height: 0; margin: 0; }
.note-boards .note-boards__gap {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  align-self: center;
  padding-bottom: var(--s5);
  margin: 0;
  font-family: var(--mono);
  /* Large enough to be a step in the sequence rather than punctuation left
     between two pictures. At the caption's 14px it read as a typo; at 28 it is
     the same size as the thing it stands in for. `--muted` and not `--ink`,
     because it is the only mark here that is not a fact. */
  font-size: 28px;
  letter-spacing: .1em;
  color: var(--muted);
}
.note-board { display: block; width: 140px; height: auto; margin-inline: auto; }
/* The sum this board's path makes, on one line. Closed up rather than spaced,
   which is what buys the line: `1=5+2+9` reads as arithmetic at this size and
   `1 = 5 + 2 + 9` would have to shrink under the floor to fit. */
.note-boards__cap {
  margin: var(--s2) 0 0;
  font-family: var(--mono);
  font-size: var(--text-label);
  color: var(--muted);
  text-align: center;
  white-space: nowrap;
}
.note-boards__target { color: var(--ink); }

/* A way out of the deep end, offered at the point where the write-up stops
   being about the game and starts being about graph theory. It is a whole
   sentence rather than a link inside one, so the target is the size of the
   thought, and it sits in the accent like every other link in this prose: the
   reader is choosing where to read, which is the same act as following a link,
   and a second treatment would be a second vocabulary for one thing.

   Set apart by space above and below rather than by a rule or a plate. It is an
   aside in the reading, not a band across the page. */
.note-body .note-skip {
  margin: var(--s6) 0 var(--s7);
}

/* A figure is printed straight onto the ground: no plate behind it drawing a
   rectangle around a rectangle.

   Centered, always, unless a figure asks not to be. Every picture here is
   narrower than the column and most are much narrower, so left-aligned they
   hung off one edge with a field of ground beside them; the column is 64
   characters of prose and a 250px phone capture is not a second column. A
   figure that wants the text edge takes `.note-fig--left`. */
.note-fig { margin: var(--s6) 0; text-align: center; }
.note-fig--left { text-align: left; }

/* A photograph whose section was never written leads the body. It is the first
   thing under the title band, so it takes no top margin of its own. */
.note-fig--lead { margin-top: 0; }
.note-fig img {
  display: block;
  /* Capped by height first, then by the column. A write-up is a page of
     writing with pictures in it, so a picture that fills the viewport stops
     the reading rather than supporting it: half the screen is the ceiling, and
     a tall phone screenshot lands well inside the column at that size instead
     of running to 800px. Width stays `auto` so nothing is ever scaled up past
     the capture's own resolution, which is 900px on the long edge. */
  max-height: 50vh;
  width: auto;
  max-width: 100%;
  height: auto;
  /* `margin-inline`, not the parent's `text-align`: the image is a block, so
     the alignment on the figure moves the caption and nothing else. */
  margin-inline: auto;
  border-radius: var(--radius-md);
}
.note-fig--left img { margin-inline: 0; }

/* A picture holds its place, and says so while it is coming.

   The box is already reserved: every figure carries `width` and `height`, so
   the aspect is known before any bytes are. This paints that reserved box a
   flat `--paper`, which turns an empty rectangle into a panel the size of the
   picture that is on its way. The picture is opaque, so it covers this the
   moment it lands: nothing has to be cleared, and with no JavaScript this is
   the whole of the feature.

   The radius matches the picture's own, or the corners of the panel and the
   corners of the photograph disagree for as long as the load takes. */
.note-fig img,
.note-fig--demo video,
/* The three prints at the foot of a write-up, on the same terms. Their card is
   `--card`, which is white in light, so an unloaded print was a white square on
   a white card and the taped frame looked empty rather than loading. `--paper`
   inside it is the panel. It is not a card inside a card: it is the print's own
   box, waiting for the print, and it is gone the moment one lands. */
.note-also__card img {
  background-color: var(--paper);
}

/* Still waiting, which only a script can know: `js/skeleton.js` puts this on
   the boxes that were not already in the cache and takes it off on `load`. A
   flat panel says a picture belongs here; the pulse says one is coming. Two
   tones from the scale, never a sweep across the box.

   Six seconds, and the slowness is the point: a skeleton that hurries reads as
   a thing that is working hard, and this is a thing that is waiting. It is a
   `background-color` animation, which the rule about animating only transform
   and opacity exists to prevent for a different reason than applies here: that
   rule is about not reflowing neighbors, and a background paints without
   touching layout at all. */
@keyframes note-skeleton {
  0%, 100% { background-color: var(--paper); }
  50% { background-color: var(--line); }
}
.note-fig img.is-loading,
.note-fig--demo video.is-loading {
  animation: note-skeleton 6s var(--ease) infinite;
}

/* The three taped prints get a blurred version of the picture they are waiting
   for, instead of the pulse. The colors are not here: `bin/build-print-wash.py`
   reads each thumbnail, reduces the square the page actually shows to 5x5, and
   writes one rule per print into `css/print-wash.css`, which loads after this
   file. Only the structure is here, because only the structure is the same for
   every print.

   It is the one gradient on the site, and `DESIGN.md` says why it is allowed
   here and nowhere else: this box holds a photograph, so a wash of color is
   color entering the page where a photograph enters it. A figure in the body of
   a write-up sits on the ground among prose and keeps the flat panel; a print in
   a white frame with tape across it does not, because a flat tan square inside a
   white frame reads as an empty print rather than one on its way.

   On `.is-loading` rather than on the image, and that is not a preference.
   `sift.png` is an icon on transparency, so a wash painted on the image's own
   background would still be showing through the corners of a picture that had
   finished loading. `is-loading` comes off on `load`, and `--paper` from the
   rule above is what is left behind the transparency, which is what shipped
   before this.

   No animation. The pulse had to say a picture was coming because a flat panel
   does not; a blur of the picture says it by being picture-shaped, and moving it
   would only misdescribe the thing about to replace it. */
.note-also__card img.is-loading {
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

@media (prefers-reduced-motion: reduce) {
  /* The flat panel alone, which already says a picture belongs here. The prints
     are not listed: their wash never moved. */
  .note-fig img.is-loading,
  .note-fig--demo video.is-loading { animation: none; }
}

/* An instructional screenshot is read, not looked at. The height cap above is
   right for a photograph, which is finished being useful the moment it has
   been seen; it rendered the four-panel install sequence at 349px wide, half
   its own resolution, where the labels inside the panels are four pixels tall
   and the steps cannot be followed. This figure is capped by the column and
   nothing else. Trade-off accepted: it is taller than half the screen on a
   short viewport, which is the cost of it being legible at all. */
.note-fig--column img { max-height: none; }

/* A screen capture of the app being used. Same treatment as a screenshot,
   because that is what it is between frames: printed on the ground, rounded at
   the corner, capped by the column rather than by the viewport. The height cap
   that suits a photograph would render a 1024px capture of a photo grid at
   half its resolution, and the filenames under the thumbnails are the thing the
   clip is in the article to show.

   `display: block` and the auto inline margin do the centering, the same way the
   image rule does it, because `text-align` on the figure moves the caption and
   nothing else.

   No `background`, and this is the one place on the site where that is worth
   saying out loud: the captures are of the app in dark mode and they land on
   cream. A plate behind them would be a rectangle around a rectangle, which the
   figure rule already refuses, and a light-mode set of captures would be a
   second 1.3 MB of payload to make the page match itself. The picture is dark
   on the page in light mode. That is what the app looks like. */
/* Two figures on one row, which nothing else on the site does. The two culling
   passes are one idea, mark now and decide later, and the second picture is the
   first one's consequence; a screen of scrolling between them made them read as
   two separate features.

   The outer margin is the figure's own `--s6`, taken off the children and put
   on the row, so a pair sits in the column at the same rhythm a single figure
   does. The gap is the column gap only: the captions carry the vertical
   spacing they already have.

   Below 620px it is one column. Two 1024px captures sharing a phone are about
   150px each, which is a picture of a picture. */
.note-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4);
  align-items: start;
  margin: var(--s6) 0;
}
.note-pair .note-fig { margin: 0; }

@media (max-width: 620px) {
  .note-pair { grid-template-columns: minmax(0, 1fr); gap: var(--s6); }
}

/* The box a clip's pause control hangs on. `inline-block` so it is exactly the
   clip's width rather than the figure's, which is centered and wider: anchored
   to the figure, the button would sit out on the ground beside the picture. */
.note-demo__box {
  display: inline-block;
  position: relative;
  max-width: 100%;
  /* The video inside is a block; without this the box keeps a text line's
     descender under it and the caption sits four pixels low. */
  line-height: 0;
}

/* Stop a clip. Always there rather than appearing on hover, because a control
   that answers "how do I make this stop" has to be findable by somebody who is
   already bothered by the motion, and hunting for it with the pointer is the
   wrong thing to ask of them.

   Drawn over the reader's picture, so it carries its own contrast: a dark fill
   under the mark and a light keyline around it, which no screenshot underneath
   can match. That is the rule about a mark on content the page does not
   control, and it is why this is not simply a tinted glyph.

   32px, the toolbar size rather than the in-card 26, because it sits over a
   photograph and because 24 is the floor for a target anybody has to hit. */
.note-demo__pause {
  position: absolute;
  left: var(--s3);
  bottom: var(--s3);
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .55);
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, .55);
  color: #fff;
  cursor: pointer;
  line-height: 0;
  opacity: .75;
  transition: opacity var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.note-demo__pause:hover,
.note-demo__pause:focus-visible { opacity: 1; background: rgba(0, 0, 0, .78); }
/* The ring is white here, not `--ink`: this one is drawn on a photograph rather
   than on the page, and ink on a dark frame is a focus ring nobody can see. */
.note-demo__pause:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.note-demo__pause svg { width: 18px; height: 18px; fill: currentColor; }

/* One glyph at a time. Playing shows the two bars, which every player on every
   platform uses to mean "press this to stop". */
.note-demo__pause .note-demo__bars { display: none; }
.note-demo__pause.is-playing .note-demo__bars { display: block; }
.note-demo__pause.is-playing .note-demo__play { display: none; }

@media (prefers-reduced-motion: reduce) {
  .note-demo__pause { transition: none; }
}

.note-fig--demo video {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  margin-inline: auto;
  border-radius: var(--radius-md);
}
/* A diagram is drawn, not photographed, so it takes none of the picture rules:
   no radius, no height cap, and it keeps its own line weight in both themes by
   painting from the tokens rather than from a file. It is also the one thing in
   the column allowed to be wider than the column: below about 600px it scrolls
   sideways inside its own box instead of shrinking its labels under the 14px
   floor. */
.note-fig--diagram { text-align: left; }
/* `position` for the same reason `.note-compare__scroll` carries it: a
   horizontal scroller clips only what it is the containing block for, and one
   `.sr-only` label inside this box would push the whole page sideways. There is
   none in it today. This is the cheaper half of the pair. */
.note-fig--diagram .note-diagram__scroll { overflow-x: auto; position: relative; }
.note-fig--diagram svg {
  display: block;
  width: 100%;
  min-width: 560px;
  height: auto;
}
.note-diagram__cell { fill: none; stroke: var(--line); }
.note-diagram__cell--used { fill: var(--paper); stroke: var(--frame-line); }
/* Three arrows rather than one line. A polyline said which four tiles were on
   the path and nothing about the order it walked them, which is the only thing
   the path is in the picture to say: the order is what fixes the digit
   sequence the table below takes apart. Each segment stops short of the numeral
   at both ends, so the arrow points at a digit instead of through it. */
.note-diagram__path {
  fill: none;
  stroke: var(--ink);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.note-diagram__arrow { fill: var(--ink); }
/* Where it begins. The first arrow already points away from the 5, so this is
   the same fact said twice, and it is worth saying twice: a reader looking for
   the start of a path looks for a mark, not for the absence of an arrowhead. */
.note-diagram__start { fill: var(--ink); }
/* The run a row is about, and the heaviest thing in the row. It is the row's
   subject; the values under it are what the subject can reach, and a reader
   scanning for `5 2 9` should find it without reading the numbers beside it.
   500, which is a face the page now asks for: the write-ups used `--mono`
   without ever loading IBM Plex Mono, so every mono token on them had been
   falling back to whatever the machine had. */
.note-diagram__digits {
  font-family: var(--mono);
  font-size: 16px;
  font-weight: 500;
  fill: var(--ink);
}
/* The snake passes behind the numeral it runs through, drawn as a halo in the
   tile's own fill rather than by breaking the line: a stroked-then-filled glyph
   keeps the text selectable and readable, where a gap in the polyline would
   have to be recomputed for every path. */
.note-diagram__digits--onpath {
  paint-order: stroke fill;
  stroke: var(--paper);
  stroke-width: 5px;
  stroke-linejoin: round;
}
/* The target, set large beside the board because it is the question the whole
   table is answering. It takes the heading's brown rather than the accent: the
   accent is rationed to three jobs on this site and marking a number in a
   diagram is not one of them, and size alone already makes it the first thing
   read. Mono, like every other numeral in these diagrams. */
.note-diagram__target {
  font-family: var(--mono);
  font-size: 32px;
  fill: var(--ink-display);
}
.note-diagram__count {
  font-family: var(--sans);
  font-size: 14px;
  fill: var(--muted);
}
/* The three runs that carry the answer. They are already the filled rows; this
   is the same distinction repeated in the text beside them, so a reader
   scanning the right-hand column alone still finds 3, 17 and 20. */
.note-diagram__count--hit { fill: var(--ink); }
/* The sample of answers a run can produce. Mono, because the point of the line
   is that the numbers under each other line up and can be compared. */
.note-diagram__vals {
  font-family: var(--mono);
  font-size: 14px;
  fill: var(--muted);
}
/* The 20 in the last row is the target, so it is drawn in the target's blue,
   the same one the mock's big number takes. Not the same hex in light, and
   that is the whole of the decision: `#0596D4` is 2.5:1 on `--paper`, and this
   is 14px mono that has to be read, not a 44px number inside a drawing of an
   app. The light value is that blue darkened until it clears 4.5:1 on the
   row's own stock. The app's own lighter blue was the dark-mode half of this
   pair and went with dark mode on 2026-09-20. */
.note-diagram__goal { fill: #0A6491; }

/* The one digit a row folds in, marked in its own label. It takes the accent,
   the same one the string and the operators take, so the digit, the step that
   used it and the line joining the rows read as one mark. */
.note-diagram__on { fill: var(--accent-ink); }

/* The answer a row reaches, circled rather than colored. The accent is spoken
   for twice over by the time it gets here, and one color meaning three things
   is one meaning too many. A circle, not a box: every other rectangle in this
   figure is a container, and a fifth one around two characters would read as
   another cell rather than as someone's ring around the number they found.
   Round caps, because the two ends are where the pen started and stopped and a
   flat cut at an overlapping tail reads as a mistake rather than a stroke. */
.note-diagram__pick {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The string up the left of the table, joining each row's answer to the answer
   of the row above. It crosses row borders on the way, so it is drawn last and
   takes the accent: this is the one line in the figure that is a path through
   the data rather than a border around it, and the board's own path already
   owns `--ink`. */
.note-diagram__climb {
  fill: none;
  stroke: var(--accent-ink);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The operator each step used, beside the step. Haloed in the ground rather
   than placed in a gap: the gaps between rows are 12px and the labels have to
   sit where the step is, not where there happens to be room. */
.note-diagram__step {
  font-family: var(--mono);
  font-size: 14px;
  fill: var(--accent-ink);
  paint-order: stroke fill;
  stroke: var(--ground);
  stroke-width: 4px;
  stroke-linejoin: round;
}
/* ---- the game's own screen, drawn inside the diagram ----

   These are Karmel's tokens, not this site's, and they are hardcoded on purpose.
   A picture of the product is allowed to look like the product, the same license
   a photograph of a red door has to be red, and the alternative is a drawing of
   a blue app in sand and brown that the reader has to be told is the app. Read
   off `src/constants.css`: --page-light, --key-light, --grey-200, --grey-700,
   --blue-500, --orange-500 and Roboto.

   The tiles are square: `--radius-none`, which is what the constants file said
   and what the running game does. A screenshot was read as rounded on
   2026-09-16 and the corners were rounded to match; that reading was wrong and
   is reverted here.

   The weights come from the components rather than the tokens, because that is
   where the game picks them: `target-display.js` sets the number to
   --weight-black and `number-tile.js` sets the digits to --weight-medium. 900
   against 500 is the whole point of the target, and drawing both at 700 flattened
   it. Roboto is requested at 400, 500, 700 and 900 for that reason.

   They do not theme with the site. The game's own note says the brand blue and
   orange are left as the game draws them. The panel behind them is the game's
   light page, which is the one the figure shows. */
.note-app { fill: #F4F7FA; }

.note-app__label {
  font-family: "Roboto", var(--sans);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.2px;
  fill: #0596D4;
}
/* --weight-black in the game. */
.note-app__target {
  font-family: "Roboto", var(--sans);
  font-size: 44px;
  font-weight: 900;
  fill: #0596D4;
}
.note-app__tile { fill: #FDFEFF; stroke: none; }
/* A tile on the chain. The game fills these rather than outlining them. */
.note-app__tile--on { fill: #D9D9D9; }
/* The one outlined tile, which in the game is where the chain currently sits.
   3px, the game's own --tile-ring. */
.note-app__tile--active { stroke: #FF9900; stroke-width: 3; }
/* --weight-medium in the game, against the target's 900. */
.note-app__digit {
  font-family: "Roboto", var(--sans);
  font-size: 27px;
  font-weight: 500;
  fill: #2E2E2E;
}

.note-diagram__label {
  font-family: var(--sans);
  font-size: 14px;
  fill: var(--muted);
}

/* The scale note under the phone mock: muted sans, centered, because a caption
   is a label on the thing above it and the mock is now centered too.

   This is `--text-micro`'s second use, and that token's comment asks for an
   argument. It is the same one: the note is an aside about the drawing rather
   than interface text, and the `<desc>` on the SVG carries the sentence in
   full for anyone who cannot read it at size. Kept at 14px on the first pass
   for the floor's sake and asked for again, which settles it. Worth knowing
   that the SVG is clamped to a 560px minimum and scaled to the column, so on a
   narrow one these 12 units land nearer 11px. */
.note-diagram__caption {
  font-family: var(--sans);
  font-size: var(--text-micro);
  fill: var(--muted);
}

/* The table's own title, centered over the 8-to-592 span the rows occupy. It
   sits a step above the labels rather than beside them, in full ink, because
   the rows under it are a single named object and the muted 14px label voice
   would have read as one more aside. 18px at 600, up from 16 at 500 on
   2026-09-16: with the scale note moved under the mock the two sat close
   enough in size to read as a pair of asides, and this one is a title. */
.note-diagram__tabletitle {
  font-family: var(--sans);
  font-size: 18px;
  font-weight: 600;
  fill: var(--ink);
}

/* The stepped figure: a panel rather than a drawing, because its content is a
   list of tokens that arrive and leave, and every frame of an SVG version would
   need its coordinates worked out by hand. It takes the diagram's vocabulary
   even so: hairline boxes, mono tokens, sans labels, and the same left
   alignment. */
.note-fig--steps { text-align: left; }
.note-steps {
  border: 1px solid var(--line);
  padding: 16px;
}
.note-steps__block + .note-steps__block { margin-top: var(--s5); }
.note-steps__act {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
  margin: 0 0 12px;
}
.note-steps__rail {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 8px;
}
/* One line or two: what the rail holds, and on the stacks a second line saying
   so. The role line keeps the token row's own 32px so it sits on the first row
   of tokens whatever the rail does underneath; the second line hangs below it at
   its own line height, so adding it moved nothing that was already aligned. On a
   stack, where the label is pinned to the floor, the pair sits above the floor
   in the same order. */
.note-steps__lab {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
  flex: none;
  width: 80px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}
.note-steps__lab-role { line-height: 32px; }
/* What the rail is, on the two rails that are stacks. It is an aside about the
   one above it, so the parentheses carry it rather than a second color or a
   smaller size: 14px is the interface floor and there is nothing under it to
   use. Whole-pixel line height, or the line box comes out fractional and the
   rounding moves the measured panel height by 1px between frames, which is
   exactly what the locked geometry exists to stop. */
.note-steps__lab-kind {
  font-size: 14px;
  line-height: 18px;
  color: var(--muted);
}
/* The rail keeps its height when it is empty. An empty stack is a fact about
   the frame, and a row that collapses to nothing moves everything under it. */
.note-steps__toks {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
  min-height: 32px;
}
/* Act one's two rails stand side by side with their feet on one line, the
   stack's floor level with the row it feeds. They wrap to two lines when the
   column is too narrow to hold both.

   `--s5` above. It collapses with the infix rail's own `--s2` rather than
   adding to it, so the number written here is the gap that appears. The three
   rails are not one list: the infix is what the act reads and the pair is what
   it produces, and at the rail gap alone a stack three tokens deep came up
   close enough under the infix to read as a fourth row of it. */
.note-steps__pair {
  margin-top: var(--s5);
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 12px 24px;
}
.note-steps__pair .note-steps__rail { margin-bottom: 0; }

/* A stack is drawn as one: the base at the bottom and the top of it at the
   top, growing upward as tokens are pushed. It is the only rail here that is
   a column, because it is the only rail that is a stack. The taps and the
   postfix are read left to right and stay rows. The height is set inline from
   the deepest the stack gets, so the panel does not grow and shrink under the
   reader between frames. */
.note-steps__rail--stack { align-items: flex-end; }
.note-steps__toks--stack {
  flex-direction: column-reverse;
  justify-content: flex-start;
  align-items: flex-start;
  /* The floor the stack sits on. Without it an empty stack is a hole in the
     panel rather than an empty container, and the reserved height above it
     reads as a gap rather than as room to grow into. */
  min-width: 32px;
  border-bottom: 1px solid var(--frame-line);
}

.note-steps__tok {
  font-family: var(--mono);
  font-size: 16px;
  min-width: 32px;
  height: 32px;
  line-height: 30px;
  padding: 0 6px;
  text-align: center;
  border: 1px solid var(--line);
  color: var(--ink);
}
.note-steps__tok--read { color: var(--muted); }
/* A paren that has been popped. It keeps its box, because it is still part of
   the expression the reader typed, and takes a line through it, because it is
   not part of anything downstream. The line stays for the rest of the walk.

   No red, and no second color of any kind: the palette has one accent, and a
   destructive hue introduced for two characters in one frame would be a token
   the rest of the site has no use for. Crossing a thing out is the gesture
   people already use for finished with, and a 1px `--muted` rule survives both
   themes. Set before `--now` and `--partner` so that on the frame the pair
   fires the two tokens keep the accent mark and the ink, and dim only once the
   read head has moved on. */
.note-steps__tok--gone {
  position: relative;
  color: var(--muted);
}
.note-steps__tok--gone::after {
  content: "";
  position: absolute;
  left: 5px;
  right: 5px;
  top: 50%;
  height: 1px;
  background: var(--muted);
  transform-origin: left center;
}
/* Drawn, not blinked: the line is struck across on the frame the pair is
   discarded and then simply stays. `transform` only, so nothing around it
   reflows. */
.note-steps__tok--gone.is-struck::after {
  animation: note-steps-strike var(--dur-base) var(--ease);
}
@keyframes note-steps-strike {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
/* The read head is an underline in the accent rather than a fill behind the
   token. The accent is a fill color with no text step of its own in either
   theme, so a character sitting on it would have to invent one.

   A closing paren's partner takes the same mark, not a second color: two
   tokens wearing one mark is what says they are a pair. That they are also
   being thrown away is said by the strike above, which arrives on the same
   frame, so the pair and its fate are one gesture rather than two colors. */
.note-steps__tok--now,
.note-steps__tok--partner {
  border-color: var(--frame-line);
  border-bottom: 3px solid var(--accent);
  color: var(--ink);
}
/* A throwaway copy of a token, flown from where the real one was standing to
   where it is going and thrown away on arrival. It is `fixed` and outside the
   panel so that nothing in the figure moves to make room for it, which is what
   keeps the measured geometry true while something is in flight. It takes the
   panel's ground rather than none, because it passes over the tokens beneath
   it and a transparent box would read as a ghost of a ghost. */
.note-steps__ghost {
  position: fixed;
  z-index: 3;
  margin: 0;
  list-style: none;
  pointer-events: none;
  background: var(--ground);
  will-change: transform, opacity;
}

@keyframes note-steps-arrive {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
.note-steps__tok.is-new {
  animation: note-steps-arrive var(--dur-base) var(--ease);
}
/* The frame's own sentence. Centered and set off below the rails rather than
   sitting flush under them: it is the one thing in the panel that is not part
   of the drawing, and left-aligned at `--s4` it read as a fifth rail with no
   label. `--s5` above and a measure well short of the panel keep it a caption,
   which is what it is. The centering follows the figure captions elsewhere on
   the page, not the running copy, which stays left-aligned everywhere.

   `min-height` holds three lines whether the sentence needs them or not, so the
   controls under it do not move between frames. */
/* `.note-body p` beats a bare class on both count and order, so this is written
   with the panel in front of it. Without that the note took the body's own
   paragraph margins and had no top margin at all, which is what had it sitting
   flush under the rails. */
.note-steps__panel .note-steps__note {
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
  text-align: center;
  max-width: 54ch;
  margin: var(--s5) auto var(--s3);
  min-height: 3em;
}
/* Step sits at the right-hand end, where the pointer can rest between presses.
   The panel holds one size across every frame, so it does not move. Reset sits
   at the other end with the frame count it acts on, out of reach of the button
   being pressed over and over. */
.note-steps__ctl {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 8px;
}
.note-steps__ctl-group {
  display: flex;
  align-items: center;
  gap: 16px;
}
/* Step is the one they came for, so it is filled. Back and Reset are plain
   text beside it, which is the weighting the house rule asks for: rank the
   actions, then give each the least chrome that still reads. An outline was
   the first answer and it was too quiet, because the figure it sits under is
   itself a panel with a border, so the button read as one more box on the
   page rather than the thing to press. The fill is `--accent` on
   `--on-accent`, the same clothes the write-up's `try it` wears, so the page
   has one primary button and not two definitions of one. */
.note-steps__btn {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--on-accent);
  background: var(--accent);
  /* The border matches the fill rather than being dropped, so the box is the
     same size in every state and the spent button below can take an edge
     without the row shifting a pixel. */
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 6px 16px;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.note-steps__btn:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}
/* Spent, not gone. `aria-disabled` rather than `disabled` keeps it readable and
   keeps it in the tab order, which is the same choice the board's dead filter
   pills make: a control that drops out of the keyboard's reach has not been
   disabled, it has disappeared. The fill comes off entirely at the end of the
   walkthrough, which is the whole point of filling it: the one button on the
   panel that is worth pressing is the one carrying color, and when there is
   nothing left to step through there is nothing carrying color. */
.note-steps__btn[aria-disabled="true"] {
  color: var(--muted);
  background: none;
  border-color: var(--line);
  cursor: default;
}
.note-steps__btn[aria-disabled="true"]:hover {
  background: none;
  border-color: var(--line);
}
.note-steps__plain {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 6px 0;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease);
}
.note-steps__plain:hover { color: var(--ink); }
.note-steps__count {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
}
/* The static figure, which is what a reader with no JavaScript gets: the taps,
   the postfix, and the answer, with no controls under it to press. */
.note-steps__still {
  display: flex;
  gap: 12px;
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--muted);
}
.note-steps__still span {
  flex: none;
  width: 60px;
  text-align: right;
}
.note-steps__still code {
  font-family: var(--mono);
  font-size: 16px;
  color: var(--ink);
}
@media (prefers-reduced-motion: reduce) {
  .note-steps__tok.is-new { animation: none; }
  .note-steps__tok--gone.is-struck::after { animation: none; }
  .note-steps__btn,
  .note-steps__plain { transition: none; }
}

/* A write-up that is not finished says so, once, at the end of what there is.
   Sans and muted, which is how the rest of the page marks a line that is about
   the page rather than part of it: a caption, a tag, the version under a
   button. Written as body prose it would read as the article's closing
   sentence, which is the opposite of what it means.

   Centered, because it belongs to no paragraph, and given a section's worth of
   space above so it does not attach itself to the picture it follows. */
.note-soon {
  font-family: var(--sans);
  font-size: var(--text-label);
  color: var(--muted);
  text-align: center;
  margin: var(--s7) 0 0;
}

/* The caption follows the picture rather than the column. A line sitting under
   a centered photograph at the far left edge belongs to neither. */
/* A caption written first in the figure is a title over the picture rather than
   a label under it, so the margin moves to the other side. The boards figure
   uses it: the five boards are five of a hundred and nothing else on the page
   says so, and a line above them says it before they are read rather than
   after. */
.note-fig figcaption:first-child { margin: 0 0 var(--s4); }

.note-fig figcaption {
  /* Sans. A caption is a label on a picture, not a sentence of the article, and
     it now has to say so explicitly: before the body went serif it simply
     inherited the sans it wanted. */
  font-family: var(--sans);
  font-size: var(--text-label);
  color: var(--muted);
  margin: var(--s3) 0 0;
}

/* Three other projects at the end of a write-up. This was a previous and next
   pair, which asked the reader to care about an ordering they have no reason to
   care about: whatever happened to sit either side of this project on the board.
   Three pictures with names under them is the board's own language, and which
   three is decided by shared tag, so a music project is followed by the other
   music projects.

   No card chrome. A tile on the board is a photograph printed on the ground with
   its name underneath, and this is the same thing one size down. */
.note-body .note-also { margin-top: var(--s8); }

.note-also__row {
  list-style: none;
  margin: 0;
  /* The strip straddles the card's top edge and hangs 7px above it, and a
     tilted card pushes its corners a few pixels past its box. Without room to
     do both the tape is cropped into a bar sitting on the card. */
  padding: var(--s4) 0 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s5);
  /* Stretch, not `start`. Three polaroids in a row are three of the same
     object, so the foot has to be the same depth on all three whether the name
     under the picture runs to one line or three. Stretching the cells and
     letting each card fill its own gives the row the tallest foot and gives
     every card that foot. */
  align-items: stretch;
}

/* The polaroid. One piece of white stock with the picture printed on it: even
   border on three sides, a deeper foot, and the name written on the foot. The
   board upstairs is the Pinterest wall; the foot of an article is not a second
   one, so these read as three things somebody set down. */
.note-also__card {
  display: flex;
  flex-direction: column;
  height: 100%;
  position: relative;
  text-decoration: none;
  background: var(--card);
  padding: var(--s3) var(--s3) 0;
  border-radius: var(--radius-sm);
  /* Not `--shadow-tile`: a tile on the board floats free and takes the shadow
     that describes four sides; this card is taped to a wall at the top. */
  box-shadow: var(--shadow-taped);
  /* `--dur-fast`, the hover token, not `--dur-base`. The move is 2px, and
     200ms over 2px is slow enough to read as the card drifting rather than
     answering the pointer. */
  transition: box-shadow var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

/* Tilt, ±0.35 to ±0.7deg per DESIGN.md. Different on each card and not in a
   pattern, because three cards leaning the same way is a design element and
   the point is a hand.

   Halved on 2026-09-16, from ±0.8 to ±1.4. At the old angles a 380px card
   dropped its far corner about 9px, which the eye reads against the straight
   run of text above it as a card that is crooked rather than one that was put
   down by a person. Half of that is 4px, which is the same gesture without the
   accusation. The strips came down with them, since the point of their angle
   is that it disagrees with the card's, and a disagreement scales. */
.note-also__row > li:nth-child(1) .note-also__card { transform: rotate(-0.55deg); }
.note-also__row > li:nth-child(2) .note-also__card { transform: rotate(0.4deg); }
.note-also__row > li:nth-child(3) .note-also__card { transform: rotate(-0.7deg); }

.note-also__card img {
  display: block;
  width: 100%;
  max-width: 100%;
  /* Not optional. Every one of these images carries a `height` attribute, which
     lands as a presentational `height` in pixels, and `aspect-ratio` only gets
     to decide the height when the height is `auto`. Without this the thumbnails
     come out at their full capture height, 900px in one case, with the square
     below quietly ignored. */
  height: auto;
  /* Square, because the five covers run from a 900x501 screenshot to a 427x940
     phone. Left at their own ratios the row is a staircase; cropped square it
     reads as one row of three. Centered rather than anchored to the top: the
     subject of every one of these covers sits in the middle of the frame, so a
     top anchor on the 427x940 phone shot spent the square on its status bar and
     on the 900x501 screenshot cut the panel off at the knees. */
  aspect-ratio: 1;
  object-fit: cover;
  object-position: center;
  /* The print is square-cornered. A rounded picture inside a square card is a
     second radius arguing with the first; the stock carries the only one. */
  border-radius: 0;
}

/* Paper painter's tape, drawn 76x21 and straddling the top edge about a third
   above, two thirds over. `.85` rather than the `.7` washi figure, because
   masking tape is opaque paper: at .7 the picture read straight through it and
   the strip looked like a color wash rather than a thing lying on top. The
   crepe grain is `#tapeGrain`, one filter per document declared at the head of
   the section, referenced by all three strips. The rotation is relative to a card that is already
   tilted, so the strip always sits at odds with the edge it crosses, which is
   what makes it read as tape rather than as a bar somebody placed. No shadow
   under it: the overlap already says it sits on top.

   Two of the three ends are torn and the middle one is cut clean, which is the
   shape in the markup rather than anything here: a `<path>` with a jag across
   each short end on the outer two, a `<rect>` on the middle. Tape comes off a
   roll differently every time, and three identical rectangles is what the tilt
   was already there to avoid. */
/* The grain's filter lives in a zero-sized carrier, not in the first strip, so
   no one strip owns a definition the other two depend on. */
.note-also__defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.note-also__tape {
  position: absolute;
  top: -7px;
  left: 50%;
  width: 76px;
  height: 21px;
  fill: var(--tape);
  opacity: .85;
  pointer-events: none;
}

.note-also__row > li:nth-child(1) .note-also__tape { transform: translateX(-50%) rotate(1.3deg); }
.note-also__row > li:nth-child(2) .note-also__tape { transform: translateX(-50%) rotate(-1.6deg); }
.note-also__row > li:nth-child(3) .note-also__tape { transform: translateX(-50%) rotate(1.1deg); }

.note-also__card:hover { box-shadow: var(--shadow-taped-lift); }

.note-also__name {
  display: block;
  /* The foot. Deeper than the three borders around the picture, which is the
     whole of what makes a piece of card a polaroid. */
  padding: var(--s3) 0 var(--s4);
  /* Takes whatever height the tallest name in the row asked for. */
  flex: 1 0 auto;
  font-family: var(--serif);
  /* The board's caption, to the pixel: 16px at 500, in the display brown. The
     same object should not be labelled two different ways on two pages. */
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.3;
  color: var(--ink-display);
}

.note-also__card:hover .note-also__name { color: var(--accent-ink); }
.note-also__card:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

@media (prefers-reduced-motion: no-preference) {
  /* The card lifts as one object, tape and all. Scaling the picture inside its
     own border would slide the print around under the stock. Each keeps its
     own tilt, or hover would snap all three straight.

     2px, halved on 2026-09-17. The card is taped down at the top, so the
     distance it can travel is the distance the tape allows, and 4px was a
     card coming off the wall. It is also the same gesture the tilt is: a
     small enough move that the eye takes it for the paper answering a hand
     rather than for an element animating. */
  .note-also__row > li:nth-child(1) .note-also__card:hover { transform: rotate(-0.55deg) translateY(-2px); }
  .note-also__row > li:nth-child(2) .note-also__card:hover { transform: rotate(0.4deg) translateY(-2px); }
  .note-also__row > li:nth-child(3) .note-also__card:hover { transform: rotate(-0.7deg) translateY(-2px); }
}

/* Scoped to `.note-body` so it outweighs the paragraph rule above it, which
   would otherwise zero the top margin and park the link under the last line. */
.note-body .note-foot { margin: var(--s8) 0 0; }
/* Sans, for the same reason as the caption: this is the way out of the page,
   not a line of it. */
.note-foot a { font-family: var(--sans); font-size: var(--text-sm); color: var(--muted); text-decoration: none; }
.note-foot a:hover { color: var(--ink); }

@media (max-width: 760px) {
  .note-grid { grid-template-columns: minmax(0, 1fr); }
  .note-head h1 { max-width: none; }
  /* The band is most of the first screen on a phone, so it gives back what it
     can: the gap between its parts and the gap under it both come in a step. */
  .note-head { margin-bottom: var(--s7); }
  .note-meta { gap: var(--s3); margin-top: var(--s4); }

  /* Still three across: it is a glance at what else there is, not a third
     column of reading. The gap tightens so the thumbnails keep their size. */
  .note-also__row { gap: var(--s3); }
  /* The stock thins with the card. 12px of border around a 90px picture is a
     frame with a photo in it rather than a photo on a card. */
  .note-also__card { padding: var(--s2) var(--s2) 0; }
  .note-also__name { padding: var(--s2) 0 var(--s3); }
}

/* The name drops to the 14px interface floor everywhere the squares are small,
   which is everything under 1000px, for one reason in two shapes. Between
   760px and 1000px the rail still takes its 200px out of the line, so three
   squares share about 440px and come out around 135px wide. Under 760px the
   rail is gone but so is the width: at 390px the card is 99px and the foot
   83px. Either way, at 16px the longest name runs to three lines and the foot
   comes out as deep as the picture, which is a label card, not a polaroid. At
   14px the same name is two lines and the foot is 56px.

   A step down the type scale rather than a cut, because down to 390px it
   keeps both the shape and every word. The cut is the floor below it, and the
   block under this one is where it lives. */
@media (max-width: 1000px) {
  .note-also__name { font-size: var(--text-label); }
}

/* The floor. Under 380px the card is 78px wide and the type is already at the
   14px interface floor, so `RFID Record Player` takes a third line there
   whatever the size, and the foot comes back out as deep as the picture. Two
   lines is the cap, and the third is cut with an ellipsis: the foot is one
   depth at every width, which is what makes three of these read as the same
   object, and the cost is one word on one card on the smallest phones. The
   whole name is on the write-up the card leads to.

   The clamp is on `.note-also__label`, an inner span, and the span is why
   there is any markup here at all. A flex item's `display` is blockified, so
   `-webkit-box` on the name itself becomes `flow-root`; Chrome 152 then drew
   the ellipsis on line two and painted `Player` on a third line hanging below
   the card, with `overflow: hidden` on the box it hung out of. Moving the
   clamp one level in, onto something that is not a flex item, is the whole
   fix: the card keeps the flex column that gives every foot one depth, and
   the clamp keeps its own box. Prefixed properties only. The standard
   `line-clamp` shorthand wins the cascade where a browser has taken part of
   it and not the rest, and then does not clip. */
@media (max-width: 380px) {
  .note-also__label {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }
}

/* Tilt goes to zero under 600px: rotation in a narrow column costs horizontal
   room the layout does not have, and the strip is enough on its own. */
@media (max-width: 600px) {
  .note-also__row > li:nth-child(n) .note-also__card,
  .note-also__row > li:nth-child(n) .note-also__card:hover { transform: none; }
  .note-also__row > li:nth-child(n) .note-also__tape { transform: translateX(-50%); }
  .note-also__tape { width: 56px; height: 15px; top: -5px; }
}

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* The comparison table on the sift write-up. Rows are jobs, columns are the
   apps that could do them, and a cell says what the job costs rather than
   whether the app has a feature. That framing is the honest one here: Preview
   can rotate a selection and can adjust a photo's brightness, so a yes-and-no
   matrix would have claimed two things a reader can disprove in half a minute.
   What Preview cannot do is any of it quickly, which is the argument the
   paragraphs above it are actually making.

   No hue anywhere in it. A comparison chart is exactly where a red column and a
   green column want to appear, and the accent is rationed to three jobs that
   this is not one of. The one distinction drawn is weight: sift's column sits
   in `--ink` and the rest in `--muted`, so the answer reads as the answer
   without being colored in. */
.note-compare__scroll {
  overflow-x: auto;
  margin: var(--s6) 0;
  /* The containing block for what is inside it, which an `overflow` box is not
     on its own. Every cell in this table carries an `.sr-only` span saying
     `Yes` or `No` in words, and `.sr-only` is `position: absolute`. With no
     positioned ancestor those 24 spans resolved against the initial containing
     block: they were laid out at their static position, up to 470px across
     inside a 560px table, and the scroller could not clip something that was
     never in its flow. The page scrolled sideways on a phone by the amount the
     furthest span stuck out. Nothing about the fix is visible, which is why it
     is written down: `overflow` clips descendants it is a containing block
     for, and it is one only when it is positioned. */
  position: relative;
}
.note-compare__scroll:focus-visible {
  outline: 2px solid var(--ink-display);
  outline-offset: 4px;
}
.note-compare {
  border-collapse: collapse;
  font-family: var(--sans);
  font-size: var(--text-sm);
  /* Wider than the column it sits in, and allowed to be: the alternative is
     five columns of two-word cells wrapping to three lines each. It scrolls
     sideways inside its own box, the way the karmel diagram does. */
  min-width: 560px;
}
/* A floor on the app columns. They are sized by their content, and `Sift` is a
   four-letter heading over a column that was all marks and no notes, so the
   first note to land in it wrapped to three lines in 44px. 96 is wide enough
   for two words a line and still leaves the table inside the column. */
.note-compare tbody td { min-width: 96px; }
.note-compare th,
.note-compare td {
  text-align: left;
  vertical-align: top;
  padding: var(--s3) var(--s4) var(--s3) 0;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-weight: 400;
}
/* The job being compared. Sticky, so it is still on screen once the table has
   been scrolled sideways to reach the last column; the ground behind it is
   opaque for the same reason. */
.note-compare tbody th {
  position: sticky;
  left: 0;
  background: var(--ground);
  color: var(--ink);
  padding-right: var(--s5);
}
.note-compare thead th {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--ink);
}
.note-compare tbody tr:last-child th,
.note-compare tbody tr:last-child td { border-bottom: 0; }
/* The app the write-up is about. Full ink against the others' muted, which is
   the whole of the emphasis. */
.note-compare .is-sift { color: var(--ink); }
/* A row label that needs a unit. `Lightweight` on its own is a feeling; the
   numbers under it are megabytes, and this says so once rather than repeating
   `MB on disk` in four cells. */
.note-compare__sub {
  display: block;
  font-weight: 400;
  color: var(--muted);
}

/* The mark, and the note under it. A check or a cross says whether the job can
   be done at all; the note says what it costs when the answer is yes but slow,
   or where you end up when it is no. Both marks are drawn rather than typed:
   the emoji ban applies, and a ✓ from the text stream is whatever the reader's
   font decides it is.

   Both take the same ink. A green check against a red cross is the reflex here
   and it is two hues this site does not have, on a page whose whole argument is
   already carried by the shapes. */
.note-compare__defs { position: absolute; }
.note-compare__mark {
  display: block;
  color: var(--ink);
}
.note-compare__note {
  display: block;
  margin-top: var(--s1);
  color: var(--muted);
  /* A size below the interface floor would be the wrong economy here; it stays
     at the table's own 16 and earns its room by being short. */
  max-width: 26ch;
}
