/*
 * maxmarquardt.space
 *
 * The palette, the metrics and the listing are barycenter.wiki's, deliberately:
 * the two sites are meant to look like they came from the same hand. What is
 * new here is `.prose` — a directory listing has no body text, and a blog is
 * almost nothing but body text.
 *
 * Colour follows bary-blocks/colors.md: a colour is named for its purpose, not
 * its value, and one name resolves to four values across two independent axes —
 * light/dark and default/increased contrast.
 *
 * Two layers, so every value is written exactly once:
 *   1. Palette   Apple's system greys and blues, sRGB, each written as a
 *                light/dark pair. The pair is the only place a hex appears.
 *   2. Semantic  The names the interface actually uses. These only ever point
 *                at palette tokens, so raising contrast re-points the palette
 *                and every role follows without being restated.
 *
 * The appearance axis is applied by `light-dark()`, which is Safari/iOS 17.5 and newer. A custom property holding an unsupported function is valid when parsed and only fails where it is substituted, so an older engine cannot fall back to an earlier declaration -- it would resolve every role to nothing and render the page with no colour at all. The roles below are therefore defined against the dark half of each pair, which is the site's default, with an @supports block upgrading them where the function exists and, where it does not, an `@supports not` block re-pointing them to the light half when the toggle asks for light. Below 17.5 the page is still dark by default and the toggle still works; the contrast axis keeps working either way, because it re-points the pairs rather than the roles.
 */

