/* ============================================================================
   lacaffar.github.io
   Black page, one typeface, two type sizes. The project covers carry the page.
   ========================================================================== */

:root {
  --bg:    #000000;
  --text:  #F0F0F0;
  --muted: rgba(240, 240, 240, 0.55);
  --faint: rgba(240, 240, 240, 0.34);
  --rule:  rgba(240, 240, 240, 0.18);

  --col:   1038px;  /* 990px of content inside 24px gutters */
  --side:  310px;   /* text side of a project row */
  --gap:   25px;    /* text to image gutter */

  --font: 'General Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
          Roboto, Helvetica, Arial, sans-serif;

  color-scheme: dark;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 400 12px/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

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

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

/* --- shared micro type ---------------------------------------------------- */

.label {           /* 12px uppercase: name, org, section labels */
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin: 0;
}

.meta {            /* 12px plain: years, secondary lines */
  font-size: 12px;
  font-weight: 400;
  color: var(--muted);
  margin: 0;
}

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

/* --- header --------------------------------------------------------------- */

.head {
  padding: 88px 0 0;
}

.head .portrait {
  width: 120px;
  height: 120px;
  object-fit: cover;
  margin-bottom: 26px;
}

.head .label { margin-bottom: 6px; }

.head .meta { margin-bottom: 14px; }

.links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 18px;
  font-size: 12px;
}

.links a {
  color: var(--text);
  padding-bottom: 1px;
  border-bottom: 1px solid transparent;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.links a:hover,
.links a:focus-visible { border-bottom-color: var(--text); }

/* --- project index -------------------------------------------------------- */

.work { padding: 130px 0 0; }

.row {
  display: grid;
  grid-template-columns: var(--side) 1fr;
  gap: var(--gap);
  align-items: start;
  margin-bottom: 62px;
}

.row-text { padding-top: 2px; }

.row h2 {
  font-size: 30px;
  line-height: 36px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0;
  display: inline;
  background-image: linear-gradient(var(--text), var(--text));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  transition: background-size 0.28s ease;
}

.row hr.rule { margin: 16px 0 12px; }

.row .meta { margin-top: 6px; }

.row-img {
  display: block;
  overflow: hidden;
  background: #111;
}

.row-img img {
  width: 100%;
  transition: opacity 0.28s ease, transform 0.5s ease;
}

.row:hover h2 { background-size: 100% 1px; }
.row:hover .row-img img { opacity: 0.86; transform: scale(1.012); }

.row a:focus-visible { outline: 1px solid var(--text); outline-offset: 4px; }

/* --- footer --------------------------------------------------------------- */

.foot {
  padding: 70px 0 64px;
  margin-top: 60px;
}

.foot hr.rule { margin-bottom: 22px; }

.foot-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 26px;
  justify-content: space-between;
  font-size: 12px;
  color: var(--muted);
}

.foot a { border-bottom: 1px solid transparent; padding-bottom: 1px; }
.foot a:hover { border-bottom-color: var(--muted); }

/* --- project pages -------------------------------------------------------- */

.back { padding: 56px 0 0; font-size: 12px; }

.back a { color: var(--muted); }
.back a:hover { color: var(--text); }

.case-head { padding: 92px 0 0; }

.case-head h1 {
  font-size: 30px;
  line-height: 36px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 16px;
}

.case-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 32px;
  padding-top: 12px;
}

.case-facts div { min-width: 0; }
.case-facts dt { font-size: 12px; color: var(--faint); margin: 0 0 3px; }
.case-facts dd {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; margin: 0;
}

.case-cover { margin: 40px 0 0; }

.fig { margin: 28px 0 24px; }
.fig img { width: 100%; }
.fig figcaption { font-size: 12px; color: var(--faint); margin-top: 10px; max-width: 68ch; }

.case-body { padding: 56px 0 0; }

.case-body section { margin-bottom: 46px; }

.case-body h2 {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; margin: 0 0 18px;
  padding-top: 18px; border-top: 1px solid var(--rule);
}

.case-body h3 {
  font-size: 15px; font-weight: 600; color: var(--text);
  margin: 26px 0 8px;
}

.case-body p {
  font-size: 15px; line-height: 1.65; color: var(--muted);
  margin: 0 0 16px; max-width: 68ch;
}

.case-body ul { margin: 0 0 16px; padding-left: 18px; max-width: 68ch; }
.case-body li { font-size: 15px; line-height: 1.65; color: var(--muted); margin-bottom: 7px; }

.log { display: grid; gap: 22px; max-width: 68ch; }
.log-item { display: grid; grid-template-columns: 132px 1fr; gap: var(--gap); }
.log-when {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--faint); padding-top: 3px;
}
.log p { margin: 0; }

.case-nav {
  display: flex; justify-content: space-between; gap: 20px;
  padding-top: 22px; border-top: 1px solid var(--rule); font-size: 12px;
}
.case-nav a { color: var(--muted); }
.case-nav a:hover { color: var(--text); }

/* --- 404 ------------------------------------------------------------------ */

.mid { padding: 180px 0 120px; }
.mid h1 { font-size: 30px; line-height: 36px; font-weight: 700; margin: 0 0 14px; }

/* --- narrow --------------------------------------------------------------- */

@media (max-width: 760px) {
  .row { grid-template-columns: 1fr; gap: 18px; }

  .row { margin-bottom: 52px; }
  .row-text { order: -1; }

  .work { padding-top: 84px; }
  .head { padding-top: 56px; }
  .head .portrait { width: 96px; height: 96px; }

  .log-item { grid-template-columns: 1fr; gap: 4px; }

  .case-head { padding-top: 60px; }
  .foot-inner { flex-direction: column; }
}

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

/* --- in progress ---------------------------------------------------------- */

.wip {
  display: inline-block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  border: 1px solid var(--rule);
  padding: 3px 8px;
}

.note {
  border-left: 1px solid var(--rule);
  padding-left: 16px;
  max-width: 62ch;
}

.note p {
  font-size: 12px;
  line-height: 1.6;
  color: var(--faint);
  margin: 0 0 8px;
}

.note p:last-child { margin-bottom: 0; }

.head .note { margin-top: 26px; }

.work .note { margin-bottom: 62px; }

/* a project that exists but has no page written yet */

.row.stub h2 { color: var(--muted); }
.row.stub .row-text .wip { margin-top: 12px; }

.hold {
  display: flex;
  align-items: flex-end;
  aspect-ratio: 16 / 10;
  border: 1px solid var(--rule);
  padding: 14px;
}

.hold span {
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
}

/* what is still missing from a page */

.todo { list-style: none; margin: 0; padding: 0; max-width: 68ch; }

.todo li {
  font-size: 12px;
  line-height: 1.6;
  color: var(--faint);
  border-top: 1px solid var(--rule);
  padding: 9px 0;
  margin: 0;
}

.case-head .wip { margin-bottom: 16px; }

/* --- case page extras ----------------------------------------------------- */

