/* ============================================================
   Design tokens.

   One serif, one column, neutral greys. Mono exists only for
   code. If a token here starts describing chrome — a badge, a
   gutter, an accent — it does not belong on this site.
   ============================================================ */

@font-face {
  font-family: 'Source Serif 4';
  src: url('/fonts/source-serif-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Source Serif 4';
  src: url('/fonts/source-serif-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Source Serif 4';
  src: url('/fonts/source-serif-400-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
/* Code only. Never headings, never metadata. */
@font-face {
  font-family: 'JetBrains Mono';
  src: url('/fonts/jetbrains-mono-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* size-adjust keeps the fallback metrically close, so swap causes no shift */
@font-face {
  font-family: 'Serif Fallback';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 96%;
  ascent-override: 92%;
  descent-override: 26%;
}

:root {
  --font-serif: 'Source Serif 4', 'Serif Fallback', Georgia, serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Type scale. Still a document rather than a landing page, but every level
     is a visible step from the one under it. The rule that sets the whole
     thing: a heading must be larger than the list it introduces, or it reads
     as that list's first row — which is exactly what a 17px client sub-head
     over 17px project titles did. */
  --fs-sm: 0.9375rem;   /* 15px  secondary */
  --fs-base: 1.0625rem; /* 17px  body, and every list on the site */
  --fs-h3: 1.1875rem;   /* 19px  client */
  --fs-h2: 1.375rem;    /* 22px  employer */
  --fs-h1: 1.625rem;    /* 26px  name */

  --lh-body: 1.6;
  --lh-tight: 1.3;

  --s-1: 0.25rem;  --s-2: 0.5rem;  --s-3: 0.75rem;  --s-4: 1rem;
  --s-5: 1.5rem;   --s-6: 2rem;    --s-7: 3rem;     --s-8: 4rem;

  /* One measure for the whole site. */
  --measure: 650px;
  --page-pad-y: 4rem;
  --page-pad-x: 2rem;

  /* Neutral greys, and there is no light theme — one palette, so nothing here
     is ever conditional. color-scheme tells the browser to render scrollbars
     and form controls to match. */
  color-scheme: dark;
  --bg: #0a0a0a;
  --ink: #ededed;
  --ink-2: #b4b4b4;
  --muted: #8f8f8f;
  --rule: #292929;
  --code-bg: #141414;
  --sel-bg: #2a2a2a;

  /* The only colour on the site, and it is the only place colour means
     something: which kind of thing a row in the timeline is. Warm against cool
     rather than two arbitrary hues — the pair reads as a distinction even to
     someone who cannot separate red from green, and both clear 7:1 on the
     background, so the label is never the thing you have to squint at. Kept
     desaturated, because two bright tags in a grey page would be badges. */
  --kind-project: #c8a45c;
  --kind-notes: #7fa3c4;
}
/* ============================================================
   Base. The body *is* the column — there is no page frame,
   no grid, no layout primitive to learn.
   ============================================================ */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--page-pad-y) var(--page-pad-x);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-serif);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

@media (max-width: 34rem) {
  body { padding: 2.5rem 1.25rem; }
}

/* ============================================================
   The home page, as three screens.

   Intro, work and nights each get a viewport and sit in the
   middle of it. Two decisions carry the whole thing:

   `min-height`, not `height` — the work section is nine rows
   with descriptions and it does not fit a short laptop, and it
   is nowhere near fitting a phone. A minimum lets the panel
   grow past the screen instead of clipping, which also means
   the centring is self-cancelling: it only applies while there
   is spare room to centre in.

   `mandatory`, and it is safe next to that `min-height`. The
   worry with mandatory is a panel taller than the screen: if
   every scroll must end on a panel top, the bottom of an
   overflowing panel is unreachable. It isn't — an oversized
   snap area makes every position that still covers the screen
   a valid one to stop at, so a tall panel scrolls through
   normally and only its edges pull. `proximity` was the
   cautious choice and the wrong one: away from a boundary it
   leaves you resting between two panels, which is the one
   thing this layout must not do.

   No script. A wheel-hijacking full-page scroll would take
   find-in-page with it, and find-in-page is how you look for a
   project by name on a page that is mostly a list of names.
   ============================================================ */
html:has(body.home) { scroll-snap-type: y mandatory; }

/* Scrolling the reader somewhere they did not ask to go is motion. */
@media (prefers-reduced-motion: reduce) {
  html:has(body.home) { scroll-snap-type: none; }
}

/* On the home page the panel is the column, so the body stops being one — it
   has to be full width for a panel to fill the screen, and it has to drop its
   vertical padding, which belongs inside the 100svh box rather than above and
   below all three of them. */
body.home {
  max-width: none;
  padding: 0;
}

.panel {
  min-height: 100svh;
  padding: var(--page-pad-y) var(--page-pad-x);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: var(--bg);
  scroll-snap-align: start;
  /* One gesture, one screen. Without this a hard flick carries its momentum
     across a snap point and lands two panels down, and with three panels that
     is most of the page gone in a movement that was meant to advance it. */
  scroll-snap-stop: always;
}

/* The measure, back where every other page keeps it. */
.panel > .flow {
  width: 100%;
  max-width: var(--measure);
}

@media (max-width: 34rem) {
  .panel { padding: 2.5rem 1.25rem; }
}

/* A section heading opens a gap above itself to separate it from what came
   before, and the marks row holds air under itself. At the edges of a panel
   both are measuring against a screen edge rather than against neighbouring
   content, and the only thing they do there is pull the centred block off
   centre by half their height. */
.panel > .flow > :first-child { margin-top: 0; }
.panel > .flow > :last-child { margin-bottom: 0; }

/* ---------- one step lighter per screen ----------
   Three greys a few points apart, so that arriving somewhere new is a fact
   about the whole field rather than something you have to infer from the
   words. Monotonic rather than alternating: light means further down, so the
   tone says which way you are facing and not merely that you moved. Panels
   are opaque and full-height, so the change is the edge between two of them
   travelling up the screen — no fade, nothing animating, nothing to support.

   `--rule` steps with `--bg` because it has to. The hairline is 1.36:1
   against the darkest panel; carried unchanged onto the lightest it falls to
   1.15:1 and the h2 underlines and the timeline spine quietly dissolve. These
   values hold that ratio flat across all three, so the rules look identical
   and the ink around them does the moving.

   Redefining the existing tokens rather than adding `--bg-2`: everything
   underneath already reads `var(--bg)` and `var(--rule)`, and inherits. */
#work {
  --bg: #141414;
  --rule: #2e2e2e;
}
#personal {
  --bg: #1e1e1e;
  --rule: #353535;
}

::selection { background: var(--sel-bg); }

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: 600;
  line-height: var(--lh-tight);
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); }