:root {
  /*
   * Dark unless the reader picks otherwise. The system preference is deliberately not consulted, so `light-dark()` resolves dark by default, with no JavaScript and on the first paint, and [data-theme="light"] is the only thing that flips it. app.js reads this back rather than restating it, so the default is set here and nowhere else.
   */
  color-scheme: dark;

  /* ----------------------------------------------------- 1. palette */
  /*
   * The accent and the greys are Apple's values from `bary-blocks/colors.md`, unmodified: Blue and Gray to Gray (6), from the Default columns here and the Increased Contrast columns in the `prefers-contrast` block below.
   *
   * Three tokens have no row in the table. `--base` is white or black at both tiers. `--ink` is #1c1c1e on white and #f2f2f7 on black, the values Gray (6) takes in the other appearance, and pure black or white at the increased tier. `--gray-strong`, secondary text, is Gray a step up: Gray's Increased Contrast value at the default tier, and at the increased tier Gray (3)'s Default value from the other appearance.
   *
   * In light, Apple's Default Blue and Gray fall under the 4.5:1 that WCAG AA asks of text, and they are used as they are. Do not darken one here without deciding to leave Apple's values; README.md, under Design, has the measured ratios.
   */
  /*              light                    dark                       */
  --base-light: #ffffff;   --base-dark: #000000;
  --ink-light: #1c1c1e;    --ink-dark: #f2f2f7;
  --gray-strong-light: #6c6c70;  --gray-strong-dark: #aeaeb2;
  --gray-light: #8e8e93;   --gray-dark: #8e8e93;
  --gray-2-light: #aeaeb2; --gray-2-dark: #636366;
  --gray-3-light: #c7c7cc; --gray-3-dark: #48484a;
  --gray-4-light: #d1d1d6; --gray-4-dark: #3a3a3c;
  --gray-5-light: #e5e5ea; --gray-5-dark: #2c2c2e;
  --gray-6-light: #f2f2f7; --gray-6-dark: #1c1c1e;
  --accent-light: #0088ff;   --accent-dark: #0091ff;

  /* The roles. Dark by default; @supports adds the appearance axis. */
  --base: var(--base-dark);
  --ink: var(--ink-dark);
  --gray-strong: var(--gray-strong-dark);
  --gray: var(--gray-dark);
  --gray-2: var(--gray-2-dark);
  --gray-3: var(--gray-3-dark);
  --gray-4: var(--gray-4-dark);
  --gray-5: var(--gray-5-dark);
  --gray-6: var(--gray-6-dark);
  --accent: var(--accent-dark);

  /* ---------------------------------------------------- 2. semantic */
  --bg: var(--base);
  --surface: var(--gray-6);
  --surface-strong: var(--gray-5);

  --text-primary: var(--ink);
  --text-secondary: var(--gray-strong);
  --text-tertiary: var(--gray);
  --text-quaternary: var(--gray-2);

  --separator: var(--gray-4);
  --separator-opaque: var(--gray-3);
  --link: var(--accent);

  /* ------------------------------------------------------ 3. metrics */
  /*
   * Two faces. Monospace is the site's voice: it is what makes the listing a directory index rather than a table of contents, and every column on the page lines up because of it. It sets everything except the words of an article.
   *
   * Those words -- a post's title, dek, body and notes -- are set in Latin Modern Roman, the face LaTeX sets by default and the one the résumé is typeset in. It is self-hosted from `/fonts`, whose README records what was changed from the original. The fallbacks are the same design where a reader has it installed, then the nearest common serif.
   */
  --font: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco,
    "Cascadia Mono", "Roboto Mono", Consolas, "Liberation Mono", monospace;
  --serif: "LM Roman 10 Web", "Latin Modern Roman", "CMU Serif", Georgia, "Times New Roman", serif;

  --text-size: 1rem;
  --line-height: 1.6;
  /*
   * The article's size. Latin Modern is small on its em: its x-height is 0.431em against SF Mono's 0.526em, both read from the font files. At 20px its lowercase stands 8.6px tall, next to 8.4px for the 16px monospace around it, so the article reads at the size of the chrome rather than a step below it.
   */
  --prose-size: 1.25rem;

  /*
   * One type size for the chrome. Every piece of text around an article is 16px at a 1.6 ratio, including the metadata columns, the theme toggle and the footer. Inside an article the rule bends: its words are set in the serif at `--prose-size`, its title and headings step up from that, and its code and notes step down.
   *
   * Two units, split by axis:
   *
   *   Horizontal -- `ch`. Every width on this page exists to hold or align
   *   characters, and in monospace `ch` is exact: one advance, 9.625px at
   *   16px, with no dependence on which letters are used. A column stated as
   *   10ch holds ten characters, not "about ten". In a proportional face this
   *   would only be an estimate from the width of a `0`, which is why `ch`
   *   is usually avoided; here it is the honest unit.
   *
   *   Vertical -- `rem`. Rhythm is set by the type size, not the advance, so
   *   heights, vertical padding and margins stay multiples of 16px with 8px
   *   as the only half-step.
   *
   * The cost is that horizontal values no longer land on the 16px grid --
   * 1ch is 9.625px. That is accepted deliberately: two characters of gutter
   * is a statement about the text, and 32px is a statement about a grid the
   * text does not actually sit on.
   */

  /*
   * One column for the whole site: header, listing, article and footer all
   * sit on it, and the running text fills it. Nothing is capped short of the
   * edge, so no paragraph looks cut off.
   *
   * Stated in characters, because the chrome is monospace and `ch` is exact there: every advance is identical, so 90ch is 90 characters rather than an estimate from the width of a `0`. An article's words are proportional, so for them the same width is only a count of monospace advances. Counting letters and spaces on every full line of the 36 posts at 1280px, it holds a median of 94 characters of Latin Modern, with nine lines in ten between 88 and 100.
   *
   * The gutters are added back because `.wrap` is `border-box`, so the cap covers the padding too. Without the calc the reader would get 90ch less two gutters, 86 characters, and the name would be a lie.
   *
   * At 94 characters the article is past WCAG 1.4.8's 80-character ceiling and past Bringhurst's 45 to 75, the craft convention behind the familiar 66. It is inside what the reading studies tested: the widest condition in the literature was Bernard et al.'s 132 cpl, which both adults and children disliked, and the controlled studies below that disagree with each other. They were run on proportional type, which the article now is, so they bear on it; none of them tested the monospace chrome.
   */
  --measure: calc(90ch + 2 * var(--gutter));
  /*
   * The edge offset, and nothing else. It keeps text off the viewport edge;
   * it is not the width control -- that is `--measure`.
   *
   * Stated in characters like the measure, so the whole layout is expressed
   * in one unit: two characters of clearance, 19.25px at the 9.625px advance.
   * That is off the 16px grid the rest of the page keeps, which is the price
   * of naming it in the same unit as the column it sits beside.
   */
  --gutter: 2ch;

  /*
   * The listing's one fixed column. The date is zero-padded, so its width is constant: `09/17/26` is 8 characters, and 10ch leaves two of slack rather than sitting exactly on the longest string. `/contact-me` widens it, where the values run longer than a date.
   */
  --col-date: 10ch;

  --radius: 6px;
  --speed: 0.18s;
}

/*
 * The same axis for engines that lack `light-dark()`. The roles above resolve to the dark half, so this re-points them when the toggle asks for light. It applies only where the function is missing; where it exists, the block after this one does the same job through `color-scheme`, so the two never both apply.
 *
 * It re-points roles rather than pairs, so the increased-contrast block below still reaches it -- that one re-points the pairs these now read from.
 */
