/* Yunmin Cha — site styles.
   Type sizes, spacing, tracking, and color come from the tokens in :root;
   reach for a token before introducing a new value. */

/* P052 roman and italic, subset to Latin, Latin Extended-A, punctuation, and arrows (see README). */
@font-face { font-family: Scholar; src: url('/assets/fonts/scholar-serif.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: Scholar; src: url('/assets/fonts/scholar-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
/* Code: Nimbus Mono PS, URW's serif monospace from the same family as P052, subset to ASCII. */
@font-face { font-family: 'Scholar Mono'; src: url('/assets/fonts/scholar-mono.woff2') format('woff2'); font-weight: 400; font-display: swap; }
/* Korean: a Noto Serif KR subset holding only the syllables the site uses (scripts/fetch_korean_font.py). */
@font-face { font-family: 'Scholar KR'; src: url('/assets/fonts/scholar-ko.woff2') format('woff2'); font-weight: 400; font-display: swap; unicode-range: U+AC00-D7A3; }

:root {
  color-scheme: light dark;

  /* Color */
  --paper: #f5f1e8;
  --paper-deep: #eee8dc;
  --ink: #2f2721;
  --muted: #675d52;
  --line: #d8cebf;
  --line-strong: #a99c8b;
  --accent: #855f40;
  --accent-soft: #e2d6c6;
  --mark-muted: #a89b8a;
  --glow: #eadcc4;
  --shadow: rgb(48 37 26 / .1);
  --selection: #dcc9ad;
  /* Fig. 1: slab faces, and the spark that marks moving work */
  --slab-top: #faf7f1;
  --slab-left: #ebe3d5;
  --slab-right: #dcd0bd;
  --spark: #b5622a;

  /* Texture. Paper grain is a tile of specks whose opacity follows fractal noise; it lies on the
     paper and on every surface laid over it, and scrolls with the page. --grain marks paper,
     --grain-ink a solid of ink such as a button. Dark mode swaps the specks' color. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g' x='0' y='0' width='1' height='1' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.26 0 0 0 0.130'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-ink: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g' x='0' y='0' width='1' height='1' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' seed='8' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.16 0 0 0 -0.080'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)'/%3E%3C/svg%3E");

  /* Families */
  --serif: Scholar, Palatino, 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
  --serif-ko: 'Scholar KR', 'Noto Serif KR', 'Noto Serif CJK KR', AppleMyungjo, 'Nanum Myeongjo', Batang, serif;
  --mono: 'Scholar Mono', 'Nimbus Mono PS', 'Courier Prime', 'Courier New', Courier, monospace;

  /* Type scale: seven steps, fluid between small and wide viewports. Each step is one level and
     serves the same kind of text on every page. Neighboring steps stay at least an eighth apart
     at every width, so two levels never look like one. */
  --step-meta: .875rem;                                     /* 14px  labels, dates, captions */
  --step-ui: 1rem;                                          /* 16px  navigation, buttons, links, data */
  --step-body: clamp(1.125rem, 1.0625rem + .2vw, 1.1875rem); /* 18–19 reading text */
  --step-lede: clamp(1.375rem, 1.2rem + .6vw, 1.625rem);    /* 22–26 ledes, titles of items */
  --step-h2: clamp(1.75rem, 1.3rem + 1.9vw, 2.75rem);       /* 28–44 section titles */
  --step-h1: clamp(2.5rem, 1.6rem + 3.6vw, 4.5rem);         /* 40–72 page titles */
  --step-display: clamp(3.5rem, 2rem + 6vw, 6.5rem);        /* 56–104 the name */

  --track-tight: -.02em;
  --track-display: -.03em;
  --track-caps: .09em;

  /* Space */
  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: .75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: clamp(3rem, 2rem + 4vw, 5rem);
  --space-9: clamp(4rem, 2.5rem + 5vw, 6.5rem);

  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --measure: 40rem;
  --grid: minmax(0, 1fr) minmax(0, 2fr); /* titles and content, below the story */
  --header-h: 4.5rem;
  --ease: cubic-bezier(.22, 1, .36, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #1b1714;
    --paper-deep: #221d19;
    --ink: #ece5d8;
    --muted: #ab9f90;
    --line: #3a322b;
    --line-strong: #62574b;
    --accent: #cf9f72;
    --accent-soft: #3a3129;
    --mark-muted: #6c6155;
    --glow: #2b231c;
    --shadow: rgb(0 0 0 / .4);
    --selection: #4a3a2c;
    --slab-top: #26201b;
    --slab-left: #171310;
    --slab-right: #110e0b;
    --spark: #f2a660;
    --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g' x='0' y='0' width='1' height='1' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.18 0 0 0 -0.090'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)'/%3E%3C/svg%3E");
    --grain-ink: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g' x='0' y='0' width='1' height='1' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' seed='8' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.22 0 0 0 0.110'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)'/%3E%3C/svg%3E");
  }
}

/* Base */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + var(--space-5)); background: var(--grain), var(--paper); }
body {
  margin: 0;
  min-height: 100vh;
  color: var(--ink);
  font-family: var(--serif);
  font-size: var(--step-body);
  line-height: 1.6;
  font-kerning: normal;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
::selection { background: var(--selection); color: var(--ink); }
a { color: inherit; text-decoration: none; }
button, input { font: inherit; color: inherit; }
button { padding: 0; border: 0; background: none; cursor: pointer; }
button, a, input { -webkit-tap-highlight-color: transparent; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
svg { display: block; }
h1, h2, h3, p, figure, blockquote, dl, dd { margin: 0; }
h1, h2, h3 { font-family: var(--serif); font-weight: 400; line-height: 1.12; letter-spacing: var(--track-tight); text-wrap: balance; }
p, li, dd, figcaption { text-wrap: pretty; }
/* Short blurbs of two or three lines break into even lines, so none ends on a word or two. */
:is(.plate-figure figcaption, .pipeline p, .entry-role, .oss-body > p, .cv-item > p) { text-wrap: balance; }
[lang="ko"] { font-family: var(--serif-ko); letter-spacing: 0; }
[hidden] { display: none !important; }
.nowrap { white-space: nowrap; }

.wrap { width: min(75rem, 100% - 2 * var(--gutter)); margin-inline: auto; }
.skip { position: fixed; top: -100px; left: var(--space-5); z-index: 100; padding: var(--space-3) var(--space-5); background: var(--ink); color: var(--paper); font-size: var(--step-ui); }
.skip:focus { top: var(--space-3); }

.label {
  font-size: var(--step-meta);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--muted);
}
.label .as-is { letter-spacing: .03em; text-transform: none; } /* SLOs, PyPI: names that keep their case */
.fig-no { font-size: var(--step-meta); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--muted); }

/* Links and buttons */

.arrow { flex: none; transition: transform .35s var(--ease); }
/* Inline, so a link that wraps is underlined on every line and keeps its arrow after the last word.
   The top padding makes an inline link a full 24px target without moving its underline. */
