/* Generated. Edit the generator, not this file. */

/* The design system. Every colour, size, space and measure is declared once
   here and referred to everywhere else, so that there is one place to change
   any of them and no second copy to disagree with.

   Two things are deliberately literal rather than tokens:

   - The breakpoint. A media query cannot read a custom property, so the value
     appears in the query itself. It is 56rem, and it appears nowhere else.
   - The colours below are hex. The tests compute contrast ratios from these
     values, which needs a form arithmetic can be done on. */

:root {
  /* One presentation, declared so native controls, form fields and scrollbars
     are drawn to match it. The filters are real inputs, so this is not
     cosmetic. There is no dark presentation and no switch: two palettes meant
     only one of them was ever actually judged. */
  color-scheme: light;

  /* --- Colour -----------------------------------------------------------

     Sampled from the photograph on the about page -- lying on the grass in the
     sun with a mandarin on his chest. Six colours measured off the image, each
     given one job, replacing a teal and a violet carried over from the
     previous site's component library that came from nobody.

     Light rather than dark was settled by measurement. The grass is the only
     one of the six that survives a light ground untouched, at 5.26:1, and it
     is deepened a little here for margin. The mandarin is 1.89:1 on the page,
     so it cannot be text, a border or a rule at any legible contrast -- see
     --accent-fill. Three colourways were built and compared by eye; the
     rejected two are kept in docs/design/colourways.html.

     Four of the six are here. The glasses and the shirt were sampled and
     declared as a pale surface and a link colour, and in the end no rule
     wanted either: the page has one ground, the set-apart blocks are set
     apart by a rule down their edge rather than by a panel behind them, and a
     link is the ink with the accent beneath it. They were carried as tokens
     nothing referred to, which is the thing this block exists to prevent, so
     they are gone. Their values are in docs/design/colourways.html if a rule
     ever wants them. */
  --ground: #FBF9F4;      /* the sunlit highlights */
  --ink: #2B2F26;         /* the cap */
  --ink-quiet: #5B6150;   /* olive grey */
  --accent: #54690D;      /* the grass: state, marks, focus */
  --rule: #DFD9C2;        /* the grass, pale: hairlines */

  /* The mandarin, exactly as sampled. At 1.89:1 against the page it fails the
     4.5:1 text floor and the 3:1 non-text floor alike, so it can be neither
     text nor a hairline. Darkened until it reads as text it becomes an ochre
     that is no longer the mandarin. The one way to keep the colour that is
     actually in the photograph is as a filled area with --ink on it, which is
     6.87:1.

     Named for what it can do. A rule setting `color: var(--accent-fill)` would
     produce text almost nobody can read and would look deliberate, so the name
     warns and a test enforces. */
  --accent-fill: #FAA62E;

  /* --- Type -------------------------------------------------------------

     Newsreader carries everything readable, display and body alike: it is a
     variable face with an optical-size axis, so a heading gets a display cut
     and prose gets a text cut without a second family arriving. Its italics
     are real, which the Swedish fiction uses.

     DM Mono carries the margin and code. JetBrains Mono was the previous
     site's face and is the most generic available signal for "this person
     writes software"; DM Mono is lighter, has more character at small sizes,
     and keeps the tabular figures the margin needs for dates and counts. */
  --face-text: Newsreader, Charter, Georgia, serif;
  --face-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Roughly 1.125 steps through the reading range, with larger jumps for
     display sizes. In rem throughout, so every size grows with a reader's own
     text size rather than overriding it. */
  --text-xs: 0.875rem;
  --text-sm: 1rem;
  --text-base: 1.1875rem;
  --text-lg: 1.3125rem;
  --text-xl: 1.4375rem;
  --text-display: 2.75rem;

  /* Loosened once for a 17px body and left behind when that became 19px:
     leading is a ratio, so the same number is more space at a larger size. */
  --leading-prose: 1.58;
  --leading-tight: 1.18;

  /* The optical-size axis, set per role rather than left to the browser's
     guess. A display size asking for a text cut looks weak; the reverse looks
     coarse at reading size. */
  --optical-text: 16;
  --optical-display: 36;

  /* --- Space ------------------------------------------------------------ */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4.5rem;

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

     One reading column with one gutter beside it, used by every page. The
     gutter carries information -- a date, a caption, a footnote, a count --
     and never decoration, which is why it falls inline below the breakpoint
     rather than being hidden.

     The measure is about 66 characters at the body size, which is where a
     serif wants to sit. */
  --measure: 40rem;

  /* Running prose is set narrower. 40rem is about 76 characters at the body
     size, which suits an entry's two-line description beside its margin and
     is too wide for paragraphs: a serif at that width is a long trip back to
     the start of the next line. This is about 64.

     Exactly two measures, and no more. A third would mean the number is being
     tuned per page rather than decided.

     Widened with the body size: a measure is a count of characters, not a
     length, so leaving it fixed while the type grows shortens every line. */
  --measure-prose: 38rem;

  --gutter: 6rem;
  --gutter-gap: 2rem;

  /* The tracks, as one token. Gutter, column, gutter -- the second gutter is
     empty and exists so the reading column is centred in the viewport rather
     than sitting a gutter's width right of centre. With only a left gutter the
     grid centres the pair, which puts the content visibly off-centre on every
     page that has nothing in the margin.

     One place the layout changes, including below the breakpoint where it
     becomes a single column. */
  --tracks: var(--gutter) minmax(0, var(--measure)) var(--gutter);

  /* Image sizes, as tokens for the same reason the measure is one: a width
     stated in a rule is a width nobody finds again. Both are deliberately
     modest -- a full-measure photograph dominates the entry it belongs to. */
  --portrait: 15rem;

  /* An entry's own margin column. Narrower than the page's gutter: it holds a
     date and a reading time, not a year. */
  --entry-aside: 5rem;

  /* A project's is wider, because what it holds is a picture rather than a
     date. Still well short of the reading column: the entry is the words. */
  --project-aside: 10rem;

  /* The hero's lede, kept shorter than the column it is set in. */
  --lede: 28rem;

  /* What the header takes from the viewport. Its padding twice, a line of
     --text-sm at the prose leading, and the hairline under it:
     1.5 + 1.5 + 1.58 + 0.06. Rounded up rather than down, since the cost of
     overestimating is a little more air under the way down and the cost of
     underestimating is the way down falling off the screen. */
  --header-height: 4.75rem;

  /* Icons are sized in em so they scale with the text they sit beside rather
     than needing a size per context. */
  --icon: 1.05em;

  /* An icon standing on its own, with no label beside it, and the target it
     sits in. 2.75rem clears the 44px a finger needs. */
  --icon-lg: 1.25rem;
  --target: 2.75rem;

  /* --- Motion ----------------------------------------------------------- */
  --motion-quick: 180ms;
  --motion-settle: 420ms;
  --ease: cubic-bezier(0.2, 0, 0.1, 1);
}