@supports not (color: light-dark(#ffffff, #000000)) {
  :root[data-theme="light"] {
    --base: var(--base-light);
    --ink: var(--ink-light);
    --gray-strong: var(--gray-strong-light);
    --gray: var(--gray-light);
    --gray-2: var(--gray-2-light);
    --gray-3: var(--gray-3-light);
    --gray-4: var(--gray-4-light);
    --gray-5: var(--gray-5-light);
    --gray-6: var(--gray-6-light);
    --accent: var(--accent-light);
  }
}

/*
 * The appearance axis, added only where `light-dark()` exists. Everything
 * above already resolves to a complete palette, so an engine that skips this
 * block still gets colour rather than none.
 */
@supports (color: light-dark(#ffffff, #000000)) {
  :root {
    --base: light-dark(var(--base-light), var(--base-dark));
    --ink: light-dark(var(--ink-light), var(--ink-dark));
    --gray-strong: light-dark(var(--gray-strong-light), var(--gray-strong-dark));
    --gray: light-dark(var(--gray-light), var(--gray-dark));
    --gray-2: light-dark(var(--gray-2-light), var(--gray-2-dark));
    --gray-3: light-dark(var(--gray-3-light), var(--gray-3-dark));
    --gray-4: light-dark(var(--gray-4-light), var(--gray-4-dark));
    --gray-5: light-dark(var(--gray-5-light), var(--gray-5-dark));
    --gray-6: light-dark(var(--gray-6-light), var(--gray-6-dark));
    --accent: light-dark(var(--accent-light), var(--accent-dark));
  }
}

/*
 * Increased contrast pulls the palette apart. It re-points the pairs, not the
 * roles, so it reaches both paths above: the light-dark() one and the dark
 * fallback. The semantic layer is untouched either way.
 */
@media (prefers-contrast: more) {
  :root {
    --ink-light: #000000;    --ink-dark: #ffffff;
    --gray-strong-light: #48484a;  --gray-strong-dark: #c7c7cc;
    --gray-light: #6c6c70;   --gray-dark: #aeaeb2;
    --gray-2-light: #8e8e93; --gray-2-dark: #7c7c80;
    --gray-3-light: #aeaeb2; --gray-3-dark: #545456;
    --gray-4-light: #bcbcc0; --gray-4-dark: #444446;
    --gray-5-light: #d8d8dc; --gray-5-dark: #363638;
    --gray-6-light: #ebebf0; --gray-6-dark: #242426;
    --accent-light: #1e6ef4;   --accent-dark: #5cb8ff;
  }
}

/* The toggle only flips the appearance axis; light-dark() does the rest. Dark needs no rule of its own, because it is the default. */
:root[data-theme="light"] {
  color-scheme: light;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --speed: 0s;
  }
}

/* ==================================================================== faces */

/*
 * Latin Modern Roman 10 in its four styles, subset to Latin (`fonts/README.md` has the details). `swap` shows the text in a fallback at once and changes face when the file lands; a post preloads the upright face so that it usually lands first, and the italic and bold are fetched only by a page that uses them. The build appends a content hash to each URL, which is what lets `/fonts` be cached for a year.
 */
@font-face {
  font-family: "LM Roman 10 Web";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/lmroman10web-regular.woff2?v=kY5-aqkE") format("woff2");
}

@font-face {
  font-family: "LM Roman 10 Web";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/lmroman10web-italic.woff2?v=NT8zd3R8") format("woff2");
}

@font-face {
  font-family: "LM Roman 10 Web";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/lmroman10web-bold.woff2?v=0y7Z53ZV") format("woff2");
}

@font-face {
  font-family: "LM Roman 10 Web";
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/lmroman10web-bolditalic.woff2?v=ghy70izA") format("woff2");
}

/* ===================================================================== base */

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

html {
  font-size: 16px;
  /*
   * The face is named here as well as on `body`, so that `ch` measures the monospace advance on the root too. Left to the UA default this element is Times, where `1ch` measures 8px instead of 9.625 and a character-based width would silently come out a fifth too narrow. A `ch` inside a custom property such as `--measure` is not resolved on `:root`, though: it is measured in the face of whichever element substitutes the variable, so a character width used inside an article measures the serif's `0`.
   */
  font-family: var(--font);
  -webkit-text-size-adjust: 100%;
  /* The header is sticky, so an anchor must clear it rather than land under it. */
  scroll-padding-top: 4rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text-primary);
  font-family: var(--font);
  font-size: var(--text-size);
  line-height: var(--line-height);
  font-variant-ligatures: none;
  -webkit-font-smoothing: antialiased;
  /*
   * Holds the footer to the bottom of the window when a page is shorter than
   * the viewport -- which the index is, and will be for a while. `dvh` rather
   * than `vh`: iOS measures `vh` against the viewport with the address bar
   * retracted, so `100vh` is taller than the window actually is and pushes the
   * footer just out of sight.
   */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* Every region of the page sits on this column. */
.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

a {
  color: var(--link);
  text-decoration: none;
  text-underline-offset: 3px;
}

a:hover {
  text-decoration: underline;
}

:focus-visible {
  outline: 2px solid var(--link);
  outline-offset: 2px;
  border-radius: 2px;
}

code,
pre,
input,
button {
  font-family: inherit;
}

[hidden] {
  display: none !important;
}

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

.skip:focus {
  left: var(--gutter);
  top: 0.5rem;
  z-index: 10;
  background: var(--surface);
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius);
}