.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px var(--gap);
  margin: 0 0 34px;
  padding-top: 18px;
  border-top: 1px solid var(--rule);
}

.stats .stat-n {
  font-size: 30px;
  line-height: 34px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 6px;
}

.stats .stat-l {
  font-size: 12px;
  line-height: 1.5;
  color: var(--faint);
  margin: 0;
  max-width: 26ch;
}

.figs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  margin: 28px 0 24px;
}

.figs figure { margin: 0; }
.figs figcaption { font-size: 12px; color: var(--faint); margin-top: 10px; }

/* an outbound link that should read as a destination, not body text */

.cta {
  display: inline-block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 3px;
  transition: border-color 0.15s ease;
}

.cta:hover, .cta:focus-visible { border-bottom-color: var(--text); }

.case-head .cta { margin-top: 16px; }

/* a two-column definition of a problem and what it forces */

.fix { display: grid; gap: 18px; max-width: 68ch; margin-bottom: 16px; }

.fix-item { display: grid; grid-template-columns: 132px 1fr; gap: var(--gap); }

.fix-when {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--faint); padding-top: 3px;
}

.fix p { margin: 0; }

@media (max-width: 760px) {
  .stats { grid-template-columns: 1fr 1fr; }
  .figs { grid-template-columns: 1fr; }
  .fix-item { grid-template-columns: 1fr; gap: 4px; }
}

/* inline prose links need to look like links */

.case-body p a,
.note a {
  color: var(--text);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  transition: border-color 0.15s ease;
}

.case-body p a:hover,
.note a:hover,
.case-body p a:focus-visible,
.note a:focus-visible { border-bottom-color: var(--text); }

/* ============================================================================
   Two panel view. Portfolio drifts left, narrative comes in from the right.
   No JavaScript: one hidden checkbox at the top of the document drives all of it.
   ========================================================================== */

:root {
  /* one slide, one easing, everything else hangs off these two numbers */
  --slide: 0.88s;
  --soft:  cubic-bezier(0.25, 1, 0.3, 1);
}

body.split { overflow: hidden; }

.view-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

/* the shell clips, the deck slides, each panel scrolls on its own */

.shell {
  width: 100%;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}

.deck {
  display: flex;
  width: 200%;
  height: 100%;
  transform: translate3d(0, 0, 0);
  transition: transform var(--slide) var(--soft);
  will-change: transform;
}

#view-toggle:checked ~ .shell .deck { transform: translate3d(-50%, 0, 0); }

.panel {
  width: 50%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: none;
  /* the panel off screen drops out of the tab order once the slide is done */
  visibility: hidden;
  transition: visibility 0s linear var(--slide);
}

.panel::-webkit-scrollbar { width: 0; height: 0; display: none; }

#view-toggle:not(:checked) ~ .shell #portfolio,
#view-toggle:checked ~ .shell #narrative {
  visibility: visible;
  transition-delay: 0s;
}

/* --- chrome that rides along with its own panel --------------------------- */

/* zero height, sticky at the top of its panel, so anything inside it holds
   still while the panel scrolls and still travels with the slide */

.dock {
  position: sticky;
  top: 0;
  height: 0;
  z-index: 10;
}

/* --- the switch ----------------------------------------------------------- */

/* two lowercase words and a loop drawn round the live one. the whole thing is
   one label for one checkbox, so a click lands anywhere and flips the view */

.switch {
  position: absolute;
  top: 34px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;

  /* it leaves with its own page, then gusts back in on the other side */
  opacity: 0;
  transform: translateX(var(--gust));
  filter: blur(5px);
  /* hidden state holds still until the slide is over, then just stops being */
  transition: opacity 0s linear var(--slide),
              transform 0s linear var(--slide),
              filter 0s linear var(--slide);
}

/* no frame, so a soft scrim keeps the words legible over whatever scrolls under */

.switch::before {
  content: "";
  position: absolute;
  inset: -30px -32px;
  /* 50% radii land the falloff exactly on the box edge, so the scrim reaches
     zero before it runs out of element and never shows a cut line */
  background: radial-gradient(50% 50% at 50% 50%,
              rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.92) 62%, rgba(0, 0, 0, 0) 100%);
  -webkit-backdrop-filter: blur(11px);
  backdrop-filter: blur(11px);
  -webkit-mask-image: radial-gradient(50% 50% at 50% 50%, #000 0%, #000 56%, transparent 100%);
  mask-image: radial-gradient(50% 50% at 50% 50%, #000 0%, #000 56%, transparent 100%);
  z-index: 0;
}

#portfolio .switch { right: 34px; --gust: 22px; }
#narrative .switch { left: 34px;  --gust: -22px; }

#view-toggle:not(:checked) ~ .shell #portfolio .switch,
#view-toggle:checked ~ .shell #narrative .switch {
  opacity: 1;
  transform: translateX(0);
  filter: blur(0);
  transition: opacity 0.9s var(--soft) 0.62s,
              transform 0.9s var(--soft) 0.62s,
              filter 0.9s var(--soft) 0.62s;
}

.switch .opt {
  position: relative;
  z-index: 1;
  padding: 11px 19px;
  text-align: center;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.005em;
  color: var(--faint);
  transition: color 0.6s var(--soft);
}

.switch .opt-p { color: var(--text); }
.switch .opt-n { color: var(--muted); }

/* the word you are not on gets a scribble under it, drawn in once the switch
   has landed, so it reads as somewhere to go rather than a caption */

.switch .opt::after {
  content: "";
  position: absolute;
  left: 19px;
  right: 19px;
  bottom: 5px;
  height: 6px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 6' preserveAspectRatio='none'%3E%3Cpath d='M1 3.6 C 9 1.8, 15 5, 23 3.4 S 37 1.9, 45 3.5 S 55 4.4, 59 2.8' fill='none' stroke='%23000' stroke-width='1.3' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 6' preserveAspectRatio='none'%3E%3Cpath d='M1 3.6 C 9 1.8, 15 5, 23 3.4 S 37 1.9, 45 3.5 S 55 4.4, 59 2.8' fill='none' stroke='%23000' stroke-width='1.3' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  transition: opacity 0.4s var(--soft);
}

#view-toggle:not(:checked) ~ .shell .switch .opt-n::after,
#view-toggle:checked ~ .shell .switch .opt-p::after {
  opacity: 0.7;
  animation: opt-hint 0.8s var(--soft) 1.7s both;
}