p { margin: 0; }
ul, ol { margin: 0; padding-left: 1.1rem; }
li::marker { color: var(--muted); }
img, svg, video { max-width: 100%; height: auto; display: block; }

/* The only ornament on the site: a hairline under every link. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--muted);
  text-decoration-thickness: 1px;
  text-underline-offset: 2.5px;
  transition: text-decoration-color 0.15s ease;
}
a:hover { text-decoration-color: var(--ink); }

:focus-visible {
  outline: 2px solid var(--ink-2);
  outline-offset: 3px;
  border-radius: 1px;
}

hr {
  border: 0;
  height: 1px;
  background: var(--rule);
  margin: var(--s-7) 0;
}

/* ---------- skip link ---------- */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 99;
  background: var(--bg);
  border: 1px solid var(--rule);
  padding: var(--s-2) var(--s-4);
  font-size: var(--fs-sm);
  text-decoration: none;
}
.skip:focus { left: var(--s-4); top: var(--s-4); }

/* ---------- vertical rhythm ----------
   Prose spacing is one gap repeated. Anything needing a
   different gap is probably a component that should not exist. */
.flow > * + * { margin-top: var(--s-5); }

.muted { color: var(--muted); }
.sm { font-size: var(--fs-sm); }

/* ---------- the one list style ----------
   Title, then a dimmed clause on the same line. Used for work,
   writing and anything else that is a list of links. */