/* Text for assistive technology alone: in the accessibility tree, drawn nowhere. `/hello` keeps its name in one while it shows a greeting in another language. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* =================================================================== header */

/*
 * Sticky, so the brand and the theme toggle stay reachable down a long post.
 * It sits below `.skip:focus`, which must stay on top of everything.
 *
 * Frosted rather than opaque: the text scrolling underneath stays faintly
 * visible, which is what tells you the page moved. Both the blur and
 * `color-mix` are required before the translucent background is applied --
 * without the blur, 50% alpha is unreadable text over text, and without
 * `color-mix` the background would be invalid and the bar fully transparent.
 */
.site-header {
  position: sticky;
  top: 0;
  z-index: 5;
  border-bottom: 1px solid var(--separator);
  background: var(--bg);
}

@supports (
  ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) and
  (background: color-mix(in srgb, red 50%, transparent))
) {
  .site-header {
    background: color-mix(in srgb, var(--bg) 50%, transparent);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    backdrop-filter: blur(20px) saturate(180%);
  }
}

/*
 * On one line `align-content` changes nothing. Once the links wrap below the M, which in SF Mono happens only below 290px, narrower than any width the smoke gate tests, it keeps the two lines together in the middle of the bar rather than pushed to its edges, and with no row gap they fit inside the bar's 4rem, which `scroll-padding-top` assumes: wrapped at 289px, the bar still measured 64px.
 */
.site-header .wrap {
  display: flex;
  align-items: center;
  align-content: center;
  gap: 0 2ch;
  min-height: calc(4rem - 1px);
  flex-wrap: wrap;
}

/* The link home is the site's mark, the favicon's M, drawn in the text colour so that it follows the theme. As a flex box the link is exactly the height of the M, with no line box around it, so `align-items` centres the letter itself on the header's line. At 1.25rem it reads as a mark; at 1rem it read as one more letter of the 16px chrome, and at 1.5rem it outweighed the links. */
.brand {
  display: flex;
  margin-right: auto;
  color: var(--text-primary);
}

.brand:hover {
  color: var(--link);
}

.brand-mark {
  display: block;
  height: 1.25rem;
  width: auto;
  fill: currentColor;
}

/*
 * The page links, kept together so they wrap as one unit: when the header is too narrow for the brand and both links, the pair drops below the brand rather than `[contact]` dropping alone. The gap matches the header's own, so on one line all three are spaced alike.
 */
.site-nav {
  display: flex;
  gap: 2ch;
}

.link-button {
  background: none;
  border: 0;
  padding: 0;
  color: var(--text-secondary);
  font: inherit;
  font-size: var(--text-size);
  /* The brackets mark it as a control, so it needs no underline as well. */
  text-decoration: none;
  cursor: pointer;
}

.link-button:hover {
  color: var(--text-primary);
}

/* ===================================================================== page */

main {
  /* Takes the slack, so the footer sits at the bottom of a short page. */
  flex: 1;
  /*
   * Both gaps match `.folder`'s 2rem, so the first and last sections sit on
   * the same rhythm as every gap between them. They were 3rem and 5rem,
   * inherited from barycenter.wiki, where the extra room at the top held a
   * Name/Size/Date column header that no longer renders on either site.
   */
  padding-block: 2rem;
}

h1,
h2,
h3 {
  line-height: 1.3;
  font-weight: 600;
}

/*
 * A row that goes nowhere — an entry with no link behind it. It keeps the
 * grid, so its columns still line up with every other section, but drops the
 * three things that would imply it can be clicked: the pointer, the hover
 * highlight, and the underline.
 */
