/*
 * Chapbook — v1.2.2
 * 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 utility classes and no
 * JavaScript requirement. It is about 970 lines and you are expected to read
 * all of them before you use it.
 *
 * WHAT IT RESETS
 *   Small, but not nothing, and worth knowing before you drop it into a page
 *   that already has styles:
 *
 *     box-sizing        border-box on everything
 *     body              margin, background, colour, face, size, leading
 *     h1-h4, p          margin: 0 — the system spaces from the container
 *     h1-h4             font-weight: 400, text-wrap: balance
 *     a                 color: inherit
 *     html              scroll-behavior: smooth (off under reduced motion)
 *     code, kbd, samp   the mono face, so Rule 01 is true by default
 *     hr                one 1px --rule hairline, so Rule 03 is true by default
 *
 *   Everything else in the document is left alone.
 *
 * 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; }

/* The browser's default selection blue was the only colour on the page the
   palette did not choose. Two declarations, both from tokens, so it inverts
   with the theme and with every skin. */
::selection {
  background: var(--ink);
  color: var(--paper);
}

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;
  -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; }

/* Rule 01 is FALSE BY DEFAULT without this. `monospace` is a keyword, not a
   face: left alone, an inline <code> renders in whatever the browser nominates
   and at whatever size it nominates it — a second monospace, on a page whose
   first rule is that the mono is the constant. Three declarations, and the
   rule becomes true instead of aspirational.

   0.9em rather than a rem: code sits inside running text and should track the
   size of the sentence it interrupts, not the root. */
code, kbd, samp {
  font-family: var(--mono);
  font-size: 0.9em;
}

/* The 0.9em step is for code inside a SENTENCE, where mono runs large beside
   the language face. <pre><code> is the ordinary markup for a code field, and
   there the step would apply a second time and set the block at 0.73rem. */
pre code { font-size: inherit; }

/* Rule 03 is FALSE BY DEFAULT without this. The UA draws <hr> as an inset
   border — two tones of a grey the palette never chose, at a width nobody
   wrote. That is a third rule weight arriving in a system whose whole claim is
   that it has two. One hairline, in the token, no margin: spacing belongs to
   the container, as it does for every other block here. */
hr {
  height: 0;
  margin: 0;
  border: 0;
  border-top: 1px solid var(--rule);
}

/*
 * 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);
}

/*
 * A margin note. Admitted ahead of the two-build rule, and the argument is
 * that almost none of it is new: the rail is already a sticky column beside
 * the content, and Rule 04 already carries mono metadata in it. This is the
 * same column carrying prose. The grid, the stickiness, the align-self that
 * makes it work and the collapse below 52rem are all built.
 *
 * Mono, because a margin note tells you what KIND of thing you are looking at
 * — a gloss on the text rather than part of it — which is Rule 01's test.
 * Faint, because a note that competes with the body is not a note.
 */
.rail .note {
  margin-top: 0.9rem;
  font-family: var(--mono);
  font-size: 0.66rem;
  line-height: 1.7;
  color: var(--faint);
}

/* The note's own lead-in, so a note can name what it is glossing without
   needing a heading level nobody asked for. */
.rail .note b {
  display: block;
  color: var(--ink);
  font-weight: 500;
}

/* 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; }

/*
 * The rhythm rule sets the TOP margin only, so anything the UA also gives a
 * bottom margin to used to close at 1em + 1.35rem and open at 1.35rem — a list
 * sat 38.6px below its neighbour and 21.6px above it, and blockquote and
 * figure carried a 40px indent nobody wrote, in px, in a system whose token
 * table says rem everywhere. Zero the UA's contribution first and let the one
 * rule below be the only thing that spaces this column.
 *
 * :where() IS LOAD-BEARING. `:is()` carries the specificity of its most
 * specific argument, so `.body > :is(p, ul, ...)` scores (0,1,1) and beats
 * `.body > * + *` at (0,1,0) — the reset wins and every gap in the column
 * collapses to zero. `:where()` contributes nothing, so this stays at (0,1,0),
 * ties with the rhythm rule below it, and loses on source order, which is
 * exactly what a reset should do.
 */