.links {
  /* No `margin: 0` here — it ties with `.flow > * + *` on specificity and
     wins on source order, which silently removes the gap above a list. */
  list-style: square;
  padding-left: 1.1rem;
  line-height: 1.45;
}
.links > li + li { margin-top: var(--s-3); }
.links > li { padding-left: 0.15rem; }
.links .note {
  color: var(--muted);
  font-size: var(--fs-sm);
}

/* ---------- writing, as a timeline ----------
   A date rail, a hairline spine, and a square on the spine per entry — the
   same square `.links` already uses as its marker, so the page gains a shape
   without gaining an ornament. */
.timeline {
  list-style: none;
  padding: 0;
  line-height: 1.45;
}
.timeline > li {
  display: grid;
  grid-template-columns: 4.25rem 1fr;
  column-gap: var(--s-4);
}
.timeline .when {
  text-align: right;
  white-space: nowrap;
  /* Small type on a larger line: nudge it onto the title's baseline. */
  padding-top: 0.12em;
}
.timeline > li > div {
  position: relative;
  border-left: 1px solid var(--rule);
  padding: 0 0 var(--s-5) var(--s-5);
}
/* The spine ends with the last title rather than running on into air. */
.timeline > li:last-child > div { padding-bottom: 0; }
.timeline > li > div::before {
  content: '';
  position: absolute;
  /* 5px square centred on the 1px rule, which sits at -1px..0. */
  left: -3px;
  top: 0.5em;
  width: 5px;
  height: 5px;
  background: var(--muted);
  transition: background 0.15s ease;
}
.timeline > li:hover > div::before { background: var(--ink); }
.timeline .note {
  display: block;
  margin-top: var(--s-1);
}
/* Which kind of thing a row is, as the verb that made it: "built X", "wrote Y".
   It leads the title but stays out of the rail — the rail already answers a
   question, when, and a second column of two repeating words would be a legend
   rather than information. No border, no pill, no smaller size, no italic and
   no extra gap: it is the same type as the title with one word space after it,
   so the row reads as a sentence rather than as a label stuck to a link. The
   colour is the only thing left marking it, which is all the distinction a
   two-word vocabulary needs. */
.timeline .kind {
  white-space: nowrap;
}
.timeline .kind-project { color: var(--kind-project); }
.timeline .kind-notes { color: var(--kind-notes); }

/* Narrow: the rail costs more width than it earns, so the date moves above
   the title and the spine goes with it. */
@media (max-width: 34rem) {
  .timeline > li { grid-template-columns: 1fr; }
  .timeline .when { text-align: left; padding-top: 0; }
  .timeline > li > div {
    border-left: 0;
    padding-left: 0;
    padding-bottom: var(--s-5);
  }
  .timeline > li > div::before { display: none; }
}

/* ---------- the tools, as marks ----------
   No `margin: 0` here, for the same reason as `.links` above. Monochrome on
   purpose: the marks inherit the page's grey rather than bringing eleven
   brand palettes onto a site that has no accent colour. */
.skills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-5);
  list-style: none;
  padding: 0;
  /* Both sides set explicitly, so the row sits in equal air. Letting
     `.flow > * + *` supply the top would give it the paragraph gap, which is
     a third of this. Never the `margin` shorthand — see `.links` above. */
  margin-top: var(--s-8);
  margin-bottom: var(--s-8);
}
.skills svg {
  width: 24px;
  height: 24px;
  fill: var(--muted);
  transition: fill 0.15s ease;
}
.skills li:hover svg { fill: var(--ink); }

/* ---------- back link at the top of every sub-page ---------- */
.back {
  display: inline-block;
  margin-bottom: var(--s-7);
  color: var(--muted);
  font-size: var(--fs-sm);
  text-decoration-color: transparent;
}
.back:hover { color: var(--ink); text-decoration-color: var(--muted); }
/* ============================================================
   Long-form articles. Same column, same serif, same link
   treatment as everything else — an article is just a page
   with more paragraphs on it.
   ============================================================ */

.prose { color: var(--ink-2); }

.prose > * + * { margin-top: var(--s-5); }

.prose strong { color: var(--ink); font-weight: 600; }
.prose em { font-style: italic; }