.text-link {
  padding-block: 3px 2px;
  border-bottom: 1px solid var(--line-strong);
  font-size: var(--step-ui);
  line-height: 1.5;
  transition: border-color .2s;
}
.text-link .arrow { display: inline-block; margin-left: var(--space-2); vertical-align: -.17em; }
p:not(.link-row) > .text-link { padding-top: 5px; }
.text-link:hover { border-color: currentColor; }
.text-link:hover .arrow { transform: translateX(3px); }
.text-link[target]:hover .arrow { transform: translate(2px, -2px); }
.text-link[download]:hover .arrow { transform: translateY(2px); }
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  min-height: 2.875rem;
  padding: 0 var(--space-5);
  border: 1px solid var(--ink);
  border-radius: 2px;
  background: var(--grain-ink), var(--ink);
  color: var(--paper);
  font-size: var(--step-ui);
  transition: background .25s, color .25s;
}
.button:hover { background: transparent; color: var(--ink); }
.button:hover .arrow { transform: translateX(3px); }
.button[download]:hover .arrow { transform: translateY(2px); }
.link-row { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5); margin-top: var(--space-4); }
/* A link inside running text: underlined in the rule color, its arrow kept with the last word. */
/* Its padding makes a full 24px target without moving the line. */
.inline-link { padding-block: 5px; text-decoration: underline 1px var(--line-strong); text-underline-offset: .22em; transition: text-decoration-color .2s; }
.inline-link:hover { text-decoration-color: currentColor; }
.inline-link .arrow { display: inline-block; width: .75em; height: .75em; margin-left: .12em; vertical-align: -.02em; }
.inline-link[target]:hover .arrow { transform: translate(1px, -1px); }

/* Header */

.site-header { position: sticky; top: 0; z-index: 30; background: var(--grain), color-mix(in srgb, var(--paper) 90%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
.nav-shell { height: var(--header-h); display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); border-bottom: 1px solid var(--line); }
.identity { display: flex; align-items: center; gap: var(--space-3); }
.monogram {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  padding-right: .15em;
  border: 1px solid var(--ink);
  font-family: var(--serif);
  font-size: 1.375rem; /* logo glyph, sized to its box */
  line-height: 1;
  letter-spacing: -.12em;
}
.identity-text { font-size: var(--step-meta); letter-spacing: var(--track-caps); text-transform: uppercase; }
.nav-links { display: flex; align-items: center; gap: var(--space-6); font-size: var(--step-ui); }
.nav-links a { position: relative; min-width: 1.5rem; padding-block: var(--space-2); color: var(--muted); text-align: center; transition: color .2s; }
.nav-links a:hover, .nav-links a[aria-current="page"] { color: var(--ink); }
.nav-links a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 2px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .35s var(--ease); }
.nav-links a:hover::after, .nav-links a[aria-current="page"]::after { transform: scaleX(1); }
.nav-links .contact-nav { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--accent); }
.menu-toggle { display: none; }

/* Home: the hero and the story of Fig. 1.
   The figure stays pinned beside the hero and the four layer descriptions that follow it. app.js
   marks the description being read with data-step on .story, and the figure focuses that layer. */

.story { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); column-gap: var(--space-7); }
.hero-copy { grid-column: 1; grid-row: 1; display: flex; flex-direction: column; justify-content: center; min-height: calc(100svh - var(--header-h)); padding-block: var(--space-7); container-type: inline-size; }
/* The name stays on one line: 18.5cqi is just under the size at which it fills its column. */
.hero-name { margin: var(--space-4) 0 var(--space-6); font-size: min(var(--step-display), 18.5cqi); line-height: .98; letter-spacing: var(--track-display); }
.hero-lede { max-width: 26ch; font-size: var(--step-lede); line-height: 1.35; text-wrap: balance; }
.hero-text { max-width: 40ch; margin-top: var(--space-4); color: var(--muted); }
.hero-actions { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-4) var(--space-6); margin-top: var(--space-6); }
.story-figure { grid-column: 2; grid-row: 1 / 3; align-self: start; position: sticky; top: var(--header-h); display: flex; align-items: center; height: calc(100svh - var(--header-h)); overflow: hidden; }
.story-steps { grid-column: 1; grid-row: 2; margin: 0; padding: 0 0 var(--space-8); list-style: none; }
.story-step { display: flex; flex-direction: column; justify-content: center; gap: var(--space-4); max-width: 34rem; min-height: 80svh; transition: opacity .5s var(--ease); }
.story-step h2 { font-size: var(--step-h2); }
.story-link { margin-top: var(--space-2); }
.story[data-step] .story-step { opacity: .3; }
.story[data-step="0"] .story-step[data-step="0"],
.story[data-step="1"] .story-step[data-step="1"],
.story[data-step="2"] .story-step[data-step="2"],
.story[data-step="3"] .story-step[data-step="3"] { opacity: 1; }

/* Fig. 1 — the stack of layers the projects connect.
   Every cycle tells one story: a request leaves the queue and flows down, its data is read from
   storage and rises, and when both reach GPU execution a burst of work crosses its lanes. Loops
   that share --cycle and --start stay in step; keyframe percentages are positions in that cycle.
   --spread separates the layers: they assemble from apart on load and open on hover. In the story,
   a camera (--cam, --cam-k) centers and enlarges (--zoom) the layer being read; on narrow screens
   app.js sets --zoom and --cam-lift for each layer so it clears that layer's card. */

