/* paysage: a portfolio. A landing page that says what you do, a grid of work,
   and case studies with room for pictures.

   Every colour is a custom property declared once, below. Nothing below reads
   a raw colour. */

@import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,200..900;1,200..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');

:root {
  --measure: 42rem;
  --wide: 78rem;
  /* Landing page: the news column's width beside the bio. The bio alone
     sets the row's height, so a wider news column (a narrower bio) makes a
     longer one. */
  --news-width: 28rem;
  /* The space between the bio and the news. */
  --news-gap: 48px;
  /* The width below which the news drops under the bio is not a property:
     a media query cannot read one. It is the `max-width` of the media query
     marked "The landing page's breakpoint", near the end. */
  /* The profile logos under the name, on the landing page. */
  --profile-icon-size: 18px;
  /* The space between two news items. */
  --news-item-gap: 0.25rem;
  /* Project page: the width of the column of the date, facts and links.
     The title column beside it takes the rest of the row. */
  --project-side-width: 22rem;

  --fg: #000;
  --muted: #626b78;
  --bg: #fff;
  --raised: #ffffff;
  --accent: #C2410C;
  --accent-soft: #fff5ee;
  --link: #1F4FD1;
  --rule: #e3e2de;
  --shadow: 0 1px 2px rgb(20 22 26 / 5%), 0 12px 28px rgb(20 22 26 / 8%);

  --sx-comment: #6b7280;
  --sx-string: #0f7b3f;
  --sx-number: #b45309;
  --sx-keyword: #b4531f;
  --sx-function: #1d4ed8;
  --sx-variable: #1f2937;
  --sx-type: #0f766e;
  --sx-punctuation: #6b7280;

  color-scheme: light;
}

* {
  box-sizing: border-box;
}

/* Below 400px the page stops shrinking and scrolls sideways instead. A column
   at least the window's height, whose main content takes the slack, so the
   footer sits at the bottom of a short page. */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  min-width: 400px;
  margin: 0;
  font-family: "Crimson Pro", serif;
  font-size: clamp(1rem, 0.97rem + 0.2vw, 1.08rem);
  line-height: 1.5;
  color: var(--fg);
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
}

a {
  color: var(--link);
  text-decoration: none;
}

/* Colour alone marks a link at rest; a thick underline answers the pointer
   and the keyboard. Links styled further down (nav, cards) set their own. */
a:hover,
a:focus-visible {
  text-decoration: underline 2px;
  text-underline-offset: 3px;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 0.25rem;
}

::selection {
  background: var(--accent-soft);
  color: var(--fg);
}

img {
  max-width: 100%;
  height: auto;
}

/* Header ------------------------------------------------------------------ */

/* The name block and the nav share a line, centred on each other, the nav
   flush right. When the two no longer fit, the nav wraps under the name, and
   `space-between` sets a lone item at the start of its line: no breakpoint,
   the content's own width decides. */
.site-header {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem 2rem;
  width: 100%;
  max-width: var(--wide);
  margin-inline: auto;
  padding: 1.5rem 1.5rem 0;
}

/* Off the top of the viewport rather than off to the side, and moved by a
   transform: taking it out of the flow and putting it back reflows the header
   under the reader the moment they tab into the page. */
.skip {
  position: absolute;
  z-index: 20;
  top: 0.5rem;
  left: 0.5rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--raised);
  color: var(--fg);
  text-decoration: none;
  transform: translateY(-250%);
}

.skip:focus {
  transform: none;
}

/* The name and its tagline, one block, which the nav centres against. On the
   landing page the portrait sits to their left at the height of the two
   lines, and both are set larger to give it room. That height is worked out
   from the sizes the lines are set in, rather than measured: an image's own
   height would otherwise set the row it should follow. */
.masthead {
  --brand-size: clamp(1.35rem, 1.2rem + 0.8vw, 1.75rem);
  --tagline-size: 1.05rem;
}

.masthead:has(.avatar) {
  --brand-size: clamp(1.75rem, 1.35rem + 1.75vw, 2.6rem);
  --tagline-size: clamp(1.15rem, 1.05rem + 0.5vw, 1.4rem);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: 1.25rem;
}

.avatar {
  grid-row: span 2;
  height: calc(var(--brand-size) * 1.1 + 0.2rem + var(--tagline-size) * 1.35);
  width: auto;
}

