/* The whole design lives in these tokens. Change a value here, it changes everywhere. */
:root {
  --paper: #07090c;
  --ink: #f1f3f4;
  --soft: #98a3ab;   /* secondary text: 8:1 on --paper, passes WCAG AA */
  --line: #20262c;   /* hairlines between rows */

  /* The site is black and white. --accent appears in two places only: the wind
     in the hero and links on hover. 4.6:1 on --paper, so it passes as text. */
  --accent: #9457eb;

  --gutter: clamp(20px, 3vw, 32px);
  --text: 17px;
  --small: 15px;
}

@font-face {
  font-family: "Hanken Grotesk";
  src: url("/fonts/hanken-grotesk-latin-wght.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap; /* show text in the fallback font while this loads */
}

* { box-sizing: border-box; margin: 0; }

body {
  background: var(--paper);
  color: var(--ink);
  font: 400 var(--text)/1.5 "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* Shared column. Anything full-bleed simply doesn't use this class. */
.page { max-width: 1240px; margin-inline: auto; padding-inline: var(--gutter); }

a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--accent); }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.skip { position: absolute; left: -999px; }
.skip:focus { left: var(--gutter); top: 12px; background: var(--paper); padding: 4px 8px; }

/* Top bar */
.brand { display: block; color: var(--ink); }
.brand svg { display: block; width: 38px; height: 40px; }
.site-nav { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding-block: 20px; font-weight: 500; font-size: var(--small); }
.site-nav a { text-decoration: none; white-space: nowrap; }
.site-nav a:hover { text-decoration: underline; color: inherit; }
.site-nav ul { display: flex; gap: clamp(14px, 2vw, 26px); list-style: none; padding: 0; }

/* ---------- The hero: one sentence, and wind moving through it ---------- */
.hero { position: relative; height: 100vh; height: 100svh; min-height: 540px; overflow: hidden; touch-action: pan-y; }
.hero-copy { position: relative; z-index: 2; height: 100%; display: flex; align-items: center; }
.hero-title { max-width: 13.5em; font-size: clamp(36px, 6vw, 92px); font-weight: 600; line-height: 1.04; letter-spacing: -.03em; }

/* The home header floats over the hero. */
.home .site-nav { position: absolute; z-index: 5; top: 0; left: 0; right: 0; }

.hero-word { display: inline-block; white-space: nowrap; }
/* Letters bend from near the baseline, so their feet stay planted like grass. */
.hero-letter { display: inline-block; transform-origin: 50% 80%; }

/* ---------- The intro: everything held back until the name has played ---------- */
.intro-stage { display: none; }
.intro .intro-stage { display: block; position: fixed; inset: 0; z-index: 4; pointer-events: none; }
.intro body { overflow: hidden; }
.intro .hero-word, .intro .site-nav nav { opacity: 0; }
.intro .brand { visibility: hidden; }
.intro-logo { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; color: var(--ink); }
.intro-name {
  position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  display: flex; flex-wrap: wrap; justify-content: center; gap: 0 .28em;
  font-size: clamp(64px, 13vw, 210px); font-weight: 600; line-height: 1; letter-spacing: -.035em;
}
.intro-word { display: inline-flex; }
.intro-slot { display: inline-block; width: .68em; height: 1em; }
.intro-letter { display: inline-block; opacity: 0; }
.intro-run .intro-letter { animation: rise .6s cubic-bezier(.2,.7,.2,1) .3s forwards; }
.intro-logo path { opacity: 0; }
.intro-run .intro-logo path { animation: fade .5s ease .3s forwards; }
@keyframes rise { from { opacity: 0; transform: translateY(.18em); } to { opacity: 1; transform: none; } }
@keyframes fade { to { opacity: 1; } }

/* The headline and nav arrive as the logo lands. */
.title-in .hero-word { animation: rise .8s cubic-bezier(.2,.7,.2,1) both; }
.title-in .site-nav nav { animation: fade .8s ease .5s both; }

@media (prefers-reduced-motion: reduce) {
  .hero-word, .site-nav nav { animation: none !important; }
}

