/*
 * Chapbook — v1.0.0
 * https://chapbook.page
 *
 * A small CSS system for documents that want to read like documents. Monospace
 * carries structure, a second face carries language, and separation comes from
 * rules and space rather than from cards.
 *
 * It is not a framework. There is no build step, no reset beyond box-sizing,
 * no utility classes and no JavaScript requirement. It is about 500 lines and
 * you are expected to read all of them before you use it.
 *
 * WHAT YOU HAVE TO SUPPLY
 *   Nothing. The defaults below use system font stacks and a neutral palette,
 *   so this file works alone. Two upgrades are documented at the bottom:
 *   self-hosting real faces, and swapping the palette for your own.
 *
 * WHAT THIS FILE WILL NOT DO
 *   It will not style your headings, your paragraphs or your links for you
 *   beyond the primitives named here. It styles a named set of components and
 *   leaves the rest of the document alone, deliberately, so it can be dropped
 *   into an existing page without a fight.
 *
 * THE NINE RULES
 *   The system is nine rules, and the CSS is only their implementation. If you
 *   change this file, change it in a direction the rules allow:
 *
 *   01  Two faces, and the mono is the constant.
 *   02  No cards — no radius, no shadow, no fill that says "separate object".
 *   03  Two weights of rule, and they mean different things.
 *   04  The numbered rail.
 *   05  One row anatomy, everywhere.
 *   06  Tokens declared three times.
 *   07  Contrast is measured, and the measurement is written down.
 *   08  A bordered control, not another word in a row of words.
 *   09  It prints.
 *
 *   The long form of each is at https://chapbook.page — and in
 *   system.md beside this file, which is the copy meant for an agent.
 *
 * MIT. Take it, change it, do not credit me.
 */

/* ========================================================================
   TOKENS                                                          Rule 06
   ========================================================================
   Declared three times so system-default, explicit light and explicit dark
   all resolve:

     :root                        the light palette, and the fallback
     @media (prefers-color-scheme: dark) :root:not([data-theme="light"])
                                  the system preference, guarded so an
                                  explicit light choice still beats it
     :root[data-theme="dark"]     the toggle, so it wins in both directions
 
   NEVER style a component from inside one of those blocks. Redefine tokens
   only. A component rule written inside a media query exists in one theme
   and not the other, and you will not notice for weeks.

   THE EIGHT COLOUR TOKENS are a contract. Keep the names even when you
   change every value — a diff between two sites built on this system should
   show only the differences that were intended.

     --paper        page ground
     --sunk         row hover, code field. Felt rather than seen.
     --rule         hairlines. Decorative; no contrast requirement.
     --ink          body, titles, and the 2px rules
     --muted        ledes, summaries, descriptions
     --faint        mono labels, meta, addresses
     --accent       links, rail numbers, status
     --accent-deep  link hover
   ------------------------------------------------------------------------ */

:root {
  /* Neutral skin — monochrome, no accent.
     Measured against #f5f5f5, the worst-case grain pixel, not against
     --paper: the overlay is dark noise on a light ground, so it moves the
     surface toward the text and every ratio below is the honest floor.

       ink    17.32:1      muted  8.93:1      faint  5.27:1

     Ink is #111 on #fcfcfc rather than #000 on #fff. At 21:1 the pure
     pairing glares on long text; this holds 18.41:1 flat and reads calmer
     without softening the character of the type. */
  --paper: #fcfcfc;
  --sunk: #f4f4f4;
  --rule: #e5e5e5;
  --ink: #111;
  --muted: #444;
  --faint: #666;

  /* The neutral skin collapses the accent onto the ink deliberately. A
     monochrome page has no hue to spend, so a link announces itself by
     decoration rather than by colour, and hover changes the underline
     rather than the colour. A coloured skin separates the two — see
     chapbook-skins.css, where they are 5.66:1 and 8.53:1 apart. */
  --accent: #111;
  --accent-deep: #111;

  --grain: 0.028;

  /* Faces ---------------------------------------------------------------
     Rule 01. The mono is the constant across every site built on this
     system; the language face is the site's own. --display is separate so
     a site can set titles in a third face, and defaults to the language
     face so that most sites never think about it.

     These are system stacks on purpose. A system that ships pointing at
     /fonts/something.woff2 is not stealable — it is a copy of one site.
     Self-hosting is the documented upgrade, not the default. */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;
  --language: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  --display: var(--language);

  /* Layout --------------------------------------------------------------
     Reconciled across the three sites that already run this system. They
     had drifted — rail 9rem against 154px, two different gutter clamps,
     rem on one site and px on the other. These are the settled values.
     Rem everywhere; do not reintroduce px. */
  --gut: clamp(1.25rem, 5vw, 2.75rem);  /* page gutter */
  --wrap: 62rem;                        /* max line of the page */
  --rail: 10rem;                        /* the numbered rail column */
  --measure: 34em;                      /* prose, in its own em */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  color-scheme: light;
}

