/* Components
 * ---------------------------------------------------------------------------
 * The shape of the content types, shared by every theme that takes it.
 *
 * Books, watching, links, photos and videos are the same object whatever the
 * site looks like, so their layout is written once here rather than once per
 * theme. Every rule below was identical in carrier.css and system.css before
 * this file existed - 76 of them, kept in step by hand until now. That hand
 * keeping is what let .shelf--wide reach one theme and not the other, which
 * cropped every video to a book cover's 2:3.
 *
 * THIS FILE CONTAINS ONLY WHAT IS TRUE FOR EVERY SKIN. Anything one theme needs
 * alone lives in that theme. A rule that exists so a theme can undo it belongs
 * to the theme, not here, or this stops being neutral and becomes the union of
 * every theme - the failure _data/themes.js already forbids one layer down.
 *
 * It is not a theme. Nobody can select it, it has no name a reader sees, and it
 * declares no colour, face or ornament: every value is a var() the skin
 * supplies. A skin that fails to declare one renders wrong and silently.
 *
 * Shape only. Which sections appear on the homepage is the skin's: Carrier lays
 * out a three-column console, System shows the writing and hides the rest, and
 * neither should have to undo the other.
 *
 * Posts contributed nothing here for a long time, which was the point working -
 * a post is prose in a column and the base HTML already carries it. That stopped
 * being true when the live corpus arrived: the archive uses five callout and
 * footnote components in its bodies, and they are the same object in every skin,
 * so they are written once here. What is NOT the same in every skin is the face,
 * and that is the one thing left to the themes.
 */

/* The heatmap. 52 columns of seven days; it scrolls on a narrow screen rather
   than shrinking the cells into mush. */
.heatmap {
  overflow-x: auto;
  padding-bottom: var(--space-2);
}

/* A month name is wider than the week it labels and is allowed to run over the
   empty cells after it. Wrapping it instead would stack the name inside one
   0.8rem column and push the strip several lines tall. */
.heatmap__months span {
  white-space: nowrap;
}

/* One grid, seven explicit rows filled column by column, so the label column
   and the day cells share rows and cannot drift apart. --heat-cell is the FLOOR
   of a week column, not its ceiling: the grid fills its container and a day is
   whatever fraction of that a week gets, but never smaller than the floor, so
   .heatmap scrolls rather than grinding the cells down. The cells stay square
   because .heatmap__day carries aspect-ratio, so a wider grid is a taller one.

   The floor is what keeps this from being the old table. There the cells had no
   floor and no ceiling and thirteen of them divided the whole measure, which is
   how a heatmap became a spreadsheet; with 52 columns the same fr arithmetic
   gives a square. */
.heatmap__grid {
  display: grid;
  grid-template-rows: repeat(7, auto);
  grid-template-columns: var(--w-heat-gutter);
  grid-auto-flow: column;
  grid-auto-columns: minmax(var(--heat-cell), 1fr);
  gap: var(--heat-gap);
}

/* The legend swatches live outside .heatmap, so every value here has to hold on
   its own rather than inherit from the grid. */
.heatmap__day {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  border-radius: var(--radius);
  background: var(--heat-0);
  border: var(--hairline) solid var(--rule);
}

.heatmap__day--1 { background: var(--heat-1); }

.heatmap__day--2 { background: var(--heat-2); }

.heatmap__day--3 { background: var(--heat-3); }

.heatmap__day--4 { background: var(--heat-4); }

/* A day the window covers but the calendar has not reached. */
.heatmap__day--future { opacity: 0.35; }

.heatmap__scale {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.heatmap__scale i {
  display: inline-block;
  width: var(--heat-cell);
}

/* The three charts abreast, wrapping to two then one as the column tightens. */
.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--w-chart-col), 1fr));
  gap: var(--space-6);
  align-items: start;
}

/* One row component for the charts and for last played: art, then the name over
   its artist, then the figure on the right. */
.chart-list,
.last-played {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-8);
}

.chart-hero__art {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius);
}

.chart-list li:first-child .chart-text { margin-top: var(--space-2); }

.chart-list li:first-child .chart-name {
  font-size: var(--text-lg);
  font-weight: 700;
}

.chart-thumb {
  flex: none;
  width: var(--art);
  height: var(--art);
  border-radius: var(--radius);
  object-fit: cover;
}

.chart-thumb--empty { background: var(--bg-alt); }

/* Track and album titles run long and this column is narrow. Breaking inside a
   word beats a title that pushes the play count off the row. */
.chart-name { overflow-wrap: anywhere; }

/* The connectives that make each row a sentence when nothing is styling it.
   Three columns say the same thing by position, so the words come out. */
.chart-list .sep,
.last-played .sep { display: none; }

.chart-by {
  font-size: var(--text-sm);
  color: var(--muted);
  overflow-wrap: anywhere;
}

.challenge-count {
  margin: 0;
  font-weight: 700;
}

.challenge-status {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
}

/* The star green for ahead, the warm accent for behind: one reads as good and
   the other as a nudge rather than an alarm. */
.challenge-status .ahead { color: var(--star); }

.challenge-status .behind { color: var(--accent-warm); }

.challenge-bar .fill { color: var(--star); }

.challenge-bar .pct {
  font-size: var(--text-xs);
  color: var(--muted);
}

.challenge-bar > span { white-space: nowrap; }

/* --- A work's own page ---------------------------------------------------
 * One page for a book and for a film, in three parts: the header carries who
 * and what and what I scored it, then my review, then the work's own reference
 * material under .work-about. Live reuses the book page's classes for its
 * watch page already, so there is one shape here and not two.
 *
 * The two columns are a HEADER treatment and not a page layout: they earn
 * their keep for art beside identity and stop earning it once the art has
 * ended. One column below 34rem, where a 200px cover beside text leaves
 * neither enough room -- that breakpoint is per theme, because the widths
 * around it are.
 * ------------------------------------------------------------------------- */

.work-head {
  display: grid;
  grid-template-columns: minmax(0, var(--w-art-col)) 1fr;
  gap: var(--space-6);
  align-items: start;
  margin: var(--space-4) 0 var(--space-8);
}

/* Most books have no cover — 59 of live's 427 — so the art column comes off
   entirely rather than leaving an empty cell for the text to sit beside. */
.work-head--noart { grid-template-columns: 1fr; }

.work-head > .art { margin: 0; background: var(--bg-alt); }

/* The image transform wraps the img in a <picture>; both are blocks or the art
   carries a descender's worth of gap under it. */
.work-head > .art picture,
.work-head > .art img { display: block; width: 100%; height: auto; }

.work-head h1 { margin: 0 0 var(--space-1); }

/* The verdict: my stars, then when I watched it. */
.work-head .verdict {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin: var(--space-2) 0 0;
}

/* Live runs the verdict's stars a step larger than a byline's. The size has to
   go on the pseudo-elements, NOT the box: .rating hides its digits with
   font-size: 0 on the box, and setting a size there put "3/5" back on screen
   under the stars. */
.work-head .verdict-stars { line-height: 1; white-space: nowrap; }

.work-head .verdict-stars::before,
.work-head .verdict-stars::after { font-size: var(--text-xl); }

.work-head .byline strong { color: var(--ink); font-weight: 600; }

.work-about .tagline { color: var(--muted); font-size: var(--text-sm); }

.work-about .award-win svg { flex: none; }

.work-about .score {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: 0 var(--space-4);
  border-left: var(--hairline) solid var(--rule);
}

.work-about .score:first-child { padding-left: 0; border-left: 0; }

.work-about .score-src {
  font-size: var(--text-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Colour is never the only signal here: the source and the number are both
   written out, and the tier only tints what is already legible. */
.work-about .score-val.good { color: var(--star); }

.work-about .score-val.mid  { color: var(--accent-warm); }

.work-about .attribution img {
  display: inline-block;
  vertical-align: middle;
  height: var(--logo-h);
  width: auto;
}

/* --- Filter and sort panel -----------------------------------------------
 * Built by /js/watch-filter.js and inserted above the list it controls, so
 * there is no markup counterpart here to style when script does not run. The
 * reasoning for building it rather than shipping it is in that file.
 * ------------------------------------------------------------------------- */

.list-controls { margin: 0 0 var(--space-6); }

.list-controls[open] > .list-controls__row { margin-top: var(--space-3); }

.list-controls__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  align-items: end;
}

/* The checkbox is the one control its label reads after. */
.list-controls .list-controls__check {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
}

/* Search is the control reached for first, so its field takes the row's slack
   while the selects and Clear keep their natural size. The label is the flex
   item, not the input, which is why this selects the label that holds one. */
.list-controls label:has(input[type="search"]) {
  flex: 1 1 var(--w-search);
  min-width: 0;
}

.list-controls label:has(input[type="search"]) input { width: 100%; }

.shelf .art { display: block; text-decoration: none; }

/* The image transform wraps the img in a <picture>, which is inline by default
   and leaves a descender's worth of gap under the art. Both are blocks. */
.shelf .art picture,
.shelf .art img {
  display: block;
  width: 100%;
  border-radius: var(--radius);
  background: var(--bg-alt);
}

/* A film poster is always 2:3; a book cover is whatever the publisher made it,
   and left at its natural height the covers ended a grid row at four different
   places. The slot is fixed and the art is CONTAINED in it rather than cropped:
   cover art is mostly type, and `cover` would cut the title off the tile whose
   title it is. What letterboxing that leaves falls on the same ground the empty
   slot already uses. */
.shelf .art img {
  aspect-ratio: 2 / 3;
  object-fit: contain;
}

/* A video poster is 16:9 where a cover and a film poster are both portrait, so
   the wide variant states its columns rather than auto-filling: three tiles at
   that ratio is a row of stills, and auto-fill at the portrait width would cut
   them to postage stamps. Down the repo's own two upper breakpoints, and no
   others. */
.shelf--wide { grid-template-columns: 1fr; }

.shelf--wide .art img { aspect-ratio: 16 / 9; }

.shelf--wide .no-art { aspect-ratio: 16 / 9; }

.shelf > li > p { margin: var(--space-2) 0 0; }

.shelf .title cite { font-style: normal; }

/* The date is the parser's, not the reader's: a shelf is already in the order
   things were finished. */
.shelf > li > time { display: none; }

/* --- The photo log ------------------------------------------------------- 
 * /photos/ tiles rather than stacks: a column of full-width photographs is a
 * scroll, and the point of a log is to see them together.
 *
 * Selected as the feed whose entries carry a u-photo, which is the microformat
 * the markup already states about itself, so the base needs no class for this.
 * Every other listing here is text.
 *
 * :not([aria-labelledby]) because that stopped being true: the homepage's
 * "Recent photos" strip is also an h-feed of u-photo entries, and this block
 * outscored the strip's own six-column grid, so the homepage silently rendered
 * a justified log with its sixth photo stretched across the full width. The
 * distinction the markup already makes is that the log IS its page and carries
 * no heading to be labelled by, while the strip is a labelled section inside a
 * page about something else.
 *
 * The live site tiles by writing each photo's aspect ratio into the markup as
 * an inline `--ar` and growing each item by it. That is a style attribute per
 * entry, which this repo's own validation rejects and a theme may not add
 * anyway. The same justified rows are reachable without it, in three parts:
 *
 *   height fixed, width auto  the item's flex basis becomes the image's own
 *                             width at the row height, so the aspect ratio
 *                             proportions the row without being written down
 *   flex-grow                 the row then fills the container exactly
 *   min-width: 100%           and this is the piece that makes it work. Growing
 *                             the ITEM does not grow the picture inside it, so
 *                             the first attempt opened each row into gaps. A
 *                             percentage min-width resolves against the grown
 *                             item at layout time but not during intrinsic
 *                             sizing, so the basis still comes from the natural
 *                             width while the rendered image is forced to fill.
 *
 * object-fit: cover absorbs the difference as a crop rather than a stretch. The
 * crop is whatever share of the row's free space this item received, which is a
 * few percent, and it is why the ratios read as true without being exact.
 * ------------------------------------------------------------------------- */

main > .h-feed:not([aria-labelledby]) > ul:has(.u-photo) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  list-style: none;
  margin: var(--space-5) 0;
  padding: 0;
}

main > .h-feed:not([aria-labelledby]) > ul:has(.u-photo) > li > a { display: block; }

main > .h-feed:not([aria-labelledby]) > ul:has(.u-photo) picture,
main > .h-feed:not([aria-labelledby]) > ul:has(.u-photo) img {
  display: block;
  height: var(--photo-row);
  width: auto;
  min-width: 100%;
  object-fit: cover;
}

/* The caption is the entry's title and date. The image is the content here and
   its alt already names the link, so the line is hidden rather than tiling a
   ragged run of text under the photographs. The Text theme has no stylesheet,
   so it keeps showing them, which is the right answer for a stacked list. */
main > .h-feed:not([aria-labelledby]) > ul:has(.u-photo) > li > p { display: none; }

/* The scale. */
.rating::before {
  mask-image: var(--icon-star-empty);
  background-color: var(--rule);
}

/* The score, clipped over it. A half lands as the left half of a star, which is
   the same shape live draws with a dedicated half-star icon. */
.rating::after {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  mask-image: var(--icon-star);
  background-color: var(--star);
}

.rating[data-rating="0.5"]::after { width: 10%; }

.rating[data-rating="1"]::after { width: 20%; }

.rating[data-rating="1.5"]::after { width: 30%; }

.rating[data-rating="2"]::after { width: 40%; }

.rating[data-rating="2.5"]::after { width: 50%; }

.rating[data-rating="3"]::after { width: 60%; }

.rating[data-rating="3.5"]::after { width: 70%; }

.rating[data-rating="4"]::after { width: 80%; }

.rating[data-rating="4.5"]::after { width: 90%; }

.rating[data-rating="5"]::after { width: 100%; }

/* --- Highlighted code ----------------------------------------------------
 * Prism marks up a code block at build time and this decides what its spans
 * mean; the colours themselves are the theme's, named as roles in its :root.
 * That split is the same one the rest of this file makes, and it is why Text
 * needs no answer here: it takes no layer, the spans go uncoloured, and the
 * code still reads as code.
 *
 * Six roles, not Prism's forty classes. A palette that distinguishes a
 * "symbol" from a "constant" is drawing distinctions the reader does not make,
 * and every extra hue is another one to hold at 4.5:1 on the code ground.
 * ------------------------------------------------------------------------- */

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata { color: var(--code-comment); font-style: italic; }

.token.punctuation,
.token.operator,
.token.entity { color: var(--code-punct); }

.token.keyword,
.token.atrule,
.token.rule,
.token.important,
.token.selector { color: var(--code-keyword); }

.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.inserted { color: var(--code-string); }

.token.number,
.token.boolean,
.token.constant,
.token.symbol,
.token.property,
.token.attr-name,
.token.tag,
.token.variable,
.token.url,
.token.deleted { color: var(--code-value); }

.token.function,
.token.class-name,
.token.builtin,
.token.namespace { color: var(--code-fn); }

/* Prism emits these for its own markup and they must not read as emphasis in
   prose that happens to sit near a code block. */
.token.bold { font-weight: 700; }
.token.italic { font-style: italic; }

/* The grammars in the corpus -- bash, yaml, json, jinja, ini -- name their
   pieces differently from the C-shaped languages above. These are the classes
   the 34 posts with code blocks actually emit, checked against the build
   rather than copied out of Prism's docs; a class nothing here uses is not
   worth a rule. Anything still unmapped inherits the body ink, which is
   legible, so this is completeness and not correctness. */

.token.shebang { color: var(--code-comment); }

.token.delimiter,
.token.template-punctuation,
.token.interpolation-punctuation,
.token.regex-delimiter { color: var(--code-punct); }

.token.section,
.token.section-name,
.token.for-or-select,
.token.title { color: var(--code-keyword); }

.token.value,
.token.content,
.token.template-string,
.token.front-matter,
.token.regex-source,
.token.datetime { color: var(--code-string); }

.token.key,
.token.literal-property,
.token.string-property,
.token.assign-left,
.token.name,
.token.parameter,
.token.environment,
.token.file-descriptor,
.token.doctype-tag,
.token.regex-flags { color: var(--code-value); }

.token.function-name,
.token.function-variable { color: var(--code-fn); }

/* Post-body components, from the live archive.
   -------------------------------------------------------------------------
   Five things the corpus writes into post bodies by hand. They render as plain
   unstyled text without these rules, which is what fifteen imported posts were
   doing: the words are all there and undifferentiated, so neither cutover gate
   could see it.

   Shape and the tokens a skin supplies, as everywhere else in this file. The
   one thing held back is font-family, because it is a face and because the two
   skins do not even name it the same - Carrier declares --mono/--body, System
   declares --font-mono/--font-ui. Each theme sets its own.

   Live's update banner also carries a CSS-masked info icon. It does not come
   across: no icon token here is shared by both skins, and the banner opens with
   a bold "Update:" that already says what the icon was saying twice. */

/* The three callouts share a box. They differ in weight, which is the whole
   distinction live draws: the update banner is an interruption and takes the
   heavy accent bar, the journal note and the aside are quieter and take a
   hairline. The fill is what keeps all three off blockquote's territory -
   blockquote is a bar with no ground, so a callout without a fill reads as a
   quotation. */
.update-banner,
.journal-note,
.aside-note {
  margin: var(--space-6) 0;
  background: var(--bg-alt);
  border-radius: var(--radius);
}

.update-banner {
  padding: var(--space-4) var(--space-5);
  border-left: var(--accent-bar) solid var(--accent-warm);
  font-size: var(--text-sm);
}

.update-banner strong { color: var(--accent-warm); }
.update-banner p { margin: 0; }
.update-banner p + p { margin-top: var(--space-2); }

/* Icon beside body, not above it, so a two-line note stays two lines. */
.journal-note,
.aside-note {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-left: var(--hairline) solid var(--rule);
}

.journal-note__icon,
.aside-note__icon {
  flex: none;
  width: var(--space-4);
  height: var(--space-4);
  margin-top: var(--space-1);
  color: var(--muted);
}

/* A journal note is a mood, so it is small, italic and muted. An aside is an
   argument the reader is meant to weigh, so it stays roman at body size and
   full ink - muting the one paragraph you most want read would defeat it. */
.journal-note p {
  margin: 0;
  font-size: var(--text-sm);
  font-style: italic;
  color: var(--muted);
}

.aside-note__body { min-width: 0; }
.aside-note p { margin: 0 0 var(--space-3); }
.aside-note p:last-child { margin-bottom: 0; }

/* Footnotes, as markdown-it-footnote emits them. The separator is a real <hr>
   and gets more air above than below, because it closes the body rather than
   opening the notes. */
.footnotes-sep {
  margin: var(--space-12) 0 var(--space-6);
  border: 0;
  border-top: var(--hairline) solid var(--rule);
}

.footnotes {
  font-size: var(--text-sm);
  color: var(--muted);
}

.footnotes ol {
  margin: var(--space-2) 0;
  padding-left: var(--space-7);
  list-style: decimal;
}

.footnotes li + li { margin-top: var(--space-2); }
.footnotes li p { margin: 0; }
.footnote-ref a { padding: 0 var(--space-1); }
.footnote-backref { margin-left: var(--space-1); }

/* A rating written into a post body as star SVGs, which is how the live
   archive does it. Not the same thing as .rating above: that one draws its
   stars from a number with a mask, and it is what every entry TYPE uses. These
   two posts carry the marks in their own markup, and the markup is theirs -
   it arrived with the corpus and is not ours to rewrite. The SVGs are already
   1em and fill="currentColor", so a colour is the whole of it. */
.post-rating,
.post-rating-inline { color: var(--star); }
.post-rating { margin: var(--space-4) 0; }