.row-static:hover {
  background: none;
}

.cell-static {
  cursor: default;
}

/*
 * The row is the link, but only its name should look like one. `a:hover`
 * underlines the whole anchor, and text-decoration propagates to every
 * descendant inline box — which drew a line under the tag and destination
 * columns too. A descendant cannot cancel an inherited decoration, so the
 * anchor must never take it: it is turned off here and put back on `.file`
 * alone, below.
 */
.cell:hover {
  text-decoration: none;
}

/*
 * Prose sitting inside a listing section, as `/hello` and `/summary` do.
 *
 * It spans the full listing width rather than a narrower reading measure, so
 * that its rules line up with every other section's. A block that stops two
 * thirds of the way across while the sections above and below it do not reads
 * as a mistake.
 */
.folder-prose {
  padding: 0.75rem 1ch;
  border-top: 1px solid var(--separator);
  border-bottom: 1px solid var(--separator);
}

.folder-prose p {
  margin: 0 0 0.75rem;
  color: var(--text-secondary);
}

.folder-prose p:last-child {
  margin-bottom: 0;
}

/* A link inside a sentence is underlined, as in an article: in running text, colour alone would be the only thing marking it. It also stays on one line, because a break inside a link splits its underline in two: on September 27, 2026, without this rule, every width tested from 360px to 1920px broke one of the three links in `/hello`, and `my op-eds` broke after its hyphen. */
.folder-prose a {
  text-decoration: underline;
  white-space: nowrap;
}

/* ================================================================== listing */

/* A directory index: a name and one value. The row is the link. */
.cell {
  display: grid;
  grid-template-columns: 1fr var(--col-date);
  gap: 0 2ch;
  /*
   * Centred, not baseline-aligned. Every column is 16px, but the row is a
   * fixed 32px band and centring is what keeps its contents on that grid.
   */
  align-items: center;
  width: 100%;
  /*
   * A floor rather than a fixed height: a title long enough to wrap makes its
   * own row taller instead of spilling out of a 32px band.
   */
  min-height: 2rem;
  padding: 0.25rem 1ch;
  background: none;
  border: 0;
  border-radius: var(--radius);
  color: inherit;
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.folder {
  margin-top: 2rem;
}

.folder:first-child {
  margin-top: 0;
}

/*
 * `/contact-me` holds an address, handles and hostnames, not dates. `/incrediblecrab` is 15 characters, so the 10ch default would spill the value leftward into the name. 16ch leaves one character of slack rather than sitting exactly on the longest string.
 */
.folder-wide {
  --col-date: 16ch;
}

.folder-name {
  margin: 0 0 0.5rem;
  padding-inline: 1ch;
  color: var(--text-secondary);
  font-size: var(--text-size);
  line-height: var(--line-height);
  font-weight: 600;
}

/*
 * The monospace stack draws no Arabic script, and the fallback Chromium and WebKit reach for is Courier New, which stretches each letter to a fixed advance; so the Arabic and Persian greetings in `/hello` are set in the system face. At the heading's line height that face grew the heading by 0.5px in Firefox, which moved everything below it; at `line-height: 1` the heading keeps its height in Chromium, Chrome, WebKit and Firefox.
 */
.folder-name :lang(ar),
.folder-name :lang(fa) {
  font-family: system-ui, sans-serif;
  line-height: 1;
}

.rows {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--separator);
}

.row {
  border-bottom: 1px solid var(--separator);
}

.row:hover {
  background: var(--surface);
}

/*
 * The name is the only elastic column. It wraps rather than truncating,
 * because a title is a sentence and an ellipsis removes its end: for a
 * question, precisely the `?` that tells the reader what kind of sentence
 * they are looking at. A title long enough to take two lines is rare; one
 * that silently loses its last word is neither rare nor caught.
 */
.col-name {
  min-width: 0;
  overflow-wrap: break-word;
}

.dir {
  color: var(--text-tertiary);
}

.file {
  color: var(--link);
}