/* With the profile logos under the tagline, the portrait spans them too: their
   row is the logos' `--profile-icon-size` plus the 8px above them.

   Its top is the name's line box, which starts 0.2em above the name's
   tallest glyphs (Crimson Pro: ascent 0.90em, ascenders 0.69em, line-height
   1.1 adds no leading). The logos' row ends at their ink, so the portrait
   runs that same 0.2em past them, and gives it back as a negative margin so
   the rows keep their height. */
.masthead:has(.profile-links) .avatar {
  --overhang: calc(var(--brand-size) * 0.2);
  grid-row: span 3;
  height: calc(var(--brand-size) * 1.1 + 0.2rem + var(--tagline-size) * 1.35 + 8px + var(--profile-icon-size) + var(--overhang));
  margin-bottom: calc(-1 * var(--overhang));
}

.avatar~* {
  grid-column: 2;
}

.brand {
  display: block;
  font-size: var(--brand-size);
  font-weight: 650;
  line-height: 1.1;
  letter-spacing: -0.025em;
  white-space: nowrap;
  color: var(--fg);
  text-decoration: none;
}

.site-tagline {
  margin: 0.2rem 0 0;
  color: var(--muted);
  font-size: var(--tagline-size);
  line-height: 1.35;
}

/* The landing page's profile logos, a row under the tagline in the tagline's
   grey, under the name rather than the portrait: the portrait spans the first
   two rows only. Each logo answers the pointer in link blue. */
.profile-links {
  display: flex;
  gap: 16px;
  margin: 8px 0 0;
  line-height: 0;
}

.profile-links a {
  color: var(--muted);
}

.profile-links a:hover,
.profile-links a:focus-visible {
  color: var(--link);
}

.profile-links svg {
  display: block;
  width: var(--profile-icon-size);
  height: var(--profile-icon-size);
}

/* Page to page, the name and tagline glide between their landing-page place
   and size and their everyday ones, and the nav holds still while the rest
   cross-fades. A cross-document view transition: the browser snapshots each
   named element on the old page and the new one and animates between the two
   boxes, so the boxes hug the text and the snapshot scales without stretching.
   A browser without it simply loads the page. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

.brand,
.site-tagline {
  width: fit-content;
}

.brand {
  view-transition-name: brand;
}

.site-tagline {
  view-transition-name: tagline;
}

.top-nav {
  view-transition-name: top-nav;
}

::view-transition-group(brand),
::view-transition-group(tagline) {
  animation-duration: 0.45s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

.top-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.5rem;
  font-size: 1.05rem;
}

.top-nav a {
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}

/* The path's slash, drawn rather than written, so each item stays one link.
   An inline-block keeps the current item's underline off it, and the empty
   alt text keeps screen readers from reading it out. */
.top-nav a::before {
  content: "/" / "";
  display: inline-block;
  margin-right: 0.5rem;
  color: var(--muted);
  opacity: 0.55;
}

.top-nav a:hover {
  color: var(--fg);
}

.top-nav a[aria-current] {
  color: var(--accent);
  text-decoration: underline 2px var(--accent);
  text-underline-offset: 0.25em;
}

/* Content shell -----------------------------------------------------------
   One width for the whole page, so the prose starts under the brand rather
   than centred inside a column the header never drew. What reads at a measure
   is capped where it sits, and the grid pages simply do not cap. */

/* Running text is justified, with hyphenation to keep the gaps even; the
   page's `lang` tells the browser which dictionary to break words with. */
.content p,
.content li,
.content .collapsible {
  text-align: justify;
  hyphens: auto;
}

.content {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--wide);
  margin-inline: auto;
  padding: 3.5rem 1.5rem 5rem;
}

.content>.prose {
  max-width: var(--measure);
}

/* Hero -------------------------------------------------------------------- */

/* The headline wraps short and the prose under it reads at the measure, so the
   cap belongs on each of them and never on the section: capping the section
   caps the prose too, at a width no paragraph should be set to. */
.hero h1 {
  max-width: 20ch;
}

.hero>.tagline {
  max-width: 34ch;
}

.hero h1 {
  font-size: clamp(2.2rem, 1.6rem + 3vw, 3.6rem);
  line-height: 1.05;
  letter-spacing: -0.035em;
  margin: 0 0 1rem;
  text-wrap: balance;
}

.tagline {
  margin: 0 0 1.25rem;
  color: var(--muted);
  font-size: 1.15rem;
  line-height: 1.5;
}



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