/* The conversation thread.
   -------------------------------------------------------------------------
   One list holding comments from the form here and replies that arrived as
   webmentions. They are the same object whatever the site looks like, so the
   shape is here; the faces stay with the skins, as everywhere else.

   An ordered list because a thread has an order that carries meaning, with the
   numbers off: the indent already says who answers whom, and a numbered reply
   four levels deep reads as an instruction. lynx still numbers them, which is
   exactly right there - it is the only thing it has to show the sequence. */
.thread {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* --depth is a value, not a hook. lib/conversation.js caps it at 6 so a long
   back-and-forth cannot walk the column off a narrow screen, and the bar is
   what makes an indented reply read as attached to the one above rather than
   merely further right. */
.thread > li {
  /* Declared here so it is never undefined, which is not tidiness: an undefined
     custom property invalidates the WHOLE declaration at computed-value time,
     so a comment that somehow arrived without a depth would lose its padding
     silently - the same failure the token contract's dangling check exists to
     catch, and the check flagged this one. The markup's inline style overrides
     it per comment; a root simply keeps the zero. */
  --depth: 0;
  margin: var(--space-5) 0;
  padding-left: calc(var(--depth) * var(--space-4));
}

.comment-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  color: var(--muted);
}

.comment-meta img,
.comment-initial {
  flex: none;
  width: var(--space-6);
  height: var(--space-6);
  border-radius: var(--radius-pill);
}

/* Stands in for a photo at exactly its size, so a thread where only some
   commenters have one still has one byline shape. */
.comment-initial {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-alt);
  font-size: var(--text-xs);
}

/* Said in words rather than colour, because a badge that means something only
   if you can see the colour means nothing in a text browser. */