.row:hover .file {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.col-date {
  color: var(--text-tertiary);
  font-size: var(--text-size);
  white-space: nowrap;
  text-align: right;
}

/* `/featured-thoughts` is one row standing alone, so it carries the post's subtitle under the title, and its date sits on the title's line rather than centred against both. Only the title underlines on hover, as in every other row. */
.featured .cell {
  align-items: baseline;
}

.dek {
  display: block;
  color: var(--text-secondary);
}

/*
 * `/resume-preview`: one row per organisation. Its name takes a fixed column -- `Microsoft` is nine characters -- set in from the row's edge and spaced from the roles by a cell's `1ch` and `2ch`, and padded like a cell so that it sits on its first role's line. The roles are a list of their own beside it, so the lines between them start where the roles do and stop short of the name they share; only a new organisation gets a line the full width of the listing. A month range is seventeen characters, `09/2023 – 05/2026` and `05/2026 – present` alike, so the value column fits it exactly; anything shorter sits right-aligned in the same column.
 */
.stint {
  --col-date: 17ch;
  display: grid;
  grid-template-columns: 9ch 1fr;
  column-gap: 2ch;
  padding-left: 1ch;
}

.stint .rows {
  border-top: 0;
}

.stint .row:last-child {
  border-bottom: 0;
}

.stint .cell {
  padding-left: 0;
}

.stint .col-name {
  color: var(--text-secondary);
}

.col-org {
  color: var(--text-tertiary);
}

.stint > .col-org {
  padding-block: 0.25rem;
  white-space: nowrap;
}

/* Each role's own copy of the name is for the narrow layout, where it replaces the shared one. */
.cell > .col-org {
  display: none;
}

/* ===================================================================== post */

.post-head {
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--separator);
}

/*
 * The words of the piece are set in the serif: title, dek, body, notes and bibliography. Everything around them -- the metadata and the headings of its folders -- stays in the chrome's monospace. The body turns ligatures off, because in monospace a ligature breaks the grid; a book face is drawn to use them, so they come back on here.
 */
.post-title,
.post-subtitle,
.prose,
.footnotes ol,
.bibliography ol {
  font-family: var(--serif);
  font-variant-ligatures: common-ligatures;
  font-kerning: normal;
}

/* LaTeX's article class sets a title upright rather than bold, at 1.73 times a 10pt text. This one is upright at 1.8 times the article's text. */
.post-title {
  margin: 0;
  font-size: 2.25rem;
  font-weight: 400;
  line-height: 3rem;
}

.post-subtitle {
  margin: 0.5rem 0 0;
  color: var(--text-secondary);
  font-size: var(--prose-size);
  line-height: 2rem;
}

/*
 * The metadata block. Two columns on a 32px band each, so the labels line up
 * with the grid the rest of the page is on. The label column is sized for the
 * longest of them, `Published`, at 9 characters.
 */
.post-meta {
  display: grid;
  grid-template-columns: 12ch 1fr;
  gap: 0 2ch;
  margin: 1.5rem 0 0;
  color: var(--text-tertiary);
}

.post-meta dt {
  color: var(--text-quaternary);
}

.post-meta dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: break-word;
}

/* ==================================================================== prose */

.prose {
  color: var(--text-primary);
  font-size: var(--prose-size);
}

.prose > *:first-child {
  margin-top: 0;
}

.prose p,
.prose ul,
.prose ol,
.prose blockquote,
.prose pre,
.prose table {
  margin: 0 0 1rem;
}

/*
 * Headings keep the 16px grid, on a 2rem line box. A section heading is bold at 1.2 times the text, the step LaTeX takes for a subsection; a subheading is bold at the text size.
 */
.prose h2 {
  margin: 2rem 0 0.5rem;
  font-size: 1.5rem;
  line-height: 2rem;
}

.prose h3 {
  margin: 2rem 0 0.5rem;
  font-size: 1em;
  line-height: 2rem;
  color: var(--text-secondary);
}

/*
 * The heading anchor. Hidden until the heading is hovered or the link itself
 * is focused -- a permanent `#` beside every heading is noise, and one that
 * can never be reached by keyboard is not a link at all.
 */
.anchor {
  margin-left: 0.5rem;
  color: var(--text-quaternary);
  opacity: 0;
  transition: opacity var(--speed);
}

.prose h2:hover .anchor,
.prose h3:hover .anchor,
.anchor:focus-visible {
  opacity: 1;
}

/*
 * Links in running prose are underlined from the start. In the listing a row
 * is unambiguously a control and blue is enough; inside a paragraph, colour
 * alone would be the only thing marking a link, which fails anyone who cannot
 * separate the two hues.
 */
.prose a {
  text-decoration: underline;
}

.prose ul,
.prose ol {
  padding-left: 4ch;
}

.prose li {
  margin-bottom: 0.5rem;
}

.prose blockquote {
  padding-left: 2ch;
  border-left: 2px solid var(--separator-opaque);
  color: var(--text-secondary);
}

.prose blockquote > *:last-child {
  margin-bottom: 0;
}

.prose hr {
  margin: 2rem 0;
  border: 0;
  border-top: 1px solid var(--separator);
}