@keyframes opt-hint {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.mark {
  position: absolute;
  z-index: 1;
  left: 0;
  top: 0;
  width: 50%;
  height: 100%;
  padding: 1px 6px;
  color: var(--text);
  pointer-events: none;
  transition: transform 0.72s var(--soft);
}

.mark svg { display: block; width: 100%; height: 100%; overflow: visible; }

#view-toggle:checked ~ .shell .mark { transform: translateX(100%); }
#view-toggle:checked ~ .shell .switch .opt-p { color: var(--muted); }
#view-toggle:checked ~ .shell .switch .opt-n { color: var(--text); }

/* on hover the other word lights up and the loop leans toward it, as if it
   already wants to go */

#view-toggle:not(:checked) ~ .shell .switch:hover .opt-n,
#view-toggle:checked ~ .shell .switch:hover .opt-p { color: var(--text); }

#view-toggle:not(:checked) ~ .shell .switch:hover .opt-n::after,
#view-toggle:checked ~ .shell .switch:hover .opt-p::after { opacity: 1; }

#view-toggle:not(:checked) ~ .shell .switch:hover .mark { transform: translateX(6%); }
#view-toggle:checked ~ .shell .switch:hover .mark { transform: translateX(94%); }

@media (prefers-reduced-motion: reduce) {
  .switch .opt::after { animation: none !important; transform: none; }
}

#view-toggle:focus-visible ~ .shell .switch { outline: 1px solid var(--rule); outline-offset: 6px; }

/* --- the page, shrunken, floating in the bottom corner -------------------- */

.minimap {
  position: absolute;
  top: calc(100vh - 34px);
  top: calc(100dvh - 34px);
  transform: translateY(-100%);
  width: 62px;
  padding: 8px;
  border: 1px solid var(--rule);
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  opacity: 0;
  transition: opacity 0.5s ease;
}

#portfolio .minimap { right: 34px; }
#narrative .minimap { left: 34px; }

/* it floats over the page, so it sits back until you look at it. on the
   narrative side it would otherwise bury the timeline marks it is drawing. */

#view-toggle:not(:checked) ~ .shell #portfolio .minimap,
#view-toggle:checked ~ .shell #narrative .minimap {
  opacity: 0.58;
  transition-delay: 0.7s;
}

.minimap:hover { opacity: 1 !important; transition-delay: 0s !important; }

/* the map is a photograph of the page it floats over, not a drawing of it:
   assets/minimap/*.png, retaken by `python .claude/make_minimaps.py` whenever
   the page changes. It is a picture and nothing else, so it takes no clicks
   and no focus. */

.mm-shot {
  display: block;
  width: 100%;
  height: auto;
}

.mm-view {
  position: absolute;
  top: 8px;
  left: 8px;
  right: 8px;
  border: 1px solid var(--text);
  background: rgba(240, 240, 240, 0.08);
  transform: translateY(0);
}

/* each map is a different page, so each box has its own height and its own run.
   the box is the share of the page one screen covers, and the run is what is
   left over, both measured off the real panels at a typical window height. */

#portfolio .mm-view { height: 34px; --travel: 97px; }
#narrative .mm-view { height: 29px; --travel: 97px; }

@keyframes mm-run {
  from { transform: translateY(0); }
  to   { transform: translateY(var(--travel)); }
}

@supports (animation-timeline: scroll()) {
  body.split { timeline-scope: --portfolio-scroll, --narrative-scroll; }
  #portfolio { scroll-timeline: --portfolio-scroll block; }
  #narrative { scroll-timeline: --narrative-scroll block; }
  .mm-view { animation: mm-run linear both; }
  #portfolio .mm-view { animation-timeline: --portfolio-scroll; }
  #narrative .mm-view { animation-timeline: --narrative-scroll; }
}

/* --- the overture ---------------------------------------------------------
   Two portraits come in off both edges, meet, hold there a moment, and spin
   away. A dot is left where they met. It waits a second, then bounces: a small
   one, a slightly bigger one, and a third it never lands. The pen picks it up
   as it leaves the ground and draws the line that becomes the rest of the page.

   The whole sequence hangs off the moment they meet, which is 48% of 2.7s.
   Move that and the pop, the bounce, the draw and the run all have to move
   with it, and the draw has to stay pinned to the END of the bounce, because
   that is the frame the dot is caught on, still falling.

   Stage units are CSS pixels on purpose: the svg is drawn at its viewBox size
   so offset-path can use the very same d and land on it exactly.
   ------------------------------------------------------------------------ */

.overture {
  display: grid;
  justify-items: center;
  padding: 24px 0 0;
}

.ov-stage {
  position: relative;
  width: 400px;
  height: 620px;
  color: var(--text);
}

/* --- the two of them ------------------------------------------------------ */

.ov-img {
  position: absolute;
  top: 90px;
  width: 120px;
  height: 120px;
  opacity: 0;
}

.ov-a { left: 80px;  animation: ov-come-a 2.7s both; }
.ov-b { left: 200px; animation: ov-come-b 2.7s both; }

@keyframes ov-come-a {
  0%   { opacity: 0; transform: translateX(-58vw) rotate(-9deg);
         animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.36); }
  12%  { opacity: 1; }
  48%  { opacity: 1; transform: translateX(0) rotate(0deg); }
  62%  { opacity: 1; transform: translateX(0) rotate(0deg);
         animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
  100% { opacity: 0; transform: translateX(-48vw) translateY(-36vh) rotate(-430deg); }
}

@keyframes ov-come-b {
  0%   { opacity: 0; transform: translateX(58vw) rotate(9deg);
         animation-timing-function: cubic-bezier(0.55, 0, 0.85, 0.36); }
  12%  { opacity: 1; }
  48%  { opacity: 1; transform: translateX(0) rotate(0deg); }
  62%  { opacity: 1; transform: translateX(0) rotate(0deg);
         animation-timing-function: cubic-bezier(0.2, 0.7, 0.3, 1); }
  100% { opacity: 0; transform: translateX(48vw) translateY(32vh) rotate(430deg); }
}

/* --- the line it leaves behind -------------------------------------------- */

.ov-draw {
  position: absolute;
  inset: 0;
  overflow: visible;
  color: var(--text);
  opacity: 0.85;
}

.ov-path {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: ov-draw 3.56s linear 3.92s both;
}

/* The speed is a simulation, not an easing curve. A pen runs away downhill and
   labours uphill, and on this letter that is most of the character: the fall
   into the crescent and the long descent of the spine get away from it, and
   the climb up the inside of the head loop is visibly hard work. Along the
   path, with theta the angle of travel below horizontal,

       dv/dt = G sin(theta) - DRAG v

   which settles toward G/DRAG on a long fall and falls back, but never below
   a walk, on a climb. It runs between 250 and 737px a second, and no
   cubic-bezier can say that: a timing function is one accelerate and one
   decelerate, and this needs six of each. Hence the keyframes, and hence
   linear on the animations, which are only playing them back.

   Generated by .claude/make_overture_path.py along with the d string, and
   they have to be regenerated together: they are a profile of that exact
   line, and pasting a new path under an old profile puts the dot back where
   the easing thinks it should be rather than where the pen is. */