.comment-badge {
  padding: 0 var(--space-1);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.comment-liked {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* A syndicated reply can be a whole blog post. Nine of them are, so the long
   ones fold rather than outrunning the entry they are attached to. */
.comment-long > summary {
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* --- Doxipedia reference page -------------------------------------------- *
 * The one page that was still on michaelharley.net's own template, kept there
 * because it is heavily designed - an anatomy diagram, abbreviation grids, a
 * pattern strip - and both easy ports degraded it: markdown loses the layout,
 * and moving the HTML across without these rules leaves 43 divs unstyled.
 *
 * So the rules came too. Retokenised onto this repo's scales rather than
 * pasted: live's --color-* names map onto --accent, --muted, --rule, --ink and
 * the rest; every length names a --space step or a --text step; 1px borders
 * name --hairline; and the 68ch measure is --measure. The enforcement grep in
 * CLAUDE.md passes over it.
 *
 * ⚠️ Rounding onto a 0.25rem grid moves a few values by up to 0.1rem from
 * live's, which is the same trade every other component here made. The shape
 * is identical; the spacing is this site's.
 * ------------------------------------------------------------------------- */
.docref-crumb { font-size:var(--text-sm); color:var(--muted); letter-spacing:0.03em; margin-bottom:var(--space-4); }
.docref-crumb a { color:var(--muted); }
.docref-eyebrow { font-size:var(--text-xs); letter-spacing:0.2em; color:var(--accent-warm); font-weight:600; margin:0 0 var(--space-2); }
.docref-epi {
  font-family:var(--body); font-style:italic; font-size:var(--text-md); color:var(--ink);
  border-left:2px solid var(--accent-warm); padding:var(--space-1) 0 var(--space-1) var(--space-4); margin:var(--space-5) 0 var(--space-2);
}
.docref-epi cite { display:block; font-style:normal; font-family:var(--mono); font-size:var(--text-xs); color:var(--muted); margin-top:var(--space-2); }
.docref-toc {
  display:flex; flex-wrap:wrap; gap:var(--space-2); align-items:center; margin:var(--space-6) 0 var(--space-2);
  padding:var(--space-3) 0; border-top:var(--hairline) solid var(--rule); border-bottom:var(--hairline) solid var(--rule);
}
.docref-toc > span { font-size:var(--text-xs); color:var(--muted); letter-spacing:0.04em; margin-right:var(--space-1); }
.docref-toc a { font-size:var(--text-sm); text-decoration:none; color:var(--ink); border:var(--hairline) solid var(--rule); padding:var(--space-1) var(--space-2); background:var(--bg-alt); }
.docref-toc a:hover { border-color:var(--accent); color:var(--accent); }
.docref-kicker { font-size:var(--text-xs); letter-spacing:0.16em; text-transform:uppercase; color:var(--muted); margin:var(--space-4) 0 var(--space-2); }
.docref-pattern { display:inline-block; font-size:var(--text-md); background:var(--bg-alt); border:var(--hairline) solid var(--rule); padding:var(--space-2) var(--space-3); margin-bottom:var(--space-4); }
.docref-pattern .ph { color:var(--accent); }
.docref-pattern .ph-b { color:var(--accent-warm); }
.docref-pattern .sep { color:var(--muted); }
.docref-anat { display:flex; flex-wrap:wrap; align-items:flex-start; font-size:var(--text-xl); font-weight:600; margin:var(--space-2) 0; }
.docref-seg { display:flex; flex-direction:column; align-items:center; }
.docref-seg .lab { font-size:var(--text-xs); font-weight:600; letter-spacing:0.12em; text-transform:uppercase; margin-top:var(--space-2); padding-top:var(--space-2); border-top:1.5px solid currentColor; width:100%; text-align:center; line-height:1.3; }
.docref-seg.is-type .tok, .docref-seg.is-type .lab { color:var(--accent); }
.docref-seg.is-desc .tok { color:var(--ink); }
.docref-seg.is-desc .lab { color:var(--muted); border-color:var(--rule); }
.docref-seg.is-scope .tok, .docref-seg.is-scope .lab { color:var(--accent-warm); }
.docref-usep { color:var(--muted); font-weight:400; padding:0 var(--space-1); align-self:flex-start; }
.docref-gloss { font-size:var(--text-sm); color:var(--muted); margin:0 0 var(--space-1); }
.docref-gloss b { color:var(--ink); font-weight:500; }
.docref-abbr { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:var(--hairline); background:var(--rule); border:var(--hairline) solid var(--rule); margin:var(--space-1) 0 var(--space-2); }
.docref-abbr div { background:var(--bg-alt); padding:var(--space-1) var(--space-2); font-size:var(--text-sm); display:flex; gap:var(--space-2); align-items:baseline; }
.docref-abbr b { color:var(--accent); font-weight:700; min-width:var(--space-8); }
.docref-abbr span { color:var(--muted); }
.docref-examples { display:flex; flex-wrap:wrap; gap:var(--space-2); margin:var(--space-1) 0 var(--space-2); }
.docref-examples span { font-size:var(--text-sm); background:var(--bg-alt); border:var(--hairline) solid var(--rule); padding:var(--space-1) var(--space-2); }
.docref-foot { margin-top:var(--space-10); font-size:var(--text-xs); color:var(--muted); border-top:var(--hairline) dashed var(--rule); padding-top:var(--space-3); }
.docref-note { font-size:var(--text-sm); color:var(--muted); background:var(--bg-alt); border-left:2px solid var(--accent); padding:var(--space-2) var(--space-4); margin:var(--space-4) 0 var(--space-6); }
.docref-note b, .docref-note code { color:var(--ink); }
.docref-abbr--wide { grid-template-columns:repeat(auto-fill, minmax(255px, 1fr)); }
.docref-abbr--wide div { display:block; }
.docref-abbr--wide b { display:inline-block; margin-right:var(--space-1); }
.docref-vargrid { display:grid; grid-template-columns:repeat(auto-fill, minmax(225px, 1fr)); gap:var(--space-3); margin:var(--space-2) 0 var(--space-4); }
.docref-vargrid > div { border:var(--hairline) solid var(--rule); background:var(--bg-alt); padding:var(--space-3) var(--space-3); }
.docref-vargrid b { display:block; color:var(--accent); font-weight:700; font-size:var(--text-md); }
.docref-vargrid p { font-size:var(--text-sm); color:var(--muted); margin:var(--space-1) 0 0; }
.docref-vargrid code { color:var(--accent-warm); }
.docref-fix { display:flex; flex-direction:column; gap:var(--space-2); margin:var(--space-2) 0 var(--space-2); }
.docref-fix > div { border:var(--hairline) solid var(--rule); background:var(--bg-alt); padding:var(--space-2) var(--space-3); }
.docref-fix .swap { font-size:var(--text-md); }
.docref-fix .dep { color:var(--loud); text-decoration:line-through; }
.docref-fix .can { color:var(--accent); font-weight:600; }
.docref-fix .arrow { color:var(--muted); padding:0 var(--space-2); }
.docref-fix p { font-size:var(--text-sm); color:var(--muted); margin:var(--space-1) 0 0; }
.docref-sub { font-size:var(--text-md); font-weight:600; color:var(--ink); margin:var(--space-6) 0 var(--space-2); }
.docref-sub::before { content:"\2500\00a0"; color:var(--rule); }
.docref-body p { max-width:var(--measure); }
.docref-body ul { max-width:var(--measure); }

/* --- Visually hidden ------------------------------------------------------ *
 * ⚠️ Restores a class michaelharley.net's stylesheet provided and this one did
 * not. The externalLinkNote transform appends
 * `<span class="visually-hidden"> (opens in a new tab)</span>` to every link
 * that opens a new tab - a real accessibility warning that is supposed to reach
 * a screen reader and nobody else. With no rule for the class it rendered as
 * literal text mid-sentence, so every external link read "Justin (opens in a
 * new tab) is someone I follow".
 *
 * Same shape as the Prism bug: live's markup depends on live's CSS, and the
 * markup came across while the stylesheet did not. Clip rather than
 * display:none, which would hide it from the screen reader too and defeat the
 * point.
 * ------------------------------------------------------------------------- */
.visually-hidden {
  position: absolute;
  width: var(--hairline);
  height: var(--hairline);
  padding: 0;
  margin: calc(var(--hairline) * -1);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- External link indicator ---------------------------------------------- *
 * Brought back from michaelharley.net's stylesheet, which the migration
 * deleted. The externalLinkNote transform still appends the spoken warning in a
 * visually-hidden span; this is the visible half, and without it an external
 * link looked identical to an internal one.
 *
 * The mask is drawn from empty `content`, so it stays out of the accessibility
 * tree - the warning is the span's job, and announcing both would say it twice.
 * Retokenised: --muted for the colour, and the sizes are em-relative so the mark
 * tracks the text it follows rather than a step on the space scale.
 * ------------------------------------------------------------------------- */
a[target="_blank"]::after {
  content: "";
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  margin-left: 0.25em;
  margin-bottom: -0.05em;
  background-color: var(--muted);
  mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M9 2L9 3L12.3 3L6 9.3L6.7 10L13 3.7L13 7L14 7L14 2L9 2z M4 4C2.9 4 2 4.9 2 6L2 12C2 13.1 2.9 14 4 14L10 14C11.1 14 12 13.1 12 12L12 7L11 8L11 12C11 12.6 10.6 13 10 13L4 13C3.4 13 3 12.6 3 12L3 6C3 5.4 3.4 5 4 5L8 5L9 4L4 4z"/></svg>');
  mask-size: contain;
  mask-repeat: no-repeat;
}

/* Carrier
 * ---------------------------------------------------------------------------
 * The BBS. Named for the handshake that started every session.
 *
 * Ported from michaelharley.net's default skin, but only the half that could
 * travel. That stylesheet has 1,033 selector clauses; 79% of them name a class,
 * and 3 of its 380 class names exist in this markup. So the palette, the type
 * scale and the generated-content ornament came over, and the structure is
 * written fresh against elements.
 *
 * Two things the original does that a theme here cannot. The sysop line, the
 * modem status lights and the block glyphs are markup on the live site, not
 * CSS, and a theme may add CSS and never markup. And the explicit light/dark
 * toggle keys off a data-theme attribute set by script; with no script and no
 * per-visitor HTML, this follows prefers-color-scheme and nothing else.
 */


/* --- Faces ---------------------------------------------------------------
 * Shipped, not assumed. A visitor who happens to have "Atkinson Hyperlegible"
 * installed gets whatever cut they have: this machine had only the Bold, so
 * every request for weight 400 was answered with the bold face and the whole
 * site rendered heavy. Naming a font is not the same as having it.
 *
 * A theme owns its typography, so these live here rather than in the base. The
 * Text theme loads no stylesheet and therefore no fonts, which is correct.
 * ------------------------------------------------------------------------- */

@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/atkinson-hyperlegible-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/atkinson-hyperlegible-latin-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/atkinson-hyperlegible-latin-700-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/assets/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/assets/fonts/ibm-plex-mono-latin-700-normal.woff2") format("woff2");
}

/* --- Vocabulary ---------------------------------------------------------- */

:root {
  /* Two palettes, each colour written once and then mapped onto the live names.
     The mapping is what repeats per scheme, never a hex, so there is no second
     copy of a colour to drift.

     Light is reverse video, not paper. Carrier's identity is the ANSI accent
     set on a terminal ground, so the light scheme flips the ground and keeps
     the accents, darkened to hold contrast. A cool neutral, deliberately not
     white: a screen in reverse video rather than a sheet of paper. Every
     foreground clears 4.5:1 on BOTH grounds, measured, the tightest being
     --link at 4.94 on --bg-alt. */
  /* Raised to 7:1 on both grounds, which is AAA for normal text, after the
     first pass measured 4.94 to 5.66: passing AA but visibly close at the
     floor. Hues are unchanged, only darkened. */
  --light-bg:            #eef1f4;
  --light-bg-alt:        #e2e7ec;
  --light-ink:           #11161c;
  --light-muted:         #434c55;
  --light-link:          #085260;
  --light-link-hover:    #8f1e18;
  --light-accent:        #085260;
  --light-accent-warm:   #713e00;
  --light-loud:          #8f1e18;
  --light-star:          #0a5724;
  --light-rule:          rgba(17, 22, 28, 0.18);
  --light-pattern-alpha: 13%;

  --dark-bg:            #0d1117;
  --dark-bg-alt:        #161b22;
  --dark-ink:           #c9d1d9;
  --dark-muted:         #8b949e;
  --dark-link:          #39c5cf;
  --dark-link-hover:    #ff6b6b;
  --dark-accent:        #39c5cf;
  --dark-accent-warm:   #f5b041;
  --dark-loud:          #ff6b6b;
  --dark-star:          #00e054;
  --dark-rule:          rgba(230, 237, 243, 0.13);
  /* Light ink on a dark field reads heavier at the same alpha. */
  --dark-pattern-alpha: 9%;

  /* The modem panel. Ornament, so it is a background image with empty
     generated content rather than text: a screen reader announces nothing,
     which is what aria-hidden does for it on the live site. Two layers so the
     RD and SD lamps can flicker without the other six going with them. */
  --light-leds: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%2215.0%22%20cy=%226%22%20r=%224%22%20fill=%22%230a5724%22/%3E%3Ctext%20x=%2215.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3EPWR%3C/text%3E%3Ccircle%20cx=%2245.0%22%20cy=%226%22%20r=%224%22%20fill=%22%230a5724%22/%3E%3Ctext%20x=%2245.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ELNK%3C/text%3E%3Ccircle%20cx=%2275.0%22%20cy=%226%22%20r=%224%22%20fill=%22%230a5724%22/%3E%3Ctext%20x=%2275.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3EMR%3C/text%3E%3Ccircle%20cx=%22105.0%22%20cy=%226%22%20r=%224%22%20fill=%22%230a5724%22/%3E%3Ctext%20x=%22105.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ECD%3C/text%3E%3Ctext%20x=%22135.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ERD%3C/text%3E%3Ctext%20x=%22165.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ESD%3C/text%3E%3Ccircle%20cx=%22195.0%22%20cy=%226%22%20r=%224%22%20fill=%22%230a5724%22/%3E%3Ctext%20x=%22195.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ETR%3C/text%3E%3Ctext%20x=%22225.0%22%20y=%2223%22%20fill=%22%23434c55%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3EAA%3C/text%3E%3C/svg%3E");
  --dark-leds: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%2215.0%22%20cy=%226%22%20r=%224%22%20fill=%22%232ee06a%22/%3E%3Ctext%20x=%2215.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3EPWR%3C/text%3E%3Ccircle%20cx=%2245.0%22%20cy=%226%22%20r=%224%22%20fill=%22%232ee06a%22/%3E%3Ctext%20x=%2245.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ELNK%3C/text%3E%3Ccircle%20cx=%2275.0%22%20cy=%226%22%20r=%224%22%20fill=%22%232ee06a%22/%3E%3Ctext%20x=%2275.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3EMR%3C/text%3E%3Ccircle%20cx=%22105.0%22%20cy=%226%22%20r=%224%22%20fill=%22%232ee06a%22/%3E%3Ctext%20x=%22105.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ECD%3C/text%3E%3Ctext%20x=%22135.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ERD%3C/text%3E%3Ctext%20x=%22165.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ESD%3C/text%3E%3Ccircle%20cx=%22195.0%22%20cy=%226%22%20r=%224%22%20fill=%22%232ee06a%22/%3E%3Ctext%20x=%22195.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3ETR%3C/text%3E%3Ctext%20x=%22225.0%22%20y=%2223%22%20fill=%22%238b949e%22%20font-family=%22monospace%22%20font-size=%228.8%22%20letter-spacing=%220.7%22%20text-anchor=%22middle%22%3EAA%3C/text%3E%3C/svg%3E");
  --light-led-rd: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%22135.0%22%20cy=%226%22%20r=%224%22%20fill=%22%23713e00%22/%3E%3C/svg%3E");
  --dark-led-rd: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%22135.0%22%20cy=%226%22%20r=%224%22%20fill=%22%23f5b041%22/%3E%3C/svg%3E");
  --light-led-sd: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%22165.0%22%20cy=%226%22%20r=%224%22%20fill=%22%23713e00%22/%3E%3C/svg%3E");
  --dark-led-sd: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%22165.0%22%20cy=%226%22%20r=%224%22%20fill=%22%23f5b041%22/%3E%3C/svg%3E");
  --light-led-aa: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%22225.0%22%20cy=%226%22%20r=%224%22%20fill=%22%23713e00%22/%3E%3C/svg%3E");
  --dark-led-aa: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22240%22%20height=%2226%22%20viewBox=%220%200%20240%2026%22%3E%3Ccircle%20cx=%22225.0%22%20cy=%226%22%20r=%224%22%20fill=%22%23f5b041%22/%3E%3C/svg%3E");

  /* Light is the default, so a root with no attribute is a light root. */
  --bg:            var(--light-bg);
  --bg-alt:        var(--light-bg-alt);
  --ink:           var(--light-ink);
  --muted:         var(--light-muted);
  --link:          var(--light-link);
  --link-hover:    var(--light-link-hover);
  --accent:        var(--light-accent);
  --accent-warm:   var(--light-accent-warm);
  --loud:          var(--light-loud);
  --star:          var(--light-star);
  --rule:          var(--light-rule);
  --pattern-alpha: var(--light-pattern-alpha);

  /* Code tokens, stated as roles rather than colours so they follow the
     scheme mapping above instead of needing a copy in each of the three
     blocks that set it. Prism's classes are mapped onto these in
     components.css: a theme names the colours, the layer decides what wears
     them. Every one of these clears 4.5:1 on --bg-alt, the ground a code
     block sits on, in both schemes -- muted on dark is the floor at 5.62. */
  --code-comment: var(--muted);
  --code-punct:   var(--muted);
  --code-keyword: var(--accent-warm);
  --code-string:  var(--star);
  --code-value:   var(--link);
  --code-fn:      var(--loud);
  --leds:          var(--light-leds);
  --led-rd:        var(--light-led-rd);
  --led-sd:        var(--light-led-sd);
  --led-aa:        var(--light-led-aa);

  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --body: "Atkinson Hyperlegible", system-ui, -apple-system, sans-serif;

  /* Wallpaper. The SVG is a mask rather than an image, so the hexagons take
     --pattern-ink and follow whichever palette is live: one file, both
     schemes. Ported from the live site unchanged. */
  --pattern-ink: color-mix(in srgb, var(--ink) var(--pattern-alpha), transparent);
  --pattern-tile: 3.75rem 2.165rem;
  --pattern-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="60" height="34.64" viewBox="0 0 60 34.64"><g fill="none" stroke="black" stroke-width="1"><path d="M-20 0 L-10 -17.32 L10 -17.32 L20 0 L10 17.32 L-10 17.32 Z"/><path d="M40 0 L50 -17.32 L70 -17.32 L80 0 L70 17.32 L50 17.32 Z"/><path d="M-20 34.64 L-10 17.32 L10 17.32 L20 34.64 L10 51.96 L-10 51.96 Z"/><path d="M40 34.64 L50 17.32 L70 17.32 L80 34.64 L70 51.96 L50 51.96 Z"/><path d="M10 17.32 L20 0 L40 0 L50 17.32 L40 34.64 L20 34.64 Z"/></g></svg>');

  /* The live site's 1.125 modular scale, ported whole. Every size below is one
     of these; none is a number I picked. */
  --text-xs:   0.75rem;
  --text-sm:   0.85rem;
  --text-base: 1rem;
  --text-md:   1.125rem;
  --text-lg:   1.25rem;
  --text-xl:   1.5rem;
  --text-2xl:  1.875rem;

  /* The live site's arrow, as a mask rather than an image so it paints in
     currentColor and follows the pill from cyan to amber on hover. Same paths
     and the same 2px stroke; drawn in CSS so no markup carries an icon. */
  --icon-arrow: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22black%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M5%2012h14%22/%3E%3Cpath%20d=%22m12%205%207%207-7%207%22/%3E%3C/svg%3E");

  /* --- Space --------------------------------------------------------------
     One grid, 0.25rem to the unit, and the number IS the unit count:
     --space-6 is six units is 1.5rem. So a size can be read back to the scale
     without a lookup, and a value that will not name itself is a value that
     does not belong on the grid.

     Only the steps actually in use are declared. An unused token is an
     invitation to reach for a number, which is the habit this replaces. */
  --space-1:   0.25rem;
  --space-2:   0.5rem;
  --space-3:   0.75rem;
  --space-4:   1rem;
  --space-5:   1.25rem;
  --space-6:   1.5rem;
  --space-7:   1.75rem;
  --space-8:   2rem;
  --space-10:  2.5rem;
  --space-12:  3rem;

  /* The horizontal inset, distinct from --space-5 despite sharing its value.
     Body, panel, card and figcaption all breathe by the same amount because
     they are the same gutter, not because 1.25rem happened to look right in
     four places. Change this and the page keeps its alignment. */
  --gutter: var(--space-5);

  /* --- Widths -------------------------------------------------------------
     A measure in characters holds its line length when the font changes, where
     a rem value does not. The live site sets 64ch; this runs a little wider. */
  /* Footer marks, ported from the live site, which carries all of these as
     inline SVG in its markup. A theme may add CSS and never markup, so each is
     a data URI here instead. Same artwork.

     Two are background images and two are masks, and which is which is decided
     by the source: currentColor does not resolve inside a data URI, because the
     image is its own document. The RSS badge and the ring carry explicit fills
     and gradients, so they stay full colour. The web and the arrows are drawn
     in currentColor, so they become masks and take the colour from the page. */
  /* The three-mark rose that closes a piece of writing. */
  /* The tag mark that leads the filed-under line. */
  /* The share mark leading the "also on" row; sibling to the tag mark. */
  --icon-elsewhere: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22><circle cx=%2218%22 cy=%225%22 r=%223%22/><circle cx=%226%22 cy=%2212%22 r=%223%22/><circle cx=%2218%22 cy=%2219%22 r=%223%22/><line x1=%228.59%22 y1=%2213.51%22 x2=%2215.42%22 y2=%2217.49%22/><line x1=%2215.41%22 y1=%226.51%22 x2=%228.59%22 y2=%2210.49%22/></svg>");
  --icon-tag: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22><path d=%22M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z%22/><circle cx=%227.5%22 cy=%227.5%22 r=%22.5%22 fill=%22black%22/></svg>");
  --icon-end: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 24%22><g stroke=%22black%22 stroke-width=%221.2%22 stroke-linecap=%22round%22 fill=%22none%22><g transform=%22translate(16 6)%22><path d=%22M0,-4V4 M-3.5,-2L3.5,2 M-3.5,2L3.5,-2%22/></g><g transform=%22translate(8 18)%22><path d=%22M0,-4V4 M-3.5,-2L3.5,2 M-3.5,2L3.5,-2%22/></g><g transform=%22translate(24 18)%22><path d=%22M0,-4V4 M-3.5,-2L3.5,2 M-3.5,2L3.5,-2%22/></g></g></svg>");
  --icon-rss: url("data:image/svg+xml,<svg viewBox=%220 0 256 256%22 xmlns=%22http://www.w3.org/2000/svg%22><rect width=%22256%22 height=%22256%22 rx=%2255%22 ry=%2255%22 fill=%22%23f26522%22/><circle cx=%2268%22 cy=%22189%22 r=%2224%22 fill=%22%23fff%22/><path d=%22M160 213h-34a82 82 0 0 0-82-82V97a116 116 0 0 1 116 116z%22 fill=%22%23fff%22/><path d=%22M224 213h-34a150 150 0 0 0-150-150V29a184 184 0 0 1 184 184z%22 fill=%22%23fff%22/></svg>");
  --icon-ring: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 48 48%22><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSa%22 x1=%2224%22 x2=%2224%22 y1=%2211.517%22 y2=%223.341%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%22.159%22 stop-color=%22%23189eec%22/><stop offset=%22.902%22 stop-color=%22%23c4f0f8%22/></linearGradient><polygon fill=%22url(%23Trh1gOXxOEXExV5q8JbzSa)%22 points=%2217.124,7.814 19.703,3.946 28.297,3.946 30.876,7.814 24,9.906%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSb%22 x1=%2224%22 x2=%2224%22 y1=%227.168%22 y2=%2216.359%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%220%22 stop-color=%22%23068bd9%22/><stop offset=%22.174%22 stop-color=%22%23049ce5%22/><stop offset=%22.445%22 stop-color=%22%2302aff4%22/><stop offset=%22.719%22 stop-color=%22%2300bbfc%22/><stop offset=%221%22 stop-color=%22%2300bfff%22/></linearGradient><polygon fill=%22url(%23Trh1gOXxOEXExV5q8JbzSb)%22 points=%2217.124,7.814 30.876,7.814 24,17.268%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSc%22 x1=%2224%22 x2=%2224%22 y1=%225.537%22 y2=%2219.087%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%22.023%22 stop-color=%22%23139cec%22/><stop offset=%22.212%22 stop-color=%22%233fb1ef%22/><stop offset=%22.48%22 stop-color=%22%2378ccf3%22/><stop offset=%22.71%22 stop-color=%22%23a1e0f6%22/><stop offset=%22.891%22 stop-color=%22%23bbecf7%22/><stop offset=%221%22 stop-color=%22%23c4f0f8%22/></linearGradient><polygon fill=%22url(%23Trh1gOXxOEXExV5q8JbzSc)%22 points=%2220.992,7.814 24,17.268 27.008,7.814%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSd%22 x1=%2224%22 x2=%2224%22 y1=%229.839%22 y2=%221.621%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%22.295%22 stop-color=%22%23189eec%22/><stop offset=%22.389%22 stop-color=%22%233bb0ef%22/><stop offset=%22.494%22 stop-color=%22%2359c1f3%22/><stop offset=%22.604%22 stop-color=%22%2371cef5%22/><stop offset=%22.72%22 stop-color=%22%2383d7f7%22/><stop offset=%22.846%22 stop-color=%22%238ddcf8%22/><stop offset=%221%22 stop-color=%22%2390def8%22/></linearGradient><polygon fill=%22url(%23Trh1gOXxOEXExV5q8JbzSd)%22 points=%2219.703,3.946 20.992,7.814 27.008,7.814 28.297,3.946%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSe%22 x1=%2224%22 x2=%2224%22 y1=%2210.193%22 y2=%225.239%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%220%22 stop-color=%22%23189eec%22/><stop offset=%22.053%22 stop-color=%22%232fa8ed%22/><stop offset=%22.168%22 stop-color=%22%235cbcf0%22/><stop offset=%22.288%22 stop-color=%22%2382ccf3%22/><stop offset=%22.411%22 stop-color=%22%23a1daf5%22/><stop offset=%22.539%22 stop-color=%22%23b9e5f6%22/><stop offset=%22.674%22 stop-color=%22%23cbecf7%22/><stop offset=%22.821%22 stop-color=%22%23d5f1f8%22/><stop offset=%221%22 stop-color=%22%23d8f2f8%22/></linearGradient><polygon fill=%22url(%23Trh1gOXxOEXExV5q8JbzSe)%22 points=%2220.992,7.814 24,3.946 27.008,7.814%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSf%22 x1=%2224%22 x2=%2224%22 y1=%22-.908%22 y2=%2262.85%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%220%22 stop-color=%22%23d9d9d9%22/><stop offset=%220%22 stop-color=%22%23717173%22/><stop offset=%220%22 stop-color=%22%231b1b1f%22/><stop offset=%22.041%22 stop-color=%22%234a4c4f%22/><stop offset=%22.095%22 stop-color=%22%23828688%22/><stop offset=%22.144%22 stop-color=%22%23afb4b6%22/><stop offset=%22.188%22 stop-color=%22%23cfd5d6%22/><stop offset=%22.225%22 stop-color=%22%23e2e9ea%22/><stop offset=%22.249%22 stop-color=%22%23e9f0f1%22/><stop offset=%22.504%22 stop-color=%22%23ccc%22/><stop offset=%22.548%22 stop-color=%22%23d2d3d3%22/><stop offset=%22.71%22 stop-color=%22%23e3e8e9%22/><stop offset=%22.814%22 stop-color=%22%23e9f0f1%22/></linearGradient><path fill=%22url(%23Trh1gOXxOEXExV5q8JbzSf)%22 d=%22M26.07,14.63c0.007-0.01,0.014-0.018,0.021-0.028c0.402-0.6,4.713-6.051,4.785-6.788 c-0.477,0.564-5.969,5.281-5.969,5.281S25.015,10.48,24,7.814c-0.993,2.676-0.91,5.295-0.91,5.295s-5.492-4.727-5.966-5.295 c0.064,0.739,4.373,6.198,4.777,6.798c0.005,0.007,0.01,0.013,0.015,0.02C14.745,15.65,9.207,21.813,9.207,29.261 c0,8.157,6.636,14.793,14.793,14.793s14.793-6.636,14.793-14.793C38.793,21.808,33.248,15.642,26.07,14.63z M24,40.468 c-6.18,0-11.207-5.027-11.207-11.207S17.82,18.054,24,18.054s11.207,5.027,11.207,11.207S30.18,40.468,24,40.468z%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSg%22 x1=%2224.003%22 x2=%2224.003%22 y1=%2229.261%22 y2=%2248.285%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%220%22 stop-color=%22%23e9f0f1%22/><stop offset=%22.302%22 stop-color=%22%23aeb3b5%22/><stop offset=%221%22 stop-color=%22%231b1b1f%22/></linearGradient><path fill=%22url(%23Trh1gOXxOEXExV5q8JbzSg)%22 d=%22M35.207,29.261c0,6.18-5.027,11.207-11.207,11.207s-11.207-5.027-11.207-11.207 C12.427,34.989,17.118,40.718,24,40.718S35.635,34.989,35.207,29.261z%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSh%22 x1=%2224%22 x2=%2224%22 y1=%2211.533%22 y2=%2237.202%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%220%22 stop-color=%22%231b1b1f%22/><stop offset=%22.007%22 stop-color=%22%23212225%22/><stop offset=%22.059%22 stop-color=%22%234b4c50%22/><stop offset=%22.117%22 stop-color=%22%23717476%22/><stop offset=%22.18%22 stop-color=%22%23919598%22/><stop offset=%22.248%22 stop-color=%22%23adb2b4%22/><stop offset=%22.324%22 stop-color=%22%23c3c9ca%22/><stop offset=%22.411%22 stop-color=%22%23d4dadc%22/><stop offset=%22.517%22 stop-color=%22%23e0e7e8%22/><stop offset=%22.66%22 stop-color=%22%23e7eeef%22/><stop offset=%221%22 stop-color=%22%23e9f0f1%22/></linearGradient><path fill=%22url(%23Trh1gOXxOEXExV5q8JbzSh)%22 d=%22M9.207,29.261c0-7.187,5.66-13.079,12.709-14.629C14.745,15.65,9.207,21.813,9.207,29.261 L9.207,29.261z M38.793,29.261c0-7.448-5.538-13.611-12.709-14.629C33.133,16.182,38.793,22.073,38.793,29.261L38.793,29.261z%22/><linearGradient id=%22Trh1gOXxOEXExV5q8JbzSi%22 x1=%2223.999%22 x2=%2223.999%22 y1=%2215.929%22 y2=%225.835%22 gradientUnits=%22userSpaceOnUse%22><stop offset=%220%22 stop-color=%22%23e9f0f1%22/><stop offset=%22.228%22 stop-color=%22%23b3b8ba%22/><stop offset=%22.748%22 stop-color=%22%232d2e31%22/><stop offset=%22.817%22 stop-color=%22%231b1b1f%22/></linearGradient><path fill=%22url(%23Trh1gOXxOEXExV5q8JbzSi)%22 d=%22M20.137,10.426l2.953,2.683l-0.172-0.15L20.02,10.56L18.53,9.233 c-0.402-0.367-1.015-1.035-1.405-1.419c0,0,1.021,0.857,1.513,1.298L20.137,10.426z M19.37,11.323l2.546,3.31l-2.471-3.366 l-1.208-1.701c-0.306-0.47-0.877-1.26-1.113-1.752c0.025,0.299,0.392,0.939,1.006,1.827C18.531,10.211,18.95,10.767,19.37,11.323z M23.999,7.814c0,0.001,0,0.001,0.001,0.002l-0.001,0.002v0.001c0-0.001,0-0.002,0.001-0.003C24,7.817,24,7.818,24,7.819V7.818 l-0.001-0.002C24,7.815,24,7.814,24,7.814l-0.001,0.002L23.999,7.814z M23.09,13.108c0.206-1.701,0.381-3.727,0.908-5.295 l-0.116,0.323C23.295,9.78,23.107,11.366,23.09,13.108z M27.861,10.426l-2.953,2.683l0.172-0.15l2.899-2.399l1.491-1.326 c0.402-0.367,1.015-1.035,1.405-1.419c0,0-1.021,0.857-1.513,1.298L27.861,10.426z M29.761,9.566l-1.208,1.701l-2.471,3.366 l2.546-3.31c0.42-0.555,0.839-1.112,1.239-1.682c0.614-0.888,0.982-1.528,1.006-1.827C30.638,8.306,30.067,9.096,29.761,9.566z M24,7.814c0.527,1.567,0.702,3.594,0.908,5.295c-0.016-1.742-0.205-3.329-0.793-4.972L24,7.814z%22/></svg>");
  --icon-web: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 50 50%22 fill=%22black%22><path d=%22M 14.9375 -0.03125 C 14.90625 -0.0234375 14.875 -0.0117188 14.84375 0 C 14.550781 0.0585938 14.296875 0.25 14.15625 0.515625 C 14.019531 0.785156 14.007813 1.097656 14.125 1.375 L 14.53125 2.375 C 14.175781 3.242188 12.832031 6.199219 9.8125 9.21875 C 6.679688 12.351563 2.609375 13.964844 1.75 14.28125 L 1.375 14.125 C 1.039063 13.949219 0.628906 13.976563 0.320313 14.199219 C 0.0117188 14.425781 -0.140625 14.804688 -0.078125 15.179688 C -0.0117188 15.554688 0.257813 15.859375 0.625 15.96875 L 1 16.125 C 1.378906 17 3.0625 21.105469 3.0625 25 C 3.0625 29.03125 0.496094 33.746094 0.21875 34.25 C 0.207031 34.261719 0.199219 34.269531 0.1875 34.28125 C -0.125 34.578125 -0.214844 35.042969 -0.03125 35.433594 C 0.15625 35.828125 0.570313 36.054688 1 36 C 1.675781 36.195313 5.949219 37.511719 9.21875 40.78125 C 12.457031 44.019531 13.78125 48.230469 14 48.96875 C 14 48.980469 14 48.988281 14 49 C 13.960938 49.414063 14.1875 49.808594 14.5625 49.992188 C 14.9375 50.171875 15.386719 50.101563 15.6875 49.8125 C 15.691406 49.808594 15.738281 49.757813 15.75 49.75 C 16.175781 49.5 20.375 47.125 25 47.125 C 29.566406 47.125 33.703125 49.417969 34.21875 49.71875 C 34.230469 49.730469 34.238281 49.738281 34.25 49.75 C 34.542969 50.058594 35 50.144531 35.386719 49.96875 C 35.773438 49.792969 36.007813 49.390625 35.96875 48.96875 C 36.140625 48.335938 37.359375 44.203125 40.78125 40.78125 C 44.191406 37.371094 48.316406 36.175781 48.96875 36 C 49.390625 36.054688 49.796875 35.835938 49.988281 35.453125 C 50.179688 35.074219 50.109375 34.617188 49.8125 34.3125 C 49.792969 34.289063 49.773438 34.269531 49.75 34.25 C 49.652344 34.09375 49.289063 33.453125 48.8125 32.0625 C 48.273438 30.480469 47.71875 28.121094 47.71875 25 C 47.71875 19.773438 48.425781 17.082031 48.6875 16.25 L 49.375 15.96875 C 49.742188 15.859375 50.011719 15.554688 50.078125 15.179688 C 50.140625 14.804688 49.988281 14.425781 49.679688 14.199219 C 49.371094 13.976563 48.960938 13.949219 48.625 14.125 L 47.90625 14.4375 C 47.058594 14.066406 43.382813 12.394531 40.75 9.90625 C 36.914063 6.28125 35.769531 2.828125 35.5625 2.125 L 35.875 1.375 C 36.023438 1.046875 35.984375 0.660156 35.769531 0.371094 C 35.558594 0.078125 35.203125 -0.0742188 34.84375 -0.03125 C 34.800781 -0.0234375 34.757813 -0.0117188 34.71875 0 C 34.402344 0.0820313 34.144531 0.316406 34.03125 0.625 L 33.75 1.3125 C 33.082031 1.628906 29.835938 3.050781 24.125 3.28125 C 20.546875 3.425781 17.148438 1.980469 16.375 1.625 L 15.96875 0.625 C 15.824219 0.230469 15.449219 -0.03125 15.03125 -0.03125 C 15 -0.03125 14.96875 -0.03125 14.9375 -0.03125 Z M 32.71875 3.8125 L 30.71875 8.625 L 30.6875 8.625 L 30.625 8.78125 C 29.804688 9.03125 27.675781 9.59375 25 9.59375 C 22.324219 9.59375 20.195313 9.03125 19.375 8.78125 L 19.3125 8.625 L 19.28125 8.625 L 17.4375 4.15625 C 18.957031 4.726563 21.328125 5.398438 24.1875 5.28125 C 28.367188 5.113281 31.125 4.375 32.71875 3.8125 Z M 34.46875 4.75 C 35.175781 6.332031 36.578125 8.730469 39.375 11.375 C 41.503906 13.386719 43.871094 14.71875 45.40625 15.46875 L 40.625 17.4375 L 40.46875 17.5 C 39.714844 17.050781 38 15.9375 36.28125 14.21875 L 36.0625 13.96875 C 34.160156 12 32.867188 9.992188 32.53125 9.46875 L 32.5625 9.375 Z M 15.5625 4.84375 L 17.4375 9.375 L 17.5 9.5 C 17.132813 10.082031 15.511719 12.613281 14.0625 14.0625 C 12.613281 15.511719 10.082031 17.132813 9.5 17.5 L 9.375 17.4375 L 4.34375 15.34375 C 6.046875 14.585938 8.742188 13.164063 11.25 10.65625 C 13.570313 8.339844 14.855469 6.238281 15.5625 4.84375 Z M 20.34375 11.125 C 21.429688 11.386719 22.972656 11.65625 25 11.65625 C 27.03125 11.65625 28.570313 11.386719 29.65625 11.125 L 27.75 15.75 C 27.171875 15.867188 25.980469 16.09375 25 16.09375 C 24.019531 16.09375 22.828125 15.867188 22.25 15.75 Z M 31.59375 11.75 C 32.285156 12.742188 33.359375 14.171875 34.84375 15.65625 C 36.171875 16.984375 37.328125 17.859375 38.1875 18.4375 L 33.46875 20.40625 C 33.054688 20.144531 32.332031 19.644531 31.46875 18.78125 C 30.574219 17.886719 29.914063 17 29.59375 16.53125 Z M 18.4375 11.8125 L 20.40625 16.5625 C 20.136719 17.019531 19.589844 17.910156 18.78125 18.71875 C 17.960938 19.539063 17.027344 20.128906 16.5625 20.40625 L 11.8125 18.4375 C 12.863281 17.730469 14.328125 16.671875 15.5 15.5 C 16.679688 14.320313 17.738281 12.859375 18.4375 11.8125 Z M 3.53125 17.1875 L 8.625 19.28125 L 8.625 19.3125 L 8.71875 19.34375 C 8.851563 20.113281 9.375 23.21875 9.375 25 C 9.375 26.78125 8.851563 29.886719 8.71875 30.65625 L 8.625 30.6875 L 8.625 30.71875 L 3.09375 33 C 3.949219 31.183594 5.0625 28.238281 5.0625 25 C 5.0625 21.765625 4.179688 18.910156 3.53125 17.1875 Z M 46.4375 17.1875 C 46.117188 18.585938 45.71875 21.011719 45.71875 25 C 45.71875 28.359375 46.300781 30.949219 46.90625 32.71875 C 46.964844 32.886719 46.984375 32.933594 47.03125 33.0625 L 41.375 30.71875 L 41.375 30.6875 L 41.25 30.625 C 41.03125 29.71875 40.59375 27.582031 40.59375 25 C 40.59375 22.417969 41.03125 20.28125 41.25 19.375 L 41.375 19.3125 L 41.375 19.28125 Z M 23.15625 17.9375 C 23.675781 18.023438 24.3125 18.09375 25 18.09375 C 25.6875 18.09375 26.324219 18.023438 26.84375 17.9375 L 25 22.34375 Z M 28.71875 18.6875 C 29.039063 19.125 29.457031 19.644531 30.03125 20.21875 C 30.523438 20.710938 30.925781 21.054688 31.28125 21.3125 L 26.875 23.125 Z M 21.34375 18.78125 L 23.125 23.125 L 18.78125 21.3125 C 19.195313 21.039063 19.671875 20.671875 20.21875 20.125 C 20.726563 19.617188 21.078125 19.175781 21.34375 18.78125 Z M 10.90625 20.25 L 15.84375 22.3125 C 16.023438 22.730469 16.46875 23.8125 16.46875 25 C 16.46875 26.1875 16.023438 27.269531 15.84375 27.6875 L 10.90625 29.75 C 11.125 28.511719 11.40625 26.679688 11.40625 25 C 11.40625 23.304688 11.117188 21.46875 10.90625 20.25 Z M 38.96875 20.3125 C 38.769531 21.394531 38.5625 22.945313 38.5625 25 C 38.5625 27.046875 38.796875 28.601563 39 29.6875 L 34.25 27.75 C 34.125 27.25 33.875 26.167969 33.875 25 C 33.875 23.832031 34.125 22.75 34.25 22.25 Z M 32.03125 23.1875 C 31.941406 23.679688 31.875 24.269531 31.875 25 C 31.875 25.746094 31.945313 26.332031 32.03125 26.8125 L 27.65625 25 Z M 18.28125 23.3125 L 22.375 25 L 18.28125 26.6875 C 18.386719 26.195313 18.46875 25.625 18.46875 25 C 18.46875 24.359375 18.390625 23.804688 18.28125 23.3125 Z M 23.125 26.875 L 21.375 31.0625 C 21.101563 30.675781 20.78125 30.25 20.34375 29.8125 C 19.839844 29.308594 19.355469 28.933594 18.90625 28.625 Z M 26.875 26.875 L 31.09375 28.625 C 30.679688 28.914063 30.226563 29.273438 29.75 29.75 C 29.273438 30.226563 28.914063 30.679688 28.625 31.09375 Z M 25 27.65625 L 26.8125 32.03125 C 26.332031 31.945313 25.746094 31.875 25 31.875 C 24.238281 31.875 23.667969 31.972656 23.1875 32.0625 Z M 16.5625 29.59375 C 16.953125 29.777344 17.933594 30.246094 18.9375 31.25 C 19.800781 32.113281 20.234375 32.945313 20.4375 33.375 L 18.375 38.375 C 17.75 37.519531 16.804688 36.304688 15.34375 34.84375 C 13.847656 33.34375 12.53125 32.300781 11.625 31.625 Z M 33.40625 29.59375 L 38.375 31.625 C 37.449219 32.3125 36.035156 33.402344 34.4375 35 C 33.078125 36.359375 32.199219 37.539063 31.625 38.34375 L 29.59375 33.40625 C 29.777344 33 30.210938 32.101563 31.15625 31.15625 C 32.101563 30.210938 33.003906 29.777344 33.40625 29.59375 Z M 9.46875 32.53125 C 10.0625 32.96875 12.113281 34.488281 13.90625 36.28125 C 15.703125 38.078125 17.078125 40.011719 17.46875 40.5625 L 17.4375 40.625 L 15.125 46.21875 C 14.445313 44.472656 13.144531 41.832031 10.65625 39.34375 C 8.15625 36.84375 5.5 35.578125 3.75 34.90625 L 9.375 32.5625 Z M 40.5625 32.53125 L 40.625 32.5625 L 46.1875 34.875 C 44.457031 35.535156 41.875 36.8125 39.34375 39.34375 C 36.8125 41.875 35.539063 44.457031 34.875 46.1875 L 32.5625 40.625 L 32.53125 40.53125 C 32.894531 40 34.070313 38.304688 35.90625 36.46875 C 37.796875 34.578125 40 32.945313 40.5625 32.53125 Z M 25 33.875 C 26.167969 33.875 27.246094 34.125 27.75 34.25 L 29.6875 38.96875 C 28.496094 38.734375 26.789063 38.46875 25 38.46875 C 23.191406 38.46875 21.46875 38.753906 20.28125 39 L 22.25 34.25 C 22.75 34.125 23.832031 33.875 25 33.875 Z M 25 40.5 C 27.144531 40.5 29.695313 41.058594 30.65625 41.28125 L 30.6875 41.375 L 30.71875 41.375 L 33 46.9375 C 31.265625 46.132813 28.46875 45.125 25 45.125 C 21.53125 45.125 18.734375 46.132813 17 46.9375 L 19.28125 41.375 L 19.3125 41.375 L 19.34375 41.28125 C 20.304688 41.058594 22.855469 40.5 25 40.5 Z%22/></svg>");
  /* The rating stars, live's artwork rather than a font's. U+2605 came from
     whatever face the fallback chain reached, so the shape changed with the
     machine and never matched the site. Masks, not background images: the
     outline is drawn in currentColor upstream and that does not resolve inside
     a data URI, so the colour has to come from the page. */
  --icon-star: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22black%22><path d=%22M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z%22/></svg>");
  --icon-star-empty: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linejoin=%22round%22 stroke-linecap=%22round%22><path d=%22M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z%22/></svg>");

  --icon-prev: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22><path d=%22m12 19-7-7 7-7%22/><path d=%22M19 12H5%22/></svg>");
  --icon-next: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22><path d=%22M5 12h14%22/><path d=%22m12 5 7 7-7 7%22/></svg>");

  --measure: 72ch;
  --measure-lede: 60ch;  /* a lede runs a step up in size, and the longer line
                            the body wants would be a slog at that size */
  --w-page: 72rem;        /* the homepage: a directory, not an article */
  --w-rail-nav: 15rem;    /* left column, the slash menu */
  --w-rail-aside: 21rem;  /* right column, the cam and listening rail */
  --w-challenge: 14rem;   /* the books rail: a widget, not the page's aside, so
                             it is narrower than --w-rail-aside and its own */
  --w-term: 7rem;         /* the label column of a description list */
  --w-heat-gutter: 2.5rem; /* the heatmap's weekday labels. Stated rather than
                             sized to content because the month strip above has
                             nothing in its first cell and still has to start its
                             week columns on the same pixel the day grid does */
  --w-chart-col: 13rem;   /* the narrowest a chart column gets before the row
                             drops from three abreast to two */

  /* --- Component sizes ----------------------------------------------------
     Not spacing. These are the intrinsic size of a specific thing, so they say
     what they are rather than borrowing a step off the space grid. --lamp-w
     is 15rem like --w-rail-nav and must stay a separate token: the panel SVG
     is 240px wide at a 16px root, and that is unrelated to the rail. */
  --avatar: 6rem;
  --face: 2rem;          /* a facepile avatar: many, small, and not the h-card's photo */
  --end-mark: 2rem;       /* the end-of-article rose */
  --arrow-mark: 1rem;     /* the webring arrows, sized in rem: the link is font-size 0 */
  --poster-w: 4rem;
  --w-search: 14rem;      /* the filter panel's search box: the width it asks
                            for before the row lets it take the slack */
  --logo-h: 0.8rem;       /* an external service's wordmark in a credit line */
  --w-art-col: 200px;     /* the art column in a work's header. In px, not rem:
  /* The widest a photo is ever RENDERED, which is not a width any rule sets:
     both grids size their tracks as fractions. Declared because `sizes` on an
     <img> needs the number and an HTML attribute cannot read a custom property,
     so without a token the figure lives only in markup and drifts silently from
     the grid that produces it.
       --w-photo-strip  the homepage strip at its widest: repeat(6) inside
                        --w-page, so 72rem / 6
       --w-photo-tile   a landscape tile in the /photos/ log, whose rows are
                        --photo-row tall, so roughly 3:2 of that */
  --w-photo-strip: 12rem;
  --w-photo-tile: 20rem;
                             it is the width the file is fetched at, and the
                             two must not drift apart. */
  --art-tile: 10rem;      /* the width a shelf column may shrink to before
                             one is dropped: a poster or a book cover */  /* a poster in the watch grid: the width a column
                            is allowed to shrink to before one is dropped */
  --art: 4rem;
  --console-h: 3.75rem;   /* the masthead frame */
  --figure-h: 32rem;      /* the tallest a photo in prose gets */
  --photo-row: 13rem;     /* the row height of the photo log's tiles */
  --heat-cell: 0.8rem;    /* one day in the heatmap: a square, and 52 of them
                             plus their gaps have to clear the measure */
  --heat-gap: 2px;        /* the mortar between two days. Off the space grid on
                             purpose: at 0.25rem the gaps outweigh the cells and
                             the year reads as a lattice rather than a block */
  --lamp-w: 15rem;
  --lamp-h: 1.625rem;

  /* --- Lines and rings ---------------------------------------------------- */
  /* The counter's ember glow, declared once: it appeared in the base rule and
     again in both dark-scheme blocks, so the same four numbers were written
     three times. */
  /* The heatmap ramp, mixed from the accent rather than declared twice: these
     resolve against whichever --accent and --bg-alt the live scheme set, so
     light and dark follow the palette without a second copy of five colours. */
  --heat-0: var(--bg-alt);
  --heat-1: color-mix(in srgb, var(--accent) 18%, var(--bg-alt));
  --heat-2: color-mix(in srgb, var(--accent) 40%, var(--bg-alt));
  --heat-3: color-mix(in srgb, var(--accent) 78%, var(--bg-alt));
  --heat-4: var(--accent);

  --ember: 0 0 4px rgba(255, 140, 0, 0.75), 0 0 1px rgba(255, 60, 0, 0.9);
  --ember-bright: 0 0 7px rgba(255, 170, 0, 0.95), 0 0 2px rgba(255, 80, 0, 0.9);
  --radius: 2px;
  --radius-pill: 999px;
  --radius-box: 0.75rem;  /* a pill that has opened into a box and can no longer be one */
  --hairline: 1px;        /* every border that is a rule */
  --accent-bar: 3px;      /* the quote / pre marker, deliberately heavier */
  --focus-w: 2px;
  --focus-offset: 2px;
}

/* --- Breakpoints ----------------------------------------------------------
 * These cannot be custom properties: var() is not valid in a media feature,
 * so a token here would fail silently and the query would never match. The
 * discipline has to be a convention instead. Three values, no others:
 *
 *   34rem  narrow phone   the lamp panel goes, the h-card stacks
 *   40rem  wide phone     the photo strip earns a third column
 *   60rem  desktop        the homepage becomes a grid
 *
 * 40rem and 60rem are min-width: they add columns. 34rem is max-width because
 * it takes two things away. Mixing the two is fine; what is not fine is two
 * boundaries for one idea, which is what this replaced. The grid switched on at
 * min-width 60rem while the watched and photo grids collapsed at max-width
 * 62rem, so between 60 and 62rem the homepage rendered as a three-column layout
 * with a one-column watched panel inside it.
 * ------------------------------------------------------------------------- */

/* Three states, which is what a toggle plus an OS preference actually needs:
 *
 *   no attribute            follow the OS
 *   data-scheme="dark"      dark, whatever the OS says
 *   data-scheme="light"     light, whatever the OS says
 *
 * The media query is guarded with :not([data-scheme="light"]) so that choosing
 * light on a machine set to dark actually wins. Only the mapping repeats here;
 * every colour is declared once, in :root.
 */
:root[data-scheme="dark"] {
  --bg:            var(--dark-bg);
  --bg-alt:        var(--dark-bg-alt);
  --ink:           var(--dark-ink);
  --muted:         var(--dark-muted);
  --link:          var(--dark-link);
  --link-hover:    var(--dark-link-hover);
  --accent:        var(--dark-accent);
  --accent-warm:   var(--dark-accent-warm);
  --loud:          var(--dark-loud);
  --star:          var(--dark-star);
  --rule:          var(--dark-rule);
  --pattern-alpha: var(--dark-pattern-alpha);
  --leds:          var(--dark-leds);
  --led-rd:        var(--dark-led-rd);
  --led-sd:        var(--dark-led-sd);
  --led-aa:        var(--dark-led-aa);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-scheme="light"]) {
    --bg:            var(--dark-bg);
    --bg-alt:        var(--dark-bg-alt);
    --ink:           var(--dark-ink);
    --muted:         var(--dark-muted);
    --link:          var(--dark-link);
    --link-hover:    var(--dark-link-hover);
    --accent:        var(--dark-accent);
    --accent-warm:   var(--dark-accent-warm);
    --loud:          var(--dark-loud);
    --star:          var(--dark-star);
    --rule:          var(--dark-rule);
    --pattern-alpha: var(--dark-pattern-alpha);
    --leds:          var(--dark-leds);
    --led-rd:        var(--dark-led-rd);
    --led-sd:        var(--dark-led-sd);
    --led-aa:        var(--dark-led-aa);
  }
}

/* --- Page ---------------------------------------------------------------- */

html {
  box-sizing: border-box;
  -webkit-text-size-adjust: 100%;
  /* Set explicitly. Without it body's background propagates to the canvas and
     covers the wallpaper, which is painted behind everything. */
  background: var(--bg);
}

/* The field behind the page, visible in the margins beside the body column. */
html::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: var(--pattern-ink);
  -webkit-mask-image: var(--pattern-image);
  mask-image: var(--pattern-image);
  -webkit-mask-size: var(--pattern-tile);
  mask-size: var(--pattern-tile);
  -webkit-mask-repeat: repeat;
  mask-repeat: repeat;
}
*, *::before, *::after { box-sizing: inherit; }

