/* ==========================================================================
   Alan Yin — portfolio
   Monochrome, typographically led. Two families, one grid, no ornament.

   Editing notes:
   - Every colour is a token on :root. Dark mode redefines the same tokens.
   - Two layout primitives, and only two:
       .work-card  metadata | text | media       — the work deck
       .record     metadata | text               — education, awards, leadership
   - Deliberately absent: shadows, radii, gradients, accent colour, icons.
     If you add one, add it everywhere or not at all.
   ========================================================================== */

/* --- Tokens --------------------------------------------------------------- */

:root {
  color-scheme: light;

  --bg:      #fdfdfc;
  --fg:      #17171a;
  --muted:   #6b6b70;
  --rule:    #e3e3df;

  /* Figures hold light-background CAD renders, so their plate stays light in
     both themes. A white render on a black page reads as a mistake. */
  --plate:        #f2f2ef;
  --plate-rule:   #e0e0dc;

  --serif: "Newsreader", Georgia, "Times New Roman", serif;
  --mono:  "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --measure: 68ch;    /* comfortable line length for running prose */
  --shell:   84rem;   /* 1344px — the page's outer width */
  --col:     10.5rem; /* metadata column */
  --rail:    11rem;   /* contents rail */
  --cardh:   60vh;    /* height of a full-size work card */
  --bar:     3.4rem;  /* height of a collapsed work card */

  --s1: 0.5rem;
  --s2: 1rem;
  --s3: 1.5rem;
  --s4: 2.5rem;
  --s5: 4rem;
  --s6: 6rem;
}

/* Light only, for now. Following the operating system meant the site opened
   white or black depending on the visitor's settings, which made it feel
   inconsistent. The dark palette is kept below and can be switched back on
   either by restoring the prefers-color-scheme query above it or by putting
   data-theme="dark" on <html>. */

:root[data-theme="dark"] {
  --bg:    #121213;
  --fg:    #ececea;
  --muted: #8e8e92;
  --rule:  #2a2a2c;
}

/* --- Base ----------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 5rem;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--serif);
  font-size: 1.0625rem;
  font-optical-sizing: auto;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.015em;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0 0 var(--s2); max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

a { color: inherit; text-decoration: none; }

/* Prose links are underlined; navigation and entry links are not, so that
   running text reads as running text. */
a.link,
.prose a:not(.tag) {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--rule);
  transition: text-decoration-color 120ms linear;
}
a.link:hover,
.prose a:not(.tag):hover { text-decoration-color: currentColor; }

:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 3px;
}

img, video { max-width: 100%; height: auto; display: block; }

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

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

/* --- Utilities ------------------------------------------------------------ */

.wrap {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: 1rem;
}

@media (min-width: 40em) {
  .wrap { padding-inline: 2rem; }
}

@media (min-width: 90em) {
  .wrap { padding-inline: 3rem; }
}

/* On a large monitor, let the page grow rather than stranding it in the
   middle of the screen. The prose measure still caps line length, so this
   widens the layout without making anything harder to read. */
@media (min-width: 100em) {
  :root { --shell: 94rem; }
}

.mono {
  font-family: var(--mono);
  font-size: 0.8125rem;
  line-height: 1.5;
  letter-spacing: 0.01em;
}

.label {
  font-family: var(--mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--muted);
}

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

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: var(--s1) var(--s2);
  background: var(--fg);
  color: var(--bg);
  z-index: 100;
}
.skip:focus { left: 0; }

/* Unfilled copy. Obvious on the page, invisible once replaced.
   Find every one with:  grep -rn TODO . */
.placeholder {
  color: var(--muted);
  font-style: italic;
}

/* --- Reveal on scroll -----------------------------------------------------
   Opt-in: site.js puts .js-reveal on <html>, so without JavaScript every
   element is simply visible. Honours prefers-reduced-motion.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  html.js-reveal .reveal {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 600ms cubic-bezier(0.22, 1, 0.36, 1),
                transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  html.js-reveal .reveal.is-visible {
    opacity: 1;
    transform: none;
  }
}

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

.masthead {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  min-height: 3.5rem;
}

.wordmark {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.nav { display: flex; align-items: center; gap: var(--s3); }

.nav a {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding-block: 0.25rem;
  transition: color 120ms linear;
}
.nav a:hover { color: var(--fg); }

.nav__toggle {
  display: none;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 0.5rem 0;
  cursor: pointer;
}

/* Progressive enhancement. Without JavaScript the nav simply wraps under the
   wordmark and every link stays reachable; site.js marks the masthead
   enhanced, and only then does the nav collapse behind the toggle. */