/* On the landing page the bio shares the row with the news: the bio at a
   short measure, the news in what is left. Below 720px the news drops under
   the bio, at the end of the media queries further down. The news fills to
   `--news-width` first and the bio takes the rest of the row, so at full
   width it is `--wide` less the news, the gap and `.content`'s side padding,
   and it only ever shrinks with the window. */
.hero:has(.news) {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--news-width));
  gap: var(--news-gap);
  align-items: start;
}

/* Beside the news, the row sets the bio's width, not `--measure`. */
.hero:has(.news)>.hero-body {
  max-width: none;
}

/* The bio alone sets the row's height: `contain: size` keeps the news from
   adding to it, and the news stretches to fill it. Its list takes what the
   heading, the latest piece and `All news` leave, and shows as many whole
   lines as fit: the list wraps into columns, each a full width, so a line
   that would not fit moves to a second column, out of sight. */
.news {
  contain: size;
  align-self: stretch;
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 20px 20px 16px;
  background: var(--accent-soft);
  /* Beside the bio, the box stops short of the bio's last line: a stretched
     grid item's margins come out of its height. */
  margin-bottom: 4rem;
}

/* The heading is a small tab pinned to the box's top-right corner, so it
   marks the section without a line of its own. */
.news-title {
  position: absolute;
  top: 0;
  right: 0;
  margin: 0;
  padding: 2px 10px 4px;
  background: var(--accent);
  color: var(--bg);
  font-size: 15px;
  font-weight: 750;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.08em;
  line-height: 1.3;
}

.news p {
  margin: 0;
  text-align: left;
  hyphens: manual;
}

.news-year {
  color: var(--muted);
  font-variant-caps: all-small-caps;
  font-variant-numeric: tabular-nums oldstyle-nums;
}

/* Each title links to its piece on the news page, in the text's own colour:
   the underline answers the pointer, as for any link. */
.news-headline a,
.news-list a {
  color: inherit;
}

/* The latest piece: year, title, one sentence, behind an accent bar. */
.news-latest {
  padding-left: 14px;
  border-left: 2px solid var(--accent);
}

.news .news-headline {
  font-size: 19px;
  font-weight: 600;
  line-height: 1.3;
  margin-block: 0.1rem 0.3rem;
}

.news-summary {
  color: var(--muted);
  font-size: 15px;
  line-height: 1.5;
}

/* The next few, one line each, the years in a column of their own, packed
   at the top: the room left under the last whole item stays above `All
   news`. */
.news-list {
  flex: 1 1 0;
  display: flex;
  flex-flow: column wrap;
  justify-content: flex-start;
  gap: var(--news-item-gap) 48px;
  overflow: hidden;
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
  font-size: 15.5px;
  line-height: 1.45;
}

.content .news-list li {
  width: 100%;
  margin: 0;
  text-align: left;
}

.news-list a {
  display: grid;
  grid-template-columns: 44px 1fr;
}

/* On hover, the year alone is underlined, not the title beside it. The latest
   piece's year sits outside its link and follows the headline's. */
.news-list a:hover,
.news-list a:focus-visible,
.news-headline a:hover,
.news-headline a:focus-visible {
  text-decoration: none;
}

.news-list a:hover>.news-year,
.news-list a:focus-visible>.news-year,
.news-latest:has(.news-headline a:hover, .news-headline a:focus-visible) .news-year {
  text-decoration: underline 2px;
  text-underline-offset: 3px;
}

.news .news-more {
  margin-top: 0.9rem;
  font-size: 15.5px;
  font-style: italic;
}

.hero-body> :first-child {
  margin-top: 0;
}

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

.hero-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1.75rem;
}

.hero-link {
  padding: 0.45rem 1rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--raised);
  color: var(--fg);
  text-decoration: none;
  font-size: 0.95rem;
}

.hero-link:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* Below the bio and news, the rest of the landing page runs full width: a
   project grid in it should not stop at the prose measure. */
.content>.home-rest {
  max-width: none;
  margin-top: 2.5rem;
}

/* Work grid --------------------------------------------------------------- */

.work {
  margin-top: 4.5rem;
}

.section-title {
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 1.25rem;
}

/* The years beside the rows: a rule with a dot per year, the one jumped to
   filled with the accent (a `<style>` beside the list pairs each link with
   its row). It stays in view while the rows scroll past. */
.work-timeline {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 2.5rem;
}

.year-nav {
  position: sticky;
  top: 1.5rem;
  align-self: start;
  padding-block: 0.8rem;
}

.year-nav ol {
  margin: 0;
  padding: 0.4rem 0 1.5rem;
  border-left: 1px solid var(--rule);
  list-style: none;
}