body {
  margin: 0 auto;
  padding: var(--space-8) var(--gutter) var(--space-12);
  max-width: var(--measure);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--body);
  /* 20px, as on the live site. The measure is 64ch, so the type size sets the
     column width: at 17px the same rule produced a 709px column against the
     live site's 829px. */
  font-size: var(--text-lg);
  line-height: 1.55;
}

/* The homepage is wider on the live site than the reading pages are: 66rem
   against 64ch. It is a directory rather than an article, so the reading
   measure does not apply to it.

   The live site selects it with a class on the body. A theme adds no markup, so
   this keys off structure instead: the homepage is the page whose main holds the
   slash menu.

   It used to key off "main > .h-card", on the reasoning that only the homepage
   carries a representative card. That stopped being true the moment the
   styleguide grew an <address class="h-card"> sample, and the styleguide
   silently inherited the entire three-column grid: headings in one column,
   their prose in another. A structural proxy for page identity has to name
   something the page is FOR, not something it merely happens to contain. */
body:has(main > nav[aria-labelledby="main-menu"]) {
  /* The live site's body is content-box, so its 66rem is the column and the
     1.25rem padding sits outside it. This box is border-box, so the padding
     has to be added back or the column comes out 40px narrower. */
  max-width: calc(var(--w-page) + var(--gutter) * 2);
}

/* The one fix the Text theme cannot make: with no stylesheet an image renders
   at its intrinsic width and an 800px photo overflows a phone. An embed is the
   same problem with a fixed width written into the markup: a 560px YouTube
   player ran off the side of a 420px viewport. */
img, video, table, pre, iframe {
  max-width: 100%;
}
img, video { height: auto; }

/* max-width alone stops the overflow but keeps the authored height, so a video
   letterboxes as it narrows. These select on the src because a theme may add
   CSS and never markup, and the embed's own URL is the only thing that says it
   is a video. `/videos/embed/` is PeerTube's path, so it holds for any
   instance rather than naming the one in use. An unrecognised embed keeps the
   max-width above and loses nothing. */