h2 { font-size: var(--small); font-weight: 600; line-height: 1.3; margin-bottom: 8px; }

/* ---------- The home page About: words made of pieces ---------- */
/* One canvas covers About and Projects, so pieces can fall from one into the other.
   It scrolls with the page, which keeps it in step with the sections underneath. */
.fall { position: relative; }
.fall-canvas { position: absolute; z-index: 3; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.about-words { min-height: 92vh; min-height: 92svh; display: flex; align-items: center; }
.about-copy { width: 100%; padding-block: 64px; }
.about-copy h2 { font-size: var(--text); font-weight: 500; color: var(--soft); margin-bottom: 1.4em; }
.fall-words { max-width: 22em; font-size: clamp(27px, 3.7vw, 54px); line-height: 1.2; font-weight: 600; letter-spacing: -.02em; }
.fall-words + .fall-words { margin-top: 1em; }
/* fall.js adds .fall-on once the pieces are drawn. Until then this is plain text. */
.fall-on .fall-words { color: transparent; }
.about-more { margin-top: 2em; color: var(--soft); }

/* ---------- The home page carousel ---------- */
.stage-head { display: flex; align-items: baseline; gap: 18px; padding: 18px var(--gutter) 10px; }
.stage-head h2 { font-size: 24px; font-weight: 500; letter-spacing: -.01em; margin: 0; }
.stage-head a { font-size: var(--small); color: var(--soft); }
.stage-frame { position: relative; height: clamp(540px, 80vh, 740px); overflow: hidden; }
.stage-frame canvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
/* An ordinary sideways scroller, with its scrollbar hidden. */
.stage-track { position: relative; display: flex; height: 100%; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; cursor: grab; }
.stage-track::-webkit-scrollbar { display: none; }
.stage-track:focus-visible { outline-offset: -4px; }
.stage-track.dragging { scroll-snap-type: none; cursor: grabbing; user-select: none; }
/* The shape owns the space above. The text sits along the bottom, beside the arrows.
   This section runs the full width of the screen, so it lines up with the edge of
   the window, not the page column. scroll-snap-stop keeps flicks to one project. */
.stage-panel { flex: 0 0 100%; scroll-snap-align: start; scroll-snap-stop: always; display: flex; align-items: flex-end; padding: 0 var(--gutter) 24px calc(var(--gutter) + 148px); }
.stage-text { max-width: 620px; }
.stage-panel h3 { margin-block: 0 6px; font-size: clamp(26px, 3vw, 40px); font-weight: 600; line-height: 1.1; letter-spacing: -.025em; }
.stage-panel h3 a { text-decoration: none; }
.stage-panel h3 a:hover { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }
.stage-controls { position: absolute; left: var(--gutter); bottom: 24px; display: flex; flex-direction: column; align-items: center; gap: 8px; font-size: var(--small); color: var(--soft); }
.stage-controls[hidden] { display: none; }
.stage-arrows { display: flex; gap: 12px; }
.stage-controls span { min-width: 3.2em; text-align: center; font-variant-numeric: tabular-nums; }
.stage-controls button { width: 52px; height: 52px; padding: 0; border: 1px solid var(--line); border-radius: 50%; background: var(--paper); color: var(--ink); font: inherit; font-size: 22px; cursor: pointer; }
.stage-controls button:hover:not(:disabled) { border-color: var(--ink); }
.stage-controls button:disabled { color: var(--line); cursor: default; }

/* Project lists: rules and alignment, no boxes. */
.list { margin-top: 32px; }
.list-head { display: flex; justify-content: space-between; align-items: baseline; padding-block: 18px 10px; border-top: 1px solid var(--ink); }
.list-head h2 { font-size: 24px; font-weight: 500; letter-spacing: -.01em; margin: 0; }
.list-head a { font-size: var(--small); color: var(--soft); }

.row { display: grid; gap: 24px; padding-block: 13px; border-top: 1px solid var(--line); align-items: baseline; grid-template-columns: 220px 1fr 150px 72px; text-decoration: none; }
a.row { transition: padding-left .15s; }
a.row:hover { padding-left: 10px; color: inherit; }
.row b { font-weight: 500; font-size: 18px; }
.row i { font-style: normal; color: var(--soft); font-size: var(--small); }
.row i:last-child { text-align: right; }
/* "You can try this here" pills beside a project title. Black and white only:
   Play is solid, Demo is an outline. */
.badge { display: inline-block; margin-left: 10px; padding: 1px 8px; border: 1px solid var(--ink); border-radius: 999px;
  font-style: normal; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; vertical-align: 2px; }
.badge-play { background: var(--ink); color: var(--paper); }
.badge-demo { color: var(--ink); }

.site-footer { width: calc(100% - 2 * var(--gutter)); max-width: calc(1240px - 2 * var(--gutter)); padding-inline: 0; margin-top: 56px; display: flex; justify-content: space-between; align-items: baseline; gap: 20px; flex-wrap: wrap; padding-block: 24px 40px; border-top: 1px solid var(--ink); }
.sign-off { font-size: 22px; font-weight: 500; letter-spacing: -.01em; }
.footer-links { display: flex; gap: 20px; font-size: var(--small); }

.not-found { min-height: 50vh; }

/* Inner pages: a title, then the same rules and rows as the home page. */
.sub-page { padding-top: clamp(28px, 6vh, 56px); }
.title { font-size: clamp(30px, 4vw, 44px); font-weight: 500; line-height: 1.15; letter-spacing: -.02em; }
.lede { max-width: 62ch; margin-top: 10px; font-size: 19px; color: var(--soft); }
.when { color: var(--soft); font-size: var(--small); }
.back { margin-top: 32px; font-size: var(--small); }
.tech { margin-top: 28px; max-width: 62ch; color: var(--soft); font-size: var(--small); }
.tech:empty { display: none; }

/* Longer writing. 62 characters wide is about where lines stay comfortable. */
.prose { max-width: 62ch; margin-top: 24px; }
.prose > * + * { margin-top: 14px; }
.prose h2 { margin-top: 32px; margin-bottom: -6px; font-size: 18px; font-weight: 600; }
.prose h2.part { margin-top: 44px; font-size: 24px; font-weight: 500; letter-spacing: -.01em; }
.prose ul, .prose ol { padding-left: 1.2em; }
.prose li + li { margin-top: 6px; }
.media { display: block; max-width: 100%; height: auto; }
img.media { max-height: 480px; width: auto; }
/* A picture on its own in a project page. Wider than the text so the words in it stay
   readable, like .media-row below, but never past its real size. The border is there
   because a dark screenshot on the dark page loses its edges. */
.prose > img.media { max-width: min(1176px, calc(100vw - 2 * var(--gutter))); max-height: none; border: 1px solid var(--line); }
video.media { width: 100%; background: var(--ink); }
/* Pictures side by side. Wider than the text, so any words in them stay readable. */
.media-row { display: flex; gap: 16px; align-items: flex-start; width: min(1176px, calc(100vw - 2 * var(--gutter))); }
.media-row img.media { flex: 1 1 0; min-width: 0; width: 100%; max-height: none; border: 1px solid var(--line); }

.about-top { display: grid; grid-template-columns: 280px minmax(0, 62ch); gap: 48px; align-items: start; margin-top: 28px; }
.about-top .prose { margin-top: 0; }
/* The cutout has no background, so the suit fades out rather than ending in a hard edge.
   Two fades, one for the bottom and one for the sides, and intersect keeps only what both
   leave visible. The side fade stops short of the ears, so only the shoulders are touched. */
.portrait { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; object-position: 50% 20%; mask-image: linear-gradient(to bottom, #000 74%, transparent 100%), linear-gradient(to right, transparent, #000 18%, #000 82%, transparent); mask-composite: intersect; }

/* The About timeline. This is the plain version: a simple list, which is what
   you get with no JavaScript or with reduced motion. */
.timeline-entries { list-style: none; padding: 0; }
.timeline-entry { display: grid; grid-template-columns: 220px minmax(0, 62ch); gap: 24px; padding-block: 13px; border-top: 1px solid var(--line); align-items: baseline; }
.timeline-text p + p, .timeline-text .media { margin-top: 12px; }
.timeline-text strong { font-weight: 600; }
/* Photos sit side by side and wrap onto new lines when they run out of room. */
.timeline-text .media { display: inline-block; vertical-align: top; max-height: 260px; max-width: 100%; margin-right: 10px; }
/* Logos are white so they read on the dark page. Any file named logo-*.webp counts. */
.timeline-text img[src*="/logo-"] { display: block; height: 30px; width: auto; margin: 2px 0 14px; }

/* The rolling version. timeline.js adds .timeline-on and builds the year:
   the big number stays put while the entries scroll past it. */
.timeline-year { display: none; }
.timeline-on .timeline-body { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 48px; align-items: start; margin-top: 24px; }
.timeline-on .timeline-year { display: flex; position: sticky; top: 28vh; font-size: clamp(96px, 12.5vw, 176px); font-weight: 500; line-height: 1; letter-spacing: -.04em; font-variant-numeric: tabular-nums; }
/* The last digit, seen through a one-digit window. At rest it's plain text sitting
   level with the others. During a roll the next digit slides in from just outside. */
.timeline-window { position: relative; display: block; height: 1em; overflow: hidden; }
.timeline-digit { display: block; }
.timeline-next { position: absolute; left: 0; top: 100%; }
.timeline-next.from-above { top: -100%; }
.timeline-on .timeline-entries { position: relative; padding-left: 32px; padding-block: 12vh 30vh; border-left: 1px solid var(--line); }
.timeline-fill { position: absolute; left: -1px; top: 0; width: 1px; height: 100%; background: var(--ink); transform: scaleY(0); transform-origin: top; }
.timeline-on .timeline-entry { display: block; padding-block: 0; border-top: 0; opacity: .32; transition: opacity .35s ease; }
/* --empty-years comes from timeline.js: years with no entry get scroll room of their own. */
.timeline-on .timeline-entry + .timeline-entry { margin-top: calc(18vh + var(--empty-years, 0) * 16vh); }
.timeline-on .timeline-entry.reached { opacity: 1; }
.timeline-on .when { display: block; margin-bottom: 8px; }

/* Games and demos keep a dark frame so they read as objects on the page. */
.project-embed-frame { position: relative; margin-top: 24px; border: 1px solid var(--ink); background: #111; }
/* The frame is the size of the board, so the walls you can see are the real ones.
   The height limit keeps the whole game on screen on a short laptop. */
.paddle-frame { width: min(100%, 720px, calc((100svh - 120px) * 1.5)); }
#gameCanvas { display: block; width: 100%; aspect-ratio: 3 / 2; touch-action: none; }

@media (max-width: 860px) {
  .stage-frame { height: clamp(500px, 72vh, 620px); }
  .stage-panel { padding: 0 var(--gutter) 84px; }
  .stage-text { max-width: 100%; }
  /* No empty column to fill on a phone, so the count sits beside the arrows. */
  .stage-controls { flex-direction: row; gap: 14px; }
  .site-nav { font-size: 14px; }
  .site-nav ul { gap: 12px; }
  .row { grid-template-columns: 1fr 64px; gap: 2px 12px; }
  .row span { grid-column: 1 / -1; grid-row: 2; }
  .row i:first-of-type { display: none; }
  .about-top { grid-template-columns: 1fr; gap: 24px; }
  .portrait { max-width: 220px; }
  .media-row { flex-direction: column; }
  .media-row img.media { flex: none; }
  .timeline-entry { grid-template-columns: 1fr; gap: 0; }
  /* No side column on a phone, so the year sticks to the top of the screen. */
  .timeline-on .timeline-body { display: block; margin-top: 0; }
  .timeline-on .timeline-year { top: 0; z-index: 1; padding-block: 12px; background: var(--paper); font-size: 72px; }
  .timeline-on .timeline-entries { padding-left: 20px; padding-block: 8vh 24vh; }
  .timeline-on .timeline-entry + .timeline-entry { margin-top: calc(14vh + var(--empty-years, 0) * 14vh); }
  .sign-off { font-size: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  a.row { transition: none; }
}