.year-nav a {
  position: relative;
  display: block;
  padding: 0.3rem 0 0.3rem 1rem;
  color: var(--muted);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

.year-nav a::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -4px;
  width: 7px;
  height: 7px;
  border: 1px solid var(--muted);
  border-radius: 50%;
  background: var(--bg);
  transform: translateY(-50%);
}

.year-nav a:hover {
  color: var(--fg);
}

.work-year-group {
  scroll-margin-top: 1rem;
}

/* Where scroll-driven animations exist, the year being read is lit as the
   page scrolls: each year's group is a view timeline (named beside the list)
   seen through a 1px line 5rem below the top of the window, and its link
   plays a one-frame animation while the group crosses that line. Before the
   newest group reaches the line, its link is lit already. */
@supports (animation-timeline: view()) {
  .work-year-group {
    view-timeline-axis: block;
    view-timeline-inset: 5rem calc(100% - 5rem - 1px);
  }

  .year-nav a {
    animation: year-current linear none;
    animation-range: cover;
  }

  .year-nav a::before {
    animation: year-current-dot linear none;
    animation-range: cover;
  }

  .year-nav li:first-child a,
  .year-nav li:first-child a::before {
    animation-fill-mode: backwards;
  }
}

@keyframes year-current {

  from,
  to {
    color: var(--accent);
    font-weight: 650;
  }
}

@keyframes year-current-dot {

  from,
  to {
    border-color: var(--accent);
    background: var(--accent);
  }
}

@media (prefers-reduced-motion: no-preference) {
  html:has(.year-nav) {
    scroll-behavior: smooth;
  }
}

/* One row per work: the picture on the left when there is one, then the
   title with its links beside it, the authors and the summary, stacked. */
.work-list {
  display: grid;
  list-style: none;
  margin: 0;
  padding: 0;
}

.work-row {
  display: grid;
  grid-template-columns: minmax(0, 9rem) minmax(0, 1fr);
  gap: 1.25rem;
  padding-block: 0.8rem;
}

/* The picture is letterboxed rather than cropped: most are figures, and a
   figure cut at the edges loses its axes. A bar laid across its top carries
   the year and, for a paper, the venue. */
.work-cover {
  position: relative;
  display: block;
  align-self: start;
  aspect-ratio: 4 / 3;
  /* border: 1px solid var(--rule); */
  background: var(--raised);
  overflow: hidden;
}

.work-cover:not(:has(img)) {
  background: var(--accent-soft);
}

.work-cover img {
  width: 100%;
  height: 100%;
  padding: 0.4rem;
  object-fit: contain;
}

.work-year {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding: 0.05rem 0.45rem;
  text-align: center;
  background: var(--fg);
  color: var(--bg);
  font-family: "IBM Plex Mono", monospace;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: 0.02em;
}

.work-body {
  display: grid;
  gap: 0.3rem;
  align-content: start;
}

.work-body p {
  margin: 0;
  text-align: start;
}

/* The title on the left, its pills pushed to the right; they fall under the
   title when the line is too short for both. */
.work-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.75rem;
}

/* The pills pushed to the right edge. */
.work-head .work-links {
  justify-content: flex-end;
  margin-top: 0 !important;
  margin-left: auto;
}

/* Where the news sits beside the bio, the pills hold one line and the title
   wraps beside them instead, the pills level with its first line. */
@media (width > 68rem) {
  .work-head {
    flex-wrap: nowrap;
    align-items: flex-start;
  }

  .work-title {
    min-width: 0;
  }

  .work-head .work-links {
    flex: none;
    flex-wrap: nowrap;
    margin-top: 0.1rem !important;
  }
}

.work-title {
  color: var(--fg);
  font-size: 1.15rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.01em;
  text-decoration: none;
}

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

.work-summary {
  color: var(--muted);
}

/* The year as text, before the title, for when the picture is hidden. */
.work-year-inline {
  display: none;
  margin-right: 0.2em;
  color: var(--muted);
  font-family: "IBM Plex Mono", monospace;
  font-size: 0.85em;
  font-weight: 400;
}

.work-authors {
  font-size: 0.95rem;
  line-height: 1.45;
}

.work-authors a {
  color: inherit;
  text-decoration: none;
}

.work-authors a:hover {
  color: var(--link);
  text-decoration: underline;
}

.work-me {
  font-weight: 650;
  text-decoration: underline 1px var(--rule);
  text-underline-offset: 0.2em;
}