@property --spread { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --p { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --cam { syntax: '<number>'; inherits: true; initial-value: 0; }
@property --cam-k { syntax: '<number>'; inherits: true; initial-value: 1.5; }
@property --zoom { syntax: '<number>'; inherits: true; initial-value: 1.45; }
@property --cam-lift { syntax: '<length>'; inherits: true; initial-value: 0px; }

.stack {
  --cycle: 4.8s;
  --start: 1.9s;
  position: relative;
  isolation: isolate;
  /* As large as the column allows while the whole figure fits in the pinned area. */
  width: min(100%, calc(.76 * (100svh - 10.5rem)));
  margin-inline: auto;
}
/* The glow is grained paper of its own, faded out at its edge, so the page's grain runs through it. */
.stack::before { content: ''; position: absolute; inset: 0 6%; z-index: -1; background: var(--grain), var(--glow); -webkit-mask: radial-gradient(closest-side, #000, transparent); mask: radial-gradient(closest-side, #000, transparent); }
.stack-grid {
  --row-gap: var(--space-2);
  --spread: 0px;
  --face: .4627; /* the top face's center: 62 of each plate's 134 viewBox units; see SLAB in build.py */
  --zoom: 1.45;
  /* The center of layer --cam-k's top face, measured from the top of the grid. */
  --cam-y: calc((var(--cam-k) + var(--face)) * (100% - 3 * var(--row-gap)) / 4 + var(--cam-k) * var(--row-gap) + (var(--cam-k) - 1.5) * var(--spread));
  position: relative;
  display: grid;
  grid-template-columns: minmax(min-content, .65fr) minmax(0, 2fr) minmax(min-content, .7fr);
  grid-template-rows: repeat(4, auto);
  row-gap: var(--row-gap);
  align-items: center;
  transform-origin: 50% var(--cam-y);
  transform: translateY(calc((50% - var(--cam-y) - var(--cam-lift)) * var(--cam))) scale(calc(1 + (var(--zoom) - 1) * var(--cam)));
  transition: --spread .9s var(--ease), --cam 1.2s var(--ease), --cam-k 1.2s var(--ease), --zoom 1.2s var(--ease), --cam-lift 1.2s var(--ease);
  animation: assemble 1.7s cubic-bezier(.3, 1.2, .45, 1) .15s backwards;
}
.stack-grid:focus-within { --spread: .875rem; }
@media (hover: hover) { .stack-grid:hover { --spread: .875rem; } }
.stack-name, .plate, .stack-project { transform: translateY(calc((var(--k, 0) - 1.5) * var(--spread))); }
.stack-name {
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--serif);
  font-size: var(--step-ui);
  font-style: italic;
  line-height: 1.3;
  color: var(--muted);
  transition: opacity .3s;
}
.stack-name::after { content: ''; flex: 1 0 var(--space-3); border-top: 1px solid var(--line-strong); transform-origin: right; animation: line-grow .8s var(--ease) backwards; animation-delay: calc(.9s + var(--k, 0) * .12s); }
.plate { grid-column: 2; width: 100%; height: auto; overflow: visible; transition: opacity .6s var(--ease); animation: plate-in .9s ease backwards; animation-delay: calc(.2s + var(--k, 0) * .1s); }
.plate .rim { fill: var(--slab-top); stroke: var(--line-strong); stroke-width: 1; stroke-linejoin: round; stroke-dasharray: 540; transition: stroke .3s; animation: rim-draw 1.4s var(--ease) backwards, ignite .9s ease-out; animation-delay: calc(.35s + var(--k, 0) * .12s), calc(1.5s + var(--k, 0) * .14s); }
.plate .rim.flash { animation: rim-draw 1.4s var(--ease) backwards, ignite .9s ease-out, flash var(--cycle) ease-out infinite; animation-delay: calc(.35s + var(--k, 0) * .12s), calc(1.5s + var(--k, 0) * .14s), var(--start); }
.plate .side-l, .plate .side-r { stroke: var(--line-strong); stroke-width: 1; stroke-linejoin: round; }
.plate .side-l { fill: var(--slab-left); }
.plate .side-r { fill: var(--slab-right); }
/* The sides are hatched like an engraving, the darker side more densely (patterns in build.py). */
.plate .hatch { stroke: none; }
.plate .hatch-line { stroke: var(--line-strong); stroke-width: .55; }
.plate .hatch-line.lit { stroke-width: .4; }
.plate > g { animation: fade-in .9s ease backwards; animation-delay: calc(.75s + var(--k, 0) * .12s); }
.plate line { stroke: var(--mark-muted); stroke-width: 1; }
.plate line.hot { stroke: var(--accent); stroke-width: 1.5; }
.plate .cell { fill: none; stroke: var(--mark-muted); stroke-width: 1; }
.plate .fill, .plate .hot { stroke: none; }
.plate .fill { fill: var(--mark-muted); }
.plate .hot { fill: var(--accent); }
.stack-project {
  grid-column: 3;
  position: relative;
  padding-block: var(--space-2); /* a taller target for a finger; the rows are taller still */
  padding-left: calc(var(--space-5) + var(--space-2));
  font-family: var(--serif);
  font-size: var(--step-ui);
  line-height: 1.25;
  transition: opacity .3s, color .2s;
}
.stack-project::before { content: ''; position: absolute; left: 0; top: 50%; width: var(--space-5); border-top: 1px solid var(--line-strong); transform-origin: left; animation: line-grow .8s var(--ease) backwards; animation-delay: calc(1.1s + var(--k, 0) * .12s); }
.stack-project.span-2 { grid-row: 1 / 3; align-self: stretch; display: flex; align-items: center; }
.stack-project.span-2::before { top: calc((100% - var(--row-gap)) / 4 - .5 * var(--spread)); bottom: calc((100% - var(--row-gap)) / 4 - .5 * var(--spread)); border: 1px solid var(--line-strong); border-left: 0; }
.stack-project:hover { color: var(--accent); }
.stack-project:focus-visible { outline: none; }
.stack-project:focus-visible > span { outline: 2px solid var(--accent); outline-offset: 4px; }

/* Moving shapes. Transforms on SVG shapes use the plate's unit square (see plate() in build.py). */
.stack .m { transform-box: fill-box; transform-origin: center; }

/* Requests: the queue breathes, deadline-bound requests send out rings, and one request a cycle,
   from a rotating spot, moves to the center. */
.plate .queue { animation: queue 3.2s ease-in-out infinite; animation-delay: calc(var(--i) * -.53s); }
.plate .ripple { fill: none; stroke: var(--accent); stroke-width: 1.5; opacity: 0; animation: ripple 2.4s cubic-bezier(.2, .6, .35, 1) infinite; animation-delay: calc(var(--i) * -.6s); }
.plate .go { fill: var(--spark); opacity: 0; animation: go calc(var(--cycle) * 3) linear infinite; animation-delay: calc(var(--start) + var(--n) * var(--cycle)); }

/* Serving runtime: pages are allocated and freed while the scheduler moves between them. */
.plate .alloc { fill: var(--mark-muted); fill-opacity: 0; animation: alloc calc(var(--cycle) * 2) ease-in-out infinite; animation-delay: calc(var(--i) * -2.3s); }
.plate rect.free { stroke: var(--mark-muted); stroke-width: 1; animation: free calc(var(--cycle) * 2) ease-in-out infinite; animation-delay: calc(var(--i) * -3.1s); }
.plate .cursor { fill: none; stroke: var(--accent); stroke-width: 1.25; opacity: 0; animation: sched calc(var(--cycle) * 2) cubic-bezier(.6, 0, .2, 1) infinite; animation-delay: var(--start); }

/* GPU execution: background work on the outer lanes. On arrival the face flashes, the accented
   lanes run kernels, and a wave of work crosses the other lanes from the center out. */
.plate .job { fill: var(--mark-muted); opacity: 0; animation: job calc(2.4s + var(--i) * .25s) linear infinite; animation-delay: calc(var(--i) * -.9s); }
.plate .kernel { fill: var(--spark); opacity: 0; animation: kernel var(--cycle) linear infinite; animation-delay: calc(var(--start) + (var(--i) - 4) * .14s); }
.plate .lane { animation: lane var(--cycle) ease-in-out infinite; animation-delay: var(--start); }
.plate .wave { animation: wave var(--cycle) ease-out infinite; animation-delay: calc(var(--start) + var(--d) * .05s); }
.plate .burst { fill: var(--spark); opacity: 0; animation: burst var(--cycle) cubic-bezier(.3, 0, .2, 1) infinite; animation-delay: calc(var(--start) + var(--d) * .05s); }

/* Memory and storage: blocks are read in turn and copies move to the center to rise. */
.plate .read { animation: read var(--cycle) ease-in-out infinite; animation-delay: calc(var(--start) + var(--i) * .2s); }
.plate .fetch { fill: var(--spark); opacity: 0; animation: fetch var(--cycle) infinite; animation-delay: calc(var(--start) + var(--i) * .18s); }

/* The vertical path. Positions are the centers of the plates' top faces and follow --spread;
   markers move by animating --p from 0 to 1 between --from and --to. */
.stack-flow {
  --row: calc((100% - 3 * var(--row-gap)) / 4);
  --r0: calc(var(--row) * var(--face) - 1.5 * var(--spread));
  --r2: calc(var(--row) * (2 + var(--face)) + 2 * var(--row-gap) + .5 * var(--spread));
  --r3: calc(var(--row) * (3 + var(--face)) + 3 * var(--row-gap) + 1.5 * var(--spread));
  grid-column: 2;
  grid-row: 1 / -1;
  position: relative;
  align-self: stretch;
  pointer-events: none;
}
.stack-flow::before { content: ''; position: absolute; left: 50%; top: var(--r0); height: calc(var(--r3) - var(--r0)); border-left: 1px solid var(--line-strong); opacity: .6; }
.stack-flow i { position: absolute; left: 50%; opacity: 0; }
.stack-flow :is(.down, .up) { top: calc(var(--from) + (var(--to) - var(--from)) * var(--p)); }
.stack-flow .down { --from: var(--r0); --to: var(--r2); --peak: calc(1 - var(--o) * .3); width: .75rem; height: .75rem; margin: -.375rem 0 0 -.375rem; border-radius: 50%; background: var(--spark); box-shadow: 0 0 0 2px var(--paper), 0 0 1rem .2rem color-mix(in srgb, var(--spark) 50%, transparent); animation: flow-down var(--cycle) infinite; animation-delay: calc(var(--start) + var(--o) * .11s); }
.stack-flow .down::after { content: ''; position: absolute; left: 50%; bottom: 100%; width: 3px; height: 3.5rem; margin-left: -1.5px; background: linear-gradient(to top, var(--spark), transparent); }
.stack-flow .down.spark { width: .375rem; height: .375rem; margin: -.1875rem 0 0 -.1875rem; box-shadow: none; }
.stack-flow .down.spark::after { content: none; }
.stack-flow .up { --from: var(--r3); --to: var(--r2); width: .85rem; height: .85rem; margin: -.425rem 0 0 -.425rem; border-radius: 2px; background: var(--spark); transform: scaleY(.5) rotate(45deg); box-shadow: 0 0 .9rem .1rem color-mix(in srgb, var(--spark) 45%, transparent); animation: flow-up var(--cycle) infinite; animation-delay: calc(var(--start) + var(--o) * .16s); }
.stack-flow .beam { top: var(--r0); height: calc(var(--r3) - var(--r0)); width: 3px; margin-left: -1.5px; border-radius: 3px; background: linear-gradient(to bottom, transparent, var(--spark) 66.7%, transparent); box-shadow: 0 0 .75rem color-mix(in srgb, var(--spark) 40%, transparent); animation: beam var(--cycle) ease-out infinite; animation-delay: var(--start); }
.stack-flow .hit { top: var(--r2); width: 28%; aspect-ratio: 2; margin: -7% 0 0 -14%; border: 2px solid var(--spark); border-radius: 50%; animation: hit var(--cycle) cubic-bezier(.2, .6, .35, 1) infinite; animation-delay: calc(var(--start) + var(--o) * .2s); }

.stack figcaption { max-width: 34rem; margin-top: var(--space-5); font-size: var(--step-meta); line-height: 1.55; color: var(--muted); transition: opacity .4s; }
.stack figcaption .fig-no { margin-right: var(--space-2); }

/* Focusing a project, or hovering one where there is a pointer, lights its layers and dims the rest.
   Touch screens skip the hover, which would otherwise stick after a tap. */
.stack-grid:has([data-thread]:focus-visible) :is(.stack-name, .plate, .stack-project) { opacity: .35; }
.stack-grid:has([data-thread="scheduling"]:focus-visible) [data-thread="scheduling"],
.stack-grid:has([data-thread="performance"]:focus-visible) [data-thread="performance"],
.stack-grid:has([data-thread="data-paths"]:focus-visible) [data-thread="data-paths"] { opacity: 1; }
.stack-grid:has([data-thread="scheduling"]:focus-visible) .plate[data-thread="scheduling"] .rim,
.stack-grid:has([data-thread="performance"]:focus-visible) .plate[data-thread="performance"] .rim,
.stack-grid:has([data-thread="data-paths"]:focus-visible) .plate[data-thread="data-paths"] .rim { stroke: var(--accent); }
@media (hover: hover) {
  .stack-grid:has([data-thread]:hover) :is(.stack-name, .plate, .stack-project) { opacity: .35; }
  .stack-grid:has([data-thread="scheduling"]:hover) [data-thread="scheduling"],
  .stack-grid:has([data-thread="performance"]:hover) [data-thread="performance"],
  .stack-grid:has([data-thread="data-paths"]:hover) [data-thread="data-paths"] { opacity: 1; }
  .stack-grid:has([data-thread="scheduling"]:hover) .plate[data-thread="scheduling"] .rim,
  .stack-grid:has([data-thread="performance"]:hover) .plate[data-thread="performance"] .rim,
  .stack-grid:has([data-thread="data-paths"]:hover) .plate[data-thread="data-paths"] .rim { stroke: var(--accent); }
}

/* While a layer's description is read, the stack opens and the camera centers and enlarges that
   layer; the others fade and the labels step aside. */
.story[data-step] .stack-grid { --spread: 1.25rem; --cam: 1; }
.story[data-step="0"] .stack-grid { --cam-k: 0; }
.story[data-step="1"] .stack-grid { --cam-k: 1; }
.story[data-step="2"] .stack-grid { --cam-k: 2; }
.story[data-step="3"] .stack-grid { --cam-k: 3; }
.story[data-step] :is(.stack-name, .stack-project, .stack figcaption) { opacity: 0; }
.story[data-step] .plate { opacity: .18; }
.story[data-step="0"] .plate[data-layer="0"],
.story[data-step="1"] .plate[data-layer="1"],
.story[data-step="2"] .plate[data-layer="2"],
.story[data-step="3"] .plate[data-layer="3"] { opacity: 1; }

/* Home: facts, sections, projects.
   Below the story every page uses one grid on wide screens: a third for titles, two thirds for
   content (--grid), with the same gap, so content starts on the same line from section to section
   and page to page. The facts' second column starts on that line too. */

.facts { display: grid; grid-template-columns: calc((100% - var(--space-6)) / 3) repeat(2, minmax(0, 1fr)); gap: var(--space-4) var(--space-6); padding-block: var(--space-5); border-block: 1px solid var(--line); }
.facts div { display: grid; align-content: start; gap: var(--space-1); }
.facts dd { font-size: var(--step-ui); line-height: 1.5; }

.section { padding-block: var(--space-9); }
.section-ruled { border-top: 1px solid var(--line); }
.section-head { display: grid; grid-template-columns: var(--grid); align-items: baseline; gap: var(--space-4) var(--space-6); margin-bottom: var(--space-7); }
.section-head:last-child { margin-bottom: 0; }
.section-head h2 { font-size: var(--step-h2); }
.section-intro { max-width: var(--measure); }
.section-tail { display: flex; justify-content: flex-end; padding-top: var(--space-5); }

.project-list { position: relative; isolation: isolate; border-top: 1px solid var(--line); }
.project-row { position: relative; display: grid; grid-template-columns: var(--grid); gap: var(--space-2) var(--space-6); padding-block: var(--space-6); border-bottom: 1px solid var(--line); }
.project-row::before { content: ''; position: absolute; inset: 0 calc(-1 * var(--space-4)); z-index: -1; background: var(--grain), var(--paper-deep); opacity: 0; transition: opacity .3s; }
.project-row:hover::before { opacity: 1; }
.project-name { grid-row: 1 / 3; }
.project-name h3 { font-size: var(--step-lede); }
.project-name .label { margin-top: var(--space-2); }
.project-desc { grid-column: 2; max-width: var(--measure); padding-right: var(--space-6); }
.project-result { grid-column: 2; font-size: var(--step-ui); color: var(--muted); }
.project-result strong { font-weight: 600; color: var(--ink); }
.project-row > .arrow { position: absolute; top: calc(var(--space-6) + .35rem); right: 0; }
.project-row:hover > .arrow { transform: translateX(3px); }

.oss-list { border-top: 1px solid var(--line); }
.oss-item { display: grid; grid-template-columns: var(--grid); align-items: baseline; gap: var(--space-3) var(--space-6); padding-block: var(--space-6); border-bottom: 1px solid var(--line); }
.oss-item:last-child { padding-bottom: 0; border-bottom: 0; } /* the next section's rule closes the list */
.oss-item h3 { font-size: var(--step-lede); }
.oss-body { max-width: var(--measure); }

/* A figure tipped in like a plate: the image on a deeper sheet inside a double hairline frame, and
   its caption below. */
.plate-figure { margin-top: var(--space-6); }
.plate-image { display: block; padding: var(--space-5) var(--space-6); border: 1px solid var(--line); outline: 1px solid var(--line); outline-offset: -.4375rem; background: var(--grain), var(--paper-deep); }
.plate-image img { display: block; width: 100%; height: auto; transition: transform .6s var(--ease); }
@media (hover: hover) { .plate-image:hover img { transform: scale(1.015); } }
.plate-figure figcaption { margin-top: var(--space-3); font-size: var(--step-meta); line-height: 1.55; color: var(--muted); }
.plate-figure .fig-no { margin-right: var(--space-2); }
/* hw-codesign's loop: three steps strung on one rule, like a flow drawn in the margin. */
.pipeline { display: grid; gap: var(--space-4); margin: var(--space-6) 0 0; padding: 0; list-style: none; font-size: var(--step-ui); line-height: 1.5; }
.pipeline li { position: relative; display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); align-items: baseline; gap: var(--space-4); padding-left: var(--space-5); }
.pipeline li::before { content: ''; position: absolute; left: 0; top: .45rem; width: .5625rem; height: .5625rem; border: 1px solid var(--accent); border-radius: 50%; }
.pipeline li:not(:last-child)::after { content: ''; position: absolute; left: calc(.28125rem - .5px); top: 1.1rem; bottom: calc(-1 * var(--space-4) - .4rem); border-left: 1px solid var(--line-strong); }
.about-copy { display: grid; gap: var(--space-4); max-width: var(--measure); }

/* Footer */

/* The footer's rules sit inside the page margins, like every other rule. The email and the links
   share a baseline. */
.footer-main { display: flex; flex-wrap: wrap; align-items: flex-end; align-items: last baseline; justify-content: space-between; gap: var(--space-6); padding-block: var(--space-8) var(--space-7); border-top: 3px double var(--line-strong); }
.footer-contact .label { margin-bottom: var(--space-3); }
.footer-email { display: block; width: fit-content; max-width: 100%; font-family: var(--serif); font-size: var(--step-h2); line-height: 1.2; letter-spacing: var(--track-tight); border-bottom: 1px solid var(--line-strong); transition: border-color .2s; overflow-wrap: anywhere; }
.footer-email:hover { border-color: currentColor; }
.footer-links { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5); }
.footer-bottom { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3) var(--space-5); padding-block: var(--space-5); border-top: 1px solid var(--line); font-size: var(--step-meta); color: var(--muted); }
.footer-place { margin-left: auto; }
.footer-controls { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3) var(--space-5); }
.footer-bottom a:hover { color: var(--ink); }
.footer-controls :is(a, button) { min-height: 1.5rem; }
.to-top { display: inline-flex; align-items: center; gap: var(--space-1); }
.to-top .arrow { width: .875rem; height: .875rem; }
.to-top:hover .arrow { transform: translateY(-2px); }
.motion-toggle { display: inline-flex; align-items: center; gap: var(--space-2); padding-block: var(--space-1); font-size: inherit; color: inherit; }
.motion-toggle:hover:not(:disabled) { color: var(--ink); }
.motion-toggle:disabled { cursor: default; }
.motion-dot { width: .375rem; height: .375rem; border-radius: 50%; background: var(--accent); }
.motion-paused .motion-dot { background: transparent; box-shadow: inset 0 0 0 1px var(--accent); }

