@import url("https://fonts.googleapis.com/css2?family=Baskervville:ital,wght@0,400..700;1,400..700&display=swap");

/* ---------------------------------------------------------------
   Jasmine Wang — personal site
   Minimal type-first layout. Dark text on warm paper.
   --------------------------------------------------------------- */

:root {
  --paper: #fdfdfb;
  --ink: #1a1a1a;
  --ink-soft: #6b6b6b;
  --ink-faint: #a3a3a3;
  --rule: #e4e1d8;

  --col: 610px;          /* centered content column */
  --nav-x: 64px;         /* left nav offset */
  --top: 128px;          /* shared top baseline for nav + content */

  --fs-body: 17px;
  --lh-body: 1.55;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

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

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  /* SF Compact ships with macOS/iOS, so Apple visitors get the real face.
     Everyone else falls through to the closest available grotesque. */
  font-family: "SF Compact Text", "SF Compact Display", "SF Compact",
    -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial,
    sans-serif;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: -0.005em;
}

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

/* ---------------------------------------------------------------
   Handwriting swap-ins
   Whole scanned words, dropped in as transparent SVG/PNG:

     <img class="hw-word" src="/assets/handwriting/jasmine-wang.svg"
          alt="Jasmine Wang">

   Height is tied to the surrounding text so the word scales with its
   heading instead of being pinned to a pixel size. Tune per-word with
   an inline `style="--hw-scale: 1.2"` when the ink needs more room.
   --------------------------------------------------------------- */
/* --hw-scale: image height, in em of the surrounding text.
   --hw-desc:  how much of that image sits below the writing's baseline,
               measured off each PNG. `vertical-align: baseline` aligns
               the image's bottom edge, not the ink's baseline, so we
               push the image back down by exactly the descender to seat
               the handwriting on the text baseline. */
.hw-word {
  --hw-scale: 1;
  --hw-desc: 0;
  display: inline-block;
  height: calc(1em * var(--hw-scale));
  width: auto;
  /* Never let a word push the column wider than the viewport. A word
     that would otherwise overflow shrinks instead. */
  max-width: 100%;
  object-fit: contain;
  vertical-align: baseline;
  transform: translateY(calc(1em * var(--hw-scale) * var(--hw-desc)));
}

/* A handwritten word is taller than the type it replaces, which would
   push its line box open and break the paragraph's rhythm. Negative
   block margins pin every word's contribution to a flat 1.2em, so lines
   stay evenly spaced no matter how large the ink is. */
p .hw-word {
  margin-block: calc((1em * var(--hw-scale) - 1.2em) / -2);
}

/* A word that's also a link: plain, always-on underline — no draw-in,
   no hover state. */
.hw-link {
  position: relative;
  display: inline-block;
  color: rgb(0 0 0 / 58%);
  font-weight: 400;
}

.hw-link .semibold {
  font-weight: 400;
}

.hw-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.08em;
  height: 1px;
  background: rgb(0 0 0 / 58%);
}

.hw-word--magical         { --hw-desc: 0.289; }
.hw-word--experience      { --hw-desc: 0.408; }
.hw-word--my-thinking-pal { --hw-desc: 0.236; }

/* ---------------------------------------------------------------
   Layout
   --------------------------------------------------------------- */
.page {
  position: relative;
  min-height: 100vh;
  padding: var(--top) 24px 120px;
}

.content {
  width: 100%;
  max-width: var(--col);
  margin: 0 auto;
}

/* ---------------------------------------------------------------
   Left nav — vertical, one item per line
   --------------------------------------------------------------- */
.nav {
  position: fixed;
  top: var(--top);
  left: var(--nav-x);
  display: flex;
  flex-direction: column;
  gap: 18px;
  font-size: 15px;
  z-index: 10;
}

.nav a {
  color: var(--ink);
  opacity: 0.55;
  transition: opacity 0.25s var(--ease);
  width: fit-content;
}

.nav a:hover,
.nav a[aria-current="page"] {
  opacity: 1;
}