iframe[src*="youtube.com"],
iframe[src*="youtube-nocookie.com"],
iframe[src*="player.vimeo.com"],
iframe[src*="/videos/embed/"] {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* A long URI or a wide payload scrolls in its own box rather than pushing the
   whole document sideways. */
pre { overflow-x: auto; }

/* --- The skip link ------------------------------------------------------- */

body > a:first-child {
  position: absolute;
  left: -9999px;
}
body > a:first-child:focus {
  position: static;
  display: inline-block;
  margin-bottom: var(--space-6);
}

/* --- Masthead and menu --------------------------------------------------- */

/* The console frame. The wordmark sits up on the top border in a notch it
   masks out of the line, which is how the live site does it. */
body > header p {
  position: relative;
  margin: 0 0 var(--space-3);
  padding: var(--space-4) var(--gutter);
  border: var(--hairline) solid var(--accent);
  border-radius: var(--radius);
  font-family: var(--mono);
  letter-spacing: 0.02em;
  min-height: var(--console-h);
}

body > header p a {
  position: absolute;
  top: -0.72em;
  left: var(--space-4);
  padding: 0 var(--space-2);
  background: var(--bg);
  font-size: var(--text-md);
  font-weight: 500;
}

/* sysop: michael - est. 2012. Real text in the base, so every theme has it. */
body > header p span {
  color: var(--muted);
  font-size: var(--text-sm);
}

body > header p::after,
body > header p::before,
body > header p span::after,
body > header p span::before {
  content: "";
  position: absolute;
  right: var(--gutter);
  top: 50%;
  width: var(--lamp-w);
  height: var(--lamp-h);
  margin-top: calc(var(--lamp-h) / -2);
  background-repeat: no-repeat;
  pointer-events: none;
}
/* Three layers, because two lamps blinking in lockstep reads as a pulse rather
   than as traffic. The panel supplies the six steady lamps and every label; RD
   and SD each get their own layer and their own rate, as on the live site. */
body > header p::after      { background-image: var(--leds); }
body > header p::before     { background-image: var(--led-rd); animation: carrier-flick 0.55s steps(1) infinite 0.2s; }
body > header p span::after,
body > header p span::after  { background-image: var(--led-sd); animation: carrier-flick 0.35s steps(1) infinite; }
body > header p span::before { background-image: var(--led-aa); animation: carrier-flick 0.8s steps(1) infinite 0.1s; }

@keyframes carrier-flick {
  0%, 55%   { opacity: 1; }
  56%, 100% { opacity: 0.18; }
}

/* The OS preference wins outright, and covers everything rather than the lamps
   specifically, so a later addition cannot forget to honour it. There is no
   in-page toggle: the live site's is JavaScript writing data-motion to the root
   and remembering it in localStorage, and this site runs no script. A site
   control needs state that survives a page load, which with no script means a
   cookie the server reads, i.e. the same mechanism the theme switch needs. */
/* The in-page toggle. Same effect as the OS preference, scoped to this site,
   and set by the enhancement script; without script the attribute never
   appears and the OS preference below is the only control. */
:root[data-motion-off] *,
:root[data-motion-off] *::before,
:root[data-motion-off] *::after {
  animation: none !important;
  transition: none !important;
}

/* The script builds this button, so styling it is not a theme adding markup.
   No brackets: they belong to the text menu items, and around an icon they
   read as noise. It sits at the far end of the flex row. */
nav[aria-label="Main"] > .js-motion { margin-left: auto; }

.js-motion {
  display: inline-flex;
  align-items: center;
  border: 0;
  background: none;
  padding: var(--space-1) var(--space-1);
  color: var(--accent-warm);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}
.js-motion:hover { color: var(--link-hover); }
.js-motion svg { width: 1em; height: 1em; }

/* The scheme toggle sits with the motion button and gets the same treatment:
   both are controls the script builds, not destinations, so neither takes the
   menu's slash. */
.js-scheme {
  display: inline-flex;
  align-items: center;
  border: 0;
  background: none;
  padding: var(--space-1);
  color: var(--accent-warm);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}
.js-scheme:hover { color: var(--link-hover); }
.js-scheme svg { width: 1em; height: 1em; }
/* One state, one icon. The sun offers light, so it shows while dark is live. */
.js-scheme [data-icon="to-dark"] { display: none; }
.js-scheme[data-showing="light"] [data-icon="to-light"] { display: none; }
.js-scheme[data-showing="light"] [data-icon="to-dark"] { display: inline; }

/* One state, one icon. */
.js-motion [data-icon="motion-off"] { display: none; }
.js-motion[aria-pressed="true"] [data-icon="motion"] { display: none; }
.js-motion[aria-pressed="true"] [data-icon="motion-off"] { display: inline; }

/* The OS preference wins outright and cannot be overridden by the button. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

@media (max-width: 34rem) {
  body > header p::after,
  body > header p::before,
  body > header p span::after,
body > header p span::before { display: none; }
}

/* The wordmark is the one loud thing on the page, as on the live site. */
body > header p a {
  color: var(--loud);
  text-decoration: none;
}
body > header p a:hover,
body > header p a:focus-visible {
  text-decoration: underline;
}

/* The nav holds the menu list and, once script adds it, the controls beside
   it. The list carries the destinations; the controls sit apart from them. */
nav[aria-label="Main"] {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* This gap only ever shows between the two controls at the end: the list is
     flex: 1 and absorbs everything before them. The menu items space
     themselves, on the list's own gap below. */
  gap: var(--space-1) var(--space-2);
  font-family: var(--mono);
  font-size: var(--text-sm);
  line-height: 1.55;
}

nav[aria-label="Main"] ul {
  flex: 1;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-6);
}

/* These are the slash pages, so the marker is the slash. Generated, because
   the markup is a plain list and must stay one. These are the slash pages, so
   the marker is the slash.

   The live site's [A]bout hotkey cannot be done here: bracketing the first
   letter means wrapping it in an element, and a theme adds no markup. */
/* No underlines in the menu row. WCAG's underline expectation is about links
   inside running prose; a bracketed monospace menu already reads as one, and
   in mono an underline collides with the brackets. */
nav[aria-label="Main"] a {
  color: var(--ink);
  text-decoration: none;
  padding: var(--space-1) 0;
  border-bottom: var(--hairline) dotted transparent;
}
nav[aria-label="Main"] a:hover,
nav[aria-label="Main"] a:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
nav[aria-label="Main"] a::before {
  content: "/";
  color: var(--accent-warm);
  font-weight: 700;
}

/* The initial in the same amber as the slash, so /About reads as one marked
   unit. No brackets: that is kbd notation and no key does anything here. */
nav[aria-label="Main"] a > span {
  color: var(--accent-warm);
  font-weight: 700;
}
/* The current page turns its brackets loud. Bold stays as well: colour alone
   would be the only signal, and on paper the amber and the red are close in
   both hue and luminance. aria-current covers a screen reader; it does not
   cover a sighted reader who cannot separate the two. */
nav[aria-label="Main"] [aria-current="page"] {
  color: var(--ink);
  text-decoration: underline;
}
nav[aria-label="Main"] [aria-current="page"]::before,
nav[aria-label="Main"] [aria-current="page"] > span {
  color: var(--loud);
}


/* --- The condensed masthead ----------------------------------------------
 * An entry page gets a slim one-row bar instead of the console: wordmark on
 * the left, menu and controls on the right. The live site opts into this with
 * `condensedHeader` frontmatter, which a theme here cannot read, so it keys off
 * what makes a page an entry: the entry IS the page, so the article is the
 * first thing in main.
 *
 * ⚠️ :first-child is doing real work and is not decoration. This used to also
 * require a breadcrumb, until the breadcrumb was dropped outright on
 * 2026-09-07. The styleguide has a sample h-entry as a child of main too, and
 * on `main > article.h-entry` alone it takes the condensed bar - the same trap
 * the homepage grid fell into when it keyed off a stray h-card. What separates
 * them is position: an entry page opens with its entry, the styleguide opens
 * with a header. Verified against the built DOM, not assumed.
 *
 * The sysop line and the lamps are hidden rather than dropped: the base emits
 * them for every theme, and the Text theme still reads them here.
 * ------------------------------------------------------------------------- */
body:has(main > article.h-entry:first-child) > header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-1) var(--space-6);
}

/* The frame comes off: no border, no notch, no reserved lamp height. */
body:has(main > article.h-entry:first-child) > header p {
  position: static;
  margin: 0;
  padding: 0;
  border: 0;
  min-height: 0;
}
body:has(main > article.h-entry:first-child) > header p a {
  position: static;
  padding: 0;
  background: none;
  font-weight: 600;
}
body:has(main > article.h-entry:first-child) > header p span { display: none; }
body:has(main > article.h-entry:first-child) > header p::before,
body:has(main > article.h-entry:first-child) > header p::after,
body:has(main > article.h-entry:first-child) > header p span::before,
body:has(main > article.h-entry:first-child) > header p span::after { display: none; }

/* Sized to its content rather than growing, so the header's space-between
   carries the whole nav to the right edge: wordmark on the left, menu and
   controls together on the right. Growing it would have pinned the menu just
   after the wordmark with only the controls at the far end. */
body:has(main > article.h-entry:first-child) > header nav[aria-label="Main"] {
  flex: 0 1 auto;
  justify-content: flex-end;
}
/* The list inside it must stop growing too, or it reclaims the gap. */
body:has(main > article.h-entry:first-child) > header nav[aria-label="Main"] ul { flex: 0 1 auto; }

/* --- Rules --------------------------------------------------------------- */

/* The base emits these to separate chrome from content, which is the only
   boundary the Text theme can draw. Here they become the terminal's own. */
hr {
  border: 0;
  border-top: var(--hairline) solid var(--rule);
  margin: var(--space-6) 0;
}


/* --- The end-of-article mark ---------------------------------------------
 * The base already emits an <hr> where the prose ends and the page furniture
 * begins, and that hr is the separator: it is what the Text theme draws and
 * what lynx prints. Carrier draws the rose ON it rather than adding a second
 * one, so there is still exactly one boundary in the markup and in the tree.
 * ------------------------------------------------------------------------- */
main > article.h-entry > hr {
  border: 0;
  width: var(--end-mark);
  height: calc(var(--end-mark) * 3 / 4);   /* the artwork is 32 by 24 */
  margin: var(--space-10) auto;
  background-color: var(--muted);
  -webkit-mask-image: var(--icon-end);
  mask-image: var(--icon-end);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* --- Headings ------------------------------------------------------------ */

h1 {
  font-family: var(--body);
  font-size: var(--text-2xl);
  font-weight: 700;
  line-height: 1.15;
  color: var(--ink);
  margin: var(--space-1) 0 var(--space-3);
}

/* The section rule: mono, uppercase, tracked out, in the cyan accent. This is
   the "— MAIN MENU" treatment, and the dashes are generated rather than typed.
   The only heading that is not the body face. */
h2 {
  font-family: var(--mono);
  font-size: var(--text-md);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin: 2.75rem 0 var(--space-4);
  display: flex;
  align-items: center;
  gap: 0.7em;
}
/* One dash in front, then a rule that flexes to fill the line. The rule is a
   border on an empty pseudo-element rather than a repeated character, so it
   ends exactly at the edge whatever the heading's length. */
h2::before { content: "\2500"; flex: 0 0 auto; }
h2::after {
  content: "";
  flex: 1 1 auto;
  border-top: var(--hairline) solid currentColor;
  opacity: 0.35;
}

/* The entry's own footer sections, and what follows the entry, are chrome
   rather than the writing: one step under the body face, where a content h2 is
   a step above it. Ported from the live site's `main > article ~ h2` and
   respelled for this markup, where Signal and the conversation sit inside the
   entry's <footer> and only "You may also like" is a sibling of it. */
article > footer h2,
main > article ~ aside > h2 {
  font-size: var(--text-sm);
}

h3 {
  font-family: var(--body);
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
  margin: var(--space-3) 0 var(--space-1);
}

h4, h5, h6 {
  font-family: var(--body);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
  margin: var(--space-3) 0 var(--space-1);
}

/* Inside an entry the headings are a second system: the section rule repeats
   at each depth, one step smaller and one step quieter, with an extra dash per
   level. Cyan, then amber, then muted. Ported from the live site's
   article > div rules; e-content is the same box here. */
.e-content h2 {
  font-size: var(--text-lg);
  letter-spacing: 0.08em;
}

.e-content h3 {
  font-family: var(--mono);
  font-size: var(--text-md);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent-warm);
  margin: var(--space-7) 0 var(--space-3);
}
.e-content h3::before { content: "\2500\2500\00a0"; }

.e-content h4 {
  font-family: var(--mono);
  font-size: var(--text-sm);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin: var(--space-6) 0 var(--space-2);
}
.e-content h4::before { content: "\2500\2500\2500\00a0"; }

/* --- Text ---------------------------------------------------------------- */

p { margin: var(--space-3) 0; }

a { color: var(--link); }
a:hover { color: var(--link-hover); }
a:focus-visible {
  outline: var(--focus-w) solid var(--link);
  outline-offset: var(--focus-offset);
}

small, time { color: var(--muted); }
abbr[title] { text-decoration-style: dotted; }

code, pre, kbd { font-family: var(--mono); font-size: 0.9375em; }

code {
  background: var(--bg-alt);
  padding: 0.1em 0.3em;
}

pre {
  background: var(--bg-alt);
  padding: var(--space-3) var(--space-4);
  border-left: var(--accent-bar) solid var(--accent);
  line-height: 1.45;
}
pre code { background: none; padding: 0; }

/* Ported from the live site. An amber bar rather than a hairline grey one, and
   the quote a shade off the body colour rather than fully muted, so it reads as
   set apart without reading as de-emphasised. */
blockquote {
  margin: var(--space-6) 0;
  padding: var(--space-2) 0 var(--space-2) var(--space-4);
  border-left: var(--accent-bar) solid var(--accent-warm);
  color: color-mix(in srgb, var(--ink) 75%, var(--muted));
  font-style: italic;
}
/* The quote's own padding does the spacing; its paragraphs should not add to it
   at the ends. */
blockquote > :first-child { margin-top: 0; }
blockquote > :last-child { margin-bottom: 0; }

/* An attribution written inside the quote. No generated dash before it: a dash
   drawn in CSS is absent everywhere a stylesheet is not, which for this site
   means the feeds and lynx, and there the attribution would run straight on
   from the quotation and read as Michael's own words. */
blockquote cite {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--mono);
  font-size: var(--text-sm);
  font-style: normal;
  color: var(--muted);
}

/* A cited quotation puts its attribution in a figcaption OUTSIDE the quote,
   because it is not part of what was said. Same treatment as the cite above. */
figure > blockquote + figcaption {
  text-align: left;
  color: var(--muted);
}
figure > blockquote + figcaption cite { font-style: normal; }

figure { margin: 0 0 var(--space-4); }

/* A photo in prose is capped by HEIGHT, not width. Width is the wrong lever:
   the live site caps this figure at 24em and that reads as a width decision,
   but what actually goes wrong is vertical. A 1200x1600 portrait filling a
   72ch column stands over 1000px tall and pushes the writing off the screen,
   while an 800x600 landscape at the same width is 480 and fine. One height
   cap fixes the first and never binds on the second, with no way to select on
   aspect ratio in CSS.

   width: auto so the ratio is kept when the cap binds; the intrinsic width
   attribute would otherwise hold and stretch the picture. */
figure img {
  max-height: var(--figure-h);
  width: auto;
}

/* Every browser italicises <address>. It is a contact block, not an aside. */
address { font-style: normal; }

/* A reply's context sits above its words, one step off the meta line it shares
   a treatment with. */
main > article.h-entry > .reply-context { margin: var(--space-4) 0 0; }
main > article.h-entry > .reply-context a { color: var(--link); }

/* --- Description lists ---------------------------------------------------
 * Contact, Listening, Slashes and the RSS page all state a label and a value.
 * Two columns, a hairline between rows, the label muted and in the mono voice
 * the rest of the site's labels use.
 *
 * The grid is on the LIST, not on a wrapper per pair. The live site wraps every
 * dt/dd in a <div> purely so CSS can grid it, and a theme here may add CSS and
 * never markup, so the rows come from the column count instead. Cheaper markup
 * for the same result.
 *
 * `main > dl` and not `dl`: the homepage's slash menu is also a dl, and it is a
 * nav's child with its own treatment further down.
 * ------------------------------------------------------------------------- */

main > dl {
  display: grid;
  grid-template-columns: minmax(var(--w-term), max-content) 1fr;
  margin: var(--space-6) 0;
  max-width: var(--measure);
}

/* No gap in either direction: both cells carry a bottom border and a gap breaks
   the rule between them, which it visibly did in the column direction. The
   space between label and value is the label's own padding-right instead, so
   the border runs under it and the two halves meet. */
main > dl dt {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
  padding: var(--space-2) var(--space-4) var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--rule);
}
main > dl dd {
  margin: 0;
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--rule);
  /* A handle like @michaelharley@infosec.exchange has no space to break at. */
  overflow-wrap: anywhere;
}
/* A term that is itself a link is a section of this site, which is what the
   Slashes page lists and the only place in the site where that happens (28 of
   28 terms there, 0 everywhere else). So it wears the same slash the main menu
   wears, from the same amber, and reads as the full version of that menu
   rather than as a different list of the same things.

   No underline, for the reason already written above the nav: WCAG's underline
   expectation is about links inside running prose, and a column where every
   term is a link is a menu. Hover draws the dotted rule instead, so the state
   is still not colour alone. */
main > dl dt > a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: var(--hairline) dotted transparent;
}
main > dl dt > a::before {
  content: "/";
  color: var(--accent-warm);
  font-weight: 700;
}
main > dl dt > a:hover,
main > dl dt > a:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
/* The nav puts the initial in amber too, but it can only do that because the
   base wraps it in a span there. Here it does not, and a theme may add CSS and
   never markup, so the slash carries it alone. */

/* The list ends at its last value, not at a rule hanging under it. */
main > dl dt:last-of-type,
main > dl dd:last-of-type { border-bottom: none; }

/* Narrow: the label takes its own line so a long handle keeps the full width
   rather than being squeezed into whatever the label leaves. */
@media (max-width: 34rem) {
  main > dl { grid-template-columns: 1fr; }
  main > dl dt { padding: var(--space-2) 0 0; border-bottom: none; }
}

/* --- The homepage grid --------------------------------------------------- */

/* The live site wraps its three columns in a div and grids that. There is no
   wrapper here and a theme may not add one, so main is the grid and every
   section is placed explicitly by the id its heading already carries. That is
   also why placement is spelled out rather than left to auto-flow: the rail
   spans two rows, and without explicit areas the watched section would drop
   into column three behind it.

   Column widths are the live site's: a narrow menu rail, a flexible middle,
   and a fixed-ish right rail. */
@media (min-width: 60rem) {
  body:has(main > nav[aria-labelledby="main-menu"]) main {
    display: grid;
    grid-template-columns: minmax(0, var(--w-rail-nav)) minmax(0, 1fr) minmax(0, var(--w-rail-aside));
    /* Declared so the rail's overhang has somewhere to go. Without explicit
       tracks the extra height a spanning item needs is split across the rows it
       spans, which stretched the menu and the posts instead of the row below
       them. The flexible third track absorbs it. */
    grid-template-rows: auto auto auto 1fr auto;
    column-gap: var(--space-6);
    row-gap: var(--space-6);
    align-items: start;
  }

  body:has(main > nav[aria-labelledby="main-menu"]) main > .h-card             { grid-column: 1 / -1; }
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="main-menu"]        { grid-column: 1; grid-row: 2; }
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="recent-posts"]     { grid-column: 2; grid-row: 2; }
  /* The rail spans every remaining row, not just two. Spanning two made it the
     tallest thing in those rows, so it set their height and left a hole under
     the shorter left-hand column. Across all of them the left content is taller
     in total, so the rail no longer drives anything. */
  body:has(main > nav[aria-labelledby="main-menu"]) main > div:has([aria-labelledby="fish-cam"]) {
    grid-column: 3;
    /* Not "2 / -1": with no grid-template-rows there is no explicit grid, so
       -1 resolves to line 1 and the rail jumps to the top. */
    grid-row: 2 / span 3;
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
  }
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="recently-watched"] { grid-column: 1 / 3; grid-row: 3; }
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="recently-read"]    { grid-column: 1 / 3; grid-row: 4; }
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="recent-photos"]    { grid-column: 1 / -1; grid-row: 5; }
  /* Hug the top of its row: the rail is longer than the left column, and the
     slack should fall below the watched panel rather than above it. */
  body:has(main > nav[aria-labelledby="main-menu"]) main > :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) { align-self: start; }

  /* The menu and the posts are a matched pair, so they share a height: the
     section stretches to the row and its panel grows to fill the section. */
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="main-menu"],
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="recent-posts"] {
    align-self: stretch;
    display: flex;
    flex-direction: column;
  }
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="main-menu"] > dl,
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby="recent-posts"] > ul {
    flex: 1 1 auto;
  }

  /* The rail is the same problem one column over: it is shorter than the three
     rows it spans, so it left a hole under Last Played. Same answer, rather
     than tuning how many plays are listed until the two columns happen to end
     level: that match holds at one width with one set of titles and breaks the
     next time one wraps. The panel grows, its content stays at the top, and
     the footer action is carried to the bottom edge. */
  body:has(main > nav[aria-labelledby="main-menu"]) main > div:has([aria-labelledby="fish-cam"]) {
    align-self: stretch;
  }
  body:has(main > nav[aria-labelledby="main-menu"]) main > div [aria-labelledby="last-played"] {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
  }
  body:has(main > nav[aria-labelledby="main-menu"]) main > div [aria-labelledby="last-played"] > ol {
    flex: 1 1 auto;
  }
  /* The rail's own headings still need their margin removed. */
  body:has(main > nav[aria-labelledby="main-menu"]) main > div:has([aria-labelledby="fish-cam"]) h2 { margin-top: 0; }

  /* In the grid the rows carry the rhythm, so the headings give up their own
     top margin entirely. Left in place it was added to the row gap and to the
     card's margin, which is what opened the hole under the hero. */
  body:has(main > nav[aria-labelledby="main-menu"]) main > [aria-labelledby] > h2 { margin-top: 0; }
  body:has(main > nav[aria-labelledby="main-menu"]) main > .h-card { margin-bottom: 0; }
}