/* Cross-document view transitions: two lines, no library. A browser without
   support skips the animation and navigates instantly, so there is nothing to
   fall back to. */
@view-transition { navigation: auto; }

/* --- The page ------------------------------------------------------------ */

html {
  background: var(--ground);
  /* The way down is an ordinary in-page link, so the browser does the
     scrolling. Two lines and no script. Switched off under reduced motion
     with everything else. */
  scroll-behavior: smooth;
}

/* A column the height of the viewport, so a short page -- the home page above
   all -- puts its footer at the foot of the window rather than partway up it,
   with the ground filling the space between. Not a fixed footer: `main` takes
   the slack, so on a long page the footer simply follows the content. */
body {
  margin: 0;
  display: flex;
  flex-direction: column;
  min-block-size: 100dvh;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--face-text);
  font-size: var(--text-base);
  font-variation-settings: "opsz" var(--optical-text);
  line-height: var(--leading-prose);
  /* Ligatures and proper figures in running text. The previous site left both
     to the browser, which on a face like this leaves typography on the table
     that costs nothing to ask for. */
  font-kerning: normal;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

/* The structural grid. Everything that holds content is laid out on it, so the
   index and a publication share one alignment rather than each having a
   layout of its own. */
.spread,
.row {
  display: grid;
  grid-template-columns: var(--tracks);
  column-gap: var(--gutter-gap);
  justify-content: center;
}

.spread { padding-inline: var(--space-5); }

.spread > *,
.row > * {
  grid-column: 2;
  min-width: 0;
}

/* A list spans the whole grid and its items re-create the tracks, so an
   entry's own gutter lines up with the page's rather than being nested inside
   the reading column. */
.spread > .full {
  grid-column: 1 / -1;
}

/* The gutter's occupants. Set quietly and ranged right, so they read as
   marginal notes against the column rather than as a second column. */
.aside {
  grid-column: 1;
  text-align: right;
  color: var(--ink-quiet);
  font-size: var(--text-sm);
  line-height: var(--leading-prose);
}

main.spread {
  /* Takes up whatever the viewport has left over, which is what holds the
     footer down. align-content keeps a short page's content at the top of
     that space rather than stretched through it. */
  flex: 1;
  align-content: start;
  padding-block: var(--space-7) var(--space-8);
  row-gap: var(--space-5);
}

/* --- Type in use -------------------------------------------------------- */

h1, h2, h3, h4 {
  font-variation-settings: "opsz" var(--optical-display);
  line-height: var(--leading-tight);
  text-wrap: balance;
  font-weight: 600;
  margin: 0;
}

/* A page's own name, at the top of the scale. It used to sit a step below,
   with --text-display reserved for the home page, so the largest size in the
   scale was never used on anything a reader came to read. */
h1 {
  font-size: var(--text-display);
  letter-spacing: -0.022em;
}

h2 { font-size: var(--text-xl); }
h3 { font-size: var(--text-lg); }

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

/* A paragraph's last line should not be a single word, and a heading should
   not break badly. Both are one declaration and both are what the typeset
   document does without being asked. */
.copy p, .description {
  text-wrap: pretty;
}

a {
  color: inherit;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 0.06em;
  text-underline-offset: 0.16em;
}

a:hover {
  color: var(--accent);
}

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

/* --- Long-form prose ----------------------------------------------------- */

/* A publication's rendering. Everything here is for running text and applies
   to nothing else on the site. */
.content {
  max-inline-size: var(--measure-prose);
}

/* Justified, as the typeset document is -- the two outputs are the same work
   in two media, and this is where that shows.

   Guarded on hyphenation being available, because justification without it
   produces the rivers of white space hyphenation exists to prevent. Where it
   is missing the prose stays ragged-right, which is what it is today and is
   no loss. The browser takes the language from the nearest `lang`, which is
   why the rendering declares its own. */
@supports (hyphens: auto) {
  .content {
    text-align: justify;
    hyphens: auto;
    /* A hyphenated word broken across a column is harder to read than a
       slightly looser line, so the engine is asked to limit how often it
       happens and never to leave a two-letter fragment. */
    hyphenate-limit-chars: 6 3 3;
  }
}

/* How a paragraph break is marked, from the convention the publication
   declares. One or the other, never both: an indent and a blank line are two
   answers to the same question, and a text giving both reads as though its
   author could not choose.

   LaTeXML wraps each paragraph in .ltx_para and emits .ltx_p inside it, so
   both carry the rule -- the margin would otherwise come from the wrapper and
   survive whatever is done to the paragraph. */
.content .ltx_para,
.content .ltx_p {
  margin-block: 0;
}

.prose-indented .ltx_p {
  text-indent: 1.5em;
}

/* The paragraph opening the work is not indented, and neither is the first
   after a heading or a figure: an indent marks a break from the paragraph
   before, and these have none. The typeset document does this itself, and the
   stylesheet has to be told. */
.prose-indented .ltx_para:first-child .ltx_p:first-child,
.prose-indented .ltx_section > .ltx_para:first-of-type .ltx_p:first-child,
.prose-indented .ltx_title + .ltx_para .ltx_p,
.prose-indented .ltx_figure + .ltx_para .ltx_p {
  text-indent: 0;
}

/* The other convention: a line of space between paragraphs and no indent. */
.prose-separated .ltx_para + .ltx_para {
  margin-block-start: var(--leading-prose, 1.6em);
}

.prose-separated .ltx_p {
  text-indent: 0;
}

/* --- A publication's header -----------------------------------------------

   The facts a reader wants before starting, each with its own mark so the row
   scans rather than reads, and a rule separating the apparatus from the work.
*/

.pub-header {
  padding-block-end: var(--space-5);
  margin-block-end: var(--space-7);
  border-bottom: 1px solid var(--rule);
}

.pub-header h1 {
  margin-block-end: var(--space-4);
}

.facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-quiet);
}

.fact {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* The row is single-line labels, so the text box hugs its glyphs. With the
     prose leading the box is half again as tall as the letters and centring
     the boxes leaves every icon sitting visibly high. */
  line-height: 1;
}

/* Tags at one edge, the typeset document at the other. */
.pub-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.pub-actions .tags {
  margin: 0;
}

