/* ET Book — Edward Tufte's Bembo-derived book face, the same one Tufte CSS uses.
   MIT licensed; see static/fonts/et-book/LICENSE. Self-hosted as .woff, which
   every current browser supports, so no second format is needed.

   Roman and bold use lining figures, the italic uses old-style figures — that is
   how the upstream family is cut and how tufte.css pairs them. */

@font-face {
  font-family: "ET Book";
  src: url("/fonts/et-book/et-book-roman-line-figures.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "ET Book";
  src: url("/fonts/et-book/et-book-display-italic-old-style-figures.woff") format("woff");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "ET Book";
  src: url("/fonts/et-book/et-book-semi-bold-old-style-figures.woff") format("woff");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "ET Book";
  src: url("/fonts/et-book/et-book-bold-line-figures.woff") format("woff");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Roboto Mono for the nav only — a mono face reads as an interface element and
   keeps the nav visually separate from the book-set prose below it. OFL 1.1; see
   static/fonts/roboto-mono/LICENSE. Google's latin and latin-ext subsets only,
   ~56KB total rather than the full family; anything outside those ranges (the
   toggle's sun/moon) falls back, as intended.

   These are variable files spanning wght 100-700, which is why there are two of
   them and not one per weight — Google serves the same file for every weight. The
   `font-weight: 100 700` range is what tells the browser that, so do not narrow it
   to a single value or every weight but 400 gets synthesised. */

@font-face {
  font-family: "Roboto Mono";
  src: url("/fonts/roboto-mono/roboto-mono-latin.woff2") format("woff2");
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Roboto Mono";
  src: url("/fonts/roboto-mono/roboto-mono-latin-ext.woff2") format("woff2");
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Warm cream paper with a deep red accent. */
:root {
  --font-body: "ET Book", Palatino, "Palatino Linotype", "Book Antiqua", Georgia, serif;
  --font-nav: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Width of the prose column. Everything that is read line-by-line is capped at
     this so the measure stays constant no matter how wide the page gets; the
     leftover width to its right is the sidenote gutter. */
  --measure: 40em;

  /* The sidenote and the space between it and the prose. Both in rem, not em:
     they are measured against the page, not against the note's smaller type,
     and the collapse breakpoint below is arithmetic on these two numbers. */
  --sidenote-width: 16rem;
  --sidenote-gap:   2.5rem;

  --bg:            #fffffc;
  --fg:            #1a1a14;
  --accent:        #a00000;
  --accent-hover:  #d02020;
  --nav-fg:        #6b6b5e;
  --nav-fg-hover:  #1a1a14;
  --footer-fg:     #8a8a7a;
  /* Sidenotes sit off to one side of the argument, so they are set a step back
     from --fg — but only a step: they are meant to be read, unlike the footer. */
  --sidenote-fg:   #56564a;
  /* Selected text and the mobile tap flash. Kept translucent so the glyphs show
     through at full --fg rather than needing a second colour to stay legible. */
  --selection:     rgba(160, 0, 0, 0.15);
  /* Lifts the resume off the page like a sheet of paper. */
  --paper-shadow:  0 2px 14px rgba(0, 0, 0, 0.14);
}

/* Dark counterpart: the cream inverted to a warm near-black, with the accent
   lifted to stay legible against it. Declared twice — once for the OS
   preference (unless the toggle has explicitly asked for light), once for the
   toggle itself, so either can win. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:            #14140d;
    --fg:            #e8e8dc;
    --accent:        #ff5c5c;
    --accent-hover:  #ff8a8a;
    --nav-fg:        #9a9a8a;
    --nav-fg-hover:  #ffffff;
    --footer-fg:     #6e6e60;
    --sidenote-fg:   #b0b0a2;
    /* Lifted alpha: the same tint reads weaker over a dark ground. */
    --selection:     rgba(255, 92, 92, 0.22);
    /* A drop shadow is invisible against a dark ground, so the sheet gets a
       faint halo instead — it softens the cut-out edge of the white page. */
    --paper-shadow:  0 0 26px rgba(255, 255, 245, 0.07);
  }
}

:root[data-theme="dark"] {
  --bg:            #14140d;
  --fg:            #e8e8dc;
  --accent:        #ff5c5c;
  --accent-hover:  #ff8a8a;
  --nav-fg:        #9a9a8a;
  --nav-fg-hover:  #ffffff;
  --footer-fg:     #6e6e60;
  --sidenote-fg:   #b0b0a2;
  --selection:     rgba(255, 92, 92, 0.22);
  --paper-shadow:  0 0 26px rgba(255, 255, 245, 0.07);
}

/* The flash mobile browsers paint over a link the instant it is tapped. It is an
   inherited property, so setting it once here covers every tappable thing on the
   page; only elements that are actually tap targets ever draw it. */
html {
  -webkit-tap-highlight-color: var(--selection);
}

/* Only the background is set. Leaving `color` alone keeps selected text at --fg,
   which a translucent tint stays legible under in both themes — naming a colour
   here would mean maintaining a second one that has to clear contrast twice.

   Do not fold ::-moz-selection in as a second selector: one unrecognised
   pseudo-element invalidates the whole rule. Firefox has taken the unprefixed
   form since 62, so it is not needed anyway. */
::selection {
  background: var(--selection);
}

body {
  font-family: var(--font-body);
  /* ET Book is a book face with a small x-height — it sets smaller than a sans at
     the same nominal size, so it wants ~18px rather than 16px to read comfortably.
     rem stays anchored to the 16px root, so nav sizing is unaffected. */
  /* font-size: 1.1875rem; */
  font-size: 1.25rem;
  line-height: 1.5;
  font-kerning: normal;
  font-variant-ligatures: common-ligatures;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1.5rem;
  background-color: var(--bg);
  color: var(--fg);
}

/* Prose columns. The page is deliberately far wider than the text: line length
   is fixed by --measure, and the ~500px left over on the right is the gutter
   that sidenotes will float into (Tufte-style, negative margin-right off this
   column). Widening the text itself instead would push past the ~75 characters
   a line can hold before it gets hard to track. */
article,
.page-list {
  max-width: var(--measure);
}

/* Headings in book-typography style: hierarchy comes from size and style rather
   than weight, so they stay at the roman weight and h3 shifts to italic. This is
   how Tufte CSS sets ET Book, and the face is drawn for it — its roman holds a
   large size without needing bold to carry authority. */
h1, h2, h3, h4, h5, h6 {
  /* color: var(--accent); */
  font-weight: 400;
  line-height: 1.2; /* display sizes need tighter leading than body text */
  text-wrap: balance;
  /* Bonded to the text below, separated from the section above. */
  margin: 2em 0 0.6em;
}

h1 {
  font-size: 2.2rem;
  margin-top: 0;
}

h2 {
  font-size: 1.8rem;
}

h3 {
  font-size: 1.3rem;
  font-style: italic;
}

main p {
  margin: 0 0 1em;
  text-wrap: pretty; /* avoids a lone word on the last line */
}

main ul, main ol {
  margin: 0 0 1.1em;
  padding-left: 1.4em;
}

main li {
  margin-bottom: 0.3em;
}

/* Images are capped at the prose column. Without this an image renders at its
   intrinsic pixel size: a screenshot from a 1600px-wide window would cross the
   sidenote gutter and, on a phone, make the whole page scroll sideways.
   height: auto keeps the aspect ratio once width is the binding constraint, and
   display: block drops the descender gap an inline image leaves under itself. */
main img {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 1.6em 0;
}

/* Sidenotes. The note floats into the gutter --measure leaves to the right of
   the prose, starting level with the line that cites it, so it is read where it
   is referenced rather than at the foot of the page.

   The float carries a negative right margin of its own width plus the gap, so
   its margin box occupies -gap of horizontal space in the column: the prose does
   not wrap around it and nothing in the text moves. Setting the margin to
   anything less than the full width would eat into the measure. */
main {
  counter-reset: sidenote;
}

.sidenote {
  float: right;
  clear: right; /* consecutive notes stack instead of landing on each other */
  width: var(--sidenote-width);
  margin-right: calc(-1 * (var(--sidenote-width) + var(--sidenote-gap)));
  margin-top: 0.25rem;
  margin-bottom: 1rem;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--sidenote-fg);
  text-align: left;
}

/* Numbering lives here rather than in the generator, so a note inserted halfway
   through an article renumbers everything after it with no rebuild logic. The
   counter is incremented by the marker, which precedes the note in the markup,
   so both ::after and ::before below read the same value. */
.sidenote-number {
  counter-increment: sidenote;
}

/* position + top rather than vertical-align: super, which grows the line box and
   would open a ragged gap in the leading of any paragraph carrying a note. */
.sidenote-number::after,
.sidenote::before {
  content: counter(sidenote);
  font-size: 0.75em;
  position: relative;
  top: -0.45em;
  color: var(--accent);
}

.sidenote::before {
  padding-right: 0.35em;
}

/* Pure state for the collapsed layout below, never rendered. display: none does
   not stop a <label for> from toggling it, which is the whole trick. */
.sidenote-toggle {
  display: none;
}

/* Below this width there is no gutter left to float into: 40em of prose at the
   1.25rem body size is 800px, plus 16rem + 2.5rem of note and 1.5rem of body
   padding either side, is ~1144px. Under that the note folds back into the text
   and its number becomes the tap target that reveals it. */
@media (max-width: 72rem) {
  .sidenote {
    display: none;
  }

  /* A bare superscript is a ~10px tap target. Vertical padding on an inline box
     overflows the line rather than growing it, and the horizontal padding is
     cancelled by an equal negative margin, so this reaches thumb size without
     moving a single word of the paragraph around it. */
  .sidenote-number {
    cursor: pointer;
    padding: 0.7em 0.4em;
    margin: 0 -0.4em;
  }

  .sidenote-toggle:checked + .sidenote {
    display: block;
    float: none;
    width: auto;
    margin: 0.8rem 0;
    padding-left: 0.9rem;
    /* currentColor, so the rule tracks --sidenote-fg across both themes without
       a fourth token to keep in step. */
    border-left: 1px solid currentColor;
  }
}

/* Header */
header {
  padding: 1.5rem 0 1rem;
  margin-bottom: 2.5rem;
}

/* Three tracks, so the name sits on the page's centre line rather than halfway
   between the two link groups — the outer 1fr columns stay equal however many
   links each side grows to, and the name never drifts when one side gains an
   entry. The groups themselves flex, so adding a link is a template edit only. */
nav {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: baseline;
  gap: 0.5rem 1.75rem;
}

.nav-left, .nav-right {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 1.75rem;
}

.nav-right {
  justify-content: flex-end;
}

nav a {
  font-family: var(--font-nav);
  text-decoration: none;
  color: var(--accent);
  /* Containing block for the underline bar below. */
  position: relative;
  /* Mono faces set wide and even, so they carry at a smaller size than the
     serif they replaced. */
  font-size: 1.2rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.nav-name {
  font-size: 1.8rem;
  font-weight: 400;
}

/* Below this the three tracks cannot hold the groups side by side, so the nav
   stacks: name first, then the two groups centred beneath it. */
@media (max-width: 34rem) {
  nav {
    grid-template-columns: 1fr;
    justify-items: center;
    row-gap: 0.75rem;
  }

  .nav-name {
    order: -1;
  }

  .nav-left, .nav-right {
    justify-content: center;
  }
}

/* An underline that grows from the centre on hover, and stays out on the page you
   are viewing. It is a pseudo-element rather than `text-decoration` so it can be
   animated, and it is laid out at full width and only scaled — so the growth is a
   compositor transform with nothing reflowing, and nothing in the nav shifts.

   `.nav-name` is excluded: it is an `<a>` with no href, the site title rather than
   a link, so there is nothing for a hover state to promise. */
nav a:not(.nav-name)::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.12em;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.18s ease-out;
}

nav a:not(.nav-name):hover::after,
nav a:not(.nav-name):focus-visible::after,
nav a.is-current::after {
  transform: scaleX(1);
}

@media (prefers-reduced-motion: reduce) {
  nav a:not(.nav-name)::after {
    transition: none;
  }
}

/* Theme toggle. The glyph comes from CSS rather than JS so it is already
   correct on first paint instead of being corrected after the script runs. */
.theme-toggle {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  color: var(--accent);
  font-family: inherit;
  font-size: 1.25rem;
  line-height: 1;
  /* Belongs with the \FE0E below: the modern spelling of the same request, for
     engines that honour it. Ignored everywhere else, which is why both are here. */
  font-variant-emoji: text;
}

.theme-toggle:hover {
  /* color: var(--accent-hover); */
}

/* U+2600 has both a text and an emoji presentation, and iOS resolves the bare
   codepoint to Apple Color Emoji — a full-colour sun next to monochrome nav text.
   The trailing U+FE0E is VS15, the variation selector that demands the text form;
   platforms that already drew a text glyph are unaffected. The moon needs no such
   selector: U+263E has no emoji presentation to fall into. */
.theme-toggle::before {
  content: "\2600\FE0E"; /* sun, shown in light mode */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle::before {
    content: "\263E"; /* moon, shown in dark mode */
  }
}

:root[data-theme="dark"] .theme-toggle::before {
  content: "\263E";
}

/* Links in page content. No underline: the accent colour is the whole
   affordance, and hover changes nothing — the same restraint the nav shows,
   where the bar is the only thing that moves. Nothing here needs
   text-underline-offset or -thickness now; those existed to keep the default
   underline off the serif descenders it used to clip. */
main a {
  color: var(--accent);
  text-decoration: none;
}

/* Math. Display formulas are spans (a block element inside cmark's <p> would be
   invalid HTML), so they are blocked out here instead. */
.math-display {
  display: block;
  margin: 1.4em 0;
  overflow-x: auto; /* a long formula scrolls rather than widening the page */
  overflow-y: hidden;
}

/* Resume. Wider than --measure, since a letter page shrunk to the prose column
   would be too small to read; the PDF is not line-by-line text anyway. */
.resume {
  max-width: 900px;
}

/* Matches the PDF's 612x792pt MediaBox, so the frame is exactly page-shaped and
   the viewer has no letterboxing of its own to add. The white background makes
   the sheet appear at once instead of a hole in the page while the PDF loads. */
.pdf-embed {
  aspect-ratio: 612 / 792;
  width: 100%;
  background: #ffffff;
  box-shadow: var(--paper-shadow);
}

.pdf-embed object {
  width: 100%;
  height: 100%;
  display: block;
}

.pdf-download {
  margin-top: 1.25rem;
  font-size: 0.95rem;
  font-style: italic;
}

/* Page list (articles) */
.page-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.page-list li {
  padding: 0 0 2.25rem;
}

.page-list a {
  color: var(--accent);
  text-decoration: none;
  font-size: 1.75rem;
  font-weight: 600; /* size carries these, matching the headings */
  line-height: 1.2;

  /* The nav's centre-out underline, drawn as a background gradient rather than a
     scaled ::after. These titles wrap — "Interactive Real-Time Fluid Simulator"
     takes two lines on a phone — and an absolutely positioned bar has no sane
     containing block across the fragments of a wrapped inline; it would either
     stretch the full column under a short last line or land somewhere arbitrary.
     A background follows the text instead, and box-decoration-break gives each
     line its own bar. Growth is centred because a 0%-wide background image sits
     centred at background-position 50%, then fills as the size animates to 100%.
     The cost is animating background-size instead of a transform, which repaints
     rather than compositing — negligible for a 2px strip, and worth it here. */
  padding-bottom: 0.12em; /* the bar paints inside the padding box, so make room */
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 50% 100%;
  background-size: 0% 2px;
  transition: background-size 0.18s ease-out;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

.page-list a:hover,
.page-list a:focus-visible {
  background-size: 100% 2px;
}

@media (prefers-reduced-motion: reduce) {
  .page-list a {
    transition: none;
  }
}

.page-description {
  margin: 0.3rem 0 0;
  text-wrap: pretty;
}

/* Both fields are optional and always render their element, so collapse the
   empties rather than leaving them to contribute stray margin. */
.page-description:empty,
.page-date:empty {
  display: none;
}

.page-date {
  margin: 0.15rem 0 0;
  /* Flushed to the right edge of the measure, so the dates form their own
     column against the ragged right of the titles and descriptions. */
  text-align: right;
  font-size: 0.9rem;
  font-style: italic; /* the family has a real italic; no faux slant */
  color: var(--footer-fg);
}

/* Footer */
footer {
  margin-top: 3rem;
  padding: 1rem 0;
  color: var(--footer-fg);
  font-size: 0.85rem;
}