.work-summary {
  font-size: 0.95rem;
  line-height: 1.5;
}

.work-links,
.work-stack {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.35rem !important;
}

/* The pills, smaller than on a project page, so a row stays a row. */
.work-links .links-icon {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  margin: 0;
  padding: 0.15rem 0.65rem;
  font-size: 0.8rem;
  line-height: 1.4;
  text-decoration: none;
}

.work-links .links-icon svg {
  height: 1.1em;
  width: auto;
}

.work-links .links-icon:hover {
  background-color: var(--accent);
}

/* On a phone the picture goes: a figure that small says little, and the
   text gets the full width. The year it carried moves before the title. */
@media (max-width: 40rem) {
  .work-row {
    grid-template-columns: 1fr;
  }

  .work-cover {
    display: none;
  }

  .work-timeline {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  /* The years turn into a horizontal bar above the works, stuck to the top
     of the window as they scroll: the rule runs along its top, a dot over
     each year. */
  .year-nav {
    top: 0;
    z-index: 1;
    margin-inline: -1.5rem;
    padding: 0.75rem 1.5rem 0.5rem;
    background: var(--bg);
  }

  .year-nav ol {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0;
    border-top: 1px solid var(--rule);
    border-left: 0;
  }

  .year-nav a {
    padding: 0.55rem 0 0;
  }

  .year-nav a::before {
    top: -4px;
    left: 50%;
    transform: translateX(-50%);
  }

  .work-year-group {
    scroll-margin-top: 3.5rem;
  }

  .work-head .work-year-inline {
    display: inline;
  }
}

.chip {
  padding: 0.1rem 0.55rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.78rem;
  text-decoration: none;
  white-space: nowrap;
}

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

.more {
  margin-top: 1.5rem;
  font-size: 0.95rem;
}

.more a {
  text-decoration: none;
}

.more a:hover {
  text-decoration: underline;
}

/* Project ----------------------------------------------------------------- */

/* The article runs full width. The article is a container so a floating
   figure can size itself to the room the measure leaves beside the text.
   Its write-up takes the article's whole width until there is room for
   figures in the margin, and reads at the measure from then on (see the
   `@container` query below). */
.project {
  container-type: inline-size;
}

/* A figure with `placement: auto, scope: "parent"` breaks out of the measure
   to the article's full width, below any figure hanging in the margin, its
   picture centred on the article and only as wide as it needs to be. */
.figure-wide {
  clear: right;
  width: 100cqi;
}

/* A figure with `placement: auto` hangs right of the text, its top level
   with the paragraph it follows, over the room between the measure and the
   article's edge. Two in a row stack rather than overlap. Too narrow a page
   for that room keeps it in the text, and lets the text run the article's
   whole width. */
@container (min-width: 68rem) {
  .project>.prose {
    max-width: var(--measure);
  }

  .figure-aside {
    --gap: 2.5rem;
    --room: calc(100cqi - var(--measure) - var(--gap));
    float: right;
    clear: right;
    width: var(--room);
    margin-right: calc(-1 * (var(--room) + var(--gap)));
  }

  .figure-aside>figure {
    margin: 0 0 1.5rem;
  }
}

/* The title set at the size of the site's name in the header of a page
   other than the landing one (`.masthead`'s `--brand-size`). */
.project-head h1 {
  font-size: clamp(1.35rem, 1.2rem + 0.8vw, 1.75rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  margin: 0 0 0.75rem;
  text-wrap: balance;
}

/* The title, authors and tagline on the left, over the rest of the row;
   the date, facts and pills on the right, at `--project-side-width`. On a
   narrow screen the facts go under them. */
.project-head {
  display: grid;
  grid-template-columns: 1fr var(--project-side-width);
  align-items: start;
  gap: 1rem 2.5rem;
  margin-bottom: 2.5rem;
}

.project-title {
  min-width: 0;
}

.facts {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  margin: 0;
  padding-top: 0.5rem;
}

/* A page with no facts beyond its date leaves no gap for them. */
.facts:empty {
  display: none;
}

/* Each fact on one line: its label, then its value. */
.fact {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.6rem;
}

/* The label at the value's size, set apart by colour alone. */
.fact dt {
  color: var(--muted);
}

.fact dd {
  margin: 0;
}

.fact-stack {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.cover {
  margin: 0 0 2.5rem;
}

.cover img {
  width: 100%;
  border-radius: 0.9rem;
}

.next-project {
  margin-top: 4rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

.next-project a {
  display: grid;
  gap: 0.25rem;
  color: var(--fg);
  text-decoration: none;
}

.next-label {
  color: var(--muted);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.next-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1.4rem;
  font-weight: 650;
  letter-spacing: -0.02em;
}

.next-project a:hover .next-title {
  color: var(--accent);
}

/* Links to paper, code... on a project page, under its title or in its
   prose. A touch larger than in a listing's rows. */
.links-icon {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.2rem 0.75rem;
  background-color: var(--fg);
  border-radius: 999px;
  color: var(--bg);
  font-family: "IBM Plex Mono", monospace;
  font-size: 0.85rem;
  font-weight: 500;
  line-height: 1.4;
  text-decoration: none;
}

.links-icon svg {
  height: 1.15em;
  width: auto;
}

.links-icon:hover {
  background-color: var(--accent);
}

/* Pills written inline in a page's prose keep apart from their neighbours. */
.prose .links-icon {
  margin: 0.15rem 0.2rem;
  vertical-align: middle;
}

/* The paper's authors, one line of names under the title, then their
   institutions, smaller and greyed, each after its footnote number. A name
   and its numbers, or an institution and its number, never split over two
   lines. */
.content .paper-authors,
.content .paper-affiliations {
  margin: 0;
  text-align: left;
}

.paper-authors span,
.paper-affiliations span {
  white-space: nowrap;
}

.content .paper-affiliations {
  margin-top: 0.25rem;
  color: var(--muted);
  font-size: 0.85rem;
}

.paper-affiliations span {
  margin-right: 0.75rem;
}

.paper-authors sup,
.paper-affiliations sup {
  margin-left: 0.1em;
  font-size: 0.7em;
}

.paper-affiliations sup {
  margin: 0 0.15em 0 0;
}

/* The column beside the title, flush right: the facts, then the date and
   the pills under them. */
.project-side {
  display: grid;
  justify-items: end;
  gap: 1rem;
}

.project-date {
  margin-inline-end: 0.4rem;
}

/* Too narrow for the title and the column side by side without squeezing
   the title onto short lines: the column goes under the title, its full
   width, the date and the pills on one row, the pills at its end. */
@media (max-width: 56rem) {
  .project-head {
    grid-template-columns: 1fr;
  }

  .project-side {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem 1.5rem;
  }

  .project-side .facts {
    padding-top: 0;
  }
}

/* The date and the pills flush right, in the column beside the title and
   on their own row under it alike; a row that wraps keeps them there. */
.project-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.4rem;
  margin-inline-start: auto;
}

/* Prose ------------------------------------------------------------------- */

.prose h2,
.prose h3 {
  margin-block: 2.2rem 0.5rem;
  line-height: 1.25;
  letter-spacing: -0.015em;
}

.prose h2 {
  font-size: 1.4rem;
}

.prose h3 {
  font-size: 1.12rem;
}

.prose h2>a,
.prose h3>a {
  color: inherit;
  text-decoration: none;
}

/* A heading with its link on the same line, the link at the right edge. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  margin-block: 2.2rem 0.5rem;
}

.prose .section-head h2 {
  margin: 0;
}

.section-more a {
  text-decoration: none;
}

.section-more a:hover {
  text-decoration: underline;
}

p,
ul,
ol {
  margin-block: 0 1.15rem;
}

blockquote {
  margin-inline: 0;
  padding-inline-start: 1.1rem;
  border-inline-start: 3px solid var(--accent);
  color: var(--muted);
  font-size: 1.05rem;
}

/* A figure's picture and caption centred in the column it is given, in the
   text or in the margin beside it. */
figure {
  margin-inline: 0;
  text-align: center;
}

/* A picture given a width is set as a block, which `text-align` leaves be. */
figure>img {
  margin-inline: auto;
}

figcaption {
  margin-top: 0.5rem;
  color: var(--muted);
  font-size: 0.88rem;
}

hr {
  margin-block: 2.5rem;
  border: 0;
  border-top: 1px solid var(--rule);
}

[role="doc-endnotes"] {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-size: 0.9rem;
}

pre {
  padding: 1rem 1.1rem;
  border: 1px solid var(--rule);
  border-radius: 0.7rem;
  background: var(--raised);
  overflow-x: auto;
  line-height: 1.55;
}

code {
  font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace;
  /* Monospace reads larger at a given size, so match the x-height instead of
     shrinking the text. */
  font-size: 1em;
  font-size-adjust: ex-height 0.53;
}

:not(pre)>code {
  padding: 0.1em 0.35em;
  border-radius: 0.35em;
  background: var(--accent-soft);
}

.sx-comment {
  color: var(--sx-comment);
  font-style: italic;
}

.sx-string,
.sx-escape {
  color: var(--sx-string);
}

.sx-number,
.sx-constant {
  color: var(--sx-number);
}

.sx-keyword,
.sx-operator {
  color: var(--sx-keyword);
}

.sx-function,
.sx-tag,
.sx-heading,
.sx-label {
  color: var(--sx-function);
}

.sx-variable,
.sx-parameter,
.sx-property,
.sx-attribute {
  color: var(--sx-variable);
}

.sx-type,
.sx-namespace {
  color: var(--sx-type);
}

.sx-punctuation {
  color: var(--sx-punctuation);
}

.sx-invalid {
  color: var(--sx-keyword);
  text-decoration: underline wavy;
}

.sx-strong {
  font-weight: 700;
}

.sx-emph {
  font-style: italic;
}

.sx-link {
  text-decoration: underline;
}

.sx-raw {
  font-style: normal;
}

/* A table wider than the column scrolls rather than crushing its cells. The
   block box caps at the column while its contents keep their natural width,
   which is what makes the overflow scrollable without a wrapper element the
   theme has no way to add. */
table {
  display: block;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  margin-block: 1.5rem;
  font-size: 0.95em;
}

th,
td {
  padding: 0.5rem 0.85rem;
  text-align: left;
  border-bottom: 1px solid var(--rule);
}

thead th {
  border-bottom: 2px solid var(--rule);
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

/* Not found --------------------------------------------------------------- */

.notfound-code {
  margin: 0;
  color: var(--rule);
  font-size: clamp(4rem, 3rem + 8vw, 8rem);
  font-weight: 650;
  line-height: 1;
  letter-spacing: -0.05em;
}

.notfound .hero-links {
  margin-top: 2rem;
}

/* Pager and footer -------------------------------------------------------- */

.pager {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 3rem;
  font-size: 0.95rem;
}

.pager .next {
  margin-left: auto;
}

.site-footer {
  width: 100%;
  max-width: var(--wide);
  margin-inline: auto;
  padding: 1.5rem 1.5rem 0.75rem;
  color: var(--muted);
  font-size: 0.88rem;
}

.footer-line {
  margin: 0;
  text-align: center;
}

.footer-sep {
  margin-inline: 0.6em;
}

.feeds a+a {
  margin-left: 0.75rem;
}

/* Collapsible ------------------------------------------------------------- */

/* The summary is the opening of the passage and always shows, in link blue
   behind a triangle. Folded, it stands alone; unfolded, the content runs on
   after it in the same paragraph. The triangle turns, and the content fades
   in and out.

   The markup leaves the `<details>` closed, which folds it on a phone. Past
   the narrow breakpoint `open` reads the other way round, so the passage
   starts unfolded and a click folds it, with no script to set `open` from the
   viewport.

   Folded content is taken out with `display: none` rather than the browser's
   own `content-visibility`, since the unfolded content is `display: contents`
   and that has no box to make invisible. Unfolding renders the body afresh,
   which is where `@starting-style` fades it in from; folding holds `display`
   back with `allow-discrete` until the body has faded out. Only a click fades
   it: the transitions want the summary focused, which a click does and a page
   load does not. The motion is dropped with the rest under
   `prefers-reduced-motion`. */
/* The pretty-printed HTML breaks the line between `</summary>` and the body,
   which reads as a space before the body's first character. That whitespace
   is set at size zero and the body put back at the passage's own size, which
   the registered property holds as a length worked out at `.collapsible`,
   where `1em` is still the surrounding text's size. */
@property --collapsible-size {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

.collapsible {
  --collapsible-size: 1em;
  margin-block: 0 1.15rem;
}

.collapsible::details-content {
  font-size: 0;
}

.collapsible-body {
  font-size: var(--collapsible-size);
}

/* Typst sets a multi-paragraph body as `<p>`s, and a block would break the
   line after the summary. The first paragraph runs on inline; the next one
   takes back the gap the inline paragraph's margin no longer gives. */
.collapsible-body>p:first-child {
  display: inline;
}

.collapsible-body>p:first-child+* {
  margin-top: 1.15rem;
}

.collapsible>summary {
  display: inline;
  cursor: pointer;
  color: var(--link);
}

.collapsible>summary::before {
  content: "▸" / "";
  display: inline-block;
  margin-right: 0.3em;
  transition: transform 0.2s ease;
}

.collapsible::details-content {
  content-visibility: visible;
  display: contents;
}

.collapsible:focus-within::details-content {
  transition: display 0.3s allow-discrete;
}

.collapsible:focus-within .collapsible-body {
  transition: opacity 0.3s ease-out;
}

/* Folded: on a phone when closed, the markup's state. */
.collapsible[open]>summary::before {
  transform: rotate(90deg);
}

.collapsible:not([open])::details-content {
  display: none;
}

.collapsible:not([open]) .collapsible-body {
  opacity: 0;
}

/* Folded: past the breakpoint when open, the other way round. */
@media (width > 40rem) {
  .collapsible:not([open])>summary::before {
    transform: rotate(90deg);
  }

  .collapsible[open]>summary::before {
    transform: none;
  }

  .collapsible:not([open])::details-content {
    display: contents;
  }

  .collapsible:not([open]) .collapsible-body {
    opacity: 1;
  }

  .collapsible[open]::details-content {
    display: none;
  }

  .collapsible[open] .collapsible-body {
    opacity: 0;
  }
}

/* Where unfolding fades in from. Last, so it outranks the unfolded opacity
   above at equal specificity. */
@starting-style {
  .collapsible:focus-within .collapsible-body {
    opacity: 0;
  }
}

/* Narrow screens ---------------------------------------------------------- */

@media (max-width: 40rem) {
  .content {
    padding-block: 2.25rem 3.5rem;
  }

  /* A phone is already narrower than the headline's own measure, so the cap
     only costs it a line. */
  .hero h1,
  .hero>.tagline {
    max-width: none;
  }

  .work {
    margin-top: 3rem;
  }

  .project-head {
    margin-bottom: 1.5rem;
  }
}

/* The landing page's breakpoint: below it, the news drops under the bio.
   A media query cannot read a custom property, so this is the one place to
   change it. */
@media (max-width: 68rem) {

  /* Narrower, the pills always go under the title, lined up with it. */
  .work-head {
    flex-direction: column;
    align-items: flex-start;
  }

  .work-head .work-links {
    justify-content: flex-start;
    margin-left: 0;
  }

  .hero:has(.news) {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 1rem;
  }

  /* The bio's own trailing margins would add to the gap: the collapsible's,
     and its last paragraph's, which runs out through it. */
  .hero-body> :last-child,
  .hero-body> :last-child> :last-child> :last-child {
    margin-bottom: 0;
  }

  /* Under the bio, the news has its own height again, and the latest piece
     plus three. */
  .news {
    contain: none;
    margin-bottom: 0;
  }

  .news-list {
    flex: none;
  }

  .news-list li:nth-child(n + 4) {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
    animation: none !important;
  }
}

@media print {

  .site-header,
  .site-footer,
  .next-project,
  .pager {
    display: none;
  }

  body {
    color: #000;
    background: #fff;
  }
}

/* News page ---------------------------------------------------------------
   The pieces run the full width of the page on a timeline: the dates in a
   column on the left, a line beside them with a dot at each piece, and the
   headings and text to the right. */
.content>.prose.news-page {
  max-width: none;
  --timeline-date: 8.5rem;
  --timeline-gap: 1.5rem;
  --timeline-dot: 9px;
}

.news-entry {
  display: grid;
  grid-template-columns: var(--timeline-date) minmax(0, 1fr);
}

/* The date column carries the line, so the line runs unbroken from one piece
   to the next; the space between pieces is padding, not margin. */
.news-entry>time {
  position: relative;
  padding-right: var(--timeline-gap);
  border-right: 2px solid var(--rule);
  color: var(--muted);
  text-align: right;
  /* On the heading's first line. */
  font-size: 0.95rem;
  line-height: 1.25;
  padding-top: 0.2rem;
}

.news-entry>time::after {
  content: "";
  position: absolute;
  top: 0.45rem;
  right: calc(var(--timeline-dot) / -2 - 1px);
  width: var(--timeline-dot);
  height: var(--timeline-dot);
  border-radius: 50%;
  background: var(--accent);
}

.news-entry-body {
  padding-left: var(--timeline-gap);
}

.prose .news-entry-body>h2 {
  margin-block: 0;
}

.news-entry-body>h2+p {
  margin-top: 0;
}

@media (max-width: 560px) {
  .content>.prose.news-page {
    --timeline-date: 5.5rem;
    --timeline-gap: 1rem;
  }

  .news-entry>time {
    font-size: 0.85rem;
  }
}