/*
 * Dark is not the light palette inverted. Light-on-dark at full contrast
 * glares harder than dark-on-light, so ink is #ededed on #111 rather than
 * #fff on #000.
 *
 * Measured against #161616. On a dark ground the overlay is light noise, so it
 * lifts the surface toward the text rather than away from it — the same worst
 * case as in light, arrived at from the opposite direction:
 *
 *   ink 15.46:1      muted 8.73:1      faint 5.24:1
 *
 * Close enough to the light column that neither theme reads as the
 * afterthought. The rule token is lifted a little above the ratio-matched
 * value because a hairline of equal contrast reads fainter on dark, and the
 * grain drops from 2.8% to 2% because light noise on a dark field is more
 * conspicuous than dark noise on a light one.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #111;
    --sunk: #191919;
    --rule: #2a2a2a;
    --ink: #ededed;
    --muted: #b4b4b4;
    --faint: #8a8a8a;
    --accent: #ededed;
    --accent-deep: #ededed;
    --grain: 0.02;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --paper: #111;
  --sunk: #191919;
  --rule: #2a2a2a;
  --ink: #ededed;
  --muted: #b4b4b4;
  --faint: #8a8a8a;
  --accent: #ededed;
  --accent-deep: #ededed;
  --grain: 0.02;
  color-scheme: dark;
}

/* ========================================================================
   BASE
   ======================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--language);
  font-size: 1.0625rem;
  line-height: 1.6;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

/* The system sets sizes on classes, not on elements. A base h1 rule gets
   overridden on every page that uses it and then it is not a base rule —
   it is a default nobody wants, written once and fought four times. */
h1, h2, h3, h4 { margin: 0; font-weight: 400; text-wrap: balance; }
p { margin: 0; }
a { color: inherit; }

/*
 * Grain. A tiled 200px SVG feTurbulence, desaturated in the filter because
 * feTurbulence is colour noise by default and any tint fights the palette.
 * It stops a large flat field reading as dead screen white without being
 * noticeable as texture.
 *
 * Fixed rather than scrolling, so it behaves like the surface rather than
 * like content. To see it at all, set --grain: 0.5 temporarily.
 *
 * Set --grain: 0 to remove it. Everything else still works.
 */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScyMDAnIGhlaWdodD0nMjAwJz48ZmlsdGVyIGlkPSduJz48ZmVUdXJidWxlbmNlIHR5cGU9J2ZyYWN0YWxOb2lzZScgYmFzZUZyZXF1ZW5jeT0nMC44NScgbnVtT2N0YXZlcz0nNCcgc3RpdGNoVGlsZXM9J3N0aXRjaCcvPjxmZUNvbG9yTWF0cml4IHR5cGU9J3NhdHVyYXRlJyB2YWx1ZXM9JzAnLz48L2ZpbHRlcj48cmVjdCB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsdGVyPSd1cmwoI24pJy8+PC9zdmc+");
  background-size: 200px 200px;
}

/* Everything real sits above the overlay. */
.bar,
main,
footer { position: relative; z-index: 1; }

.wrap {
  width: min(var(--wrap), calc(100% - var(--gut) * 2));
  margin-inline: auto;
}

/* ========================================================================
   UTILITIES
   ======================================================================== */

/* Visually hidden, still read aloud. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 20;
  padding: 0.7rem 1rem;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--mono);
  font-size: 0.75rem;
  text-decoration: none;
}

.skip:focus { left: 0.5rem; top: 0.5rem; }

/* ========================================================================
   TYPE                                                            Rule 01
   ========================================================================
   If a piece of text tells you WHAT KIND OF THING you are looking at, it is
   mono, small, uppercase and tracked. If it SAYS SOMETHING, it is the
   language face. That is the whole rule, and it decides every case.

   Mono:      labels, numbers, addresses, dates, nav, buttons, footer, code
   Language:  titles, ledes, body, descriptions
   ------------------------------------------------------------------------ */

/* The mono label. The most reused object in the system. */
.label {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--faint);
}

.lede {
  max-width: 36em;
  color: var(--muted);
  font-size: 1.1rem;
  text-wrap: pretty;
}