@media (max-width: 47.99em) {
  .masthead__inner { flex-wrap: wrap; }
  .nav {
    flex-wrap: wrap;
    gap: var(--s1) var(--s3);
    padding-bottom: var(--s2);
  }

  .masthead[data-enhanced="true"] .nav__toggle { display: block; }
  .masthead[data-enhanced="true"] .nav {
    display: none;
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    flex-direction: column;
    align-items: flex-start;
    flex-wrap: nowrap;
    gap: 0;
    padding: var(--s2) 1rem var(--s3);
    background: var(--bg);
    border-bottom: 1px solid var(--rule);
  }
  .masthead[data-enhanced="true"] .nav[data-open="true"] { display: flex; }
  .masthead[data-enhanced="true"] .nav a { padding-block: var(--s1); font-size: 0.875rem; }
}

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

/* The landing. Fills the screen on a desktop so the name and the three ways
   in are the whole first impression, then hands off to the work. */
.hero {
  min-height: calc(100vh - 3.5rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: var(--s5);
}

.hero__inner { flex: 1; display: flex; flex-direction: column; justify-content: center; }

.hero__name {
  font-size: clamp(2.75rem, 9vw, 6rem);
  letter-spacing: -0.03em;
  margin-bottom: var(--s3);
}

.hero__credential {
  font-family: var(--mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin-bottom: var(--s4);
}

.hero__lede {
  font-size: 1.1875rem;
  line-height: 1.6;
  max-width: 58ch;
}

/* Three ways in, as plain bordered panels. */
.hero__actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 14rem));
  gap: var(--s2);
  margin-top: var(--s5);
}

.tile {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  border: 1px solid var(--rule);
  padding: var(--s3);
  transition: border-color 140ms linear;
}
.tile:hover { border-color: var(--muted); }

.tile__label { font-size: 1.125rem; }
.tile__note {
  font-family: var(--mono);
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}

.hero__cue {
  margin: 0;
  padding-top: var(--s4);
}

@media (max-width: 40em) {
  .hero { min-height: 0; padding-block: var(--s5) var(--s4); }
  .hero__actions { grid-template-columns: 1fr; margin-top: var(--s4); }
}

/* --- Landing intro ------------------------------------------------------
   The name types itself in, then the rest of the hero rises in sequence.

   `js-intro` is set by a small inline script in the document head rather than
   by site.js, because site.js loads at the end of the body — by then the hero
   has already painted, and hiding it at that point is a visible flash.

   Every rule uses `animation-fill-mode: both` and keeps the hidden state
   inside the keyframes rather than on the element. If animations never run
   for any reason, nothing is left stranded at opacity 0.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  /* One letter at a time. A clip-path wipe with steps() divides the BOX into
     equal fractions, which do not line up with characters of different
     widths — so an 'l' and a 'Y' get the same slice and it reads as a wipe,
     not as typing. Per-character spans are the only honest way to do it.

     Each letter appears almost instantly; the stagger is the typing. */
  html.js-intro .hero__name .ch {
    animation: letter 50ms linear both;
    animation-delay: calc(140ms + var(--i, 0) * 58ms);
  }

  html.js-intro .hero__inner > *:not(.hero__name),
  html.js-intro .hero__cue {
    animation: rise 620ms cubic-bezier(0.22, 1, 0.36, 1) both;
  }

  /* The name finishes around 600ms, so the rest follows from there. */
  html.js-intro .hero__inner > *:nth-child(2) { animation-delay: 700ms; }
  html.js-intro .hero__inner > *:nth-child(3) { animation-delay: 830ms; }
  html.js-intro .hero__inner > *:nth-child(4) { animation-delay: 950ms; }
  html.js-intro .hero__inner > *:nth-child(5) { animation-delay: 1080ms; }
  html.js-intro .hero__cue { animation-delay: 1230ms; }
}

@keyframes letter {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* --- Sections ------------------------------------------------------------- */

.section { padding-block: var(--s5); border-top: 1px solid var(--rule); }

.section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s4);
}

.section__title {
  font-size: clamp(1.5rem, 3vw, 1.875rem);
}