/* ---------------------------------------------------------------
   Type
   --------------------------------------------------------------- */
h1 {
  margin: 0 0 44px;
  font-family: "Baskervville", Baskerville, "Times New Roman", serif;
  font-size: 27px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

h2 {
  margin: 0 0 6px;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

p {
  margin: 0 0 20px;
}

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

.semibold {
  font-weight: 600;
}

/* ---------------------------------------------------------------
   Quote block (home)
   --------------------------------------------------------------- */
.quote {
  /* Zero the UA's 40px side margins so the quote sits flush with the
     paragraphs above it and wraps at exactly the same width. */
  margin: 96px 0 0;
  color: var(--ink);
  transition: color 1.2s var(--ease);
}

.quote em {
  display: block;
  font-style: italic;
}

.quote cite {
  display: block;
  margin-top: 20px;
  font-style: normal;
}

/* ---------------------------------------------------------------
   Home intro animation
   The quote never moves: the intro reserves its layout space from
   the first frame and only fades in.
   --------------------------------------------------------------- */
.reveal {
  opacity: 0;
  transform: translateY(6px);
}

.is-ready .reveal {
  opacity: 1;
  transform: none;
  transition: opacity 1.1s var(--ease), transform 1.1s var(--ease);
}

.is-ready .quote {
  color: var(--ink-soft);
}

/* Stagger */
.is-ready .reveal--1 { transition-delay: 0.05s; }
.is-ready .reveal--2 { transition-delay: 0.20s; }
.is-ready .reveal--3 { transition-delay: 0.35s; }

/* Anyone who already saw the intro, or prefers less motion, skips it. */
.no-intro .reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

.no-intro .quote {
  color: var(--ink-soft);
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  .quote {
    transition: none !important;
  }
}

/* ---------------------------------------------------------------
   Lists (projects, writing)
   --------------------------------------------------------------- */
.entries {
  margin: 0;
  padding: 0;
  list-style: none;
}

.entry {
  padding: 22px 0;
  border-top: 1px solid var(--rule);
}

.entry:last-child {
  border-bottom: 1px solid var(--rule);
}

.entry a {
  display: block;
}

.entry a:hover h2 {
  opacity: 0.55;
}

.entry h2 {
  transition: opacity 0.2s var(--ease);
}

.entry .meta {
  margin: 0;
  color: var(--ink-faint);
  font-size: 14px;
}

.entry .blurb {
  margin: 8px 0 0;
  color: var(--ink-soft);
}

.entry-preview-reveal {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition:
    grid-template-rows 0.45s var(--ease),
    opacity 0.3s var(--ease);
}

.entry-preview-clip {
  min-height: 0;
  overflow: hidden;
}

.entry:focus-within .entry-preview-reveal {
  grid-template-rows: 1fr;
  opacity: 1;
}

.entry:hover .entry-preview-reveal {
  grid-template-rows: 1fr;
  opacity: 1;
}

.entry-preview {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 20px;
  border-radius: 4px;
}

/* ---------------------------------------------------------------
   Reading + world map
   --------------------------------------------------------------- */
.read-page .page {
  height: 100dvh;
  min-height: 0;
  padding-top: var(--top);
  padding-bottom: 0;
  overflow: hidden;
}

.read-page {
  height: 100dvh;
  overflow: hidden;
}

.read-page .nav {
  top: var(--top);
}

.read-page .nav a {
  opacity: 1;
}

.read-page .content {
  max-width: var(--col);
}

.read-page h1 {
  margin-bottom: 20px;
}

.read-page .content > .muted {
  max-width: 650px;
  color: var(--ink);
}

/* The map breaks out of the text column and gets its own viewport. The frame
   carries the sizing so the map and the shelf layer share one coordinate
   space — the map clips, the shelf layer deliberately does not. */
.map-frame {
  position: relative;
  width: calc(50vw + 145px);
  height: min(700px, 54dvh, calc(100dvh - 324px));
  margin: 40px 0 0;
}

/* Shelves live outside the map so a row of spines can run past the map's edge
   instead of being sliced off by it. */
.shelf-layer {
  position: absolute;
  inset: 0;
  z-index: 4;
  overflow: visible;
  pointer-events: none;
}

.map-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--paper);
  cursor: default;
  touch-action: none;
  user-select: none;
  overscroll-behavior: contain;
  outline: none;
}