/* An offer of the same work in another form, so it reads as a sentence rather
   than as a button. The icon carries the sense of downloading and the words
   carry what is being downloaded.

   The same size as the tags it sits beside: the two are one row, and a larger
   one reads as the more important of them. */
.offer {
  margin-inline-start: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  color: var(--ink-quiet);
  text-decoration: none;
  transition: color var(--motion-quick) var(--ease);
}

.offer:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

/* --- Headings within the work ---------------------------------------------

   The global rule sets every heading's margin to zero, which is right for a
   card's title and wrong inside a work: a section heading with no space above
   it and none below reads as a line of the paragraph before it. */

.content .ltx_section,
.content .ltx_subsection,
.content .ltx_subsubsection {
  margin-block-start: var(--space-7);
}

.content .ltx_title_section {
  font-size: var(--text-xl);
  margin-block-end: var(--space-4);
}

.content .ltx_title_subsection {
  font-size: var(--text-lg);
  margin-block-end: var(--space-3);
}

.content .ltx_title_subsubsection {
  font-size: var(--text-base);
  margin-block-end: var(--space-3);
}

/* The number a heading carries, set apart from its words. */
.content .ltx_tag_section,
.content .ltx_tag_subsection,
.content .ltx_tag_subsubsection {
  color: var(--ink-quiet);
  margin-inline-end: var(--space-3);
}

/* --- Mathematics ---------------------------------------------------------

   LaTeXML emits MathML, which browsers lay out natively, so there is nothing
   to render and only spacing to get right. */

/* An expression inside a sentence must not push its line apart from the lines
   around it. A tall fraction or a subscripted sum otherwise sets the line
   height of the one paragraph that contains it. */
.content math[display="inline"],
.content .ltx_Math {
  line-height: 1;
  vertical-align: baseline;
}

/* An expression on its own is a table of cells: the expression in the middle,
   an empty pad either side, and the number in the last when there is one.
   The pads are what centre it, and they only do so if they are told to take
   the space -- LaTeXML's own stylesheet sets them, and the pipeline runs with
   --nodefaultresources, so this has to.

   Left as a table. Setting it to display:block to contain a long expression
   threw the cell structure away, and the centring with it. */
.content .ltx_equation.ltx_eqn_table {
  inline-size: 100%;
  max-inline-size: 100%;
  margin-block: var(--space-5);
  border-collapse: collapse;
}

.content .ltx_eqn_cell.ltx_eqn_center_padleft,
.content .ltx_eqn_cell.ltx_eqn_center_padright {
  inline-size: 50%;
}

.content .ltx_eqn_cell.ltx_align_center {
  text-align: center;
}

/* The number sits against the right edge of the reading column, set quietly
   so it reads as a label on the expression rather than part of it. An
   expression the source does not number has no such cell at all. */
.content .ltx_eqn_eqno {
  text-align: right;
  vertical-align: middle;
  color: var(--ink-quiet);
  font-size: var(--text-sm);
  white-space: nowrap;
}

/* --- Stated results ------------------------------------------------------

   A definition, a theorem and a proof are set apart from the prose and from
   each other. The rule in the margin marks the claim; the proof is marked by
   its heading and its end, which is the convention a reader of mathematics
   already knows. */

.content .ltx_theorem {
  margin-block: var(--space-6);
  padding-inline-start: var(--space-5);
  border-inline-start: 2px solid var(--rule);
}

.content .ltx_theorem .ltx_title_theorem {
  font-size: var(--text-base);
  font-variation-settings: "opsz" var(--optical-text);
  margin-block-end: var(--space-2);
}

/* The label and number -- "Definition 1" -- in the accent, so the kind of
   result is legible at a glance down the column. */
.content .ltx_tag_theorem {
  color: var(--accent);
}

/* A theorem is conventionally set in italic and a definition is not; LaTeXML
   carries that from amsthm, so nothing here overrides it. */
.content .ltx_proof {
  margin-block: var(--space-6);
}

.content .ltx_proof .ltx_title_proof {
  font-size: var(--text-base);
  font-variation-settings: "opsz" var(--optical-text);
  font-style: italic;
  margin-block-end: var(--space-2);
}

/* The end of the argument. LaTeXML emits the symbol itself -- generating a
   second one here is how a proof came to end with two squares -- and the
   post-processing wraps it so it can go where a printed proof puts it: flush
   with the right edge of the measure, on the last line if there is room. */
.content .qed {
  float: right;
  margin-inline-start: var(--space-3);
  color: var(--ink-quiet);
}

/* The float has to be contained, or it escapes into whatever follows. */
.content .ltx_proof::after {
  content: "";
  display: block;
  clear: both;
}

/* --- References ---------------------------------------------------------- */

.content .ltx_bibliography {
  margin-block-start: var(--space-8);
  padding-block-start: var(--space-5);
  border-top: 1px solid var(--rule);
  /* The apparatus is not the work: set smaller and ragged, since a
     bibliography entry is not running prose and justifying one produces gaps
     a reader reads as meaning. */
  font-size: var(--text-sm);
  text-align: left;
}