/* --- Section panels ------------------------------------------------------ */

/* Each homepage section's content sits on its own ground, so a section reads as
   a module rather than as loose text under a heading.

   On the live site the heading sits outside a wrapper div and the panel is that
   div. There is no wrapper here and a theme may not add one, so the panel is
   the section's own list: same effect, one element fewer. */
/* Scoped to the homepage, which is the only page with sections at all apart
   from the entry footer. There the section IS the panel, so this rule handed
   the ground to the tallies and left the button behind on the page ground.
   Same failure the styleguide h-card caused: a selector that says "a section's
   list" when it means "the homepage's panel". The `ol` arm was left unscoped
   when the `ul` arm was fixed, so the comment described a scoping only half of
   this selector had: /listening/'s charts and last-played picked up the
   homepage's panel ground the moment they became sections. */
body:has(main > nav[aria-labelledby="main-menu"]) main section > ul,
body:has(main > nav[aria-labelledby="main-menu"]) main section > ol,
main > nav[aria-labelledby] > dl {
  background: var(--bg-alt);
  padding: var(--space-3) var(--gutter) var(--space-4);
  margin: 0;
}

/* The "all of these" link is the panel's footer action, set off by a hairline
   so it reads as part of the module rather than as stray prose after it. */
/* Same predicate as the pill below: a section's final paragraph holding
   nothing but a link is its footer action, whether the panel above it is a
   list or a figure. Keyed on that rather than on "p after a ul", which left
   the cam's action unaligned because a figure precedes it. */
main section > p:last-child:has(> a:only-child),
main > nav[aria-labelledby] > dl + p {
  margin: 0;
  padding: var(--space-3) var(--gutter);
  background: var(--bg-alt);
  border-top: var(--hairline) solid var(--rule);
  text-align: right;
}

/* The panel's footer action is a pill, as on the live site: it is a control,
   and a bare underlined link there reads as one more entry in the list.

   "a:only-child" is what makes this a rule about actions rather than a list of
   pages: a final paragraph containing nothing but a link is a call to action,
   wherever it appears. That catches About the cam as well without naming it. */
main section > p:last-child > a:only-child,
main > nav[aria-labelledby] > dl + p > a {
  display: inline-block;
  padding: var(--space-2) var(--space-4);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--bg);
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--accent);
  text-decoration: none;
}
main section > p:last-child > a:only-child::after,
main > nav[aria-labelledby] > dl + p > a::after {
  content: "";
  display: inline-block;
  width: 1em;
  height: 1em;
  margin-left: 0.4em;
  vertical-align: -0.15em;
  background: currentColor;
  -webkit-mask-image: var(--icon-arrow);
  mask-image: var(--icon-arrow);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
main section > p:last-child > a:only-child:hover,
main > nav[aria-labelledby] > dl + p > a:hover {
  border-color: var(--accent-warm);
  color: var(--accent-warm);
}

/* Orientation, not an action. A pill here gave a breadcrumb the weight of a
   call to action at the top of a piece someone arrived to read, which invites
   them straight back out; the live site keeps its trail as plain text for the
   same reason. Quiet mono, the accent only on the arrow.

   The base writes the mark as an aria-hidden "⬅", right for a text browser and
   wrong beside the site's own arrows. Carrier hides the character and draws the
   arrow it uses everywhere else, flipped; lynx still renders the glyph. */
main > nav[aria-label="Breadcrumb"] { margin-bottom: var(--space-6); }
main > nav[aria-label="Breadcrumb"] p { margin: 0; }
main > nav[aria-label="Breadcrumb"] a > span { display: none; }
main > nav[aria-label="Breadcrumb"] a {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
  text-decoration: none;
}
main > nav[aria-label="Breadcrumb"] a:hover { color: var(--accent); text-decoration: underline; }
main > nav[aria-label="Breadcrumb"] a::before {
  content: "";
  display: inline-block;
  width: 1em;
  height: 1em;
  margin-right: 0.4em;
  vertical-align: -0.15em;
  background: var(--accent);
  transform: scaleX(-1);
  -webkit-mask-image: var(--icon-arrow);
  mask-image: var(--icon-arrow);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* The cam figure: the frame is the panel, so the caption sits on the same
   ground rather than floating under a bare image. */
main [aria-labelledby="fish-cam"] > figure {
  margin: 0;
  background: var(--bg-alt);
}
main [aria-labelledby="fish-cam"] > figure img {
  display: block;
  width: 100%;
  height: auto;
}
main [aria-labelledby="fish-cam"] > figcaption,
main [aria-labelledby="fish-cam"] figcaption {
  padding: var(--space-3) var(--gutter);
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}

/* --- Shelf cards: recently watched, recently read ------------------------ */

/* Three to a line, cover beside the text. Small covers on purpose: the width
   goes into fitting more entries rather than into any single one.

   Both sections are the same card and share every rule here. They are listed
   rather than given a class because a theme may add CSS and never markup, so
   the section's own id is the only handle. A third shelf joins the :is() list
   and inherits the lot; copying the block instead is how the second copy
   starts drifting from the first. */
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) ul {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2) var(--space-3);
  list-style: none;
  margin: 0;
}
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]).h-feed ul > li,
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]).h-feed ul > li:first-child {
  display: grid;
  grid-template-columns: auto 1fr;
  /* Declared, so the poster can span them all and the rows stay content-sized.
     Spanning only two left the poster's extra height to inflate the title's
     row, which opened a 44px hole between the title and the year.

     Four rows, and the LAST one is the flexible one. A fifth spare row under
     the date parked the overhang below the text, so the column ended a poster's
     worth short of the bottom; putting the slack in the date's own row instead
     lets the date sit at the end of it. Title against the top of the poster,
     date against the bottom, and the year and rating stay gathered under the
     title rather than drifting apart. */
  grid-template-rows: auto auto auto 1fr;
  column-gap: var(--space-2);
  align-items: start;
  padding: 0;
  border: 0;
  min-width: 0;
}
/* A film poster is reliably 2:3, so cropping one to that loses nothing. Book
   covers are not: half of these arrive as 400x400 squares, and cover-cropping
   a square to 2:3 takes a third off each side, straight through the title.
   Contained instead, so the box stays uniform and no artwork is cut. */
main [aria-labelledby="recently-read"] li img {
  object-fit: contain;
  object-position: top;
}

main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li a {
  display: contents;
  color: inherit;
  text-decoration: none;
}
/* The grid item is normally the <picture>, not the <img>. The transform wraps
   the image, and a.u-url is display:contents, so picture is what li actually
   lays out; grid placement on the img landed on a non-item and did nothing.

   Both are placed, because "the transform wraps every image" is not something
   this rule can rely on. A file the transform cannot read is emitted bare, and
   then the img IS the grid item: one such cover put its whole 96px into row 1
   and pushed that card's text below every other card in the row. Whichever
   element ends up being the item gets the placement; the other is not a grid
   item and ignores it.
   That is why row 1 resolved to 75px for a 31px title: the picture was
   auto-placed into it at its own height, leaving a 44px hole above the year.
   Three earlier attempts measured identical afterwards because all three
   moved the img. */
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li :is(picture, img) {
  grid-column: 1;
  /* Spans every row, which is what makes the poster the card's height. The
     overhang lands in the 1fr last row rather than being shared out across the
     content-sized ones, so the first three rows stay the size of their text. */
  grid-row: 1 / -1;
  display: block;
}
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li img {
  /* Block, or the inline baseline adds ~9px of descender leading under a 66px
     poster and the picture measures 75. */
  display: block;
  width: var(--poster-w);
  aspect-ratio: 2 / 3;
  height: auto;
  object-fit: cover;
  border-radius: var(--radius);
}
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li cite {
  grid-column: 2;
  grid-row: 1;
  /* One step under the reading size. These are cards in a narrow column, not
     page prose, and the smaller title also buys back characters before the
     ellipsis. It matches the rating's step, but the stars are compact green
     glyphs against a cyan link in the body face, so they do not compete. */
  font-size: var(--text-md);
  font-style: normal;
  color: var(--link);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li a:hover cite { text-decoration: underline; }
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .meta { grid-row: 2; }
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .rating { grid-row: 3; }
/* The end of the flexible row, which is the bottom of the poster beside it. */
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .dt-published {
  grid-row: 4;
  align-self: end;
}
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .meta,
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .dt-published {
  grid-column: 2;
  font-family: var(--mono);
  font-size: var(--text-xs);
}
/* Column only. The rating draws its own glyphs at its own size and needs
   font-size: 0 on the box to keep the number under them; sharing the meta
   sizing re-opened the box, so the fill clipped against the wrong width and
   every card read as five full stars. */
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .rating { grid-column: 2; }
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .meta,
main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) li .dt-published { color: var(--muted); }

@media (min-width: 60rem) {
  main :is([aria-labelledby="recently-watched"], [aria-labelledby="recently-read"]) ul { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* --- Listening ------------------------------------------------------------
 * The stats band, the activity heatmap, the three charts and the last-played
 * rows, all ported from the live site's layout.
 *
 * The heatmap replaced a table that painted a play count into every cell. That
 * markup was the more honest one and it read correctly in lynx, but on screen
 * thirteen weeks stretched across the measure rendered as a spreadsheet, and a
 * spreadsheet is not what a year of listening looks like. The page now carries
 * live's grid markup, so this file only sizes and paints it.
 * ------------------------------------------------------------------------- */

/* Stacked, not side by side: a 52-column grid needs the whole measure, and
   halving it for a stats column beside it puts the cells under a hairline. */
.listening-dash {
  display: grid;
  gap: var(--space-6);
}

/* The all-time stats: value first, then its label, five across. Ported from
   the live site, which reads "6,049 Minutes" rather than "Minutes: 6,049" —
   the number is the content and the word is the caption on it. */
/* Two columns, then five. Stated rather than left to auto-fit, because the
   count that matters is the number of STATS, not the number that happens to
   fit: auto-fit gave four, which leaves a last row of two beside two empty
   cells and reads as a broken grid. Ten divides by two and by five, so both
   settings fill their last row exactly. Stating the count is also what lets the
   band run the full width of its section: the reason to cap it at the measure
   was that a wider window would add a column, and it cannot. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2) var(--space-4);
  /* The whole readout drops a step off the body face. On the live site this
     comes from the panel these stats sit inside, which sets the entire meta
     zone to --text-sm. Left to inherit, the figures came out at the 1.25rem
     body size and read as headings rather than as a readout. */
  font-size: var(--text-sm);
}
@media (min-width: 40rem) {
  .stat-grid { grid-template-columns: repeat(5, 1fr); }
}
.stat-grid .stat {
  display: flex;
  align-items: baseline;
  gap: 0.45em;
  /* A caption that wraps under its own figure breaks the row's baseline and
     makes one cell twice the height of its neighbours. */
  white-space: nowrap;
}
/* The order swap is the whole reason the pair is wrapped: a dd may not precede
   its dt in the markup, so the reversal has to happen in layout. */
.stat-grid dd {
  order: 1;
  margin: 0;
  padding: 0;
  border-bottom: none;
  font-family: var(--mono);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.stat-grid dt {
  order: 2;
  padding: 0;
  border-bottom: none;
  /* Inherits the grid's size rather than taking --text-sm again from the
     shared dl rule, which at this scale would put it a step below the figure
     instead of level with it. */
  font-family: var(--body);
  font-size: inherit;
  color: var(--muted);
}
/* The month strip takes the same column template as the grid under it, so each
   label lands on the week it names. */
.heatmap__months {
  display: grid;
  grid-template-columns: var(--w-heat-gutter);
  grid-auto-flow: column;
  grid-auto-columns: minmax(var(--heat-cell), 1fr);
  gap: var(--heat-gap);
  margin-bottom: var(--space-1);
  font-family: var(--mono);
  font-size: var(--text-xs);
  line-height: 1;
  color: var(--muted);
}
.heatmap__wd {
  display: flex;
  align-items: center;
  padding-right: var(--space-2);
  font-family: var(--mono);
  font-size: var(--text-xs);
  line-height: 1;
  white-space: nowrap;
  color: var(--muted);
}

.heatmap__legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-2) 0 var(--space-8);
  font-family: var(--mono);
  font-size: var(--text-xs);
  color: var(--muted);
}

/* The period strip. Live's is a row of tab buttons; here each period is its own
   page, so these are links and the current one says so with aria-current rather
   than aria-selected. Same pill the panel footer actions use, because it is the
   same kind of thing: a control, where a bare underlined link would read as one
   more entry in the list under it. */
.period-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0 var(--space-6);
  padding: 0;
  list-style: none;
}
.period-nav a {
  display: inline-block;
  padding: var(--space-2) var(--space-4);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--bg);
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--accent);
  text-decoration: none;
}
/* The page you are on. It stays a link so the markup does not have to branch,
   and so the strip reads the same in a browser that draws none of this. */
.period-nav a[aria-current="page"] {
  background: var(--bg-alt);
  border-color: var(--accent);
  color: var(--ink);
}
.chart-card h3 {
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.chart-list li,
.last-played li {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) dotted var(--rule);
}
.chart-list li:last-child,
.last-played li:last-child { border-bottom: none; }

/* The top entry of each chart is the hero: the cover runs the full column with
   the name under it, which is Koito's own lead card without the text overlay
   (unreadable against arbitrary artwork). display:block is what drops the text
   below the art instead of beside it, so the row component needs no variant. */
.chart-list li:first-child {
  display: block;
  border-bottom: none;
  margin-bottom: var(--space-2);
}

.chart-text {
  min-width: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}
.playcount,
.last-played time {
  flex: none;
  font-family: var(--mono);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--muted);
}

/* Under the narrowest breakpoint the stamp takes its own line rather than
   squeezing the title to nothing. */
@media (max-width: 34rem) {
  .last-played li { flex-wrap: wrap; }
  .last-played time { width: 100%; padding-left: calc(var(--art) + var(--space-3)); }
}

/* --- Books ----------------------------------------------------------------
 * The shelves beside the reading challenge, ported from the live site.
 * ------------------------------------------------------------------------- */

/* Two columns, the widget in the rail. The rail is first in the markup so that
   one column puts it above the shelves; the areas put it back on the right when
   there is room for two. */
.with-sidebar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--w-challenge));
  grid-template-areas: "shelves rail";
  column-gap: var(--space-10);
  row-gap: var(--space-6);
  align-items: start;
}
.with-sidebar > .widget { grid-area: rail; }
.with-sidebar > .shelves { grid-area: shelves; min-width: 0; }
/* The shelf heading gives up the 2.75rem every other h2 opens with. That margin
   is the rhythm between sections in a column of prose; here the heading starts
   the row, so it only pushed the shelf a step and a half below the widget it is
   supposed to sit level with. */
.with-sidebar > .shelves > section:first-child > h2 { margin-top: 0; }
@media (max-width: 34rem) {
  .with-sidebar {
    grid-template-columns: 1fr;
    grid-template-areas: "rail" "shelves";
  }
}

/* A boxed card for rail content, and the one heading on the site that gives up
   the rule its siblings carry: at this width the rule is longer than the label
   and reads as a broken divider rather than a heading. */
.widget {
  padding: var(--space-4) var(--gutter) var(--space-5);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius-box);
  background: var(--bg-alt);
  /* The whole widget drops a step. On the live site the rail inherits a base
     around 1rem; here it is 1.25rem, and at that size twelve mono blocks are
     wider than the rail and the bar wrapped its own percentage onto four
     lines. Same adaptation the listening stats readout makes. */
  font-size: var(--text-sm);
}
.widget .widget__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
}
.widget .widget__title::before,
.widget .widget__title::after { content: none; }

.challenge-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  font-family: var(--mono);
  line-height: 1;
}
.challenge-bar .track { color: var(--rule); }

/* The feed mark beside a listing's own heading. Sized in em by the markup so it
   tracks the h1; all this does is keep it off the text baseline's underline. */
h1 > .feed-mark { text-decoration: none; }
/* The markup's own 0.9em is the size the icon is worth beside body text; beside
   a 1.875rem heading it reads as a second title. Live sizes it down here for
   the same reason, to the same 0.6em. Proportional units throughout: the mark
   belongs to the heading, so it should track whatever size that heading is. */
h1 > .feed-mark > svg {
  width: 0.6em;
  height: 0.6em;
  vertical-align: 0.05em;
  margin-left: 0.15em;
}

/* --- A work's own page ---------------------------------------------------
 * A book, a film, a season. Three parts, in this order: who and what and what
 * I scored it, then what I wrote, then the work's own reference material.
 *
 * Mine first, theirs second. Live opens with the publisher's tagline, the
 * publisher's synopsis and the crowd's scoreboard, and reaches my review
 * seventh on a page that exists to hold it.
 *
 * The two columns are a HEADER treatment and not a page layout. They earn
 * their keep for art beside identity and stop earning it below that, where the
 * art has ended: run down the whole page they set the synopsis at 460px while
 * the review beside them ran the body's full 72ch, which is two measures for
 * the same kind of prose.
 * ------------------------------------------------------------------------- */

@media (max-width: 34rem) {
  .work-head { grid-template-columns: 1fr; }
}
/* Most books have no cover — 59 of live's 427 — so the art column comes off
   entirely rather than leaving an empty cell for the text to sit beside. */
/* The image transform wraps the img in a <picture>; both are blocks or the art
   carries a descender's worth of gap under it. */

/* My verdict: the stars, then when I finished it. */
/* The size goes on the pseudo-elements, NOT the box: .rating hides its digits
   with font-size: 0 on the box, and setting a size there put "3/5" back on
   screen under the stars. */
.work-head .verdict-watched {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}

/* Who made it, and the one or two facts that place the work. */
.work-head .byline {
  margin: var(--space-3) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}
.work-head .meta {
  margin: var(--space-1) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
  line-height: 1.5;
}

/* --- What the work says about itself -------------------------------------
 * Below my review and under its own heading, so it reads as reference rather
 * than as more of my writing.
 * ------------------------------------------------------------------------- */