.body > :where(p, ul, ol, dl, blockquote, figure, pre, hr, table) { margin: 0; }

/* The UA indents a list by 40px — a px value in a system whose token table
   says rem everywhere, and one that does not move with the reader's font
   size. Same specificity trick, same reason. */
.body > :where(ul, ol) { padding-left: 1.4rem; }

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

/* `p { margin: 0 }` is global but the rhythm rule only reaches direct
   children, so two paragraphs inside a quote or a list item ran together at a
   measured 0px gap. Smaller than the column rhythm, because these are
   paragraphs inside one object rather than objects in a column. */
.body :is(blockquote, li, figure) > p + p { margin-top: 0.9rem; }

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

.footnote { margin-top: 1.1rem; }

/*
 * A sub-heading inside the column.
 *
 * An h3 arrived at 19.89px against 17px body text, at the same weight — a
 * difference no reader sees, which makes it a heading in the markup and not
 * on the page. Mono and tracked rather than larger: it names the part of the
 * document you are in, which is Rule 01's test for structure, and it matches
 * the rail's own h2 so the two read as the same kind of object.
 *
 * One treatment, not a scale. A scale is the beginning of a framework.
 */
.body h3 {
  margin-top: 1.9rem;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink);
}

/* The paragraph after a sub-heading belongs to it, so it closes up. */
.body h3 + * { margin-top: 0.6rem; }

/*
 * Lists. The marker is faint because it is punctuation rather than content —
 * it says "this is an item", which no reader needs told twice.
 */
.body :is(ul, ol) > li + li { margin-top: 0.4rem; }
.body :is(ul, ol) > li::marker { color: var(--faint); }

/*
 * A quotation is opened by the document weight, which is the same 2px in ink
 * that opens the page under the masthead. No fill and no italic: the rule and
 * the indent are the whole treatment.
 */
.body blockquote {
  padding-left: 1.15rem;
  border-left: 2px solid var(--ink);
  color: var(--ink);
}

/* A citation is metadata about the quote rather than part of it. */
.body blockquote cite {
  display: block;
  margin-top: 0.6rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  font-style: normal;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
}

/* ========================================================================
   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); }

/* ========================================================================
   TABLES                                                          Rule 02
   ========================================================================
   Hairlines and one document rule, and nothing else. No zebra fill: the
   system has exactly one fill and it is the row hover, so a striped table
   would be the second — and a second fill is how a system starts drawing
   cards without noticing.

   Mono throughout, because a table is structure by definition. Every cell
   tells you what kind of thing it holds; none of them is saying something.
   ------------------------------------------------------------------------ */

/* Wide content scrolls inside its own column and never takes the page with
   it. This is the wrapper's whole job. */
.tablewrap {
  margin-top: 1.1rem;
  overflow-x: auto;
}

.tbl {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.7rem;
  line-height: 1.6;
  white-space: nowrap;
}

.tbl th,
.tbl td {
  padding: 0.55rem 1rem 0.55rem 0;
  text-align: left;
  vertical-align: baseline;
  border-bottom: 1px solid var(--rule);
}

/* The head rule is the document weight: it opens the table the way the 2px
   under the masthead opens the page. */
.tbl thead th {
  border-bottom: 2px solid var(--ink);
  color: var(--ink);
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}

.tbl tbody th {
  color: var(--ink);
  font-weight: 400;
}

.tbl td { color: var(--faint); }

/* Digits in a column want a common width or the column is not a column. */
.tbl .num { font-variant-numeric: tabular-nums; }

/* The one cell that may hold prose is the one cell allowed to wrap. */
.tbl .prose {
  white-space: normal;
  min-width: 12rem;
  color: var(--muted);
}

.tbl code { font-family: inherit; color: var(--ink); }