/* Inner pages */

.page-intro { padding-block: var(--space-8) var(--space-7); border-bottom: 3px double var(--line-strong); container-type: inline-size; }
.page-intro h1 { max-width: 20ch; margin-top: var(--space-4); font-size: var(--step-h1); }
.page-intro .lede { max-width: 44ch; margin-top: var(--space-5); font-size: var(--step-lede); line-height: 1.45; color: var(--muted); }
.page-intro .page-note { max-width: var(--measure); margin-top: var(--space-4); color: var(--muted); }
.page-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4) var(--space-6); margin-top: var(--space-6); }
.page-missing { padding-bottom: var(--space-9); border-bottom: 0; }
.name-ko { margin-left: .2em; font-family: var(--serif-ko); font-size: var(--step-lede); letter-spacing: 0; color: var(--muted); }
.cv-contact { display: none; } /* printed only; on screen the footer carries it */

/* Research */

/* Each title spans the grid, and its date and topics sit beside the body, on the body's first
   baseline. The title carries the anchor, so a link to a thread lands it just below the header. */
.research-entry { display: grid; grid-template-columns: var(--grid); align-items: baseline; gap: var(--space-5) var(--space-6); padding-block: var(--space-8); border-bottom: 1px solid var(--line); }
.research-entry:last-child { border-bottom: 0; }
.research-entry > h2 { grid-column: 1 / -1; font-size: var(--step-h2); }
.entry-aside { grid-column: 1; }
.tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-4) 0 0; padding: 0; list-style: none; }
.tags li { padding: .125rem var(--space-2); border: 1px solid var(--line); border-radius: 2px; background: var(--grain), var(--paper-deep); font-size: var(--step-meta); color: var(--muted); }
.entry-body { grid-column: 2; min-width: 0; }
.entry-body > * + * { margin-top: var(--space-5); }
.entry-body > .oss-block + .oss-block { margin-top: var(--space-7); }
.entry-text { max-width: var(--measure); }
.entry-status { font-size: var(--step-ui); color: var(--muted); }
.entry-subtitle { max-width: 36ch; font-size: var(--step-lede); line-height: 1.4; }
.entry-points { max-width: var(--measure); margin: 0; padding-left: 0; } /* bullets hang in the gutter */
.entry-points li + li { margin-top: var(--space-3); }
.entry-points li::marker { color: var(--accent); }
.oss-block h3 { font-size: var(--step-lede); }
.oss-block > .label { margin: var(--space-2) 0 var(--space-4); }
.entry-lab { margin-top: var(--space-3); font-size: var(--step-ui); line-height: 1.5; color: var(--muted); }
.entry-lab a { color: var(--ink); }
.entry-role { max-width: var(--measure); }
.entry-role .label { margin-right: var(--space-2); color: var(--accent); }