@keyframes ov-draw {
   0.00% { stroke-dashoffset: 100.00; }
   1.39% { stroke-dashoffset: 98.91; }
   2.78% { stroke-dashoffset: 97.55; }
   4.17% { stroke-dashoffset: 96.21; }
   5.56% { stroke-dashoffset: 94.86; }
   6.94% { stroke-dashoffset: 93.42; }
   8.33% { stroke-dashoffset: 91.78; }
   9.72% { stroke-dashoffset: 89.88; }
  11.11% { stroke-dashoffset: 87.79; }
  12.50% { stroke-dashoffset: 85.71; }
  13.89% { stroke-dashoffset: 83.84; }
  15.28% { stroke-dashoffset: 82.28; }
  16.67% { stroke-dashoffset: 81.04; }
  18.06% { stroke-dashoffset: 80.09; }
  19.44% { stroke-dashoffset: 79.21; }
  20.83% { stroke-dashoffset: 78.33; }
  22.22% { stroke-dashoffset: 77.44; }
  23.61% { stroke-dashoffset: 76.56; }
  25.00% { stroke-dashoffset: 75.67; }
  26.39% { stroke-dashoffset: 74.79; }
  27.78% { stroke-dashoffset: 73.91; }
  29.17% { stroke-dashoffset: 73.02; }
  30.56% { stroke-dashoffset: 72.14; }
  31.94% { stroke-dashoffset: 71.25; }
  33.33% { stroke-dashoffset: 70.37; }
  34.72% { stroke-dashoffset: 69.49; }
  36.11% { stroke-dashoffset: 68.60; }
  37.50% { stroke-dashoffset: 67.72; }
  38.89% { stroke-dashoffset: 66.83; }
  40.28% { stroke-dashoffset: 65.93; }
  41.67% { stroke-dashoffset: 64.90; }
  43.06% { stroke-dashoffset: 63.68; }
  44.44% { stroke-dashoffset: 62.23; }
  45.83% { stroke-dashoffset: 60.57; }
  47.22% { stroke-dashoffset: 58.70; }
  48.61% { stroke-dashoffset: 56.64; }
  50.00% { stroke-dashoffset: 54.41; }
  51.39% { stroke-dashoffset: 52.03; }
  52.78% { stroke-dashoffset: 49.55; }
  54.17% { stroke-dashoffset: 46.98; }
  55.56% { stroke-dashoffset: 44.38; }
  56.94% { stroke-dashoffset: 41.83; }
  58.33% { stroke-dashoffset: 39.46; }
  59.72% { stroke-dashoffset: 37.52; }
  61.11% { stroke-dashoffset: 36.23; }
  62.50% { stroke-dashoffset: 35.34; }
  63.89% { stroke-dashoffset: 34.46; }
  65.28% { stroke-dashoffset: 33.58; }
  66.67% { stroke-dashoffset: 32.69; }
  68.06% { stroke-dashoffset: 31.81; }
  69.44% { stroke-dashoffset: 30.92; }
  70.83% { stroke-dashoffset: 30.04; }
  72.22% { stroke-dashoffset: 29.16; }
  73.61% { stroke-dashoffset: 28.27; }
  75.00% { stroke-dashoffset: 27.39; }
  76.39% { stroke-dashoffset: 26.50; }
  77.78% { stroke-dashoffset: 25.62; }
  79.17% { stroke-dashoffset: 24.73; }
  80.56% { stroke-dashoffset: 23.84; }
  81.94% { stroke-dashoffset: 22.92; }
  83.33% { stroke-dashoffset: 21.96; }
  84.72% { stroke-dashoffset: 20.93; }
  86.11% { stroke-dashoffset: 19.81; }
  87.50% { stroke-dashoffset: 18.59; }
  88.89% { stroke-dashoffset: 17.23; }
  90.28% { stroke-dashoffset: 15.66; }
  91.67% { stroke-dashoffset: 13.82; }
  93.06% { stroke-dashoffset: 11.76; }
  94.44% { stroke-dashoffset: 9.57; }
  95.83% { stroke-dashoffset: 7.28; }
  97.22% { stroke-dashoffset: 4.93; }
  98.61% { stroke-dashoffset: 2.53; }
  100.00% { stroke-dashoffset: 0.00; }
}

/* --- the dot -------------------------------------------------------------- */

/* three nested spans so nothing fights over transform: the runner is carried
   along the path, the bob does the squash, the dot pops and then leaves. */

.ov-runner {
  position: absolute;
  left: 0;
  top: 0;
  width: 14px;
  height: 14px;
  /* offset-anchor already puts the middle of this box on the line. the old
     -7px margin that used to do that job by hand is still a shift on top of
     it, which is why the dot rode 10px up and left of its own pen tip. */
  offset-path: path("M200 150 C 200 150, 199.7 158.4, 199 161.7 C 198.4 164.2, 197.8 166.2, 196.6 168.1 C 195.4 170.1, 194 171.8, 191.9 173.2 C 189.1 175.1, 184.2 176.1, 180.4 177.3 C 176.7 178.5, 173.5 179, 169.3 180.5 C 163.4 182.7, 155 186.2, 149 190.2 C 143.3 194, 138.1 198.4, 133.9 203.6 C 129.7 208.7, 125.6 214.8, 123.5 221.2 C 121.3 227.8, 120.4 235.9, 121.2 242.9 C 122.1 250, 124.9 257.7, 128.8 263.5 C 132.5 269.2, 137.6 274.1, 143.8 277.9 C 151 282.2, 161.3 285.3, 169.9 286.7 C 177.7 288, 185.1 287.4, 193 286.7 C 201.3 286, 210.4 284.7, 218.5 282.5 C 226.2 280.4, 233.3 277.6, 240.5 274.1 C 248.2 270.4, 256.1 265.7, 263.2 260.8 C 270.1 256, 276.5 250.7, 282.5 245 C 288.6 239.1, 294.2 232.8, 299.3 226 C 304.5 219, 309.7 211.2, 313.5 203.6 C 317 196.7, 319.9 188.6, 321.7 182.6 C 323 178.2, 323.6 174.8, 324.1 171 C 324.6 167.1, 325 163.1, 324.7 159.4 C 324.4 155.8, 323.9 151.9, 322.4 148.9 C 321 146, 318.6 143.3, 315.9 141.6 C 313.2 139.9, 309.5 139.1, 306.1 138.8 C 302.7 138.5, 299.4 138.8, 295.8 139.9 C 291.5 141.1, 287 143.5, 282.4 146.7 C 276.7 150.7, 270.2 157.1, 264.8 163.2 C 259.1 169.7, 253.9 177.2, 249.1 184.6 C 244.2 192, 239.5 200.1, 235.6 207.6 C 232 214.4, 229.4 220.5, 226.2 227.5 C 222.6 235.2, 218.9 243.9, 215.2 251.9 C 211.7 259.7, 208.3 267.1, 204.7 274.7 C 201.2 282.3, 197.5 290, 193.8 297.6 C 190.1 305.3, 186.6 312.8, 182.6 320.5 C 178.5 328.5, 174.3 336.7, 169.5 344.6 C 164.5 352.7, 158.4 361.1, 153.1 368.4 C 148.3 375, 144.2 381, 139 386.7 C 133.8 392.3, 128 397.7, 121.9 402.2 C 115.8 406.7, 109.1 410.9, 102.3 414 C 95.7 417, 87.5 419.4, 81.6 420.4 C 77.3 421.2, 73.9 421.1, 70.2 420.8 C 66.4 420.5, 62.6 419.8, 59.2 418.7 C 55.8 417.6, 52.5 416.1, 49.8 414.1 C 47.2 412.1, 44.8 409.4, 43.4 406.6 C 41.9 403.7, 41.1 400.2, 41.1 396.9 C 41.1 393.6, 42.1 390, 43.5 386.9 C 45 383.8, 47.4 381, 49.8 378.5 C 52.3 376, 54.7 374.2, 58.3 372.1 C 63.6 368.9, 71.7 365.2, 78.9 362.8 C 86.3 360.4, 94.2 358.7, 102 357.6 C 109.8 356.5, 117.4 355.9, 125.6 356 C 134.6 356.2, 145 357.6, 154 359 C 162.2 360.3, 169.8 362.1, 177.5 364 C 185 365.8, 192.1 367.7, 199.7 370 C 208 372.6, 217.5 375.8, 225.5 379.1 C 232.7 382.1, 239 385.1, 245.5 388.6 C 251.9 392.1, 258.3 395.5, 264.3 400 C 270.6 404.8, 277.3 410.1, 282.1 416.6 C 287 423.2, 290.9 432.3, 293 439.5 C 294.7 445.4, 294.9 452.3, 294.9 456.5 C 295 458.9, 294.8 460, 294.4 462.1 C 293.7 465, 292.5 469, 291.1 472.1 C 289.7 475, 287.9 476.8, 286 480.1 C 283 485.2, 279.5 493.5, 275.7 499.9 C 271.9 506.2, 267.7 512.2, 263.3 518.1 C 258.8 524, 253.9 529.6, 249 535.1 C 244.1 540.7, 238.8 546, 234 551.6 C 229.2 557.2, 224.2 562.7, 220.1 568.8 C 215.9 574.9, 212.1 581.4, 209.1 588.1 C 206.2 594.8, 203.9 602.6, 202.4 609.1 C 201.1 614.4, 200 624, 200 624");
  offset-rotate: 0deg;
  offset-anchor: 50% 50%;
  offset-distance: 0%;
  /* the same duration, delay and easing as .ov-path above, always. they are
     two animations drawing one line, and any daylight between them is the dot
     coming off the pen. There is no easing on either any more: the keyframes
     carry the speed and linear just plays them. The profile starts at 253px a
     second because that is what the dot is doing as it comes off the third
     hop, which is the one number in the simulation that is not free. */
  animation: ov-run 3.56s linear 3.92s both;
}