/* ========================================================================
   THE TEXT CONTROL                                                Rule 08
   ========================================================================
   Rule 08 shipped half-built: .theme is the icon form and has been in the
   system since v1.0.0, while the text form lived in the specimen's own
   stylesheet as .skinbtn and was therefore not part of anything anyone could
   take. This is that form, promoted and renamed.

   A border, never a fill. The pressed state deepens the border to ink rather
   than filling the box, because a filled button on this page would be the
   first card in a system whose second rule is that there are none.
   ------------------------------------------------------------------------ */

.btns {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.btn {
  padding: 0.42rem 0.85rem;
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--faint);
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}

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

/* Pressed is a border in ink. aria-pressed rather than a class, so the
   appearance cannot drift from what a screen reader is told. */
.btn[aria-pressed="true"] {
  color: var(--ink);
  border-color: var(--ink);
}

/* ========================================================================
   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.
   ------------------------------------------------------------------------ */

/*
 * The rail collapses on its own width, not the window's.
 *
 * The media query below measures the viewport, so a .wrap dropped into a 24rem
 * sidebar on a wide screen kept a 10rem rail beside 14rem of content and the
 * text stopped being readable. The container query measures the column the
 * block is actually in, which is the thing that was always meant.
 *
 * BOTH are kept, and that is deliberate rather than lazy. The media query is
 * the floor for browsers without container queries, and the two agree at the
 * same 52rem, so no browser gets a different layout — only a less precise
 * reason for it. Where both apply, the container query is later and wins.
 */
.wrap { container-type: inline-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;
    flex-wrap: wrap;
    gap: 0.85rem;
  }
  .rail h2 { margin-top: 0; }
  /* A note is prose and does not belong on the metadata line. */
  .rail .note { flex-basis: 100%; margin-top: 0.5rem; }
}

@container (max-width: 52rem) {
  .blk { grid-template-columns: 1fr; gap: 0.9rem; }
  .rail {
    position: static;
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.85rem;
  }
  .rail h2 { margin-top: 0; }
  .rail .note { flex-basis: 100%; margin-top: 0.5rem; }
}

@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;
  }
}

/* ========================================================================
   FORCED COLOURS AND HIGH CONTRAST
   ========================================================================
   The gap that sat least comfortably beside Rule 07. A system that fails its
   own build over 4.5:1 had nothing at all to say about Windows High Contrast,
   where the user has replaced the palette outright and every token in the
   table above stops applying.

   Three things this system does are exactly what forced colours disturb: a
   noise overlay the user did not ask for, a hairline drawn as a background on
   a pseudo-element rather than as a border, and 1px edges that a forced
   palette may not repaint. Each is addressed rather than left to luck.
   ------------------------------------------------------------------------ */

@media (forced-colors: active) {
  /*
   * The grain is decoration over content in a mode that exists to remove
   * decoration. It is also a background image, which forced colours does not
   * repaint, so it survives as literal noise over text that has been
   * recoloured underneath it. Off entirely.
   */
  body::before { display: none; }

  /*
   * The row's hover hairline is a background on ::after, and a background is
   * not a border — forced colours leaves it whatever it was and it can vanish
   * against the forced canvas. Repainted in the system's own text colour so
   * the affordance survives.
   */
  .row::after { background: CanvasText; }

  /* Edges the reader navigates by, stated in system colours so the forced
     palette can paint them rather than guess. */
  .btn,
  .theme {
    border-color: ButtonBorder;
    color: ButtonText;
  }

  .btn:hover,
  .btn[aria-pressed="true"],
  .theme:hover { border-color: Highlight; color: Highlight; }

  /* The theme control is a drawn mark, so it takes the forced text colour
     rather than a token that no longer applies. */
  .tsvg .ring { stroke: CanvasText; }
  .tsvg .half { fill: CanvasText; }

  /* Focus must survive a palette the system did not choose. */
  :focus-visible { outline-color: Highlight; }
}

/*
 * prefers-contrast: more is the user asking for edges they can see, not for a
 * different design. The grain comes off because noise is the opposite of the
 * request, and the tokens that were tuned for calm move to the ends of their
 * range.
 */