.work-about .meta {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
  line-height: 1.5;
}
/* The synopsis is someone else's copy, so it sits a step off the body ink
   rather than fully muted: still prose, plainly not mine. */
.work-about .overview {
  color: color-mix(in srgb, var(--ink) 70%, var(--muted));
  line-height: 1.55;
}

/* The one loud element on the page, and only for a win. */
.work-about .award-win {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  margin: var(--space-4) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--accent-warm);
}

/* "the crowd" names whose scores follow. It is the only thing separating my
   rating from theirs, so it is text rather than a rule or a shade. */
.work-about .crowd-label {
  margin: var(--space-5) 0 var(--space-1);
  font-family: var(--mono);
  font-size: var(--text-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
.work-about .scoreboard { display: flex; flex-wrap: wrap; font-family: var(--mono); }
/* Tabular figures so the numbers line up across the row rather than wandering
   with the width of a 1. */
.work-about .score-val {
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* Colour is never the only signal: the source and the number are both written
   out, and the tier only tints what is already legible. */
.work-about .score-val.low  { color: var(--loud); }

/* The logo is a vector lockup and keeps its own proportions; only its height is
   ours to set. In rem rather than em: it sits beside --text-xs and must not
   shrink with it to illegibility. */
.work-about .attribution {
  margin: var(--space-3) 0 0;
  font-family: var(--mono);
  font-size: var(--text-xs);
  color: var(--muted);
}

/* --- Filter and sort panel -----------------------------------------------
 * Built by /js/watch-filter.js and inserted above the list it controls, so
 * there is no markup counterpart here to style when script does not run. The
 * reasoning for building it rather than shipping it is in that file.
 * ------------------------------------------------------------------------- */

.list-controls { margin: 0 0 var(--space-6); }
.list-controls > summary {
  font-family: var(--mono);
  font-size: var(--text-sm);
  width: fit-content;
  cursor: pointer;
}
/* Every control sits under its own name, so the row reads as labelled fields
   rather than a line of anonymous boxes. */
.list-controls label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  color: var(--muted);
}
.list-controls__count {
  margin: var(--space-3) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}

/* --- A shelf -------------------------------------------------------------
 * Books and watching are one shape — a work I consumed, its art, my rating,
 * the date — and they render through one include, so they get one grid. Live
 * draws books as 44px rows and watching as posters; that difference was its
 * own, not something the content asked for.
 *
 * Also used by the genre pages and want-to-read, which list the same entries
 * and used to draw them as plain text lines. Links keep the text list: a
 * repost has no artwork, so a grid of them would be a grid of placeholders.
 * ------------------------------------------------------------------------- */

.shelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--art-tile)), 1fr));
  gap: var(--space-4);
  list-style: none;
  margin: var(--space-5) 0;
  padding: 0;
}
.shelf > li { margin: 0; padding: 0; }
@media (min-width: 40rem) { .shelf--wide { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 60rem) { .shelf--wide { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* A work with no art shows its title in the slot instead of an empty box, so
   the tile still reads as that work and the grid rows stay level. */
.shelf .no-art {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 2 / 3;
  padding: var(--space-2);
  background: var(--bg-alt);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: var(--text-xs);
  line-height: 1.2;
  text-align: center;
  color: var(--muted);
}
.shelf .title {
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.shelf .byline {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}
/* The generic feed-list rules give an entry a rule beneath it and padding, both
   of which belong to a list you read down rather than a grid. They only reach
   a shelf inside a .h-feed wrapper, which is every listing page. Matched on an
   ANCESTOR rather than a direct child: on Books the wrapper sits inside the
   shelf's <section>, so a `>` combinator missed it and the tiles came back
   wearing the row rules. */
main .h-feed ul.shelf > li,
main .h-feed ul.shelf > li:first-child {
  padding: 0;
  border-bottom: 0;
}
main > .h-feed:not([aria-labelledby]) > ul:has(.u-photo) > li {
  flex: 1 1 auto;
  margin: 0;
  /* The feed's row rule belongs to a list of entries you read down. */
  border-bottom: 0;
  padding: 0;
}

/* --- Recent photos ------------------------------------------------------- */

/* A strip of square crops, six across. The titles and dates stay in the markup
   and are hidden here: a text browser cannot show the photographs, so the words
   are the only thing it has, while here the pictures are the point. */
main > [aria-labelledby="recent-photos"] ul {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  list-style: none;
  margin: 0;
}
main > [aria-labelledby="recent-photos"].h-feed ul > li,
main > [aria-labelledby="recent-photos"].h-feed ul > li:first-child {
  padding: 0;
  border: 0;
}
main > [aria-labelledby="recent-photos"] a,
main > [aria-labelledby="recent-photos"] picture { display: block; }
main > [aria-labelledby="recent-photos"] img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius);
}
main > [aria-labelledby="recent-photos"] a:focus-visible {
  outline: var(--focus-w) solid var(--accent);
  outline-offset: var(--focus-offset);
}
main > [aria-labelledby="recent-photos"] li > p { display: none; }

@media (min-width: 40rem) {
  main > [aria-labelledby="recent-photos"] ul { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 60rem) {
  main > [aria-labelledby="recent-photos"] ul { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* --- Last played --------------------------------------------------------- */

/* Art, then track over artist over time. The connectives that make the linear
   reading a sentence are hidden here: position says the same thing. */
main [aria-labelledby="last-played"] ol {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  list-style: none;
  margin: 0;
}
main [aria-labelledby="last-played"] li {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--space-2);
  align-items: center;
}
main [aria-labelledby="last-played"] li img {
  grid-row: 1 / span 3;
  width: var(--art);
  height: var(--art);
  border-radius: var(--radius);
  object-fit: cover;
}
main [aria-labelledby="last-played"] li .p-name   { grid-column: 2; }
main [aria-labelledby="last-played"] li .p-author { grid-column: 2; }
main [aria-labelledby="last-played"] li time      { grid-column: 2; }
/* Titles run long and this column is narrow: clip rather than wrap to three
   lines and shove the row out of alignment. */
main [aria-labelledby="last-played"] li .p-name,
main [aria-labelledby="last-played"] li .p-author,
main [aria-labelledby="last-played"] li time {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
main [aria-labelledby="last-played"] li .p-author,
main [aria-labelledby="last-played"] li time {
  font-family: var(--mono);
  font-size: var(--text-xs);
  color: var(--muted);
}
main [aria-labelledby="last-played"] li time { font-variant-numeric: tabular-nums; }
main [aria-labelledby="last-played"] .sep { display: none; }

/* --- Ratings --------------------------------------------------------------
 * The markup carries the number and nothing else: "3.5/5", with the mf2 on it.
 * Both rows of stars are drawn here, which is the right side of the line for
 * once. Stars are a picture of a number, and the number is the content.
 *
 * That also buys the two things a text run could not. A half star is a real
 * half, because ::after is clipped to a width rather than reaching for U+2BEA,
 * which neither of this site's faces has and which lands as tofu wherever the
 * fallback lacks it. And the empty stars behind it state the scale, so a three
 * is visibly three OUT OF FIVE rather than just three shapes.
 *
 * The Text theme has no stylesheet, so it shows "3.5/5" — which is the same
 * fact, and the one lynx already printed in the places that spelled it out.
 * ------------------------------------------------------------------------- */
.rating {
  position: relative;
  display: inline-block;
  /* The fill is a percentage of THIS box, so the box has to be the width of the
     five stars and nothing else. inline-block gives that in a text run, but a
     .rating that is also a grid item gets blockified and stretched to its
     column, and the fill then clipped against the column: a 2.5 drew four stars
     on the homepage cards. max-content holds the box to its glyphs either way. */
  width: max-content;
  /* Hides the digits without taking them out of the box or the tree. The
     pseudo-elements set their own size back, and role="img" on the span means a
     screen reader reads the label rather than either. */
  font-size: 0;
  vertical-align: -0.08em;
}
/* Five stars tiled across a five-em box, so a star is exactly 1em and the fill
   percentages land on star boundaries. */
.rating::before,
.rating::after {
  content: "";
  display: block;
  /* Live's star is a 1em SVG inside a --text-sm byline, so 0.85rem. This runs a
     step larger because the whole site does: live's body is 1rem against this
     one's 1.25, and a star sized to live's absolute number reads small here
     rather than the same. Same adaptation the listening stats and the challenge
     widget make.

     The earlier --text-2xl was compensating for U+2605, which carries its own
     padding inside the glyph box. The SVG fills the box, so it needed most of
     that back. */
  font-size: var(--text-md);
  width: 5em;
  height: 1em;
  mask-repeat: repeat-x;
  mask-size: 1em 1em;
}

/* --- Feed items ---------------------------------------------------------- */

/* No markers: a feed is a sequence of entries, not a bulleted list, and the
   hairline between them already does the separating. */
main :is(.h-feed, aside[aria-labelledby]) ul {
  list-style: none;
}
main :is(.h-feed, aside[aria-labelledby]) ul > li {
  padding: var(--space-4) 0;
  border-bottom: var(--hairline) solid var(--rule);
}
main :is(.h-feed, aside[aria-labelledby]) ul > li:first-child { padding-top: var(--space-1); }
/* The last hairline would float above the panel's footer action. */
main :is(.h-feed, aside[aria-labelledby]) ul > li:last-child { border-bottom: 0; }

/* An entry title is a list-item title, not page prose: the body face one step
   down from the reading size, at 600 rather than bold. */
main :is(.h-feed, aside[aria-labelledby]) ul > li > p:first-child {
  margin: 0;
  font-family: var(--body);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.35;
}
main :is(.h-feed, aside[aria-labelledby]) ul > li > p:first-child a { text-decoration: none; }
main :is(.h-feed, aside[aria-labelledby]) ul > li > p:first-child a:hover { text-decoration: underline; }

/* The meta line: muted mono, with the icons in the accent so they anchor the
   row rather than dissolving into it. Inline rather than flexed, so the date
   and the reading time read as two groups.

   An entry page's own header takes the same rule rather than a copy: it is the
   same line saying the same things, just not inside a feed. So does a plain
   page's, which carries one date where an entry carries two. */
main :is(.h-feed, aside[aria-labelledby]) ul > li > p + p,
main > article.h-entry > header p,
main > header p,
main > article.h-entry > .reply-context {
  margin: var(--space-1) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}
main :is(.h-feed, aside[aria-labelledby]) ul > li > p + p svg,
main > article.h-entry > header p svg,
main > header p svg,
main > article.h-entry > .reply-context svg {
  vertical-align: -0.15em;
  margin-right: 0.3em;
  color: var(--accent);
}
/* The clock opens the second group, so the gap goes before it. */
main :is(.h-feed, aside[aria-labelledby]) ul > li > p + p time + svg,
main > article.h-entry > header p time + svg { margin-left: 0.85em; }

/* A listing's lede: the page's own description, read out loud rather than only
   sitting in <head>. It beats the meta-line rule above by its class, which is
   the point — the two paragraphs in a page header are not the same kind of
   thing. */
main > header p.lede {
  /* The meta-line rule this overrides carries no bottom margin, because a date
     is the last thing in a header. A lede is not: whatever the page opens with
     follows it directly, and without this the filter panel sat on the sentence. */
  margin: var(--space-2) 0 var(--space-6);
  font-family: inherit;
  font-size: var(--text-lg);
  line-height: 1.45;
  color: var(--ink);
  max-width: var(--measure-lede);
}

/* --- The main menu ------------------------------------------------------- */

/* A label and its sub-label, two lines per row. The live site builds this from
   a ul of links each followed by a <small>; here it is a dl, so the same shape
   comes from dt and dd. The dd has to lose the browser's 40px indent: the
   description belongs under its label, not beside it. */
main > nav[aria-labelledby] > dl {
  font-family: var(--mono);
  font-size: var(--text-sm);
}
main > nav[aria-labelledby] dt {
  margin-top: var(--space-4);
}
main > nav[aria-labelledby] dt:first-of-type {
  margin-top: 0;
}
main > nav[aria-labelledby] dt a {
  color: var(--ink);
  text-decoration: none;
}
/* Same treatment as the top nav: a slash, then the initial, both amber, so
   /Books reads as one marked unit. These are the slash pages either way. */
main > nav[aria-labelledby] dt a::before {
  content: "/";
  color: var(--accent-warm);
  font-weight: 700;
}
main > nav[aria-labelledby] dt a > span {
  color: var(--accent-warm);
  font-weight: 700;
}
main > nav[aria-labelledby] dt a:hover,
main > nav[aria-labelledby] dt a:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}
main > nav[aria-labelledby] dd {
  margin: var(--space-1) 0 0;
  color: var(--muted);
}

/* --- The representative h-card ------------------------------------------- */

/* This is the site's identity record: the thing other sites parse to learn who
   I am. It should read as one card rather than a heading, an orphaned avatar,
   a line of prose and a bulleted list that happen to be adjacent.

   The frame echoes the console in the header, in --rule rather than --accent so
   it sits below it. The DOM order stays h1, photo, note, contacts. */
main > .h-card {
  /* Only the representative card, which is the one direct child of main.
     Every feed also emits a <data class="p-author h-card"> for its author, and
     an unscoped .h-card painted each of those as an empty bordered panel.

     Not a grid. A row-spanning avatar contributes its height to every row it
     spans, so a 96px photo beside a 35px heading forced the first row to 105px
     and opened a hole under the name. Positioned instead: the text is ordinary
     flow and sets its own rhythm, and the photo cannot push it around. */
  position: relative;
  margin: 0 0 var(--space-8);
  padding: var(--gutter) var(--gutter) var(--gutter) calc(var(--avatar) + var(--gutter) * 2);
  min-height: calc(var(--avatar) + var(--gutter) * 2);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius);
  background: var(--bg-alt);
}
/* The image transform wraps every <img> in a <picture>, so the photo is not a
   child of the card and "> .u-photo" silently matches nothing. */
main > .h-card > picture,
main > .h-card > .u-photo {
  position: absolute;
  left: var(--gutter);
  top: var(--gutter);
  border-radius: var(--radius);
}
main > .h-card > h1      { margin: 0 0 var(--space-1); }
/* The homepage runs to 66rem, so the only prose in the card would otherwise
   set a line over 100 characters. Its measure is its own. */
/* A tagline, not a paragraph of the page. Atkinson ships 400 and 700 and
   nothing between, so weight is not available as a lever: the separation from
   the name comes from colour and one step down the scale. Muted holds 5.6:1 on
   the card in dark and 4.6:1 in light, so both clear AA.

   No measure of its own. The card is already bounded (66rem on the homepage,
   64ch elsewhere), and an extra cap here only forced a wrap the card had room
   for: the line needs 698px and the card offers 860. */
main > .h-card > .p-note {
  margin: 0 0 var(--space-3);
  font-size: var(--text-md);
  color: var(--muted);
}

/* The rel=me set: identity proofs, not a list to read down. A row, and in mono
   because they are addresses. */
main > .h-card address ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-5);
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
}

@media (max-width: 34rem) {
  main > .h-card { padding-left: var(--gutter); }
  main > .h-card > picture,
  main > .h-card > .u-photo { position: static; display: block; margin-bottom: var(--space-3); }
}
figcaption { font-size: var(--text-sm); color: var(--muted); }


/* --- The filed-under line ------------------------------------------------
 * The live site builds this as a nav with an inline icon and a visually hidden
 * "Filed under:". The base here writes it as a sentence instead, which a text
 * browser reads properly and which needs no hidden text, so Carrier only has to
 * dress it: the mark, the accent, and a hash on each tag.
 *
 * Selected as the footer paragraph holding categories, since a theme has no
 * class to hook and nothing else in an entry's footer carries one.
 * ------------------------------------------------------------------------- */
main > article.h-entry > footer p:has(.p-category) {
  margin: var(--space-6) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}
main > article.h-entry > footer p:has(.p-category)::before {
  content: "";
  display: inline-block;
  width: 1.35em;
  height: 1.35em;
  margin-right: 0.4em;
  vertical-align: -0.3em;
  background-color: var(--accent);
  -webkit-mask-image: var(--icon-tag);
  mask-image: var(--icon-tag);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
main > article.h-entry > footer .p-category {
  color: var(--accent);
  text-decoration: none;
}
/* The hash is ornament, so it is drawn rather than written: in lynx and in the
   feeds the sentence already reads as prose and a bare "#" would be noise. */
main > article.h-entry > footer .p-category::before { content: "#"; }
main > article.h-entry > footer .p-category:hover {
  color: var(--link-hover);
  text-decoration: underline;
}


/* --- The "also on" row ---------------------------------------------------
 * Mirrors of this entry elsewhere. No pill chrome, as on the live site: these
 * are a secondary affordance and bordered chips would outweigh the tags row
 * above, which is the primary navigation. The brand marks give the row enough
 * structure on their own.
 *
 * Same handle as the tags line: the footer paragraph holding the syndication
 * links, since a theme has no class to hook.
 * ------------------------------------------------------------------------- */
main > article.h-entry > footer p:has(.u-syndication) {
  margin: var(--space-4) 0 0;
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}
/* The lead-in glyph mirrors the tag mark's, so the two rows read as siblings. */
main > article.h-entry > footer p:has(.u-syndication)::before {
  content: "";
  display: inline-block;
  width: 1.35em;
  height: 1.35em;
  margin-right: 0.4em;
  vertical-align: -0.3em;
  background-color: var(--accent);
  -webkit-mask-image: var(--icon-elsewhere);
  mask-image: var(--icon-elsewhere);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
main > article.h-entry > footer .u-syndication {
  color: var(--link);
  text-decoration: none;
  white-space: nowrap;
}
/* The mark takes the link's colour, so each destination reads as one object. */
main > article.h-entry > footer .u-syndication svg {
  width: 1.1em;
  height: 1.1em;
  vertical-align: -0.18em;
  margin-right: 0.15em;
  color: inherit;
}
main > article.h-entry > footer .u-syndication:hover {
  color: var(--link-hover);
  text-decoration: underline;
}


/* --- You may also like ---------------------------------------------------
 * Renders through feed-items.html, so it takes the feed's rules above rather
 * than a second set: a suggestion should look like an entry in any other
 * listing, because that is what it is.
 * ------------------------------------------------------------------------- */
main > aside[aria-labelledby] > ul {
  background: var(--bg-alt);
  padding: var(--space-3) var(--gutter) var(--space-4);
}


/* --- Signal --------------------------------------------------------------
 * The reaction affordance. Selected as the section labelled by the Signal
 * heading, which is the handle the markup already gives.
 *
 * The one filled button on the page. Every other form button is an outlined
 * pill, because it submits something you filled in; this submits nothing you
 * typed and is the action actually on offer, so it carries the weight.
 * ------------------------------------------------------------------------- */
/* The entry footer's panels. Signal and the conversation are the same kind of
   object — a box of controls under a chrome heading — so the ground, the inset
   and the mono voice are declared once here and each says only what is its
   own below. Matches the live site, where one rule dresses the whole meta
   zone. */
article > footer > section[aria-labelledby] {
  margin: var(--space-2) 0 var(--space-4);
  padding: var(--space-4) var(--gutter);
  background: var(--bg-alt);
  font-family: var(--mono);
  font-size: var(--text-sm);
}

/* The action, then the readout. Column, so a busy post's tallies wrap under the
   button instead of turning it into just another chip in a ragged row. */
main section[aria-labelledby="signal-heading"] {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
}

main section[aria-labelledby="signal-heading"] button {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  /* The same height as the tallies beside it, wider by a step. Filled among
     outlines is what marks it as the action; being taller as well just made it
     look like a different kind of control. Still ~32px, over WCAG 2.5.8's
     24px minimum, so the height is a design value here and not a floor. */
  padding: var(--space-1) var(--space-3);
  border: var(--hairline) solid var(--accent-warm);
  border-radius: var(--radius-pill);
  background: var(--accent-warm);
  color: var(--bg);
  font-family: var(--mono);
  font-size: var(--text-sm);
  cursor: pointer;
}
main section[aria-labelledby="signal-heading"] button:hover {
  background: var(--bg);
  color: var(--accent-warm);
}
/* The mark carries the button's colour, so the control reads as one object. */
main section[aria-labelledby="signal-heading"] button svg {
  color: currentColor;
  flex: none;
}

/* The tallies: the read-only half of Signal, wrapping as a group under the
   button rather than beside it. A busy post carries five or six of these, and
   in one row the one pressable thing became just another chip in the pile. */
.tallies {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-2) var(--space-3);
}
.tallies > li { margin: 0; }

/* A count that expands to show who. Block, so the facepile opens UNDER the
   summary instead of beside it, and the padding lives on the summary so the
   whole row is the click target. One step tighter than the button beside it:
   these are read-only, and at the button's size they competed with it. */
.tallies > li > details { display: block; }
.tallies > li > details > summary {
  display: flex;
  align-items: center;
  gap: 0.45em;
  padding: var(--space-1) var(--space-2);
}
/* Open, it is a box with a facepile in it and cannot stay pill-shaped. */
.tallies > li > details[open] { border-radius: var(--radius-box); }
.tallies > li > details[open] > summary { padding-bottom: 0; }

/* The type icon closes the row: the marker already owns the front, and two
   glyphs before the number read as one smeared marker. */
.tallies > li > details > summary svg {
  flex: none;
  color: var(--accent-warm);
}
.tallies > li > details:hover > summary svg { color: inherit; }

/* Faces align under the summary text and carry the box's bottom padding. */
.faces {
  list-style: none;
  margin: 0;
  padding: 0 var(--space-2) var(--space-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}
.faces li { margin: 0; }
.faces img,
.faces .no-photo {
  display: block;
  width: var(--face);
  height: var(--face);
  border-radius: var(--radius-pill);
  object-fit: cover;
}
/* Someone who reacted with no photo on file: their initial, in the same circle,
   so the row keeps its rhythm rather than gapping where a face is missing. */
.faces .no-photo {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-alt);
  border: var(--hairline) solid var(--rule);
  font-size: var(--text-xs);
  color: var(--muted);
}

/* An off-site count. Not a disclosure: there is nothing here to expand, only
   somewhere to go, so it is a link wearing the same pill. */
.tallies .chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  padding: var(--space-1) var(--space-2);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
/* The arrow sits in the marker's column so a chip's number lines up with a
   tally's, rather than starting three characters early. */
.tallies .chip svg {
  flex: none;
  width: 3ch;
  color: var(--accent-warm);
}
.tallies .chip:hover svg { color: inherit; }

/* --- Disclosures --------------------------------------------------------- */

/* The BBS marker, ported from the live site, where it is a global treatment
   rather than one section's. Amber, this site's engagement accent: the same
   colour as the like button's fill and the Signal type icons. */
details > summary {
  list-style: none;
  cursor: pointer;
  /* The same vertical rhythm a menu link gets, so a row that opens a form and
     a row that goes somewhere sit on the same beat. The tallies override it:
     their summary fills a pill and carries that padding instead. */
  padding: var(--space-1) 0;
}
details > summary::-webkit-details-marker { display: none; }
details > summary::before {
  content: "[+] ";
  color: var(--accent-warm);
  font-weight: 700;
}
details[open] > summary::before { content: "[-] "; }

/* --- Lists --------------------------------------------------------------- */

ul, ol, dl { margin: 0 0 var(--space-4); }
li { margin-bottom: 0; }

dt { font-family: var(--mono); font-weight: 700; }
dd { margin: 0 0 var(--space-2) var(--space-6); }

/* --- Tables -------------------------------------------------------------- */

table {
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: var(--text-sm);
}
caption {
  text-align: left;
  color: var(--muted);
  font-size: var(--text-sm);
  padding-bottom: var(--space-2);
}
th, td {
  border: var(--hairline) solid var(--rule);
  padding: var(--space-1) var(--space-2);
  text-align: right;
}
th[scope="row"] { text-align: left; }

/* --- The conversation menu -----------------------------------------------
 * BBS-shaped, as on the live site: numbered rows for the channels that go
 * straight out, [+] rows for the two that open a form in place. The list needs
 * no class — it is the only one in the section, and the two kinds of row are
 * told apart by what they hold rather than by a hook the base would have to
 * carry.
 * ------------------------------------------------------------------------- */

main section[aria-labelledby="join-heading"] > ul {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: convo;
}
main section[aria-labelledby="join-heading"] > ul > li { margin: 0; }

/* A row that is a link is a channel and takes the next number. A row that
   opens a form is not counted: it goes nowhere, so a key for it would promise
   a destination. */
main section[aria-labelledby="join-heading"] > ul > li:has(> a) {
  counter-increment: convo;
}
/* The menu's own link treatment, matching the top nav: no standing underline,
   which would run under the [n] key as well, so hover draws a dotted rule
   instead. */
main section[aria-labelledby="join-heading"] > ul > li > a {
  display: inline-block;
  padding: var(--space-1) 0;
  color: var(--link);
  text-decoration: none;
  border-bottom: var(--hairline) dotted transparent;
}
main section[aria-labelledby="join-heading"] > ul > li > a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
main section[aria-labelledby="join-heading"] > ul > li > a::before {
  content: "[" counter(convo) "] ";
  color: var(--accent-warm);
  font-weight: 700;
}
/* An open form needs clearing before the next row, or the menu reads as one
   run of fields with a stray summary in the middle. */
main section[aria-labelledby="join-heading"] details[open] {
  margin-bottom: var(--space-5);
}

/* The forms themselves: one column, the paragraphs carrying the rhythm rather
   than each one's own margin. */
main section[aria-labelledby="join-heading"] form {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
}
main section[aria-labelledby="join-heading"] form p { margin: 0; }
main section[aria-labelledby="join-heading"] form input,
main section[aria-labelledby="join-heading"] form textarea {
  width: 100%;
}
main section[aria-labelledby="join-heading"] form textarea { resize: vertical; }
/* Left at its natural width. A submit button stretched across the column reads
   as a banner, not a control. */
main section[aria-labelledby="join-heading"] form button { justify-self: start; }
/* Explanatory copy, not a field. */
main section[aria-labelledby="join-heading"] form > p:not(:has(label, button)) {
  color: var(--muted);
}

/* The seam between the two ways to comment. Keyed on position rather than a
   class: it is the paragraph standing between the anonymous form and the
   sign-in one, which is exactly what it says. */
main section[aria-labelledby="join-heading"] details > form + p {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-5) 0 var(--space-2);
  color: var(--muted);
}
main section[aria-labelledby="join-heading"] details > form + p::before,
main section[aria-labelledby="join-heading"] details > form + p::after {
  content: "";
  flex: 1 1 auto;
  border-top: var(--hairline) solid var(--rule);
}
/* The sign-in explanation sits under that seam, so it keeps a paragraph's gap
   above the field rather than the form grid's. */
main section[aria-labelledby="join-heading"] details > form + p + p {
  margin: 0;
  color: var(--muted);
}

/* --- Forms --------------------------------------------------------------- */

label { font-family: var(--mono); font-size: var(--text-sm); }

/* The one place the nearest-step rule is overridden. This was 0.35rem, which
   rounds down to --space-1, and rounding down took the control from exactly
   44px to 41px: 44 is WCAG 2.5.8's target-size minimum and the old value was
   evidently chosen to hit it. A touch target has a floor, so it rounds up. */
input, textarea, select, button {
  font: inherit;
  font-family: var(--mono);
  background: var(--bg);
  color: var(--ink);
  border: var(--hairline) solid var(--rule);
  padding: var(--space-2);
  max-width: 100%;
}
input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible {
  outline: var(--focus-w) solid var(--link);
  outline-offset: var(--focus-offset);
}

/* A form button is a pill, the same shape as the site's other actions. No
   brackets: they read as kbd notation, promising a key that does nothing, and
   the menu dropped them for that reason already.

   Outlined rather than filled, which is the hierarchy: an outlined pill submits
   something you filled in or takes you somewhere, and the one filled pill on a
   page is the action being offered. */
/* Pill chrome only. Display, padding and cursor stay with each thing that
   wears one: a filled action, a disclosure and an off-site link are the same
   shape and not the same object, and putting layout in here gave the tallies
   the button's padding twice over and laid their facepiles out sideways. */
form button,
.list-controls button,
.tallies > li > details,
.tallies .chip {
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius-pill);
  background: var(--bg);
  color: var(--accent);
  font-family: var(--mono);
  font-size: var(--text-sm);
}
form button,
.list-controls button {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
}
form button:hover,
.list-controls button:hover,
.tallies > li > details:hover,
.tallies .chip:hover {
  border-color: var(--accent-warm);
  color: var(--accent-warm);
}