.section__note {
  max-width: 54ch;
  color: var(--muted);
  margin-top: calc(var(--s4) * -1 + var(--s1));
  margin-bottom: var(--s4);
}

/* --- Work showcase: the deck ----------------------------------------------
   A pinned stage holds one full-size card plus at most TWO collapsed bars
   above it. Cards rise from below as you scroll, collapse into a titled bar
   once passed, and leave the stack when a third bar would appear.

   Capping the stack at two is what keeps every card the same height — an
   uncapped stack pushes each successive card further down and makes the last
   project visibly smaller than the first.

   The deck does not engage without JavaScript, below 64em, or under
   prefers-reduced-motion. In those cases the markup is what it looks like:
   a plain vertical list of cards.
   ------------------------------------------------------------------------- */

.work-layout { display: grid; gap: var(--s4); }

@media (min-width: 64em) {
  .work-layout { grid-template-columns: var(--rail) minmax(0, 1fr); gap: var(--s6); }
}

/* --- contents rail ------------------------------------------------------- */

/* Hidden until site.js enables it, since without JS it has nothing to do. */
.rail[hidden] { display: none; }
.rail { align-self: start; }

@media (min-width: 64em) {
  .rail { position: sticky; top: 5.5rem; }
}

/* No rules between items — it is an index at the side of the page, not a
   table. The cards are what should hold the eye. */
.rail__label { margin-bottom: var(--s3); }
.rail__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.15rem; }
.rail__item { border: 0; }

.rail__btn {
  display: flex;
  gap: 0.7rem;
  width: 100%;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.6875rem;
  line-height: 1.5;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-align: left;
  padding: 0.4rem 0;
  transition: color 140ms linear;
}
.rail__btn:hover { color: var(--fg); }
.rail__n { flex: none; }

/* --- the card ------------------------------------------------------------ */

.deck { position: relative; }
.deck__stage { display: grid; gap: var(--s3); }

.work-card {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 42%;
  border: 1px solid var(--rule);
  background: var(--bg);
  min-height: 19rem;
  overflow: hidden;
}

.work-card__body {
  display: flex;
  flex-direction: column;
  padding: var(--s4);
  min-width: 0;
}

.work-card__top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s3);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.01em;
  color: var(--muted);
  padding-bottom: var(--s3);
  margin-bottom: var(--s3);
  border-bottom: 1px solid var(--rule);
}

/* Index ruler:  01 ─────── 6 */
.work-index { display: inline-flex; align-items: center; gap: 0.6rem; }
.work-index b { font-weight: 400; color: var(--fg); }
.work-index__rule { display: block; width: 4.5rem; height: 1px; background: var(--rule); }

/* Context tag: a left rule rather than a box. */
.context {
  font-family: var(--mono);
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  border-left: 2px solid var(--rule);
  padding-left: 0.6rem;
  align-self: flex-start;
  margin: 0 0 var(--s3);
}

.work-card__title {
  font-size: clamp(1.5rem, 2.7vw, 2.125rem);
  margin-bottom: var(--s3);
  display: flex;
  align-items: baseline;
  gap: 0;
}
.work-card__title a:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.14em;
}

/* Miniature index, shown only once a card has collapsed into a bar. */
.work-mini {
  font-family: var(--mono);
  font-style: normal;
  font-size: 0.75rem;
  color: var(--muted);
  opacity: 0;
  width: 0;
  display: inline-block;
  overflow: hidden;
  flex: none;
}

/* Clamped to three lines so cards stay even whatever length the text runs. */
.work-card__summary {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin: 0;
  max-width: 46ch;
}

.work-card__foot { margin-top: auto; padding-top: var(--s3); }