.map-wrap.is-dragging {
  cursor: default;
}

/* The prototype has no hard frame. The map simply dissolves into the page
   on all four sides. */
.map-wrap::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background:
    linear-gradient(
      to right,
      var(--paper) 0,
      transparent 8%,
      transparent 92%,
      var(--paper) 100%
    ),
    linear-gradient(
      to bottom,
      var(--paper) 0,
      transparent 7%,
      transparent 88%,
      var(--paper) 100%
    );
  pointer-events: none;
}

.map-stage {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  transform-origin: 0 0;
}

.worldmap {
  width: 100%;
  height: 100%;
  overflow: visible;
  shape-rendering: geometricPrecision;
}

/* Countries read as soft color blocks. A hairline in the page color keeps
   neighboring shapes visibly separate at every zoom level. */
.worldmap .countries path {
  fill: rgba(0, 0, 0, 0.03);
  stroke: var(--paper);
  stroke-width: 0.8;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  transition: fill 0.18s var(--ease);
  pointer-events: none;
}

.worldmap .countries path.is-visited {
  fill: rgba(0, 0, 0, 0.08);
  pointer-events: auto;
}

/* A country with a shelf answers the pointer whether or not it is also a place
   Jasmine has stood in. */
.worldmap .countries path.has-shelf {
  pointer-events: auto;
  cursor: pointer;
}

.worldmap .countries path.is-visited:hover,
.worldmap .countries path.has-shelf:hover,
.worldmap .countries path.is-shelf-active,
.worldmap .countries:has(#area-country-156:hover) #area-country-158 {
  fill: rgba(0, 0, 0, 0.12);
}

.worldmap .countries:has(#area-country-156.is-shelf-active) #area-country-158 {
  fill: rgba(0, 0, 0, 0.12);
}

/* Book shelf: spines face forward until one volume turns to reveal its cover.
   The 3D rig has one rule — a single perspective lives here on the shelf, and
   every element between it and the faces keeps preserve-3d so all the books
   share one viewpoint. Put a perspective on the individual books instead and
   each one flattens into its own little scene, which reads as a row of tilted
   cards rather than a shelf receding into space. */
.book-shelf {
  /* One place to resize the whole shelf. The books hold this size at every
     zoom level — tie them to the map's scale and the spines stop being
     readable the moment you zoom out. */
  --book-height: 184px;
  --shelf-thickness: 26px;
  --shelf-cover-width: 148px;
  position: absolute;
  z-index: 3;
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: calc(var(--book-height) + 28px);
  padding: 16px 20px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(24px, -50%);
  transition:
    opacity 0.16s var(--ease),
    transform 0.22s var(--ease),
    visibility 0s linear 0.22s;
  perspective: 2800px;
  perspective-origin: 0 50%;
  transform-style: preserve-3d;
}

.book-shelf.is-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate(0, -50%);
  transition-delay: 0s;
}

.shelf-book {
  --book-cover: #dededb;
  --book-spine: #c4c4c0;
  --book-ink: #171717;
  --book-cover-width: var(--shelf-cover-width);
  position: relative;
  flex: 0 0 var(--shelf-thickness);
  width: var(--shelf-thickness);
  height: var(--book-height);
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--book-ink);
  font: inherit;
  cursor: pointer;
  transform-style: preserve-3d;
  transition:
    flex-basis 0.42s var(--ease) 0.08s,
    width 0.42s var(--ease) 0.08s;
}

/* No two volumes are quite the same height — a shelf of identical blocks
   reads as a chart, not as books. */
.shelf-book:nth-child(2n) { height: calc(var(--book-height) + 12px); }
.shelf-book:nth-child(3n) { height: calc(var(--book-height) - 10px); }
.shelf-book:nth-child(4n) { height: calc(var(--book-height) + 4px); }