@keyframes ov-run {
   0.00% { offset-distance: 0.00%; }
   1.39% { offset-distance: 1.09%; }
   2.78% { offset-distance: 2.45%; }
   4.17% { offset-distance: 3.79%; }
   5.56% { offset-distance: 5.14%; }
   6.94% { offset-distance: 6.58%; }
   8.33% { offset-distance: 8.22%; }
   9.72% { offset-distance: 10.12%; }
  11.11% { offset-distance: 12.21%; }
  12.50% { offset-distance: 14.29%; }
  13.89% { offset-distance: 16.16%; }
  15.28% { offset-distance: 17.72%; }
  16.67% { offset-distance: 18.96%; }
  18.06% { offset-distance: 19.91%; }
  19.44% { offset-distance: 20.79%; }
  20.83% { offset-distance: 21.67%; }
  22.22% { offset-distance: 22.56%; }
  23.61% { offset-distance: 23.44%; }
  25.00% { offset-distance: 24.33%; }
  26.39% { offset-distance: 25.21%; }
  27.78% { offset-distance: 26.09%; }
  29.17% { offset-distance: 26.98%; }
  30.56% { offset-distance: 27.86%; }
  31.94% { offset-distance: 28.75%; }
  33.33% { offset-distance: 29.63%; }
  34.72% { offset-distance: 30.51%; }
  36.11% { offset-distance: 31.40%; }
  37.50% { offset-distance: 32.28%; }
  38.89% { offset-distance: 33.17%; }
  40.28% { offset-distance: 34.07%; }
  41.67% { offset-distance: 35.10%; }
  43.06% { offset-distance: 36.32%; }
  44.44% { offset-distance: 37.77%; }
  45.83% { offset-distance: 39.43%; }
  47.22% { offset-distance: 41.30%; }
  48.61% { offset-distance: 43.36%; }
  50.00% { offset-distance: 45.59%; }
  51.39% { offset-distance: 47.97%; }
  52.78% { offset-distance: 50.45%; }
  54.17% { offset-distance: 53.02%; }
  55.56% { offset-distance: 55.62%; }
  56.94% { offset-distance: 58.17%; }
  58.33% { offset-distance: 60.54%; }
  59.72% { offset-distance: 62.48%; }
  61.11% { offset-distance: 63.77%; }
  62.50% { offset-distance: 64.66%; }
  63.89% { offset-distance: 65.54%; }
  65.28% { offset-distance: 66.42%; }
  66.67% { offset-distance: 67.31%; }
  68.06% { offset-distance: 68.19%; }
  69.44% { offset-distance: 69.08%; }
  70.83% { offset-distance: 69.96%; }
  72.22% { offset-distance: 70.84%; }
  73.61% { offset-distance: 71.73%; }
  75.00% { offset-distance: 72.61%; }
  76.39% { offset-distance: 73.50%; }
  77.78% { offset-distance: 74.38%; }
  79.17% { offset-distance: 75.27%; }
  80.56% { offset-distance: 76.16%; }
  81.94% { offset-distance: 77.08%; }
  83.33% { offset-distance: 78.04%; }
  84.72% { offset-distance: 79.07%; }
  86.11% { offset-distance: 80.19%; }
  87.50% { offset-distance: 81.41%; }
  88.89% { offset-distance: 82.77%; }
  90.28% { offset-distance: 84.34%; }
  91.67% { offset-distance: 86.18%; }
  93.06% { offset-distance: 88.24%; }
  94.44% { offset-distance: 90.43%; }
  95.83% { offset-distance: 92.72%; }
  97.22% { offset-distance: 95.07%; }
  98.61% { offset-distance: 97.47%; }
  100.00% { offset-distance: 100.00%; }
}

.ov-bob {
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: 50% 100%;
  animation: ov-bounce 2.2s 1.72s both;
}

.ov-dot {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: var(--text);
  transform: scale(0);
  /* the dot leaves only once the line has landed: the run ends at 3.92 +
     3.56 = 7.48s, and it holds on the rail a quarter second before it goes.
     Fading it any earlier and it thins out to nothing halfway down the last
     straight, which now runs 234px and is the easiest stretch to watch. */
  animation: ov-pop 0.34s cubic-bezier(0.2, 1.5, 0.4, 1) 1.3s both,
             ov-leave 0.55s ease 7.73s both;
}

@keyframes ov-pop   { to { transform: scale(1); } }
@keyframes ov-leave { to { opacity: 0; } }