.content .ltx_biblist {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Each entry hangs from its number, so the numbers line up down the margin
   and a long entry's second line does not start under its own label. */
.content .ltx_bibitem {
  margin-block-end: var(--space-4);
  padding-inline-start: var(--space-6);
  text-indent: calc(-1 * var(--space-6));
}

.content .ltx_tag_bibitem {
  color: var(--ink-quiet);
  margin-inline-end: var(--space-2);
}

/* --- Figures, captions and footnotes --------------------------------------

   The grid's left gutter carries information about the column beside it, and
   this is its first real use. */

.content figure,
.content .ltx_figure,
.content .ltx_float {
  margin-block: var(--space-6);
  margin-inline: 0;
}

.content .ltx_figure img,
.content .ltx_float img,
.content .ltx_picture {
  display: block;
  max-inline-size: 100%;
  block-size: auto;
  margin-inline: auto;
}

/* A caption is information about the figure, not another paragraph of the
   work, so it is set apart rather than reading on from the prose. */
/* Centred under its figure, which is where the typeset document puts it: the
   figure environment is set with \centering and the caption goes with it.
   A caption left-ranged under a centred image reads as detached from it. */
.content .ltx_caption {
  margin-block-start: var(--space-3);
  color: var(--ink-quiet);
  font-size: var(--text-sm);
  line-height: var(--leading-prose);
  text-align: center;
  text-wrap: pretty;
  text-indent: 0;
}

.content .ltx_tag_figure {
  color: var(--ink);
}

/* The footnote, lifted out of the sentence it interrupted and placed after
   its paragraph. The content is relatively positioned so the note can be
   taken into the gutter beside the text it belongs to. */
.content {
  position: relative;
}

.footnote {
  color: var(--ink-quiet);
  font-size: var(--text-sm);
  line-height: var(--leading-prose);
  text-align: left;
  text-indent: 0;
  margin-block: var(--space-4);
}

.footnote-text {
  display: inline;
}

/* The marker and the way back, both quiet: a footnote is an aside, and a
   number set in the accent down the column reads as emphasis it has not
   earned. */
.footnote-mark,
.footnote-back {
  color: var(--ink-quiet);
  text-decoration: none;
}

.footnote-mark:hover,
.footnote-back:hover,
.footnote-mark:focus-visible,
.footnote-back:focus-visible {
  color: var(--accent);
}

.footnote-back {
  margin-inline-end: var(--space-2);
}

/* The target a reader has just jumped to is marked for a moment, so landing
   on a note or returning to a marker does not leave them hunting for where
   they arrived. */
.footnote:target,
.footnote-mark:target {
  color: var(--ink);
}

/* At the reading width the note goes into the gutter, beside the paragraph it
   belongs to rather than beneath it. Taken out of flow so it does not push the
   prose apart; its vertical position is where it sat in the text. */
@media (min-width: 56.01rem) {
  .footnote {
    position: absolute;
    inset-inline-start: calc(-1 * (var(--gutter) + var(--gutter-gap)));
    inline-size: var(--gutter);
    margin-block: 0;
    text-align: right;
  }

  .footnote-back {
    float: none;
  }
}

/* --- The chrome --------------------------------------------------------- */

.skip {
  position: absolute;
  left: -9999px;
}

.skip:focus {
  position: static;
  display: inline-block;
  margin: var(--space-3) var(--space-5);
}

header.site {
  padding-block: var(--space-5);
  border-bottom: 1px solid var(--rule);
}

/* The wordmark and the navigation share one line in the reading column, so the
   chrome sits on the same measure as the prose beneath it. */
/* Spans all three tracks, so the wordmark sits at one edge of the page and
   the navigation at the other, with the reading column centred between them
   rather than lining up with the wordmark. The chrome frames the content; it
   does not share its measure. */
.site-bar {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-5);
}

footer.site > * {
  grid-column: 1 / -1;
}

/* The wordmark is the one place the monospace face appears in the chrome. */
.wordmark {
  font-family: var(--face-mono);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: 0.02em;
  text-decoration: none;
}

header.site nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  font-size: var(--text-sm);
}

/* Carried over from the previous site, which had this and little else worth
   keeping: the rule wipes in from one side and out to the other, because the
   transform-origin swaps between the two states. It answers a hover and costs
   four declarations. */
header.site nav a {
  position: relative;
  text-decoration: none;
  color: var(--ink-quiet);
  transition: color var(--motion-quick) var(--ease);
}

header.site nav a::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: -0.2em;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--motion-quick) var(--ease);
}

header.site nav a:hover {
  color: var(--ink);
}

header.site nav a:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* No top margin: `main` already carries the space above it, and a margin here
   would be added to a gap the flex column has already distributed. */
/* The menu button, which exists only on a narrow screen. Above the
   breakpoint the navigation is three words on a line and a disclosure in
   front of them would be a tap in the way of a link; the button is not drawn
   at all there, and the navigation below is simply visible. */
.menu-button {
  display: none;
}

/* Clipped rather than hidden, like the archive's filter inputs: CSS needs a
   :checked to react to, and display:none would take the control off the
   keyboard and leave the menu unopenable without a mouse. */