.prose strong {
  font-weight: 600;
}

/*
 * Inside an article, the pieces that belong to the chrome go back to its face: code, and a figure's title and caption. A chart's own lettering is monospace, drawn into the SVG where the page's fonts cannot reach, so the title over it and the caption under it match the labels in it. The stack is named rather than left to the browser, which sets `code` and `pre` in its own generic `monospace`, a different and usually worse face.
 */
.prose code,
.prose pre,
.prose .chart-title,
.prose .chart figcaption {
  font-family: var(--font);
  font-variant-ligatures: none;
}

/* 0.8em puts the monospace x-height within a hundredth of an em of the serif around it: 0.8 × 0.526 against 0.431. */
.prose :not(pre) > code {
  padding: 0.125rem 0.25rem;
  background: var(--surface);
  border-radius: 3px;
  font-size: 0.8em;
}

.prose pre.code {
  padding: 1rem;
  background: var(--surface);
  border: 1px solid var(--separator);
  border-radius: var(--radius);
  /*
   * A long line scrolls rather than wraps. Wrapped code hides the fact that a
   * line is too long, and in Python the indentation is the syntax.
   */
  overflow-x: auto;
  font-size: 0.875rem;
  line-height: 1.6;
}

.prose pre.code code {
  padding: 0;
  background: none;
  font-size: inherit;
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875em;
}

.prose th,
.prose td {
  padding: 0.5rem;
  border-bottom: 1px solid var(--separator);
  text-align: left;
  vertical-align: top;
}

.prose th {
  color: var(--text-secondary);
  font-weight: 600;
  border-bottom-color: var(--separator-opaque);
}

/*
 * A table is the one thing here that cannot be made narrower without becoming
 * unreadable, so it is allowed to scroll inside its own box rather than push
 * the page sideways.
 */
.prose .table-scroll {
  margin: 0 0 1rem;
  overflow-x: auto;
}

.prose .table-scroll table {
  margin: 0;
}

/* Display maths is wider than the measure more often than not. */
.prose .katex-display {
  margin: 1rem 0;
  overflow-x: auto;
  overflow-y: hidden;
  padding-block: 0.25rem;
}

/*
 * A figure is an SVG with a baked-in width, so it is scaled down to fit rather
 * than allowed to set the page width. `height: auto` overrides the intrinsic
 * height attribute, which is present only so the space is reserved before the
 * image loads and nothing jumps. Each chart ships two drawings in a
 * <picture>: the desktop one, and one drawn at phone width with its own type
 * size, because a desktop figure shrunk to a phone column halves every label.
 */
.prose .chart {
  margin: 1.5rem 0;
}

.prose .chart picture {
  display: block;
}

.prose .chart img {
  display: block;
  width: 100%;
  max-width: 100%;
  height: auto;
}

/* The title says what is plotted, at the chrome's size and in its heading weight; the caption under the drawing says what it shows. */
.prose .chart-title {
  margin: 0 0 0.5rem;
  color: var(--text-primary);
  font-size: var(--text-size);
  font-weight: 600;
  line-height: var(--line-height);
}

.prose .chart figcaption {
  color: var(--text-tertiary);
  font-size: var(--text-size);
  margin-top: 0.5rem;
}

/* ================================================================ footnotes */

.fn-ref a {
  text-decoration: none;
  padding-inline: 0.125rem;
}

.footnotes {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--separator);
  color: var(--text-secondary);
  font-size: 0.875rem;
}

/* A note is set at 0.8 of the text, the ratio of LaTeX's footnote size to its body. */
.footnotes ol {
  margin: 0;
  padding-left: 4ch;
  font-size: 1rem;
}

.footnotes li {
  margin-bottom: 0.5rem;
}

.footnotes a {
  text-decoration: underline;
}

.fn-back {
  text-decoration: none;
}

/* A note jumped to from the text is marked, or you land on a list and guess. */
.footnotes li:target {
  color: var(--text-primary);
}

/* ================================================================ citations */

/*
 * A citation prints as Wikipedia prints one: a bracketed number, raised, in the link colour. It takes no underline, because the brackets already mark it and a paragraph of underlined numbers reads as noise; the pointer or the keyboard brings one back. Its zero line height keeps a raised number from pushing its line away from the next. It never breaks across lines, so a locator such as [1, O’Connor, J., dissenting] is read whole, and a word joiner ahead of it keeps it on the line of the word it follows, which the line breaker would otherwise leave behind after a period. The joiner is silent to a screen reader where the engine takes alternative text for generated content.
 */