.prose :is(h2, h3, h4) { color: var(--ink); }
.prose h2 { font-size: var(--fs-h2); margin-top: var(--s-7); }
.prose h3 { font-size: var(--fs-h3); margin-top: var(--s-6); }
.prose h4 {
  font-size: var(--fs-base);
  font-weight: 400;
  font-style: italic;
  color: var(--muted);
  margin-top: var(--s-5);
}

.prose ul,
.prose ol { padding-left: 1.2rem; }
.prose li + li { margin-top: var(--s-2); }

.prose blockquote {
  margin: 0;
  padding-left: var(--s-4);
  border-left: 1px solid var(--rule);
  color: var(--muted);
  font-style: italic;
}

.prose code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  background: var(--code-bg);
  padding: 0.12em 0.34em;
  border-radius: 3px;
}

.prose pre {
  margin: 0;
  background: var(--code-bg);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: var(--s-4);
  overflow-x: auto;
  font-size: var(--fs-sm);
  line-height: 1.55;
}
.prose pre code {
  background: none;
  padding: 0;
  font-size: 0.9em;
}

.prose figure { margin: 0; }
.prose figcaption {
  margin-top: var(--s-2);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.prose hr { margin: var(--s-7) 0; }

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.prose :is(th, td) {
  text-align: left;
  padding: var(--s-2) var(--s-3) var(--s-2) 0;
  border-bottom: 1px solid var(--rule);
}
.prose th { color: var(--ink); font-weight: 600; }

/* ============================================================
   Page furniture. Global rules, in one file — there are few
   enough of them that this is simpler, not worse.
   ============================================================ */

h1 { margin-bottom: var(--s-5); }

/* An article's byline sits tight under its title. */
article h1 { margin-bottom: var(--s-2); }
.meta {
  margin: 0 0 var(--s-6);
  line-height: 1.5;
}

/* Body copy is one step down from headings, everywhere. */
.flow p,
.flow li { color: var(--ink-2); }
.flow .muted,
.flow p.muted { color: var(--muted); }

/* Section ledes sit above a list and should recede a little. */
body.section .flow > p:first-child { color: var(--muted); }

/* On a list of case studies the summaries need more air than titles alone. */
body.section .links > li + li { margin-top: var(--s-4); }

/* ---------- the name and its links, with the portrait opposite ---------- */
.masthead {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  margin-bottom: var(--s-6);
}
/* Fills the row so the toggle's margin-left: auto has room to push against. */
.masthead > div { flex: 1; min-width: 0; }
.masthead h1 { margin-bottom: var(--s-2); }
.avatar {
  flex: none;
  width: 72px;
  height: 72px;
  border-radius: 50%;
}

/* ---------- the link row under the name ---------- */
.elsewhere {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin: 0;
  font-size: var(--fs-sm);
}
.elsewhere a {
  color: var(--muted);
  text-decoration-color: var(--rule);
}
.elsewhere a:hover {
  color: var(--ink);
  text-decoration-color: var(--muted);
}

/* ---------- a case study's résumé line ---------- */
.subtitle {
  margin: 0 0 var(--s-2);
  color: var(--ink-2);
  font-style: italic;
}

/* Section headings — the employer, and the nights. Three things separate one
   from the sub-head under it, and they pull in different directions on purpose:
   it is larger, it opens a gap above itself, and a hairline closes it off
   underneath. A heading that is ruled off and then left alone reads as the top
   of a section; the client sub-head below does the opposite and hugs its list.
   The gap above the first one collapses against the marks row's larger margin,
   so it is unaffected. The rule is the same hairline as the timeline spine and
   the table borders — the site's existing one, not a new ornament. */
.flow h2 {
  margin-top: var(--s-7);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--rule);
  font-size: var(--fs-h2);
}

/* Client sub-heads. The employer is stated once above; these name who the work
   was for, so they step down from it in size and weight. They stay at full
   brightness, though: the list under them is --ink-2 at 400, so with the
   italic gone, colour is what keeps a client from reading as a project. */
.flow h3 {
  margin-bottom: calc(var(--s-5) * -1 + var(--s-3));
  font-size: var(--fs-h3);
  font-weight: 400;
  color: var(--ink);
}

/* The repo link trails its entry without competing with the title. */
.links .repo {
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
}