.menu-switch {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

footer.site {
  padding-block: var(--space-5);
  border-top: 1px solid var(--rule);
  color: var(--ink-quiet);
  font-size: var(--text-sm);
}

/* The span the site has run, at one edge, with the links out at the other --
   the same arrangement as the chrome at the top, so the page is bracketed by
   two bars that behave alike. */
.colophon {
  margin: 0;
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

/* --- The home page's hero ----------------------------------------------- */

/* The one orchestrated moment on the site: the opening's three parts arrive as
   a single sequence rather than each element fading in on its own. A stagger
   across every entry is the generic default and it delays the content on the
   one page where the content is the point. */
.hero-greeting,
.nameplate,
.hero-lede,
.hero-ways,
.way-down {
  animation: settle var(--motion-settle) var(--ease) both;
}

.hero-greeting,
.nameplate { animation-delay: 0ms; }
.hero-lede { animation-delay: 90ms; }
.hero-ways { animation-delay: 180ms; }

/* The arrow arrives last, which is what draws the eye to it -- rather than a
   bounce running forever, which is motion nobody asked for and the second
   untriggered animation on a site whose budget is one. Arriving after the
   greeting also says the right thing: there is more here, once you have read
   this. */
.way-down { animation-delay: 420ms; }

@keyframes settle {
  from { opacity: 0; transform: translateY(0.4rem); }
  to { opacity: 1; transform: none; }
}

/* Generous vertical room, as the previous site had it: the nameplate is the
   whole page and should not be crowded by what follows it. Ranged left on the
   page's tracks like everything else -- this used to be the one centred page,
   which is what made it read as a different site from the one behind it. */
.hero {
  padding-block: var(--space-8) var(--space-7);
}

/* The greeting, in the margin. The monospace face echoes the previous site's
   typed hero, and placing it here makes it the same kind of marginal note as a
   date or a year rather than a second headline. */
.hero-greeting {
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.01em;
  margin: 0;
}

/* The nameplate takes the display step in the text face. The previous pass
   spent the top of the scale on a monospace greeting and left every page
   heading at a size below it, so the scale's largest size was never used for
   anything a reader was meant to read. */
.nameplate {
  font-size: var(--text-display);
  letter-spacing: -0.022em;
  margin-bottom: var(--space-4);
}

/* The lede is kept shorter than the column, so it reads as one line of
   introduction rather than as a paragraph. */
.hero-lede .lede {
  margin-bottom: var(--space-6);
  max-inline-size: var(--lede);
}

.hero-ways {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  font-size: var(--text-lg);
}

/* The same underline the navigation uses, resting rather than wiping: these
   are the page's only real content, so they are marked as links from the
   start rather than revealing themselves on hover. */
.hero-ways a {
  text-decoration: none;
  border-block-end: 1px solid var(--accent);
  padding-block-end: 0.1em;
  transition: color var(--motion-quick) var(--ease);
}

.hero-ways a:hover {
  color: var(--accent);
}

.lede {
  font-size: var(--text-lg);
  color: var(--ink-quiet);
  text-wrap: pretty;
}

.intro {
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule);
}

/* --- The latest publication, on the home page -------------------------- */

/* The opening is the first screen, and the way down is the last thing in it.
   A column rather than a hero with a height and an arrow after it: that way
   the arrow was the hero's height plus its own past the fold, which put the
   one thing announcing there is more just off the bottom of the screen. Laid
   out like this it cannot leave the screen it belongs to, whatever the
   reader's text size or window.

   Only the header is subtracted. The footer is below the teaser, a screen
   further on, so taking it off here shortened the opening for no reason. */
.opening {
  display: flex;
  flex-direction: column;

  /* Exactly everything between the top of the window and the top of this
     block, and nothing more. Both terms are needed and the second was the one
     missing once: main's own top padding is three rem the opening is pushed
     down by, so without it the block ran that much past the fold and took the
     way down with it.

     Nothing further is subtracted, and that is the point. Shortening this
     block to give the arrow room ends it above the fold -- which puts the
     teaser's first line on screen and loses the only thing being below the
     fold was for. The air under the arrow belongs inside this block, as the
     way down's own padding, where it holds the arrow clear of the edge
     without moving the edge. */
  min-block-size: calc(100dvh - var(--header-height) - var(--space-7));
}

/* Takes the slack, so the nameplate sits in the middle of the screen and the
   way down at the foot of it however tall the window is. */
.home-hero {
  flex: 1;
  align-content: center;
}

/* The way down. Centred because it belongs to the page rather than to the
   reading column, and quiet enough to read as an offer rather than an
   instruction. */
.way-down {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  /* The clearance between the arrow and the bottom of the screen. It lives
     here rather than coming off the opening's height, so that holding the
     arrow clear of the edge does not also lift the teaser into view. */
  padding-block-end: var(--space-6);
  color: var(--ink-quiet);
  text-decoration: none;
  transition: color var(--motion-quick) var(--ease);
}

/* It says where it goes. An arrow on its own asks a reader to guess that
   anything is down there, which is the whole thing this is meant to answer. */
.way-down-label {
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
}

.way-down:hover .way-down-label {
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

.way-down:hover {
  color: var(--accent);
}

.way-down .icon {
  inline-size: var(--icon-lg);
  block-size: var(--icon-lg);
}

/* Marked in the gutter like a year or a date, so the teaser is one more thing
   the margin introduces rather than a panel of its own. */
.latest-marker {
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  margin: 0;
}

/* The space before the quotation is also what keeps it off the first screen.
   The opening's height rests on an estimate of the header, and the header is
   not the same height on a phone as on a desktop -- so the fold lands within
   a rem or so of where the arithmetic says, and a teaser that begins
   immediately shows its first line when the estimate runs short. Beginning it
   a clear space further down absorbs that, and the separation is wanted
   anyway: the offer is a different thing from the nameplate above it. */
.latest {
  padding-block: var(--space-8) var(--space-7);
}

.latest-lead {
  color: var(--ink-quiet);
  font-size: var(--text-sm);
  margin-bottom: var(--space-4);
}

/* The title carries all three: italic because it is the name of a work, bold
   because it is the one thing here to act on, and underlined because it is a
   link and the lead around it is not. */
.latest-title {
  font-style: italic;
  font-weight: 600;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 0.07em;
  text-underline-offset: 0.18em;
}

.latest-title:hover {
  color: var(--accent);
}

/* The quotation: the author's own words, set as a quotation and in his own
   voice rather than in the site's. The rule down its edge is the accent doing
   what the accent does everywhere else -- marking, not decorating. */
.latest-quote {
  margin: 0;
  padding-inline-start: var(--space-4);
  border-inline-start: 2px solid var(--accent);
  color: var(--ink);
  font-style: italic;
  max-inline-size: var(--measure-prose);
}

.latest-quote p {
  margin: 0;
}

/* --- Entries ------------------------------------------------------------ */

/* An entry is not a container. It is a row on the page's own tracks: its date
   and reading time in the margin, its words in the reading column, and a
   hairline above separating it from the one before.

   Cards were tried and reverted once, because without them a list of projects
   read as undifferentiated prose. That observation was right; deleting the
   card was the wrong answer to it. What differentiates an entry here is the
   margin and the rule, which is how a chronology or a well-set bibliography
   does it -- so the separation survives without every entry being a box.

   There is no hover lift and no shadow. The title answers the pointer and
   nothing else moves. */
.ordering + .pubs .year:first-child .entry:first-child {
  border-block-start: 0;
}

.entry {
  display: grid;
  grid-template-columns: var(--entry-aside) minmax(0, 1fr);
  column-gap: var(--gutter-gap);
  align-items: start;
  border-block-start: 1px solid var(--rule);
  padding-block: var(--space-5);
}

/* The margin's own column within an entry, ranged right against the words so
   the dates form a line down the page rather than a ragged edge. */
.entry > .aside-in {
  text-align: right;
  color: var(--ink-quiet);
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-prose);
  margin: 0;
}

.entry-body > :last-child {
  margin-bottom: 0;
}

/* The thumbnail sits in the margin rather than at full measure. A picture the
   width of the column dominates the entry it belongs to, which is the other
   half of why the card layout did not work.

   No crop and no fixed proportion. A 4/3 box suited a grid of equal cards;
   these are ruled rows of whatever height their contents need, so an image
   can keep its own shape -- which also means a logo is not cut off at the top
   and bottom to fit a frame meant for photographs. */
.thumb {
  inline-size: 100%;
  block-size: auto;
  margin-block-end: var(--space-2);
}

/* --- Entries, metadata, tags -------------------------------------------- */

.meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--ink-quiet);
  margin: 0 0 var(--space-2);
}

.tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: 0;
  margin: 0;
  font-size: var(--text-xs);
  color: var(--ink-quiet);
}

/* One flowing row of words rather than three stacked blocks. The row gap is
   much smaller than the column gap: when the groups wrap -- which on a phone
   they always do -- the lines should read as one paragraph of controls, not
   as separate sections with air between them. */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-4);
  border: 0;
  padding: 0;
}

/* A group holds together on one line where it can, and its label stays with
   its first control rather than orphaning above them. */
.filter-group {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1);
}

/* Set in the ink and in the heavier weight, so a group's label reads as the
   thing being filtered by rather than as another of the choices beside it.
   They sat in the quiet ink at the same weight as the controls, which made a
   row of words with no evident grammar.

   Still sentence case: a tracked-out all-caps label above a group of controls
   signals that a form was styled and nothing about the content. */
.filter-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
  margin-inline-end: var(--space-1);
}