.cite {
  line-height: 0;
  white-space: nowrap;
}

.cite::before {
  content: '\2060';
  content: '\2060' / '';
}

.cite a {
  text-decoration: none;
}

.cite a:hover,
.cite a:focus-visible {
  text-decoration: underline;
}

/* The number a row's back-link returns to is marked, as the row it came from was. */
.cite:target a {
  background: var(--surface-strong);
}

/* ============================================================= bibliography */

/*
 * Set like the notes above it, in the notes' 4ch gutter, but each row opens with the number the text prints, [1], as IEEE numbers a reference list, in place of a list number of its own. The number is the row's link back to the text, in the link colour and without an underline, like the number it answers. It hangs in the gutter as LaTeX's thebibliography hangs a label, right-aligned and 0.5em (\labelsep) short of the entry, so an entry's first line starts where its second does; a number too wide for the gutter grows into the margin rather than into the entry. Because it is positioned out of the line, the space after it in the markup, there for a feed reader, which has no stylesheet, falls at the start of the line, where it collapses. A URL wraps between the parts of its path, where references.ts allows a break, and anywhere inside a part too long for the line rather than push a phone's column wide.
 */
.bibliography {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--separator);
  color: var(--text-secondary);
  font-size: 0.875rem;
}

.bibliography ol {
  margin: 0;
  padding-left: 4ch;
  list-style: none;
  font-size: 1rem;
}

.bibliography li {
  position: relative;
  margin-bottom: 0.5rem;
  overflow-wrap: anywhere;
}

.bibliography li:target {
  color: var(--text-primary);
}

.bibliography a {
  text-decoration: underline;
}

.bibliography .cite-back {
  position: absolute;
  right: 100%;
  margin-right: 0.5em;
  white-space: nowrap;
  text-decoration: none;
}

.bibliography .cite-back:hover,
.bibliography .cite-back:focus-visible {
  text-decoration: underline;
}

/* =================================================================== footer */

.site-footer {
  border-top: 1px solid var(--separator);
  color: var(--text-tertiary);
  font-size: var(--text-size);
}

.site-footer .wrap {
  display: flex;
  align-items: center;
  gap: 0.5rem 2ch;
  min-height: calc(4rem - 1px);
  flex-wrap: wrap;
}

/* Secondary rather than the footer's tertiary, so that at rest the privacy link matches the theme toggle beside it (`.link-button`). On hover each keeps its own kind: the link takes the link colour, the button brightens. */
.site-footer a {
  color: var(--text-secondary);
}

.site-footer a:hover {
  color: var(--link);
}

/*
 * The privacy link and the theme toggle, together at the far end of the footer, opposite the notice. They share one box so that they wrap as a unit, as the header's links do: where the footer is too narrow for all three, the pair drops to a line of its own rather than the toggle dropping alone, and `margin-left: auto` keeps it at the right on either line. The gap matches the footer's own, so on one line all three are spaced alike.
 */
.footer-end {
  display: flex;
  gap: 2ch;
  margin-left: auto;
}

/* =================================================================== narrow */

/*
 * One tier, 767, matching barycenter.wiki and mlot.ai so the numbers are the
 * same across projects. It drops the listing's value column rather than
 * reflowing: the listing is a table, and a wrapped table row is unreadable.
 *
 * Below 767 only the name is worth the width.
 */
@media (max-width: 767px) {
  .cell {
    grid-template-columns: 1fr;
  }

  .col-date {
    display: none;
  }

  /*
   * A timeline without its dates is only a list, so `/resume-preview` keeps them and stacks instead: every role reads as its organisation, the role and its dates, a line each and all flush left. The shared name gives way to the copy each role carries, so an organisation is named on every one of its roles, and with nothing set in, every line between rows runs the full width of the listing.
   */
  .stint {
    display: block;
    padding-left: 0;
  }

  .stint > .col-org {
    display: none;
  }

  .cell > .col-org {
    display: block;
  }

  .stint .cell {
    padding-left: 1ch;
  }

  .stint .col-date {
    display: block;
    text-align: left;
  }

  /*
   * The metadata labels stack above their values. Two columns inside a 320px
   * viewport leaves the value column too narrow to hold a date on one line.
   */
  .post-meta {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .post-meta dt {
    margin-top: 0.5rem;
  }

  .post-meta dt:first-child {
    margin-top: 0;
  }

  /* The title steps down with the column, to 28px on a 40px line. */
  .post-title {
    font-size: 1.75rem;
    line-height: 2.5rem;
  }
}