.work-card__links {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s2);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
}
.work-card__links a { color: var(--muted); transition: color 120ms linear; }
.work-card__links a:hover {
  color: var(--fg);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.work-card__media {
  position: relative;
  border-left: 1px solid var(--rule);
  background: var(--plate);
  overflow: hidden;
}
.work-card__media img {
  position: absolute;
  inset: 0;
  margin: auto;
  width: auto;
  height: auto;
  max-width: calc(100% - 2 * var(--s3));
  max-height: calc(100% - 2 * var(--s3));
  object-fit: contain;
}
.work-card__media--empty {
  background: none;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 0.6875rem;
  line-height: 1.8;
  color: var(--muted);
  text-align: center;
}

/* --- deck mode, added by site.js ----------------------------------------
   Everything here is driven off three custom properties site.js writes per
   card as you scroll: --c (how collapsed, 0 to 1), --y (vertical offset) and
   --o (opacity). There are no CSS transitions, deliberately: the movement is
   tied to the scroll position, so a card shrinks into its bar continuously
   rather than snapping between a set of states.
   ------------------------------------------------------------------------- */

html.js-deck {
  --title-lg: 2rem;     /* a card's title at full size */
  --title-sm: 1.0625rem; /* the same title once it is a bar */
}

html.js-deck .deck__stage {
  display: block;
  position: sticky;
  top: 5.5rem;
  height: calc(var(--cardh) + 2 * var(--bar));
}

html.js-deck .work-card {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  min-height: 0;
  height: calc(var(--cardh) - (var(--cardh) - var(--bar)) * var(--c, 0));
  transform: translateY(var(--y, 0px));
  opacity: var(--o, 1);
  will-change: transform, height;
}

/* Centring is a no-op while the card is open, because the foot's auto margin
   claims the free space first. Once everything but the title has collapsed to
   nothing, it is what puts the title on the bar's centre line. */
html.js-deck .work-card__body {
  justify-content: center;
  padding-block: calc(var(--s4) * (1 - var(--c, 0)));
}

/* Each of these has to lose its HEIGHT, not just its opacity — an invisible
   element that still occupies space is what pushes a bar's title off centre. */
html.js-deck .work-card__top,
html.js-deck .context {
  max-height: calc(3.5rem * (1 - var(--c, 0)));
  margin-bottom: calc(var(--s3) * (1 - var(--c, 0)));
  padding-bottom: 0;
  opacity: calc(1 - var(--c, 0) * 1.7);
  overflow: hidden;
  min-height: 0;  /* flex items refuse to shrink past their content without it */
}

html.js-deck .work-card__summary,
html.js-deck .work-card__foot {
  max-height: calc(24rem * (1 - var(--c, 0)));
  opacity: calc(1 - var(--c, 0) * 1.7);
  overflow: hidden;
  min-height: 0;
}

/* max-height alone will not collapse these — the foot keeps its content
   height, and its auto top margin claims the bar's free space and shoves the
   title upward. Both are gone by the time the card reads as a bar, and they
   are already fully transparent by then (opacity hits 0 at --c ≈ 0.59), so
   taking them out of the layout at 0.65 is invisible. Keep that ordering if
   you retune either number. */
html.js-deck .work-card[data-bar="true"] .work-card__summary,
html.js-deck .work-card[data-bar="true"] .work-card__foot,
html.js-deck .work-card[data-bar="true"] .work-card__top,
html.js-deck .work-card[data-bar="true"] .context { display: none; }

html.js-deck .work-card__media {
  opacity: calc(1 - var(--c, 0) * 1.7);
}

html.js-deck .work-card__title {
  font-size: calc(var(--title-lg) - (var(--title-lg) - var(--title-sm)) * var(--c, 0));
  margin-bottom: calc(var(--s3) * (1 - var(--c, 0)));
  gap: calc(0.9rem * var(--c, 0));
}

/* The small index only a collapsed bar shows. */
html.js-deck .work-mini {
  opacity: calc(var(--c, 0) * 2 - 1);
  width: calc(1.5rem * var(--c, 0));
}

/* --- end of the exhibit ------------------------------------------------- */

.deck-end {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding-block: var(--s4);
  border-top: 1px solid var(--rule);
}

.backtotop {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  color: var(--muted);
  border: 1px solid var(--rule);
  padding: 0.6rem 1rem;
  transition: color 140ms linear, border-color 140ms linear;
}
.backtotop:hover { color: var(--fg); border-color: var(--muted); }

/* --- embedded document --------------------------------------------------- */

.doc-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin: 0 0 var(--s4);
  max-width: none;
}

.doc-embed {
  border: 1px solid var(--rule);
  background: var(--plate);
  height: min(78vh, 60rem);
  margin-bottom: var(--s5);
}
.doc-embed object { width: 100%; height: 100%; display: block; }

.doc-embed__fallback {
  display: grid;
  place-content: center;
  gap: var(--s2);
  height: 100%;
  padding: var(--s4);
  text-align: center;
  color: var(--muted);
}