/* Notes (shown once notes are published) */

.journal { padding-block: var(--space-6) var(--space-9); }
.journal-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-5); margin-bottom: var(--space-4); }
.filters { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.filter { padding: var(--space-1) var(--space-3); border: 1px solid transparent; border-radius: 2px; font-size: var(--step-ui); }
.filter[aria-pressed="true"] { border-color: var(--ink); background: var(--grain-ink), var(--ink); color: var(--paper); }
.filter:hover:not([aria-pressed="true"]) { background: var(--paper-deep); }
.search-box { display: flex; align-items: center; gap: var(--space-3); min-width: 14rem; padding-block: var(--space-2); border-bottom: 1px solid var(--muted); }
.search-box input { width: 100%; border: 0; background: transparent; font-size: var(--step-ui); outline-offset: var(--space-2); }
.journal-count { margin-bottom: var(--space-4); font-size: var(--step-meta); color: var(--muted); }
/* Each piece is a row of the site's grid: its date and kind beside the title's first baseline. */
.journal-card { position: relative; display: grid; grid-template-columns: var(--grid); align-items: baseline; gap: var(--space-3) var(--space-6); padding-block: var(--space-6); border-top: 1px solid var(--line); }
.journal-card > .arrow { position: absolute; top: calc(var(--space-6) + .56 * var(--step-h2) - 8px); right: 0; }
.journal-card:hover > .arrow { transform: translateX(3px); }
.journal-card:last-child { border-bottom: 1px solid var(--line); }
/* Without the search tools, the intro's rule already sits above the first note. */
.journal > #journal-list:first-child > .journal-card:first-child { border-top: 0; }
.journal-card::before { content: ''; position: absolute; inset: 0 calc(-1 * var(--space-4)); z-index: -1; background: var(--grain), var(--paper-deep); opacity: 0; transition: opacity .3s; }
.journal-card:hover::before { opacity: 1; }
.journal-date { display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--step-meta); color: var(--muted); }
.journal-card h2 { padding-right: var(--space-6); font-size: var(--step-h2); }
.journal-card p { max-width: var(--measure); margin-top: var(--space-3); }
.journal-card .journal-meta { margin-top: var(--space-4); }
.empty-state { padding-block: var(--space-8); border-block: 1px solid var(--line); text-align: center; color: var(--muted); }
.empty-state .text-link { margin-top: var(--space-4); }