/* Three hops. A small one, a slightly bigger one, and a third the dot never
   gets to finish: it comes off the top of that one still falling, and the pen
   takes it at 100%, on the way down, which is why the line starts by falling
   too. The handoff used to be at the 69% keyframe, on the way up, so the dot
   stopped dead at the top of the hop before the drawing began.

   What makes it read as weight rather than a pulse: each hop rises on an ease
   out quad and falls on an ease in quad, which is the exact shape of gravity,
   and the second hop hangs 1.35x as long as the first because its apex is 1.9x
   as high and air time goes with the square root of height. Nothing pauses at
   an apex. Contact is a few frames of squash and it is gone again. */

@keyframes ov-bounce {
  0%, 5%   { transform: translateY(0) scale(1, 1);
             animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
  9%       { transform: translateY(0) scale(1.14, 0.86);
             animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
  10%      { transform: translateY(-1px) scale(0.93, 1.09);
             animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); }
  20%      { transform: translateY(-16px) scale(1, 1);
             animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); }
  30%      { transform: translateY(-1px) scale(0.93, 1.09);
             animation-timing-function: cubic-bezier(0.5, 0, 0.7, 1); }
  31%      { transform: translateY(0) scale(1.18, 0.82);
             animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
  33%      { transform: translateY(-1px) scale(0.91, 1.11);
             animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); }
  46%      { transform: translateY(-30px) scale(1, 1);
             animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); }
  60%      { transform: translateY(-1px) scale(0.91, 1.11);
             animation-timing-function: cubic-bezier(0.5, 0, 0.7, 1); }
  63%      { transform: translateY(0) scale(1.3, 0.7);
             animation-timing-function: cubic-bezier(0.15, 0.85, 0.35, 1); }
  69%      { transform: translateY(-4px) scale(0.8, 1.24);
             animation-timing-function: cubic-bezier(0.25, 0.55, 0.4, 1); }
  91%      { transform: translateY(-25px) scale(1, 1);
             animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); }
  100%     { transform: translateY(0) scale(1, 1); }
}

/* The panel is hidden until the switch is thrown, so the whole opening would
   otherwise have played to an empty room. Hold every part of it at zero and
   let it go only once this side is actually up. */

.ov-img, .ov-path, .ov-runner, .ov-bob, .ov-dot { animation-play-state: paused; }

#view-toggle:checked ~ .shell .ov-img,
#view-toggle:checked ~ .shell .ov-path,
#view-toggle:checked ~ .shell .ov-runner,
#view-toggle:checked ~ .shell .ov-bob,
#view-toggle:checked ~ .shell .ov-dot { animation-play-state: running; }

/* --- the timeline --------------------------------------------------------
   One field, every beat pinned to it at x percent of the width and y px down.
   The svg behind is stretched over the same field with viewBox 0 0 100 H and
   preserveAspectRatio none, so a path point and a beat with the same numbers
   always meet. Both are written by .claude/make_timeline.py; change them
   there, never one without the other.

   White zigzags the whole width until NC State. From there the line is red,
   holds the middle, and everything since comes off it in white. The reading line is
   joined to nothing on purpose.
   ------------------------------------------------------------------------ */

:root { --wolf: #CC0000; --soft-text: rgba(240, 240, 240, 0.82); }

.line { padding: 88px 0 96px; }

.tl { padding: 0 24px; }

.tl-field {
  position: relative;
  height: calc(var(--h) * 1px);
  container-type: inline-size;
}

.tl-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.tl-lines path { fill: none; stroke-linecap: round; vector-effect: non-scaling-stroke; }

/* the walk carries straight on from the overture's line, so it matches it */
.tl-walk   { stroke: var(--text); stroke-width: 1.7; opacity: 0.85; }
.tl-spine  { stroke: var(--wolf); stroke-width: 2.4; }
.tl-branch { stroke: var(--text); stroke-width: 1.4; opacity: 0.75; }
.tl-rule   { stroke: var(--text); stroke-width: 1.1; opacity: 0.38; }

.tl-beat {
  position: absolute;
  left: calc(var(--x) * 1%);
  top: calc(var(--y) * 1px);
  width: 0;
  height: 0;
}

.tl-node {
  position: absolute;
  left: -22px;
  top: -22px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--bg);
  color: var(--text);
}

.tl-node svg { display: block; width: 100%; height: 100%; overflow: visible; }

.tl-node path,
.tl-node circle,
.tl-node ellipse {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.tl-ring { opacity: 0.4; transition: opacity 0.5s var(--soft); }
.tl-ring path { stroke-width: 1.3; }
.tl-ico path, .tl-ico circle, .tl-ico ellipse { stroke-width: 1.5; }

.on-spine .tl-ring { color: var(--wolf); opacity: 0.9; }

.tl-beat:hover .tl-ring { opacity: 0.72; }
.tl-beat.on-spine:hover .tl-ring { opacity: 1; }

/* the words sit on the outside of their node, never across a line */

.tl-body { position: absolute; top: -16px; }

.side-l .tl-body {
  right: 36px;
  text-align: right;
  width: min(300px, calc(var(--x) * 1cqw - 52px));
}

.side-r .tl-body {
  left: 36px;
  width: min(300px, calc((100 - var(--x)) * 1cqw - 52px));
}

.below .tl-body {
  top: 34px;
  left: -130px;
  width: 260px;
  text-align: center;
}

/* squeezed between two lines, so it gets less room either side: a fifth of
   the field, which still clears both lines down to the narrowest desktop */
.below.narrow .tl-body { --w: min(240px, 20cqw); left: calc(var(--w) / -2); width: var(--w); }

/* the timeline is read on black at 12 and 13px, so nothing in it goes below
   --muted: --faint is under 3:1 against the page and was hard to read */

.tl-year {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.02em;
  color: var(--muted);
  margin: 0 0 3px;
}

.tl h2 {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.005em;
  color: var(--text);
  margin: 0;
}

.tl-say {
  font-size: 15px;
  line-height: 1.55;
  color: var(--text);
  margin: 0;
}

/* where it happened, and as what: the line a recruiter is looking for */

.tl-org {
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--muted);
  margin: 2px 0 0;
}

/* one thing that happened there, the kind you can check against the resume */

.tl-fact {
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: 0.005em;
  color: var(--soft-text);
  margin: 7px 0 0;
}

.tl-link { font-size: 12px; line-height: 1.5; margin: 5px 0 0; }

.tl-fact a,
.tl-link a {
  color: var(--text);
  border-bottom: 1px solid var(--muted);
  transition: border-color 0.15s ease;
}

.tl-fact a:hover,
.tl-fact a:focus-visible,
.tl-link a:hover,
.tl-link a:focus-visible { border-bottom-color: var(--text); }

/* short lines in narrow columns: even them out rather than leave one word
   hanging on the last line */
.tl h2, .tl-org, .tl-fact, .tl-turn p, .tl-end .tl-say { text-wrap: balance; }

.tl-turn { margin-top: 9px; }