@media (prefers-contrast: more) {
  :root {
    --grain: 0;
    --muted: var(--ink);
    --rule: var(--faint);
  }
}

/* ========================================================================
   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.
   ------------------------------------------------------------------------ */

/*
 * The sheet itself.
 *
 * Outside @media print on purpose: @page only ever applies to paged media, so
 * wrapping it in a media query says nothing extra and buries it inside a block
 * that is already about colour.
 *
 * 18mm is the one measurement on the page that nobody was making. Left to the
 * browser's print dialogue it is a per-installation default, which means the
 * same document printed in two offices is set two different ways — the one
 * outcome a system like this exists to prevent. The prose components carry
 * their own measure in `em` on top of this, so the page margin is a decision
 * about the edge of the sheet rather than about line length.
 */
@page {
  margin: 18mm;
}

@media print {
  /*
   * EVERY DECLARATION HERE IS !important, AND THAT IS LOAD-BEARING.
   *
   * A media query does not change specificity. This block is `:root`, which
   * scores (0,1,0), and it loses to `:root[data-theme="dark"]` and to every
   * skin selector at (0,2,0) — so until v1.0.1 the print palette applied only
   * to the neutral skin, and only for a reader who had never touched the
   * toggle. Anyone who chose dark and pressed print got #ededed body text on
   * white paper, at about 1.1:1. An effectively blank page. The specimen was
   * affected too: its skin picker sets data-skin on the root.
   *
   * Raising the selector to (0,3,0) would work today and break again the first
   * time a skin adds a third attribute. `!important` is the one mechanism
   * specificity cannot outrank, which makes it the correct tool rather than a
   * shortcut — this block has to beat every palette that will ever be written
   * against this system, including palettes that do not exist yet.
   *
   * build.js asserts it. See Rule 09 in src/rules.js.
   */
  :root {
    --paper: #fff !important;
    --ink: #000 !important;   /* ink on paper is not backlit; it does not glare */
    --muted: #2e2e2e !important;
    --faint: #545454 !important;
    --accent: #000 !important;
    --accent-deep: #000 !important;
    --rule: #c9c9c9 !important;
    --sunk: #fff !important;
    --grain: 0 !important;

    /* The dark blocks set this, and a dark color-scheme surviving into print
       lets the browser paint its own dark canvas under an ink-on-white page. */
    color-scheme: light !important;
  }

  /* 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; }

  /*
   * Orphans and widows: a line stranded at the foot of a sheet, or alone at
   * the head of the next. Print's oldest fault, and CSS has carried the
   * properties for two decades. Three is the conventional floor — two admits
   * a stranded pair that still reads as broken, and four starts pushing whole
   * paragraphs across for the sake of one line.
   *
   * These are the paged-media equivalent of the break-inside rules below: the
   * page break is the one piece of layout the stylesheet cannot see, so every
   * instruction about it has to be given in advance.
   */
  p,
  li,
  dd,
  blockquote { orphans: 3; widows: 3; }

  /* A heading at the foot of a sheet with its section overleaf is a heading
     that has stopped doing its job. `avoid` is a request rather than a
     guarantee — the browser honours it where the next block will fit. */
  h1,
  h2,
  h3,
  h4 { break-after: avoid; }

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

  .rail { position: static; }

  .blk,
  .row,
  .tbl tr,
  .rail .note { break-inside: avoid; }

  /* The rail is static in print, so a note sits above its section rather than
     beside it. It is content and it prints — the controls above do not. */
  .rail .note { margin-top: 0.5rem; }

  /* A table that scrolls on screen has nowhere to scroll on paper, so it
     wraps instead of being clipped at the page edge. */
  .tablewrap { overflow-x: visible !important; }
  .tbl { white-space: normal; font-size: 8pt; }

  .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.

   NEVER MARK A TOKEN !important IN A SKIN. The print block is the one place
   in this file that uses it, and it uses it so that ink-on-white beats every
   palette on the way to paper. A skin that answers with !important of its own
   wins that fight and prints itself, which is Rule 09 lost. build.js checks
   this and fails the build.
   ======================================================================== */