/* Text, not pills. A filter is a word you are choosing, and a bordered capsule
   around every word turns a line of choices into a row of buttons.

   The inputs stay real and stay in the document: CSS needs a :checked to react
   to and the whole filtering mechanism rests on it. They are taken out of the
   visual flow without being hidden from assistive technology or from the
   keyboard -- clip rather than display:none, which would make them
   unfocusable and the filters unusable without a mouse. */
.filters label {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
  color: var(--ink-quiet);
  cursor: pointer;
  transition: color var(--motion-quick) var(--ease), background-color var(--motion-quick) var(--ease);
}

.filters label input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* How much of the archive a tag holds, so the filters say something before
   they are used. Set smaller and dimmed rather than given a colour of its
   own: it rides along with the pill's text through hover and selection, which
   is what keeps it from needing a rule per state. Tabular figures so the
   numbers do not change the pill's width as the archive grows. */
.filters .tag-count {
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

.filters label:hover {
  color: var(--ink);
}

/* Selected: the mandarin fills the word, with the page's own ink on it. This
   is the one place --accent-fill appears, and the only way the colour that is
   actually in the photograph can be used -- as text it is 1.89:1.

   The weight, not the fill, is what carries the state for a reader who does
   not distinguish the colours: a filled area and a colour change are both
   colour, and the floor is that selection survives a single-tone rendering. */
.filters label:has(:checked) {
  background: var(--accent-fill);
  color: var(--ink);
  font-weight: 600;
}

/* The ring follows the input's focus onto the pill, since the input itself is
   no longer what a reader sees. */
.filters label:has(:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 0.15em;
}

/* The list is a column so that reversing it is one declaration. Its items are
   the years, each holding its own entries -- so the reversal applies at both
   levels, which the generated rules do. */
.pubs {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}

/* A year, on the page's own tracks: the marker in the gutter, the entries in
   the reading column. The row gap only tells below the breakpoint, where the
   tracks collapse and the marker falls above the entries it marks. */
.year {
  row-gap: var(--space-3);
}

/* The year marks a group rather than titling one, so it is set as quietly as
   anything else in the gutter -- the figures lining up down the margin are
   what make the archive read as a chronology. */
.year-marker {
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

/* No gap between entries. Each carries its own vertical padding and the rule
   that separates it from the one above, so a gap as well would leave the rule
   floating between two entries rather than belonging to the lower one. */
.year-entries {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
}

/* No rule hides an entry by default. Everything the generator appends below
   narrows an already complete list, which is why a browser that does not
   understand :has() shows the whole archive rather than an empty one. */
.pub { display: block; }

/* What a selection with no members says. Hidden here and revealed only by a
   generated rule, which is the same way round as the entries: a client that
   cannot run the filtering shows the whole archive and this line stays down.
   Told that nothing matches while the archive is in front of you would be
   worse than being told nothing at all. */
.no-match {
  display: none;
  color: var(--ink-quiet);
}

.pub h3 {
  margin-bottom: var(--space-1);
}

.description {
  color: var(--ink-quiet);
}

.downloads {
  font-size: var(--text-sm);
}

/* The read count keeps its place in the row whether or not it has arrived:
   a gap where one fact should be reads as something broken. What must stay
   distinguishable is a count that was never retrieved from a work nobody has
   read -- "N/A" is not zero, and htmx replaces the value rather than the
   mark beside it. */
.reads-value {
  font-variant-numeric: tabular-nums;
}

/* A draft says so on its own page. It is only ever seen locally -- a build for
   deployment does not produce the page at all -- so this is a note to the
   author rather than to a reader. */
.draft {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: 1px dashed var(--rule);
  color: var(--ink-quiet);
  font-size: var(--text-sm);
}

/* --- Projects and the portrait ------------------------------------------ */

/* As the publications index: the entries' own padding and rules do the
   separating, so there is no gap between them. */
.projects {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
}

/* A project's margin is wider than a publication's. A publication puts a date
   there and a date is a known width; a project puts a picture there, and at
   the width of a date it was a thumbnail of a thumbnail. */
.projects .entry {
  grid-template-columns: var(--project-aside) minmax(0, 1fr);
}

.project {
  row-gap: var(--space-1);
}

.project img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  margin-bottom: var(--space-4);
  border: 1px solid var(--rule);
}

.project h2 {
  margin-bottom: var(--space-1);
}

/* The about page: the portrait in one column, everything written in the other.
   The photograph is one thing and wants one column of its own rather than
   interrupting the text it sits in.

   The resume goes beneath the portrait rather than beneath the prose. It was
   a paragraph on its own under the text, which read as something left over
   after the page had finished -- a single link with nothing around it and no
   reason to be where it was. Under the portrait it belongs to the column that
   is about who he is rather than to the one he wrote, and it gives that
   column something to do below the image. */
.about {
  display: grid;
  grid-template-columns: var(--portrait) minmax(0, 1fr);
  column-gap: var(--space-6);
  row-gap: var(--space-3);
  align-items: start;
}

.about > .portrait {
  grid-column: 1;
  grid-row: 1;
  inline-size: 100%;
  block-size: auto;
  border: 1px solid var(--rule);
}

.about > .downloads {
  grid-column: 1;
  grid-row: 2;
  margin: 0;
}

/* Spans both rows, so the prose runs the full height beside the portrait and
   the resume rather than stopping level with the image. */
.about > .about-body {
  grid-column: 2;
  grid-row: 1 / span 2;
}

/* The same quiet, marginal treatment the typeset-document offer has: it is an
   offer of a document, in a narrow column, and should read as one. */
.about > .downloads a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  color: var(--ink-quiet);
  text-decoration: none;
  transition: color var(--motion-quick) var(--ease);
}

.about > .downloads a:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

.about-body > :last-child {
  margin-bottom: 0;
}

/* The about page's links out: a row of icon-only targets.

   Not to be confused with .published-elsewhere, which marks an index entry
   whose work lives on another site. The two were one class name once, and the
   marker inherited this list's margins and laid itself out as a flex
   container -- which pushed it out of line with the facts it sits among. */