@media (max-width: 40em) {
  /* Phones mostly refuse to render a PDF inline, and a short scrolling box is
     worse than none — the download link above is the real route there. */
  .doc-embed { height: 24rem; }
}

@media (max-width: 63.99em) {
  .work-card { grid-template-columns: minmax(0, 1fr); }
  .work-card__media {
    border-left: 0;
    border-top: 1px solid var(--rule);
    order: 2;
    height: 18rem;
  }
  .work-card__body { order: 1; }
}

@media (max-width: 40em) {
  .work-card__body { padding: var(--s3); }
  .work-card__media { height: 14rem; }
}

/* --- The record primitive -------------------------------------------------
   Mono metadata left, prose right. Education, awards and leadership.
   ------------------------------------------------------------------------- */

.record {
  display: grid;
  grid-template-columns: var(--col) minmax(0, 1fr);
  gap: var(--s4);
  padding-block: var(--s4);
  border-top: 1px solid var(--rule);
}
.record:first-of-type { border-top: 0; padding-top: 0; }

.record__meta {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.55;
  color: var(--muted);
  letter-spacing: 0.01em;
}

.record__meta > * { display: block; }
.record__index { color: var(--fg); }

.record__org {
  font-size: 1.25rem;
  margin-bottom: 0.125rem;
}

.record__role {
  font-style: italic;
  color: var(--muted);
  margin-bottom: var(--s2);
}

@media (max-width: 40em) {
  .record { grid-template-columns: 1fr; gap: var(--s2); }
}

/* Bulleted achievements. Hanging dashes so the text edge stays true. */
.bullets {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: var(--measure);
}
.bullets li {
  position: relative;
  padding-left: 1.05rem;
  margin-bottom: var(--s1);
}
.bullets li::before {
  content: "—";
  position: absolute;
  left: 0;
  color: var(--muted);
}
.bullets li:last-child { margin-bottom: 0; }

/* Quiet pointer between places that describe the same work. */
.crosslink {
  position: relative;
  z-index: 2;
  display: inline-block;
  margin-top: var(--s2);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  transition: color 120ms linear;
}
.crosslink:hover { color: var(--fg); }

/* --- Tag lists ------------------------------------------------------------ */

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tag {
  font-family: var(--mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  white-space: nowrap;
}
.tag + .tag::before {
  content: "·";
  margin-right: var(--s2);
  color: var(--rule);
}

/* --- Skills --------------------------------------------------------------- */

.skills { display: grid; gap: 0; margin: 0; }

.skills__group {
  display: grid;
  grid-template-columns: var(--col) minmax(0, 1fr);
  gap: var(--s4);
  padding-block: var(--s3);
  border-top: 1px solid var(--rule);
}
.skills__group:first-child { border-top: 0; padding-top: 0; }

.skills__group dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  line-height: 1.7;
}
.skills__group dd { margin: 0; }

@media (max-width: 40em) {
  .skills__group { grid-template-columns: 1fr; gap: var(--s1); }
}

/* --- Contact -------------------------------------------------------------- */

.contact { display: grid; gap: 0; max-width: 34rem; margin: 0; }

.contact__row {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: var(--s3);
  padding-block: var(--s2);
  border-top: 1px solid var(--rule);
  align-items: baseline;
}
.contact__row:first-child { border-top: 0; }

.contact__row a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--rule);
  transition: text-decoration-color 120ms linear;
  word-break: break-word;
}
.contact__row a:hover { text-decoration-color: currentColor; }

/* --- Footer --------------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--rule);
  padding-block: var(--s4) var(--s5);
  margin-top: var(--s5);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2) var(--s4);
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--muted);
}

.footer__links { display: flex; flex-wrap: wrap; gap: var(--s3); }
.footer__links a { transition: color 120ms linear; }
.footer__links a:hover { color: var(--fg); }

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

.backlink {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  padding-block: var(--s4) 0;
  transition: color 120ms linear;
}
.backlink:hover { color: var(--fg); }

.project__head { padding-block: var(--s4) var(--s5); }

.project__title {
  font-size: clamp(2rem, 5vw, 3.75rem);
  letter-spacing: -0.025em;
  margin-bottom: var(--s3);
}

.project__standfirst {
  font-size: 1.1875rem;
  line-height: 1.6;
  color: var(--muted);
  max-width: 56ch;
}

/* Metadata block: label/value pairs, rules between. */
.spec {
  display: grid;
  grid-template-columns: var(--col) minmax(0, 1fr);
  gap: 0 var(--s4);
  margin: 0;
  border-top: 1px solid var(--rule);
}