.reading-progress { position: fixed; top: 0; left: 0; z-index: 50; width: 0; height: 2px; background: var(--accent); }
.back-link { display: inline-block; margin-bottom: var(--space-6); color: var(--muted); }
/* A piece of writing sits on the site's grid. Its title spans the page; below the masthead, its byline
   and facts stay in view in the left third while the text runs in the right two thirds. */
.article-grid { display: grid; grid-template-columns: var(--grid); align-items: start; gap: var(--space-6); padding-block: var(--space-7) var(--space-9); }
.article-meta { position: sticky; top: calc(var(--header-h) + var(--space-5)); display: grid; gap: var(--space-4); }
.article-byline { display: flex; justify-content: space-between; gap: var(--space-4); padding-bottom: var(--space-3); border-bottom: 1px solid var(--line); font-size: var(--step-meta); color: var(--muted); }
.article-body { min-width: 0; line-height: 1.7; }
.article-body > :is(p, h2, ul, ol, blockquote) { max-width: var(--measure); }
.article-body > h2:first-child { margin-top: 0; }
.article-body > * + * { margin-top: var(--space-5); }
.article-body h2 { margin-top: var(--space-7); font-size: var(--step-lede); }
.article-body .formula { padding: var(--space-5) var(--space-4); border-block: 1px solid var(--line); font-size: var(--step-lede); text-align: center; overflow-x: auto; }
.article-body blockquote { padding-left: var(--space-5); border-left: 2px solid var(--accent); font-size: var(--step-lede); line-height: 1.45; }
.article-body .note-aside { padding-top: var(--space-4); border-top: 1px solid var(--line); font-size: var(--step-ui); color: var(--muted); }
.article-next { display: grid; gap: var(--space-2); margin-top: var(--space-7); padding-top: var(--space-5); border-top: 1px solid var(--line); }
.article-next-title { font-size: var(--step-lede); }
/* A case study opens with its facts on a card, and sets numbers in tables ruled like a journal's. */
.article-facts { padding: var(--space-4) var(--space-5); border: 1px solid var(--line); background: var(--grain), var(--paper-deep); font-size: var(--step-ui); line-height: 1.5; }
.article-facts div + div { margin-top: var(--space-3); }
.article-facts dt { font-size: var(--step-meta); letter-spacing: var(--track-caps); text-transform: uppercase; color: var(--muted); }
.article-body code { font-family: var(--mono); font-size: .9em; -webkit-text-stroke: .02em currentColor; }
.article-body :not(pre) > code { white-space: nowrap; }
.article-body pre { padding: var(--space-4) var(--space-5); border-left: 2px solid var(--line-strong); background: var(--grain), var(--paper-deep); font-size: var(--step-ui); line-height: 1.55; overflow-x: auto; }
.article-body pre code { font-size: 1em; }
.table-wrap { overflow-x: auto; }
.article-body table { width: 100%; border-collapse: collapse; border-top: 1.5px solid var(--line-strong); border-bottom: 1.5px solid var(--line-strong); font-size: var(--step-ui); line-height: 1.45; }
.article-body caption { padding-bottom: var(--space-3); font-size: var(--step-meta); line-height: 1.55; color: var(--muted); text-align: left; }
.article-body caption .fig-no { margin-right: var(--space-2); }
.article-body th { padding: var(--space-2) var(--space-4) var(--space-2) 0; border-bottom: 1px solid var(--line); font-size: var(--step-meta); font-weight: 400; letter-spacing: var(--track-caps); text-align: left; text-transform: uppercase; color: var(--muted); }
.article-body td { padding: var(--space-3) var(--space-4) var(--space-3) 0; border-top: 1px solid var(--line); vertical-align: baseline; }
.article-body tbody tr:first-child td { border-top: 0; }
.article-body :is(th, td).num { padding-right: 0; text-align: right; white-space: nowrap; }
.article-body .cases { padding: 0; list-style: none; border-block: 1px solid var(--line); font-size: var(--step-ui); line-height: 1.5; }
.article-body .cases li { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); align-items: baseline; gap: var(--space-4); padding-block: var(--space-2); }
.article-body .cases li + li { border-top: 1px dotted var(--line-strong); }
.article-body .cases span { color: var(--accent); }

/* CV */

/* The section links run across the top. Each section is a row of the grid: its title on the left,
   on the first entry's baseline, and its entries on the right. */
.cv-nav { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-5); padding-block: var(--space-5); border-bottom: 1px solid var(--line); font-size: var(--step-ui); }
.cv-nav a { padding-block: var(--space-1); color: var(--muted); transition: color .2s; }
.cv-nav a:hover { color: var(--ink); }
.cv-section { display: grid; grid-template-columns: var(--grid); align-items: baseline; gap: var(--space-5) var(--space-6); padding-block: var(--space-7); border-bottom: 1px solid var(--line); }
.cv-section:last-child { padding-bottom: var(--space-9); border-bottom: 0; }
.cv-section h2 { font-size: var(--step-lede); letter-spacing: 0; }
/* Entries fill the column, so dates end on the page's right edge; their text keeps the measure. */
.cv-entries { min-width: 0; }
.cv-entries :is(p, dd) { max-width: var(--measure); }
.cv-intro { margin-bottom: var(--space-5); color: var(--muted); }
.cv-item + .cv-item { margin-top: var(--space-5); }
.cv-item-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-1) var(--space-3); }
.cv-item h3 { font-size: var(--step-body); line-height: 1.35; letter-spacing: 0; }
.cv-date { font-size: var(--step-meta); color: var(--muted); white-space: nowrap; }
.cv-place { margin-top: 2px; font-size: var(--step-meta); color: var(--muted); }
.cv-item > p:not(.cv-place) { margin-top: var(--space-2); font-size: var(--step-ui); line-height: 1.6; }
.cv-item > .link-row { margin-top: var(--space-2); }
.cv-roles { margin: var(--space-2) 0 0; padding: 0; list-style: none; font-size: var(--step-ui); }
.cv-roles li { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); padding-block: var(--space-1); }
/* Dotted leaders run from a title to its date, as in a table of contents. Their dots sit on the
   baseline: an empty box aligns its bottom edge there. */