.elsewhere {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.empty {
  color: var(--ink-quiet);
}

/* --- Icons -------------------------------------------------------------- */

/* Inlined Lucide outlines. They inherit currentColor, so they follow the text
   they belong to into either colour mode and into the selected state of a
   filter pill without a second rule.

   Sized in em and nudged onto the text's baseline: an icon aligned to the box
   rather than the baseline sits visibly high beside a serif. */
.icon {
  inline-size: var(--icon);
  block-size: var(--icon);
  flex: none;
  vertical-align: -0.14em;
}

/* Inside a flex row the icon is placed by align-items, and the baseline nudge
   that inline usage needs would push it back off centre. */
.fact .icon,
.offer .icon {
  vertical-align: baseline;
}

/* Beside a word, the icon needs the gap its container would otherwise not
   give it. */
.downloads .icon {
  margin-inline-end: var(--space-2);
}

.downloads a {
  display: inline-flex;
  align-items: baseline;
  text-decoration: none;
}

.downloads a:hover {
  color: var(--accent);
}

/* The links out carry no label, so each is a target with the icon centred in
   it rather than a word with a glyph in front. Sized so a finger can hit it. */
/* Icons alone, with no box around them. The target is still the full 2.75rem
   a finger needs -- the box was what made them look like buttons, not the
   size. */
.elsewhere a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--target);
  block-size: var(--target);
  color: var(--ink-quiet);
  text-decoration: none;
  transition: color var(--motion-quick) var(--ease);
}

.elsewhere .icon {
  inline-size: var(--icon-lg);
  block-size: var(--icon-lg);
  vertical-align: baseline;
}

.elsewhere a:hover {
  color: var(--accent);
}

/* The order sits with the list it orders, not among the filters. Ranged to
   the right so its edge lines up with the entries beneath it -- which it can
   do here, because it is alone on its line and no longer the last item of a
   row that wraps. */
.ordering {
  display: flex;
  justify-content: flex-end;
  padding-block-end: var(--space-3);
  border-block-end: 1px solid var(--rule);
  margin-block-end: var(--space-2);
}

/* The order toggle. One control showing the order the list is in.

   It used to be ranged to the right, so that its edge lined up with the right
   edge of the entries beneath it. That reads well on one line and badly on
   two: as soon as the row wraps -- always, on a phone -- the toggle sits alone
   on the last line with the whole width of the page to its left. It flows with
   the groups now.

   Only one of the two states is in flow at a time; the other is removed. The
   checkbox itself is clipped away like the filters' inputs, so the label is
   the control. Set in the monospace face, because it states a state rather
   than naming a thing. */
.order-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--face-mono);
  font-size: var(--text-xs);
  color: var(--ink-quiet);
  cursor: pointer;
  transition: color var(--motion-quick) var(--ease);
}

.order-toggle:hover {
  color: var(--accent);
}

.order-toggle input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.order-state > span {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* Newest first is the default, so the oldest-first face waits out of flow. */
.order-toggle .when-oldest {
  display: none;
}

.order-toggle:has(:checked) .when-newest {
  display: none;
}

.order-toggle:has(:checked) .when-oldest {
  display: inline-flex;
}

.order-toggle:hover {
  color: var(--ink);
  border-color: var(--accent);
}

.order-toggle:has(:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 0.15em;
}

/* --- Focus -------------------------------------------------------------- */

/* The indicator is never removed, and it is drawn in the accent, which the
   contrast floor already holds for. :focus-visible rather than :focus so that
   a mouse click does not leave a ring behind it. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0.15em;
  border-radius: 1px;
}

a:focus-visible,
.filters label:has(input:focus-visible),
.menu-switch:focus-visible + .menu-button,
.skip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 0.15em;
}

/* --- Below the breakpoint ----------------------------------------------- */

/* One column, and the gutter's contents fall inline where they belong. They
   are never hidden: a date that disappears on a phone is information lost to a
   layout. */
@media (max-width: 56rem) {
  :root {
    --tracks: minmax(0, var(--measure));
    --text-display: 2.125rem;
  }

  .spread,
  .row {
    justify-content: start;
  }

  .spread > *,
  .row > *,
  .spread > .full,
  .aside {
    grid-column: 1;
  }

  .aside {
    text-align: left;
  }

  /* An entry's margin falls above its words rather than beside them: a
     five-rem column on a phone leaves neither enough room. The date and the
     reading time are still stated -- they are information, and a layout is
     not a reason to drop them -- but they range left with everything else and
     read as a line above the title they belong to. */
  .entry,
  /* Matching the specificity of `.projects .entry` above, not merely sitting
     inside a media query: a query adds none, so the wider rule went on
     winning here and a project kept a ten-rem picture beside eight rem of
     text. */
  .projects .entry {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-2);
  }

  /* Still stacked rather than run onto one line. Collapsing the break would
     mean hiding an element inside the margin, and nothing in the margin is
     ever hidden -- that rule is what stops a date disappearing on a phone,
     and it is not worth weakening to save a line. */
  .entry > .aside-in {
    text-align: left;
  }

  .thumb {
    max-inline-size: var(--portrait);
  }

  /* The menu. The button appears, the navigation goes behind it, and
     :checked brings it back as a column across the whole bar -- which is
     what flex-basis 100% does in a wrapping row.

     The navigation is genuinely hidden when the menu is closed, which is
     what a disclosure is: there is a control that reveals it, it is the
     first thing after the wordmark, and it says what it is. */
  .menu-button {
    display: inline-flex;
    align-items: center;
    color: var(--ink-quiet);
    cursor: pointer;
    transition: color var(--motion-quick) var(--ease);
  }

  .menu-button:hover {
    color: var(--accent);
  }

  .menu-button .icon {
    inline-size: var(--icon-lg);
    block-size: var(--icon-lg);
  }

  /* One of the two marks at a time, the other taken out of the flow -- the
     same swap the order toggle makes between its two states. */
  .menu-button .when-open,
  .menu-switch:checked + .menu-button .when-closed {
    display: none;
  }

  .menu-switch:checked + .menu-button .when-open {
    display: inline-flex;
  }

  header.site nav {
    display: none;
  }

  .menu-switch:checked ~ nav {
    display: flex;
    flex-direction: column;
    flex-basis: 100%;
    gap: var(--space-3);
    padding-block-start: var(--space-3);
  }

  /* The about page stacks, with the portrait kept to its own width rather
     than stretched across the screen. */
  .about {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-5);
  }

  .about > .portrait,
  .about > .about-body,
  .about > .downloads {
    grid-column: 1;
  }

  .about > .portrait,
  .about > .about-body,
  .about > .downloads {
    grid-row: auto;
  }

  .about > .portrait {
    max-inline-size: var(--portrait);
  }
}

/* --- Reduced motion ----------------------------------------------------- */

/* Everything this stylesheet animates, off. Including the navigation between
   pages, which is motion a reader did not ask for either. */
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }

  *, *::before, *::after {
    animation-duration: 0s;
    animation-delay: 0s;
    transition-duration: 0s;
    transition-delay: 0s;
  }
}

/* --- Filtering, generated from the catalogue --------------------------- */