.tl-turn p {
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: 0.005em;
  color: var(--muted);
  margin: 0;
}

.tl-turn .tl-but { color: var(--soft-text); }

.tl-pic {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 12px;
}

/* the reading line: a label sat on the left end of its rule */

.tl-reading {
  position: absolute;
  left: 0;
  bottom: calc((var(--h) - var(--y)) * 1px + 12px);
  width: min(340px, 42cqw);
}

.tl-note {
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
  margin: 6px 0 0;
}

/* the end: the dot turns up again under the last stop, with where it is
   headed and how to reach me, so the story finishes on something to do */

.tl-end {
  max-width: 440px;
  margin: 40px auto 0;
  padding: 0 24px;
  text-align: center;
}

.tl-dot {
  display: block;
  width: 14px;
  height: 14px;
  margin: 0 auto 18px;
  border-radius: 50%;
  background: var(--text);
}

.tl-end .tl-say { margin: 0 0 18px; }

/* these are the thing to do at the end, so they look like links before you
   hover, not only after */
.tl-end .links { justify-content: center; gap: 0 22px; font-size: 13px; }
.tl-end .links a { border-bottom-color: rgba(240, 240, 240, 0.35); }
.tl-end .links a:hover,
.tl-end .links a:focus-visible { border-bottom-color: var(--text); }

/* --- headers clear the switch --------------------------------------------- */

.panel .head { padding-top: 104px; }

#portfolio .head .label { margin-bottom: 16px; }

/* the narrative opens by telling you what you are about to watch */

.nv-lede {
  font-size: 30px;
  line-height: 36px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 10px 0 4px;
}

/* hangs off the end of the line above it, a step down and to the right */

.nv-aside {
  font-size: 15px;
  color: var(--muted);
  margin: 0 0 0 128px;
}

/* --- the bar that closes both pages --------------------------------------- */

.colobar {
  background: rgba(240, 240, 240, 0.14);
  border-top: 1px solid var(--rule);
  padding: 6px 24px;
  text-align: center;
}

.colobar p {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text);
  letter-spacing: 0.02em;
  margin: 0;
}

.panel .foot { padding-bottom: 44px; margin-top: 48px; }

/* --- narrow --------------------------------------------------------------- */

@media (max-width: 760px) {
  .switch { top: 24px; }
  #portfolio .switch { right: 24px; }
  #narrative .switch { left: 24px; }
  .switch::before { inset: -20px -22px; }
  .switch .opt { padding: 10px 15px; font-size: 12px; }
  .switch .opt::after { left: 15px; right: 15px; bottom: 4px; }

  .panel .head { padding-top: 92px; }
  .line { padding-top: 84px; }

  .minimap { top: calc(100dvh - 24px); }
  #portfolio .minimap { right: 24px; }
  #narrative .minimap { left: 24px; }

  /* no room to zigzag: the field becomes a plain list down a rail on the
     left, white until NC State and red after, and the reading line a rule
     across the column between the two */
  .tl-field { height: auto; container-type: normal; }
  .tl-lines { display: none; }
  .tl-beat { position: relative; left: auto; top: auto; width: auto; height: auto;
             padding: 0 0 46px 58px; }
  .tl-beat::before { content: ""; position: absolute; left: 21px; top: 22px; bottom: -22px;
                     width: 1.5px; background: var(--text); opacity: 0.7; }
  .tl-beat.ncsu::before { background: var(--wolf); opacity: 1; }
  .tl-beat:last-child::before { display: none; }
  .tl-node { left: 0; top: 0; }
  .side-l .tl-body, .side-r .tl-body, .below .tl-body {
    position: static; width: auto; text-align: left; padding-top: 6px; }
  .tl-reading { position: relative; left: auto; bottom: auto; width: auto;
                margin: 0 0 46px; padding: 0 0 14px 58px; border-bottom: 1px solid var(--rule); }
  .below.narrow .tl-body { --w: auto; }
  /* the end sits on the rail like a stop, the dot where a node would be */
  .tl-end { position: relative; max-width: none; margin: 0; padding: 0 0 0 58px; text-align: left; }
  .tl-dot { position: absolute; left: 15px; top: 2px; margin: 0; }
  .tl-end .links { justify-content: flex-start; }
  .nv-aside { margin-left: 64px; }

  .ov-stage { transform: scale(0.78); transform-origin: 50% 0; height: 500px; }
}

/* someone who does not want motion gets the drawn line and none of the show */

@media (prefers-reduced-motion: reduce) {
  .ov-img, .ov-runner, .ov-bob, .ov-dot { animation: none; opacity: 0; }
  .ov-path { animation: none; stroke-dashoffset: 0; }
}

/* ============================================================================
   story pages: the two ML write ups walk through how the work was thought
   through, step by step, with small interactive pieces. They are the only
   pages that ship script, and they still read fine without it.
   ========================================================================== */

/* the short version, for someone who reads one block and leaves */

.tldr {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px var(--gap);
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  margin: 0 0 8px;
}

.case-body .tldr p { font-size: 15px; line-height: 1.55; color: var(--text); margin: 0; }
.case-body .tldr .tldr-k {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--faint); margin: 0 0 8px;
}

/* a numbered step: the number sits in front of the heading, faint */

.case-body h2 .step-n {
  display: inline-block;
  min-width: 34px;
  color: var(--faint);
  font-weight: 600;
}

/* what I was thinking at that step, brighter than the prose around it */

.case-body .thought {
  font-size: 15px;
  line-height: 1.6;
  color: var(--text);
  border-left: 1px solid var(--text);
  padding-left: 16px;
  margin: 0 0 22px;
  max-width: 62ch;
}

.case-body .thought .t-k {
  display: block;
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 4px;
}

/* plain tables for numbers that need comparing */