.leader { flex: 1 1 0; min-width: var(--space-6); height: .3em; background: radial-gradient(circle, var(--line-strong) .75px, transparent 1.1px) 0 100% / 7px 3px repeat-x; }
.skills { display: grid; gap: var(--space-4); }
.skills dt { font-size: var(--step-ui); font-style: italic; }
.skills dd { margin-top: 2px; font-size: var(--step-ui); }

/* Motion */

.initial-rise { animation: rise-in .9s var(--ease) backwards; }
.delay-one { animation-delay: .08s; }
.delay-two { animation-delay: .16s; }
.delay-three { animation-delay: .24s; }
.reveal.ready { opacity: 0; transform: translateY(1rem); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.reveal.ready.in { opacity: 1; transform: none; }
@keyframes rise-in { from { opacity: 0; transform: translateY(1rem); } }
@keyframes fade-in { from { opacity: 0; } }
@keyframes plate-in { from { opacity: 0; } }
@keyframes assemble { from { --spread: 4.5rem; } }
@keyframes rim-draw { from { stroke-dashoffset: 540; } }
@keyframes ignite { 0%, 100% { stroke: var(--line-strong); } 35% { stroke: var(--spark); stroke-width: 2.5; } }
@keyframes flash { 0%, 49% { fill: var(--slab-top); } 52% { fill: color-mix(in srgb, var(--spark) 22%, var(--slab-top)); } 85%, 100% { fill: var(--slab-top); } }
@keyframes line-grow { from { transform: scaleX(0); } }
@keyframes queue { 50% { transform: scale(.72); } }
@keyframes ripple { from { opacity: .9; transform: scale(1); } to { opacity: 0; transform: scale(3); } }
@keyframes go {
  0% { opacity: 0; transform: translate(var(--dx), var(--dy)); animation-timing-function: cubic-bezier(.5, 0, .3, 1); }
  .6% { opacity: 1; }
  3.3% { opacity: 1; transform: none; }
  4% { opacity: 0; transform: none; }
}
@keyframes alloc { 35%, 75% { fill-opacity: 1; } }
@keyframes free { 40%, 70% { fill-opacity: .12; } }
@keyframes sched {
  0%, 9% { opacity: 1; transform: translate(.57px, 0); }
  12.5%, 21.5% { opacity: 1; transform: translate(.57px, .38px); }
  25%, 34% { opacity: 1; transform: translate(.38px, .19px); }
  37.5%, 46.5% { opacity: 1; transform: translate(.38px, .76px); }
  50%, 59% { opacity: 1; transform: translate(.19px, .57px); }
  62.5%, 71.5% { opacity: 1; transform: translate(.76px, 0); }
  75%, 84% { opacity: 1; transform: translate(.19px, .38px); }
  87.5%, 96.5% { opacity: 1; transform: translate(.57px, .76px); }
  100% { opacity: 1; transform: translate(.57px, 0); }
}
@keyframes job { 0% { opacity: 0; transform: none; } 12%, 85% { opacity: 1; } 100% { opacity: 0; transform: translate(.78px, 0); } }
@keyframes kernel { 0%, 50% { opacity: 0; transform: none; } 53%, 84% { opacity: 1; } 88%, 100% { opacity: 0; transform: translate(.78px, 0); } }
@keyframes lane { 0%, 46%, 96%, 100% { stroke-opacity: .5; } 54%, 84% { stroke: var(--spark); stroke-opacity: 1; stroke-width: 2.25; } }
@keyframes wave { 0%, 50% { stroke: var(--mark-muted); } 53% { stroke: var(--spark); stroke-width: 1.75; } 74%, 100% { stroke: var(--mark-muted); stroke-width: 1; } }
@keyframes burst { 0%, 50% { opacity: 0; transform: none; } 52%, 68% { opacity: 1; } 72%, 100% { opacity: 0; transform: translate(.74px, 0); } }
@keyframes read { 0%, 10%, 26%, 100% { fill-opacity: .45; } 16% { fill-opacity: 1; } }
@keyframes fetch {
  0%, 24% { opacity: 0; transform: none; }
  26% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5, 0, .3, 1); }
  32% { opacity: 1; transform: translate(var(--dx), var(--dy)); }
  35%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); }
}
@keyframes flow-down {
  0%, 10% { --p: 0; opacity: 0; animation-timing-function: cubic-bezier(.45, 0, .55, 1); }
  13%, 47% { opacity: var(--peak); }
  50%, 100% { --p: 1; opacity: 0; }
}
@keyframes flow-up {
  0%, 32% { --p: 0; opacity: 0; animation-timing-function: cubic-bezier(.45, 0, .55, 1); }
  35%, 47% { opacity: 1; }
  50%, 100% { --p: 1; opacity: 0; }
}
@keyframes beam { 0%, 46% { opacity: 0; } 51% { opacity: .9; } 72%, 100% { opacity: 0; } }
@keyframes hit { 0%, 49% { opacity: 0; transform: scale(.15); } 51% { opacity: 1; } 82%, 100% { opacity: 0; transform: scale(1.8); } }
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: fade-in .2s ease reverse; }
::view-transition-new(root) { animation: fade-in .35s ease; }

/* app.js marks Fig. 1 idle while it is off screen: its loops pause, and on a phone, where the figure
   starts below the hero, its entrance plays when it scrolls into view. */
.stack.idle, .stack.idle *, .stack.idle *::before, .stack.idle *::after { animation-play-state: paused !important; }

.motion-paused * { scroll-behavior: auto !important; }
.motion-paused :is(.initial-rise, .plate, .plate .rim, .plate > g, .stack .m, .stack-grid) { animation: none; }
.motion-paused .stack-grid { --spread: 0px !important; --cam: 0 !important; }
.motion-paused .stack-name::after, .motion-paused .stack-project::before { animation: none; }
.motion-paused .reveal.ready { opacity: 1; transform: none; transition: none; }
.motion-paused .stack-flow i { display: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
  .reveal.ready { opacity: 1; transform: none; }
  .stack .m { animation: none; }
  .stack-grid { --spread: 0px !important; --cam: 0 !important; }
  .stack-flow i { display: none; }
}

/* With motion paused or reduced, the story highlights a layer without zooming. On narrow screens the
   cards would hide it, so the camera still moves there, in a cut rather than a glide. */
@media (max-width: 52rem) {
  .motion-paused .story[data-step] .stack-grid { --cam: 1 !important; transition: none; }
}
@media (max-width: 52rem) and (prefers-reduced-motion: reduce) {
  .story[data-step] .stack-grid { --cam: 1 !important; }
}

/* Responsive */

@media (max-width: 64rem) {
  .story { column-gap: var(--space-6); }
  /* Below this width a third is too narrow for a title, so titles sit above their content. */
  :root { --grid: minmax(0, 1fr); }
  .facts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .project-name { grid-row: auto; padding-right: var(--space-6); }
  .project-desc, .project-result, .entry-aside, .entry-body { grid-column: 1; }
  .entry-points { padding-left: 1.1em; }
  .journal-card > .arrow { top: calc(var(--space-6) + .2rem); }
  .article-meta { position: static; }
}