/* Mono running text — for asides, footnotes and anything that is the page
   talking about itself rather than talking to you. */
.small {
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.7;
  color: var(--faint);
}

.small a {
  color: var(--faint);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  transition: color 200ms var(--ease);
}

.small a:hover,
.small a:focus-visible { color: var(--ink); }

/* ------------------------------------------------------------------------
   Prose links. The system does not restyle a bare <a>, because it has to be
   droppable into an existing page without a fight. It styles unclassed links
   inside prose regions instead — every component in this file carries a
   class, so :not([class]) separates "a link in a sentence" from "a link that
   IS a component" without listing the components.
   ------------------------------------------------------------------------ */

.body a:not([class]),
.lede a:not([class]),
.intro a:not([class]) {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  transition: color 200ms var(--ease), text-decoration-thickness 200ms var(--ease);
}

.body a:not([class]):hover,
.body a:not([class]):focus-visible,
.lede a:not([class]):hover,
.lede a:not([class]):focus-visible,
.intro a:not([class]):hover,
.intro a:not([class]):focus-visible {
  color: var(--accent-deep);
  text-decoration-thickness: 2px;
}

/* One code treatment everywhere. The only bordered, filled object in the
   system — and it earns it, because a code field is genuinely a different
   surface rather than a card around ordinary content. */