/* Fallback palette only. Every book that carries a colour sampled from its own
   cover overrides these from the script, so this cycle is what an entry with no
   `spine` set falls back to rather than the shelf's actual look. */
.shelf-book:nth-child(8n + 1) { --book-cover: #e7d9cc; --book-spine: #b55e46; }
.shelf-book:nth-child(8n + 2) { --book-cover: #dfe5dc; --book-spine: #53695e; }
.shelf-book:nth-child(8n + 3) { --book-cover: #d8dde2; --book-spine: #737d87; }
.shelf-book:nth-child(8n + 4) { --book-cover: #eadb9c; --book-spine: #bd9c32; }
.shelf-book:nth-child(8n + 5) { --book-cover: #d8d1c5; --book-spine: #59534c; }
.shelf-book:nth-child(8n + 6) { --book-cover: #d9cfda; --book-spine: #756279; }
.shelf-book:nth-child(8n + 7) { --book-cover: #cbd9e2; --book-spine: #52748a; }
.shelf-book:nth-child(8n + 8) { --book-cover: #e4c9c5; --book-spine: #915c56; }

/* The volume is a box hinged on its spine. It rests turned almost fully away,
   which leaves the spine square to the reader and sends the body of the book
   back into the shelf; hover swings it flat and the cover comes around.
   The rotation is positive on purpose — negative angles swing the body out of
   the screen toward the reader, which reads inside-out. */
.shelf-book__volume {
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--book-cover-width);
  height: 100%;
  transform-style: preserve-3d;
  transform-origin: left center;
  transform: rotateY(84deg);
  transition: transform 0.46s var(--ease) 0.08s;
}

.shelf-book:hover,
.shelf-book:focus-visible {
  flex-basis: var(--book-cover-width);
  width: var(--book-cover-width);
  outline: none;
  transition-delay: 0s;
}

/* The final book expands into empty space. Without a little hysteresis its
   moving right edge can pass back and forth beneath a stationary pointer,
   rapidly toggling :hover and making the cover tremble. This transparent
   extension keeps the expanded state until the pointer has clearly moved
   beyond the book; it only exists on the outer edge, so it cannot block an
   adjacent spine or make switching between books feel sticky. */
.shelf-book:last-child:hover {
  z-index: 2;
}

.shelf-book:last-child:hover::after {
  content: "";
  position: absolute;
  z-index: 2;
  top: -8px;
  right: -14px;
  bottom: -8px;
  left: 0;
  background: transparent;
  pointer-events: auto;
}

.shelf-book:hover .shelf-book__volume,
.shelf-book:focus-visible .shelf-book__volume {
  transform: rotateY(0deg) translateZ(20px);
  transition-delay: 0s;
}

.shelf-book__cover,
.shelf-book__spine,
.shelf-book__pages {
  position: absolute;
  backface-visibility: hidden;
}

/* Front board, sitting one thickness proud of the volume's back plane so it
   lines up with the outer edge of the spine and the page block. */
.shelf-book__cover {
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 22px 18px 18px;
  overflow: hidden;
  isolation: isolate;
  border: 1px solid rgba(0, 0, 0, 0.16);
  border-radius: 2px 6px 6px 2px;
  background: var(--book-cover);
  box-shadow:
    inset 7px 0 0 color-mix(in srgb, var(--book-spine) 58%, transparent),
    inset -1px 0 0 rgba(255, 255, 255, 0.2),
    inset 0 -2px 2px rgba(0, 0, 0, 0.16),
    1px 1px 0 color-mix(in srgb, var(--book-spine) 58%, #232323),
    3px 3px 1px rgba(0, 0, 0, 0.12),
    7px 9px 14px rgba(0, 0, 0, 0.16);
  transform: translateZ(var(--shelf-thickness));
}

/* A translucent finish sits over both artwork and typeset fallback covers.
   The crossing gradients are intentionally faint: together they read as
   coated paper/cloth grain, while the edge lighting gives the board a gently
   pillowed surface instead of turning the artwork into a flat colour card. */
.shelf-book__cover::before {
  content: "";
  position: absolute;
  z-index: 2;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      103deg,
      rgba(255, 255, 255, 0.028) 0,
      rgba(255, 255, 255, 0.028) 1px,
      rgba(0, 0, 0, 0.018) 1px,
      rgba(0, 0, 0, 0.018) 3px
    ),
    linear-gradient(
      90deg,
      rgba(255, 255, 255, 0.18) 0,
      transparent 4%,
      transparent 93%,
      rgba(255, 255, 255, 0.2) 97%,
      rgba(0, 0, 0, 0.13) 100%
    ),
    linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.16),
      transparent 7%,
      transparent 92%,
      rgba(0, 0, 0, 0.17)
    );
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.12),
    inset -2px -2px 3px rgba(0, 0, 0, 0.08);
  mix-blend-mode: soft-light;
}

/* The narrow pressed channel beside the spine is the strongest tactile cue
   in the reference: a dark recess followed immediately by a raised highlight. */
.shelf-book__cover::after {
  content: "";
  position: absolute;
  z-index: 3;
  top: 2px;
  bottom: 2px;
  left: 7px;
  width: 7px;
  border-radius: 50%;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    rgba(0, 0, 0, 0.2),
    rgba(0, 0, 0, 0.05) 34%,
    rgba(255, 255, 255, 0.3) 58%,
    transparent 100%
  );
  box-shadow:
    -1px 0 1px rgba(0, 0, 0, 0.12),
    1px 0 1px rgba(255, 255, 255, 0.12);
  opacity: 0.72;
}

.shelf-book__cover > * {
  position: relative;
  z-index: 1;
}

/* Artwork is a real image rather than a cover background. JavaScript sizes
   the physical board from its natural ratio, so there is nothing to crop and
   no leftover area that needs filling with a flat colour. */
.shelf-book__cover.has-art {
  padding: 0;
  background: transparent;
}

.shelf-book__cover-art {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  object-fit: contain;
}

.shelf-book__cover-title {
  font-size: 18px;
  font-weight: 650;
  line-height: 1.14;
  letter-spacing: -0.01em;
}

.shelf-book__cover-author {
  font-size: 14px;
  line-height: 1.2;
  opacity: 0.65;
}

/* Hinge face. Its width is the book's real thickness, so the spine you see and
   the slot the book occupies in the row are the same measurement. */
.shelf-book__spine {
  top: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--shelf-thickness);
  padding: 14px 2px;
  overflow: hidden;
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 2.5px 1.5px 1.5px 2.5px;
  background:
    repeating-linear-gradient(
      0deg,
      transparent 0,
      transparent 3px,
      rgba(255, 255, 255, 0.025) 3px,
      rgba(255, 255, 255, 0.025) 4px
    ),
    linear-gradient(
      90deg,
      rgba(0, 0, 0, 0.26),
      rgba(255, 255, 255, 0.2) 34%,
      rgba(255, 255, 255, 0.06) 58%,
      rgba(0, 0, 0, 0.22) 100%
    ),
    var(--book-spine);
  box-shadow:
    inset -2px 0 2px rgba(0, 0, 0, 0.2),
    inset 2px 0 2px rgba(255, 255, 255, 0.16),
    1px 1px 2px rgba(0, 0, 0, 0.12);
  color: color-mix(in srgb, white 88%, var(--book-spine));
  transform: rotateY(-90deg);
  transform-origin: left center;
}

.shelf-book__spine-title {
  max-height: 100%;
  overflow: hidden;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  text-overflow: ellipsis;
  white-space: nowrap;
  writing-mode: vertical-rl;
}

/* Fore-edge — the page block at the far end, mirroring the spine. It gives the
   volume a closing face so the book has genuine depth rather than two boards. */
.shelf-book__pages {
  top: 6px;
  bottom: 6px;
  left: 100%;
  width: var(--shelf-thickness);
  overflow: hidden;
  border-radius: 0 2.5px 2.5px 0;
  background:
    repeating-linear-gradient(
      to bottom,
      rgba(98, 88, 70, 0.13) 0,
      rgba(98, 88, 70, 0.13) 1px,
      transparent 1px,
      transparent 3px
    ),
    linear-gradient(
      to right,
      #ded9cc 0%,
      #f8f6ef 18%,
      #eeeae0 72%,
      #d2ccbd 100%
    );
  box-shadow:
    inset 2px 0 2px rgba(0, 0, 0, 0.12),
    inset -1px 0 1px rgba(255, 255, 255, 0.5);
  transform: rotateY(-90deg);
  transform-origin: left center;
}

/* ---------------------------------------------------------------
   Small screens — nav folds to a horizontal row above the content
   --------------------------------------------------------------- */
@media (max-width: 1024px) {
  :root {
    --top: 40px;
  }

  .page {
    padding: 0 24px 80px;
  }

  .nav {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 18px;
    width: 100%;
    max-width: calc(var(--col) + 48px);
    padding: 32px 24px 44px;
    margin: 0 auto;
  }

  .quote {
    margin-top: 64px;
  }

  .read-page {
    display: flex;
    flex-direction: column;
  }

  .map-frame {
    width: calc(100vw - 48px);
    height: min(620px, 54dvh, calc(100dvh - 281px));
    margin: 40px 0 0;
  }

  /* The shelf keeps its desktop proportions but a phone has nothing like the
     room for them, so the whole rig is scaled down rather than re-specced. */
  .book-shelf {
    transform: translate(16px, -50%) scale(0.5);
    transform-origin: left center;
  }

  .book-shelf.is-visible {
    transform: translate(0, -50%) scale(0.5);
  }

  .read-page .page {
    flex: 1;
    height: auto;
    padding-top: 0;
    padding-bottom: 0;
  }

  .read-page .nav {
    top: auto;
  }
}

/* ---------------------------------------------------------------
   Project detail page (I build. entries)
   --------------------------------------------------------------- */
.back-link {
  display: inline-block;
  margin-bottom: 28px;
  color: var(--ink-soft);
  font-size: 14px;
  transition: opacity 0.2s var(--ease);
}

.back-link:hover {
  opacity: 0.6;
}

.project .meta {
  margin: -32px 0 32px;
  color: var(--ink-faint);
  font-size: 14px;
}

.essay .meta {
  margin: -32px 0 32px;
  color: var(--ink-faint);
  font-size: 14px;
}

.essay-note {
  color: var(--ink-soft);
  font-size: 15px;
}

.essay-figure {
  margin: 32px 0;
}

.essay-figure img {
  display: block;
  width: 100%;
  height: auto;
}

.essay-figure figcaption {
  margin-top: 10px;
  color: var(--ink-faint);
  font-size: 13px;
  line-height: 1.45;
}

.essay-figure cite {
  font-style: italic;
}

.essay-quote {
  margin: 32px 0;
  padding-left: 20px;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
}

.essay-quote p {
  margin: 0;
}

.project a:not(.back-link),
.essay a:not(.back-link) {
  text-decoration: underline;
  text-decoration-color: var(--ink-faint);
  text-underline-offset: 2px;
  transition: text-decoration-color 0.2s var(--ease);
}

.project a:not(.back-link):hover,
.essay a:not(.back-link):hover {
  text-decoration-color: var(--ink);
}

h2.section {
  margin: 48px 0 18px;
  font-size: 20px;
}

h2.section.sub {
  margin-top: 36px;
  font-size: 17px;
}

.project-figure {
  margin: 32px 0;
}

.project-figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
}

.project-video-frame {
  overflow: hidden;
  border-radius: 4px;
  background: #101010;
}

.project-video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

.project-animation {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
}

.project-list {
  margin: 0 0 20px;
  padding-left: 22px;
}

.project-list li {
  margin-bottom: 14px;
}

.callout {
  margin: 32px 0;
  padding: 16px 20px;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
  font-size: 15px;
}

.callout p {
  margin: 0;
}