@media (max-width: 52rem) {
  .story { grid-template-columns: minmax(0, 1fr); }
  .hero-copy { min-height: 0; padding-block: var(--space-7) var(--space-6); }
  /* The figure fills the screen, spanning the steps' row so it stays pinned while they pass, and
     each description crosses it as a card. The camera lifts the focused layer above the card. */
  .story-figure { grid-column: 1; grid-row: 2 / 4; z-index: 1; }
  .story-steps { grid-column: 1; grid-row: 3; z-index: 2; padding: 90svh 0 12svh; pointer-events: none; }
  .story-step { min-height: 0; max-width: none; margin-bottom: 70svh; padding: var(--space-4) var(--space-5); gap: var(--space-2); border: 1px solid var(--line); border-radius: 2px; background: var(--grain), color-mix(in srgb, var(--paper) 90%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); box-shadow: 0 12px 32px var(--shadow); pointer-events: auto; }
  .story-step:last-child { margin-bottom: 0; }
  .story-step h2 { font-size: var(--step-lede); }
  .story-step p:not(.label) { font-size: var(--step-ui); line-height: 1.5; }
  .story-link { margin-top: var(--space-1); }
  .stack { width: min(100%, calc((100svh - var(--header-h) - 6rem) / 1.32)); }
  /* app.js refines --zoom and --cam-lift to fit the focused layer between the header and the cards. */
  .stack-grid { --cam-lift: calc(.28 * (100svh - var(--header-h))); }
}

@media (max-width: 40rem) {
  .menu-toggle { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) 0 var(--space-3) var(--space-3); font-size: var(--step-ui); }
  .menu-symbol { display: flex; flex-direction: column; gap: 5px; width: 1.25rem; }
  .menu-symbol::before, .menu-symbol::after { content: ''; height: 1px; background: currentColor; transition: transform .3s var(--ease); }
  .menu-toggle[aria-expanded="true"] .menu-symbol::before { transform: translateY(3px) rotate(45deg); }
  .menu-toggle[aria-expanded="true"] .menu-symbol::after { transform: translateY(-3px) rotate(-45deg); }
  .nav-links { position: absolute; top: var(--header-h); left: 0; right: 0; display: none; flex-direction: column; align-items: flex-start; gap: var(--space-4); padding: var(--space-5) var(--gutter) var(--space-6); border-bottom: 1px solid var(--line); background: var(--grain), var(--paper); box-shadow: 0 14px 24px var(--shadow); }
  .nav-links.open { display: flex; animation: fade-in .25s; }
  .nav-links a { padding-block: 0; font-family: var(--serif); font-size: var(--step-h2); color: var(--ink); }
  .nav-links .contact-nav { margin-top: var(--space-2); font-size: var(--step-ui); }
  .facts { grid-template-columns: minmax(0, 1fr); }
  .stack-grid { --zoom: 1.4; grid-template-columns: minmax(min-content, .75fr) minmax(0, 1.6fr) minmax(min-content, .75fr); }
  .stack { width: min(100%, calc((100svh - var(--header-h) - 6rem) / 1.14)); }
  .stack-name::after { flex-basis: var(--space-2); }
  .stack-project { padding-left: var(--space-4); }
  .stack-project::before { width: var(--space-3); }
  /* On phones a date always sits under its title, rather than beside it only when the title is short. */
  .cv-item-head { flex-direction: column; gap: 0; }
  .leader { display: none; }
  .pipeline li, .article-body .cases li { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .article-body pre { font-size: var(--step-meta); }
  .plate-image { padding: var(--space-4); }
  .footer-main { flex-direction: column; align-items: flex-start; }
  .footer-bottom { flex-direction: column; align-items: stretch; }
  .footer-place { margin-left: 0; }
  .footer-controls { justify-content: space-between; }
  .journal-date { flex-direction: row; gap: var(--space-4); }
}

@media (max-width: 40rem) {
  .no-js .menu-toggle { display: none; }
  .no-js .nav-shell { height: auto; flex-wrap: wrap; gap: var(--space-3) var(--space-5); padding-block: var(--space-4); }
  .no-js .nav-links { position: static; display: flex; flex-direction: row; flex-wrap: wrap; gap: var(--space-4); padding: 0; border: 0; background: none; box-shadow: none; }
  .no-js .nav-links a { font-size: var(--step-ui); }
  .no-js .nav-links .contact-nav { margin-top: 0; }
}
.no-js :is(.motion-toggle, .journal-tools, #print-cv) { display: none; }

/* A screen too short to hold the whole figure, such as a phone on its side or a short window, reads
   the story in order instead: the figure in place, then the four descriptions. app.js leaves the
   layers unfocused while the figure is not pinned. */
@media (max-height: 32rem), (min-width: 40.0625rem) and (max-height: 40rem) {
  .hero-copy { min-height: 0; }
  .story-figure { position: static; height: auto; overflow: visible; }
  .stack { width: min(100%, 30rem); }
  .story-step { min-height: 0; }
}
@media (max-width: 52rem) and (max-height: 32rem), (min-width: 40.0625rem) and (max-width: 52rem) and (max-height: 40rem) {
  .story-figure { grid-row: 2; }
  .story-steps { padding: var(--space-6) 0 var(--space-8); }
  .story-step { margin-bottom: var(--space-4); }
}
@media (min-width: 52.0625rem) and (max-height: 40rem) {
  .story-figure { grid-row: 1; padding-block: var(--space-6); }
  .story-steps { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-7); padding-block: var(--space-6) var(--space-8); }
  .story-step { justify-content: flex-start; }
  .story-step h2 { font-size: var(--step-lede); }
}

/* A two-part label stacks when its column is too narrow to hold it on one line. */
@container (max-width: 28rem) {
  .label-pair > span { display: block; }
  .label-pair > .sep { display: none; }
}

/* Footer controls get a full-size target on touch screens. */
@media (pointer: coarse) {
  .footer-controls :is(a, button) { min-height: 2.75rem; }
}

/* Print */

@media print {
  /* Paper keeps the same levels, set in points. */
  :root {
    --paper: #fff; --paper-deep: #fff; --ink: #000; --muted: #444; --line: #ccc; --line-strong: #999; --grain: none; --grain-ink: none;
    --step-meta: 8.5pt; --step-ui: 9.5pt; --step-body: 10.5pt; --step-lede: 13pt; --step-h2: 16pt; --step-h1: 26pt; --step-display: 32pt;
  }
  html, body { background: #fff; color: #000; }
  .site-header, .site-footer, .cv-nav, .page-actions, .skip, .reading-progress, .stack-flow { display: none !important; }
  .wrap { width: 100%; }
  .story-figure { position: static; height: auto; }
  .stack, .stack * { animation: none !important; } /* print the figure at rest, even if it never scrolled into view */
  .page-intro { padding: 0 0 12pt; }
  .cv-contact { display: block; margin-top: 6pt; font-size: var(--step-ui); color: var(--muted); }
  .cv-section { padding-block: 12pt; }
  .cv-section:last-child { padding-bottom: 0; }
  .cv-item { break-inside: avoid; }
  .cv-section > h2 { break-after: avoid; }
  .cv-link:not(.link-row) { display: none; }
  .reveal.ready { opacity: 1; transform: none; }
  a[href^="mailto"]::after { content: ' (' attr(href) ')'; font-size: 9pt; }
}