.ordering:has(#order-oldest:checked) ~ .pubs,
.ordering:has(#order-oldest:checked) ~ .pubs .year-entries { flex-direction: column-reverse; }

.filters:has(.f-tag:checked) ~ .pubs .pub,
.filters:has(.f-tag:checked) ~ .pubs .year,
.filters:has(.f-lang:checked:not(#lang-any)) ~ .pubs .pub,
.filters:has(.f-lang:checked:not(#lang-any)) ~ .pubs .year { display: none; }

.filters:has(#tag-dsl:checked):has(#lang-any:checked) ~ .pubs .pub.t-dsl { display: block; }
.filters:has(#tag-dsl:checked):has(#lang-any:checked) ~ .pubs .year:has(.pub.t-dsl) { display: grid; }
.filters:has(#tag-fiction:checked):has(#lang-any:checked) ~ .pubs .pub.t-fiction { display: block; }
.filters:has(#tag-fiction:checked):has(#lang-any:checked) ~ .pubs .year:has(.pub.t-fiction) { display: grid; }
.filters:has(#tag-go:checked):has(#lang-any:checked) ~ .pubs .pub.t-go { display: block; }
.filters:has(#tag-go:checked):has(#lang-any:checked) ~ .pubs .year:has(.pub.t-go) { display: grid; }
.filters:has(#tag-mathematics:checked):has(#lang-any:checked) ~ .pubs .pub.t-mathematics { display: block; }
.filters:has(#tag-mathematics:checked):has(#lang-any:checked) ~ .pubs .year:has(.pub.t-mathematics) { display: grid; }
.filters:has(#tag-software-engineering:checked):has(#lang-any:checked) ~ .pubs .pub.t-software-engineering { display: block; }
.filters:has(#tag-software-engineering:checked):has(#lang-any:checked) ~ .pubs .year:has(.pub.t-software-engineering) { display: grid; }

.filters:not(:has(.f-tag:checked)):has(#lang-en:checked) ~ .pubs .pub.lang-en { display: block; }
.filters:not(:has(.f-tag:checked)):has(#lang-en:checked) ~ .pubs .year:has(.pub.lang-en) { display: grid; }
.filters:not(:has(.f-tag:checked)):has(#lang-sv:checked) ~ .pubs .pub.lang-sv { display: block; }
.filters:not(:has(.f-tag:checked)):has(#lang-sv:checked) ~ .pubs .year:has(.pub.lang-sv) { display: grid; }

.filters:has(#tag-dsl:checked):has(#lang-en:checked) ~ .pubs .pub.t-dsl.lang-en { display: block; }
.filters:has(#tag-dsl:checked):has(#lang-en:checked) ~ .pubs .year:has(.pub.t-dsl.lang-en) { display: grid; }
.filters:has(#tag-dsl:checked):has(#lang-sv:checked) ~ .pubs .pub.t-dsl.lang-sv { display: block; }
.filters:has(#tag-dsl:checked):has(#lang-sv:checked) ~ .pubs .year:has(.pub.t-dsl.lang-sv) { display: grid; }
.filters:has(#tag-fiction:checked):has(#lang-en:checked) ~ .pubs .pub.t-fiction.lang-en { display: block; }
.filters:has(#tag-fiction:checked):has(#lang-en:checked) ~ .pubs .year:has(.pub.t-fiction.lang-en) { display: grid; }
.filters:has(#tag-fiction:checked):has(#lang-sv:checked) ~ .pubs .pub.t-fiction.lang-sv { display: block; }
.filters:has(#tag-fiction:checked):has(#lang-sv:checked) ~ .pubs .year:has(.pub.t-fiction.lang-sv) { display: grid; }
.filters:has(#tag-go:checked):has(#lang-en:checked) ~ .pubs .pub.t-go.lang-en { display: block; }
.filters:has(#tag-go:checked):has(#lang-en:checked) ~ .pubs .year:has(.pub.t-go.lang-en) { display: grid; }
.filters:has(#tag-go:checked):has(#lang-sv:checked) ~ .pubs .pub.t-go.lang-sv { display: block; }
.filters:has(#tag-go:checked):has(#lang-sv:checked) ~ .pubs .year:has(.pub.t-go.lang-sv) { display: grid; }
.filters:has(#tag-mathematics:checked):has(#lang-en:checked) ~ .pubs .pub.t-mathematics.lang-en { display: block; }
.filters:has(#tag-mathematics:checked):has(#lang-en:checked) ~ .pubs .year:has(.pub.t-mathematics.lang-en) { display: grid; }
.filters:has(#tag-mathematics:checked):has(#lang-sv:checked) ~ .pubs .pub.t-mathematics.lang-sv { display: block; }
.filters:has(#tag-mathematics:checked):has(#lang-sv:checked) ~ .pubs .year:has(.pub.t-mathematics.lang-sv) { display: grid; }
.filters:has(#tag-software-engineering:checked):has(#lang-en:checked) ~ .pubs .pub.t-software-engineering.lang-en { display: block; }
.filters:has(#tag-software-engineering:checked):has(#lang-en:checked) ~ .pubs .year:has(.pub.t-software-engineering.lang-en) { display: grid; }
.filters:has(#tag-software-engineering:checked):has(#lang-sv:checked) ~ .pubs .pub.t-software-engineering.lang-sv { display: block; }
.filters:has(#tag-software-engineering:checked):has(#lang-sv:checked) ~ .pubs .year:has(.pub.t-software-engineering.lang-sv) { display: grid; }

/* A selection with no members says so; a selection with some takes it back. */

.filters:has(#tag-dsl:checked):has(#lang-sv:checked) ~ .pubs .no-match { display: block; }
.filters:has(#tag-fiction:checked):has(#lang-en:checked) ~ .pubs .no-match { display: block; }
.filters:has(#tag-go:checked):has(#lang-sv:checked) ~ .pubs .no-match { display: block; }
.filters:has(#tag-mathematics:checked):has(#lang-sv:checked) ~ .pubs .no-match { display: block; }
.filters:has(#tag-software-engineering:checked):has(#lang-sv:checked) ~ .pubs .no-match { display: block; }

.filters:has(#tag-dsl:checked):has(#lang-en:checked) ~ .pubs .no-match { display: none; }
.filters:has(#tag-fiction:checked):has(#lang-sv:checked) ~ .pubs .no-match { display: none; }
.filters:has(#tag-go:checked):has(#lang-en:checked) ~ .pubs .no-match { display: none; }
.filters:has(#tag-mathematics:checked):has(#lang-en:checked) ~ .pubs .no-match { display: none; }
.filters:has(#tag-software-engineering:checked):has(#lang-en:checked) ~ .pubs .no-match { display: none; }