.code {
  margin: 0;
  padding: 0.875rem 1rem;
  overflow-x: auto;
  background: var(--sunk);
  border: 1px solid var(--rule);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 0.8125rem;
  line-height: 1.65;
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* ========================================================================
   MASTHEAD                                                        Rule 03
   ========================================================================
   A 2px ink rule under the bar, matching the one that opens the first block
   and the one above the footer. Those three lines are what hold a page
   together at a glance — a 1px hairline separates peers, a 2px ink rule
   opens and closes the document. Do not add a fourth weight.
   ------------------------------------------------------------------------ */

.bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 1.4rem 0 0.75rem;
  border-bottom: 2px solid var(--ink);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.brand {
  font-weight: 500;
  text-decoration: none;
  color: var(--ink);
}

.bar-end {
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.barlinks { display: flex; gap: 1.25rem; }

.barlinks a {
  color: var(--faint);
  text-decoration: none;
  transition: color 200ms var(--ease);
}

.barlinks a:hover,
.barlinks a:focus-visible { color: var(--ink); }

/* ========================================================================
   THEME TOGGLE                                                    Rule 08
   ========================================================================
   A bordered 2rem square, not another word in a row of words — it DOES
   something where the links beside it only GO somewhere, and the border is
   what says so.

   Ship it with the `hidden` attribute set and let script reveal it, so a
   visitor without JavaScript is never offered a button that cannot work.
   chapbook-theme.js beside this file does that in 24 lines.
   ------------------------------------------------------------------------ */

.theme {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  flex: none;
  padding: 0;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--faint);
  cursor: pointer;
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}

.theme:hover { color: var(--ink); border-color: var(--ink); }

.tsvg { width: 1rem; height: 1rem; }

.tsvg .ring { stroke: currentColor; stroke-width: 1.5; }

/*
 * The filled half rotates about the CIRCLE's centre, not its own bounding
 * box, so transform-box: view-box is required. Without it the semicircle
 * spins around its own middle and wobbles out of the ring.
 */
.tsvg .half {
  fill: currentColor;
  transform-box: view-box;
  transform-origin: 12px 12px;
  transition: transform 400ms var(--ease);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tsvg .half { transform: rotate(180deg); }
}

:root[data-theme="dark"] .tsvg .half { transform: rotate(180deg); }

/*
 * Colours cross-fade only WHILE the theme is changing. The script adds
 * .theming, swaps the attribute, and removes it after 320ms. Left on
 * permanently this would also animate the row hover fill, which has to be
 * instant or the rows feel like they are lagging behind the pointer.
 */
.theming * {
  transition: background-color 300ms ease, color 300ms ease,
    border-color 300ms ease, fill 300ms ease, stroke 300ms ease !important;
}

/* ========================================================================
   HERO
   ======================================================================== */

.hero { padding: clamp(2.75rem, 8vw, 4.5rem) 0 0; }

.eyebrow {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
}

.name {
  margin: 1rem 0 0;
  font-family: var(--display);
  font-size: clamp(2.5rem, 8vw, 4rem);
  line-height: 1.02;
  letter-spacing: -0.032em;
}

.intro {
  margin-top: 1.35rem;
  max-width: 32em;
  color: var(--muted);
  font-size: 1.125rem;
  text-wrap: pretty;
}

/* The mono aside is the page telling you how to read it, so it is set in the
   structural face rather than the reading one. */
.aside {
  margin-top: 1.6rem;
  max-width: 44em;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.75;
  color: var(--faint);
}

.aside b { color: var(--ink); font-weight: 500; }

/* ========================================================================
   THE NUMBERED RAIL                                               Rule 04
   ========================================================================
   A sticky left column carrying a number and a mono section name; content on
   the right. The label stays beside its content for as long as that content
   is on screen.

   `align-self: start` on .rail is what makes the stickiness work. A grid item
   stretches to the full row height by default, and a stretched item has
   nothing to stick WITHIN — remove that line and the rail silently stops
   moving, with no error anywhere.
   ------------------------------------------------------------------------ */

.blk {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  gap: var(--gut);
  padding: clamp(2rem, 5vw, 2.75rem) 0;
  border-top: 1px solid var(--rule);
}

.blk:first-of-type {
  margin-top: clamp(2.75rem, 7vw, 4rem);
  border-top: 2px solid var(--ink);
}

.rail {
  position: sticky;
  top: 1.4rem;
  align-self: start;
}

/* Tabular so the numerals hold a common width down the page, and in the
   accent because the number is the entry's mark. Omit it entirely on a
   single-section page: a section number is a promise that another one
   follows, and a lone "01" breaks that promise. */
.rail .n {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

.rail h2 {
  margin-top: 0.3rem;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink);
}

/* min-width: 0 lets a wide child — a code block, a table — scroll inside the
   column instead of blowing the grid out sideways. */
.body { min-width: 0; }

.body > * + * { margin-top: 1.35rem; }

.body .lede { max-width: var(--measure); }

.footnote { margin-top: 1.1rem; }

/* ========================================================================
   INDEX ROWS                                                      Rule 05
   ========================================================================
   One anatomy for every list on every page: a mono status left, a title in
   the display face, a mark right, then an optional description and a mono
   line carrying the destination. The whole row is the link; the mark is
   decorative and must carry aria-hidden.

   Use ↗ for a destination that leaves the site and → for one that does not,
   so the mark keeps meaning something.
   ------------------------------------------------------------------------ */

/*
 * The rules bleed 0.75rem past the text on each side so the hover fill has
 * somewhere to sit without the row's text shifting relative to the prose
 * above it. The negative margin lives on the container and the matching
 * padding on the row — that is what keeps the container's top border and the
 * rows' bottom borders the same width.
 */
.index {
  border-top: 1px solid var(--rule);
  margin-inline: -0.75rem;
}

/* Where an index OPENS a block there is already a rule directly above it —
   the one separating the sections. A second hairline a few rem below the
   first reads as a doubled border rather than as structure, so the index
   gives way and only draws its own when prose precedes it. */
.body > .index:first-child { border-top: 0; }

.row {
  position: relative;
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0 1.5rem;
  padding: 1.35rem 0.75rem 1.4rem;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  transition: background-color 200ms var(--ease);
}

/* Rule 02: this is the only fill in the system, at about 1.07:1. Felt rather
   than seen. If you can identify its colour, it is too strong. */
.row:hover,
.row:focus-visible { background: var(--sunk); }

/*
 * The hairline redraws in ink from the left on hover and on keyboard focus.
 * It sits at bottom: -1px so it COVERS the border rather than stacking above
 * it; at bottom: 0 it lands on the padding edge and shifts the row by a pixel
 * every time the pointer crosses it.
 */
.row::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 420ms var(--ease);
}

.row:hover::after,
.row:focus-visible::after { transform: scaleX(1); }

.row-label {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--faint);
}

.row-title {
  font-family: var(--display);
  font-size: clamp(1.15rem, 3vw, 1.45rem);
  line-height: 1.2;
  letter-spacing: -0.02em;
}

/* The whole row is the link, but the title still carries the affordance —
   otherwise nothing on the row looks clickable until the pointer is on it. */
.row:hover .row-title,
.row:focus-visible .row-title {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.12em;
}

.row-desc {
  grid-column: 2 / 4;
  margin-top: 0.5rem;
  max-width: 40em;
  color: var(--muted);
  font-size: 0.97rem;
  line-height: 1.5;
  text-wrap: pretty;
}

.row-meta {
  grid-column: 2 / 4;
  margin-top: 0.55rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.02em;
  color: var(--faint);
}

/* Long addresses have to be allowed to break, or a 34-character alias
   overflows a 375px viewport. */
.row-meta.breakable {
  word-break: break-word;
  font-variant-numeric: tabular-nums;
}