.spec dt {
  font-family: var(--mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  padding-block: var(--s2);
  border-bottom: 1px solid var(--rule);
  line-height: 1.6;
}

.spec dd {
  margin: 0;
  padding-block: var(--s2);
  border-bottom: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 0.8125rem;
  line-height: 1.6;
}
.spec dd a {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--rule);
}
.spec dd a:hover { text-decoration-color: currentColor; }

@media (max-width: 40em) {
  .spec { grid-template-columns: 1fr; gap: 0; }
  .spec dt { border-bottom: 0; padding-bottom: 0; }
  .spec dd { padding-top: 0.25rem; }
}

/* Prose flow on project pages. Headings sit tight to the text they introduce;
   figures run wider than the measure. */
.prose { padding-block: var(--s5); }

.prose > h2 {
  font-size: clamp(1.375rem, 2.6vw, 1.75rem);
  margin: var(--s5) 0 var(--s2);
  max-width: var(--measure);
}
.prose > h2:first-child { margin-top: 0; }

.prose > h3 {
  font-size: 1.125rem;
  margin: var(--s4) 0 var(--s1);
  max-width: var(--measure);
}

.prose > p,
.prose > ul,
.prose > ol,
.prose > dl { max-width: var(--measure); }

.prose > ol,
.prose > ul { padding-left: 1.25rem; margin: 0 0 var(--s2); }
.prose li { margin-bottom: var(--s1); }

.prose blockquote {
  margin: var(--s3) 0;
  padding-left: var(--s3);
  border-left: 1px solid var(--rule);
  color: var(--muted);
  max-width: var(--measure);
}

/* Inline code and display formulae. Bounded by a rule, never a fill. */
code {
  font-family: var(--mono);
  font-size: 0.875em;
}

.formula {
  font-family: var(--mono);
  font-size: 0.8125rem;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--rule);
  overflow-x: auto;
  margin: var(--s3) 0;
  max-width: var(--measure);
  color: var(--fg);
}

/* --- Figures -------------------------------------------------------------- */

figure {
  margin: var(--s4) 0;
  max-width: 56rem;
}

figure img,
figure video {
  width: 100%;
  background: var(--plate);
  border: 1px solid var(--plate-rule);
}

/* Photographs need no plate; the border alone frames them. */
figure.photo img { background: transparent; }

/* A portrait photograph at full measure runs absurdly tall. Narrow its figure
   instead of letterboxing it. */
figure.portrait { max-width: 30rem; }

figcaption {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--muted);
  margin-top: var(--s2);
}

.figure-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s3);
  margin: var(--s4) 0;
  max-width: 56rem;
}
.figure-pair figure { margin: 0; max-width: none; }

@media (max-width: 34em) {
  .figure-pair { grid-template-columns: 1fr; }
}

/* One caption beneath a pair of figures. */
.pair-caption {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--muted);
  margin-top: calc(var(--s4) * -1 + var(--s2));
  margin-bottom: var(--s4);
  max-width: 56rem;
}

/* An empty slot waiting on an asset. */
.figure-slot {
  border: 1px dashed var(--rule);
  padding: var(--s5) var(--s3);
  text-align: center;
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--muted);
  max-width: 56rem;
  margin: var(--s4) 0;
}

/* --- Tables --------------------------------------------------------------- */

.table-scroll { overflow-x: auto; margin: var(--s3) 0; max-width: 56rem; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

th, td {
  text-align: left;
  vertical-align: top;
  padding: var(--s2) var(--s3) var(--s2) 0;
  border-bottom: 1px solid var(--rule);
}

thead th {
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  padding-top: 0;
}

tbody th {
  font-weight: 400;
  padding-right: var(--s4);
}

td.mono, th.mono { font-family: var(--mono); font-size: 0.8125rem; }

/* --- Print ---------------------------------------------------------------- */

@media print {
  .masthead, .footer, .nav__toggle, .backlink { display: none; }
  body { font-size: 11pt; }
  a { text-decoration: none; }
  .section, .record, .work-card { break-inside: avoid; }
  html.js-reveal .reveal { opacity: 1 !important; transform: none !important; }
}