.tbl-wrap { overflow-x: auto; max-width: 100%; margin: 6px 0 22px; }
.tbl { width: 100%; max-width: 68ch; border-collapse: collapse; }
.tbl th, .tbl td {
  text-align: left; font-size: 12px; line-height: 1.5; padding: 8px 14px 8px 0;
  border-top: 1px solid var(--rule); vertical-align: top;
}
.tbl th { color: var(--faint); font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
.tbl td { color: var(--muted); }
.tbl td.n { color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tbl tr.me td { color: var(--text); font-weight: 600; }

/* buttons inside the interactive pieces read like the .cta links */

.g-btn {
  font: 700 12px/1 var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  padding: 6px 0 4px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
.g-btn:hover, .g-btn:focus-visible { border-bottom-color: var(--text); outline: none; }
.g-btn[disabled] { color: var(--faint); cursor: default; border-bottom-color: transparent; }
.g-btn.off { opacity: 0.4; }

/* --- the planning map ------------------------------------------------------- */

.map {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 18px var(--gap);
  margin: 10px 0 14px;
}

.map-col h4 {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--faint); margin: 0 0 10px; padding-bottom: 8px; border-bottom: 1px solid var(--rule);
}

.m-item {
  display: block;
  width: 100%;
  text-align: left;
  font: 400 12px/1.45 var(--font);
  color: var(--muted);
  background: none;
  border: 0;
  border-left: 1px solid var(--rule);
  padding: 6px 0 6px 10px;
  margin: 0 0 2px;
  cursor: pointer;
  transition: opacity 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.m-item b { color: var(--text); font-weight: 600; margin-right: 4px; }
.m-item:focus-visible { outline: 1px solid var(--text); outline-offset: 2px; }

.map.lit .m-item { opacity: 0.25; }
.map.lit .m-item.on { opacity: 1; color: var(--text); border-left-color: var(--text); }
.map.lit .m-item.self { opacity: 1; color: var(--text); border-left: 2px solid var(--text); padding-left: 9px; }

.map-foot { font-size: 12px; color: var(--faint); margin: 0 0 22px; max-width: 68ch; }

/* --- spot the satellites -------------------------------------------------- */

.game { margin: 18px 0 28px; }

.g-bar {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px 16px;
  flex-wrap: wrap; margin-bottom: 10px;
}

.g-status { font-size: 15px; color: var(--text); }
.g-time { font-variant-numeric: tabular-nums; }
.g-acts { display: flex; gap: 22px; }

.g-stage { position: relative; background: #111; user-select: none; -webkit-user-select: none; touch-action: manipulation; }
.g-stage img { width: 100%; display: block; transition: filter 0.3s ease; }
.g-stage svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.g-stage.live { cursor: crosshair; }
.g-stage.idle img { filter: brightness(0.5); }

.g-cover {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.g-cover span {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text); background: rgba(0, 0, 0, 0.66); padding: 9px 13px;
}
.g-stage:not(.idle) .g-cover { display: none; }

.g-you path     { fill: none; stroke: #F0F0F0; stroke-width: 2.4; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.g-hand circle  { fill: none; stroke: #F0F0F0; stroke-width: 1.6; stroke-dasharray: 3 4; vector-effect: non-scaling-stroke; }
.g-model circle { fill: none; stroke: #E84834; stroke-width: 2; vector-effect: non-scaling-stroke; }
.g-parts path   { fill: none; stroke: rgba(240, 240, 240, 0.75); stroke-width: 1; vector-effect: non-scaling-stroke; }
.g-hide { display: none; }

.g-layers { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 12px 0 0; }
.g-layers[hidden] { display: none; }
.sw {
  display: inline-block; width: 16px; height: 0; vertical-align: middle; margin: -2px 7px 0 0;
  border-top: 2px solid #F0F0F0;
}
.sw-hand  { border-top-style: dashed; }
.sw-model { border-top-color: #E84834; }
.sw-parts { border-top-width: 1px; }

.g-score { margin-top: 18px; }
.g-score[hidden] { display: none; }

/* --- the melt: cool it down ----------------------------------------------- */

.melt { margin: 18px 0 28px; }

.melt-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px 22px; margin-bottom: 12px; }
.melt-top label { font-size: 12px; color: var(--faint); }

.melt select {
  font: 400 12px/1.4 var(--font); color: var(--text); background: #000;
  border: 0; border-bottom: 1px solid var(--rule); padding: 4px 4px 4px 0; max-width: 100%;
}

.melt-chart { width: 100%; height: auto; display: block; overflow: visible; }
.melt-chart text { font: 400 11px var(--font); fill: rgba(240, 240, 240, 0.45); }
.melt-chart .ax { stroke: rgba(240, 240, 240, 0.3); stroke-width: 1; fill: none; }
.melt-chart .lvl { stroke: rgba(240, 240, 240, 0.16); stroke-width: 1; stroke-dasharray: 3 5; }
.melt-chart .zone { fill: rgba(240, 240, 240, 0.05); }
.melt-chart .earth { fill: none; stroke: #F0F0F0; stroke-width: 2; stroke-linecap: round; }
.melt-chart .moon { fill: rgba(240, 240, 240, 0.25); stroke: none; }
.melt-chart .now { stroke: #F0F0F0; stroke-width: 1; opacity: 0.55; }
.melt-chart .dot { fill: #F0F0F0; }
.melt-chart .lab-b { fill: rgba(240, 240, 240, 0.72); font-weight: 600; letter-spacing: 0.06em; }

.melt input[type="range"] { width: 100%; margin: 14px 0 6px; accent-color: #F0F0F0; cursor: ew-resize; }

.melt-read {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px var(--gap);
  padding-top: 14px; border-top: 1px solid var(--rule); margin-top: 8px;
}
.case-body .melt-read p { margin: 0; font-size: 12px; color: var(--faint); line-height: 1.5; }
.case-body .melt-read .r-n {
  font-size: 30px; line-height: 34px; font-weight: 700; letter-spacing: -0.01em;
  color: var(--text); margin: 0 0 4px; font-variant-numeric: tabular-nums;
}
.case-body .melt-read .r-s { font-size: 15px; color: var(--text); margin: 0 0 4px; line-height: 1.35; }

/* --- make the call ---------------------------------------------------------- */

.call { margin: 18px 0 28px; }
.case-body .call-q { font-size: 15px; color: var(--text); margin: 0 0 12px; }
.call-acts { display: flex; flex-wrap: wrap; gap: 22px; margin-bottom: 14px; }
.call-out[hidden] { display: none; }

.dots { display: grid; gap: 9px; max-width: 68ch; margin: 14px 0 10px; }
.dots-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: center; }
.dots-row span { font-size: 12px; color: var(--muted); }
.dots-row.meas span { color: var(--text); }
.dots-row i {
  display: inline-block; width: 11px; height: 11px; border-radius: 50%;
  border: 1px solid rgba(240, 240, 240, 0.45); margin-left: 5px;
}
.dots-row i.y { background: #F0F0F0; border-color: #F0F0F0; }

@media (max-width: 760px) {
  .tldr { grid-template-columns: 1fr; gap: 18px; }
  .map { grid-template-columns: 1fr 1fr; }
  .melt-read { grid-template-columns: 1fr; }
  .case-body .melt-read .r-n { font-size: 24px; line-height: 28px; }
  .dots-row i { width: 9px; height: 9px; margin-left: 3px; }
}

/* --- notes ------------------------------------------------------------------
   short written pieces, listed under the projects on the index. The lunar
   factory sketchbook has its own stylesheet, assets/css/sketchbook.css. */

.notes { margin: 10px 0 0; }
.notes .label { color: var(--faint); margin-bottom: 12px; }
.notes a {
  display: flex; justify-content: space-between; gap: 16px; align-items: center;
  padding: 12px 0; border-top: 1px solid var(--rule);
}
.notes a > span:first-child { font-size: 15px; color: var(--text); }
.notes a > span .meta { display: block; margin-top: 4px; }
.notes a img { width: 220px; height: auto; flex: none; opacity: 0.9; transition: opacity 0.28s ease; }
.notes a:hover > span:first-child { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.notes a:hover img { opacity: 1; }
@media (max-width: 760px) { .notes a img { width: 120px; } }