.row-mark {
  color: var(--faint);
  font-size: 1.05rem;
  transition: transform 420ms var(--ease);
}

.row:hover .row-mark,
.row:focus-visible .row-mark { transform: translate(3px, -3px); }

/* ========================================================================
   FOOTER                                                          Rule 03
   ======================================================================== */

footer {
  margin-top: clamp(3rem, 8vw, 4.5rem);
  padding: 1.25rem 0 3.5rem;
  border-top: 2px solid var(--ink);
}

.foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.6rem 1.5rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--faint);
}

.foot b {
  color: var(--ink);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ========================================================================
   NARROW
   ========================================================================
   Two breakpoints, both in rem so they respond to the user's font size.
   ------------------------------------------------------------------------ */

@media (max-width: 52rem) {
  /* The rail stops being a column and becomes a line above its content. */
  .blk { grid-template-columns: 1fr; gap: 0.9rem; }
  .rail {
    position: static;
    display: flex;
    align-items: baseline;
    gap: 0.85rem;
  }
  .rail h2 { margin-top: 0; }
}

@media (max-width: 36rem) {
  .barlinks { display: none; }
  /* The status label stacks above its row rather than sitting beside it. */
  .row { grid-template-columns: minmax(0, 1fr) auto; gap: 0; }
  .row-label { grid-column: 1 / 3; margin-bottom: 0.4rem; }
  .row-desc,
  .row-meta { grid-column: 1 / 3; }
}

/* ========================================================================
   FOCUS — visible, never removed
   ======================================================================== */

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* ========================================================================
   MOTION
   ========================================================================
   The hairline still redraws; it arrives rather than travels.
   ------------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* ========================================================================
   PRINT                                                           Rule 09
   ========================================================================
   Print is a real target, not an afterthought. Ink on white, controls gone,
   structural blocks kept whole, and link destinations expanded after the
   link text — a printed page has no hover and no address bar, so a bare
   "read more" prints as a dead end.
   ------------------------------------------------------------------------ */

@media print {
  :root {
    --paper: #fff;
    --ink: #000;          /* ink on paper is not backlit; it does not glare */
    --muted: #2e2e2e;
    --faint: #545454;
    --accent: #000;
    --accent-deep: #000;
    --rule: #c9c9c9;
    --sunk: #fff;
    --grain: 0;
  }

  /* Tighter than a document meant to be read at length, because an index
     wants to stay on one page. */
  body { font-size: 10.5pt; line-height: 1.4; }

  .skip,
  .barlinks,
  .theme,
  .row-mark { display: none !important; }

  .rail { position: static; }

  .blk,
  .row { break-inside: avoid; }

  .row:hover { background: none; }

  /* Expand destinations. Scoped to content links: applied to every anchor it
     would also print the href of every nav item, which is noise. */
  .row[href^="http"]::after,
  .small a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-family: var(--mono);
    font-size: 0.85em;
    color: var(--faint);
    word-break: break-all;
  }

  /* The row's ::after is already the hover hairline, so the parts of it that
     would turn the expanded URL into a 1px black bar are reset here.

     grid-column is the one that is easy to miss: .row is a grid, so ::after
     becomes an auto-placed grid item and lands in the 6.5rem label column,
     where word-break renders a URL as five lines of four characters. Spanning
     every column puts it on its own full-width line, which is what an
     expanded address wants. */
  .row[href^="http"]::after {
    grid-column: 1 / -1;
    margin-top: 0.35rem;
    position: static;
    height: auto;
    background: none;
    transform: none;
  }

  footer { padding-bottom: 0; }
}

/* ========================================================================
   UPGRADE 1 — real faces
   ========================================================================
   The defaults are system stacks so this file works alone. To self-host,
   add @font-face rules and repoint two tokens. Nothing else changes:

     @font-face {
       font-family: "IBM Plex Mono";
       src: url("/fonts/ibm-plex-mono-400.woff2") format("woff2");
       font-weight: 400; font-display: swap;
     }
     :root {
       --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
       --language: "Newsreader", Georgia, "Times New Roman", serif;
     }

   Always keep the original stack as the fallback. Preload only the face that
   paints the largest text — a swap is very visible at 4rem and barely
   noticeable at 0.7rem.

   UPGRADE 2 — your own palette
   ----------------------------------------------------------------------
   Redefine the eight colour tokens in all three blocks. Keep the names.
   chapbook-skins.css has two worked examples, and system.md has the
   measuring procedure — every text token must clear 4.5:1 against the
   worst-case grain pixel, not against --paper.
   ======================================================================== */