/* --- Footer --------------------------------------------------------------
 * Scoped to body > footer throughout. A bare `footer` also matches the one
 * inside every entry, which centred the tags and the "also on" row and stripped
 * their links to the site footer's inherit-and-no-underline treatment.
 * ------------------------------------------------------------------------- */

/* No border-top here. The base emits an <hr> between main and the footer, and
   that hr stays because it is the only boundary the Text theme can draw, so a
   rule here is the second line for one boundary. The hr's own margins do the
   spacing, which also makes the bottom symmetrical with the top: the same rule,
   the same gap either side of it, above the content and below it. */
body > footer {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
  text-align: center;
}
body > footer a { color: inherit; text-decoration: none; }
/* A link inside running prose has to be findable without colour, and these
   inherit the footer's colour exactly, so colour is not even doing the work.
   The nav lists below are exempt: a list of links reads as one from position.
   WCAG 1.4.1. */
body > footer p a { text-decoration: underline; }
/* The footer is already at the small size, so <small> inside it would compound
   to 11px. The element still means fine print; it just does not shrink twice. */
body > footer small { font-size: inherit; }
body > footer a:hover { color: var(--link-hover); }
body > footer ul {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  /* The footer is text-align: center, which a flex row ignores. */
  justify-content: center;
  gap: 0 var(--space-5);
}

/* --- Footer marks --------------------------------------------------------
 * The live site's footer treatment, reproduced without its markup: it carries
 * inline <svg> in each link, which a theme may not add, so these are generated
 * content selected by href and rel.
 * ------------------------------------------------------------------------- */

/* Every mark shares a box; only the artwork and how it is painted differ. */
body > footer a[href="/subscribe/"]::before,
body > footer nav[aria-label="IndieWeb webring"] a[rel="prev"]::before,
body > footer nav[aria-label="IndieWeb webring"] a[rel="next"]::after,
body > footer nav[aria-label="IndieWeb webring"] a:not([rel])::before,
body > footer nav[aria-label="IndieWeb webring"] a:not([rel])::after {
  content: "";
  display: inline-block;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

/* RSS is a logotype, so it keeps its own orange in both schemes rather than
   being recoloured to the palette, exactly as on the live site. WCAG 1.4.11
   exempts logotypes from the contrast floor, and the word carries the meaning
   either way. */
body > footer a[href="/subscribe/"]::before {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  margin-right: 0.25em;
  background-image: var(--icon-rss);
}

/* The webring row: mono and muted, the hub in the accent, as on the live
   site. Its markup there is three bare anchors; here it is a list, so the
   spacing comes from the row's gap rather than each arrow's padding. */
body > footer nav[aria-label="IndieWeb webring"] {
  font-family: var(--mono);
  font-size: var(--text-sm);
  color: var(--muted);
}
body > footer nav[aria-label="IndieWeb webring"] a { color: var(--accent); }
body > footer nav[aria-label="IndieWeb webring"] a:not([rel]) {
  border-bottom: var(--hairline) solid transparent;
}
body > footer nav[aria-label="IndieWeb webring"] a:not([rel]):hover {
  border-bottom-color: var(--accent);
}

/* Icon only, as on the live site, where these links hold nothing but an SVG
   and an aria-label. The words cannot leave the markup to get there: lynx is
   this project's acceptance test and it renders neither SVG nor aria-label, so
   dropping them would leave a bullet linking to nothing. So Carrier hides them
   and the Text theme keeps them, which is the same trade Recent Photos makes.

   font-size: 0 rather than display: none, because the text is still the
   accessible name and has to stay in the tree. The mark is sized in rem for
   that reason: an em here would be a multiple of zero. */
body > footer nav[aria-label="IndieWeb webring"] a[rel] {
  font-size: 0;
  line-height: 1;
  /* Once the words go, the link is only as big as the mark: 15px, under WCAG
     2.5.8's 24px target. The live site pads its arrows for the same reason.
     Rounds up rather than to nearest, the same exception form controls get,
     because a target has a floor. */
  padding: var(--space-2);
}

/* The arrows ride on currentColor and follow the link, so they are masks. */
body > footer nav[aria-label="IndieWeb webring"] a[rel="prev"]::before,
body > footer nav[aria-label="IndieWeb webring"] a[rel="next"]::after {
  width: var(--arrow-mark);
  height: var(--arrow-mark);
  vertical-align: -0.18em;
  background-color: currentColor;
}
body > footer nav[aria-label="IndieWeb webring"] a[rel="prev"]::before {
  -webkit-mask-image: var(--icon-prev);
  mask-image: var(--icon-prev);
}
body > footer nav[aria-label="IndieWeb webring"] a[rel="next"]::after {
  -webkit-mask-image: var(--icon-next);
  mask-image: var(--icon-next);
}
body > footer nav[aria-label="IndieWeb webring"] a[rel]:hover { color: var(--ink); }

/* Web before the words, ring after, which is the order on the live site. The
   web is monochrome and takes the body colour rather than the link's; the ring
   keeps its own gradients, so it is an image and not a mask. */
body > footer nav[aria-label="IndieWeb webring"] a:not([rel])::before,
body > footer nav[aria-label="IndieWeb webring"] a:not([rel])::after {
  width: 1.25em;
  height: 1.25em;
  vertical-align: -0.25em;
}
body > footer nav[aria-label="IndieWeb webring"] a:not([rel])::before {
  -webkit-mask-image: var(--icon-web);
  mask-image: var(--icon-web);
  background-color: var(--ink);
  margin-right: 0.45em;
}
body > footer nav[aria-label="IndieWeb webring"] a:not([rel])::after {
  /* Light uses the silhouette, not the artwork. The ring is drawn in silvers
     and near-whites for a dark field: measured against this ground, 100% of its
     pixels fall under 3:1 and it vanishes completely. Its outline is a true
     ring with a hole through the middle, so it masks cleanly and reads in the
     body colour, matching the web glyph on the other side of the words. */
  -webkit-mask-image: var(--icon-ring);
  mask-image: var(--icon-ring);
  background-color: var(--ink);
  margin-left: 0.45em;
}

/* Dark keeps the real artwork: only 8.2% of it falls under 3:1 there, so the
   gradients survive the ground and are worth having. */
:root[data-scheme="dark"] footer nav[aria-label="IndieWeb webring"] a:not([rel])::after {
  -webkit-mask-image: none;
  mask-image: none;
  background-color: transparent;
  background-image: var(--icon-ring);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-scheme="light"]) footer nav[aria-label="IndieWeb webring"] a:not([rel])::after {
    -webkit-mask-image: none;
    mask-image: none;
    background-color: transparent;
    background-image: var(--icon-ring);
  }
}

/* A taller hit area for footer links, as on the live site. */
body > footer nav a { display: inline-block; padding: var(--space-1) 0; }

/* The 88x31 wall. The only div in the footer, which is the handle: these are
   decorative images carrying no text, so there is nothing semantic to select
   and a class here would be a hook the base is carrying for one theme.

   Pixelated on purpose. These are fixed-size pixel art and the browser's
   default smoothing blurs them at any scale the layout imposes. */
body > footer > div {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) 0;
}
body > footer > div img {
  display: block;
  image-rendering: pixelated;
}

/* --- The hit counter -----------------------------------------------------
 * The live site gives each numeral its own lit cell, which takes a span per
 * digit. There is one element here, so the digits are separated with tracking
 * and the whole readout is the cell. Same idea: a dark CRT panel with the
 * number burning orange on it.
 *
 * Selected by structure rather than a class: it is the footer paragraph that
 * contains a <data>, and nothing else here does.
 * ------------------------------------------------------------------------- */
body > footer p:has(> data) {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--text-xs);
}

body > footer data {
  /* One cell per digit, as on the live site, but without its span per digit.
     The face is monospace, so every character advances by exactly 1ch: set the
     tracking to --cell-gap and a repeating gradient of that same pitch lands one
     lit cell under each glyph and a gap in each space. Change either value and
     the cells follow, because both come from the same two numbers. */
  --cell-gap: 0.28em;

  display: inline-block;
  margin-left: 0.4em;
  padding: var(--space-1);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius);

  /* Light stays in the palette. A near-black panel in a light footer reads as a
     hole punched in the page rather than as a display, so light keeps the cells,
     the tracking and the leading zeros but drops the CRT: it is the same counter
     in the page's own colours. --accent-warm holds 7.06:1 on --bg-alt. */
  background-color: var(--bg-alt);
  background-image: repeating-linear-gradient(
    to right,
    var(--bg) 0, var(--bg) 1ch,
    transparent 1ch, transparent calc(1ch + var(--cell-gap)));
  background-origin: content-box;
  background-clip: content-box;
  color: var(--accent-warm);

  font-family: var(--mono);
  font-weight: 700;
  font-size: var(--text-sm);
  letter-spacing: var(--cell-gap);
}

/* Dark is where a lit display belongs, so there it gets its own fixed colours,
   the ember glow and the flicker. */
:root[data-scheme="dark"] body > footer data {
  border-color: #3a1608;
  background-color: #0a0406;
  background-image: repeating-linear-gradient(
    to right,
    #1b0a10 0, #1b0a10 1ch,
    transparent 1ch, transparent calc(1ch + var(--cell-gap)));
  color: #ffb000;
  text-shadow: var(--ember);
  animation: hit-flame 1.8s steps(3, end) infinite;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-scheme="light"]) body > footer data {
    border-color: #3a1608;
    background-color: #0a0406;
    background-image: repeating-linear-gradient(
      to right,
      #1b0a10 0, #1b0a10 1ch,
      transparent 1ch, transparent calc(1ch + var(--cell-gap)));
    color: #ffb000;
    text-shadow: var(--ember);
    animation: hit-flame 1.8s steps(3, end) infinite;
  }
}

@keyframes hit-flame {
  0%, 100% {
    color: #ffb000;
    text-shadow: var(--ember);
  }
  50% {
    color: #ffd24a;
    text-shadow: var(--ember-bright);
  }
}


/* The one part of the post-body callouts the shared layer holds back, because a
   face is never the layer's to choose. Mono is the archive's own convention for
   both: an update banner is an editorial interruption and the footnotes are
   apparatus, and neither is the prose. */
.update-banner,
.footnotes {
  font-family: var(--mono);
}
  /* The widest a photo is ever RENDERED, which is not a width any rule sets:
     both grids size their tracks as fractions. Declared because `sizes` on an
     <img> needs the number and an HTML attribute cannot read a custom property,
     so without a token the figure would live only in markup and drift silently
     from the grid that produces it.
       --w-photo-strip  the homepage strip at its widest: repeat(6) inside
                        --w-page, so 72rem / 6.
       --w-photo-tile   a landscape tile in the /photos/ log, whose rows are
                        --photo-row tall, so roughly 3:2 of that. */
  --w-photo-strip: 12rem;
  --w-photo-tile: 20rem;
