/* ════════════════════════════════════════════════════════════════════════
   site.css — the whole stylesheet. One file, no preprocessor, no framework.

   Order matters, because CSS resolves ties by source order:
     1. Design tokens      the only place a colour or size is chosen
     2. Theming            light, OS-dark, and explicit-choice-dark
     3. Reset & base       sensible defaults for bare elements
     4. Layout             container, sections, grids
     5. Components         buttons, cards, timeline, forms, prose
     6. Utilities          visually-hidden, and friends
     7. Motion & print     preferences and paper

   Nothing here is nested more than a level or two deep, and there are no
   `!important` rules outside the reduced-motion block. If a change feels like
   it needs one, the cascade is telling you the selector is in the wrong place.
   ════════════════════════════════════════════════════════════════════════ */

/* ── 1. Design tokens ─────────────────────────────────────────────────── */

:root {
  /* Colour: DARK is the base — see the theming note in section 2. */
  --bg: #0f1115;
  --bg-subtle: #14171d;
  --surface: #171a21;
  --surface-raised: #1d212a;
  --text: #e8eaf0;
  --text-muted: #a0a7b8;
  --border: #262b35;
  --border-strong: #39404d;
  --accent: #8ab0ff;
  --accent-hover: #a6c3ff;
  --accent-contrast: #0f1115;
  --accent-subtle: #182238;
  --focus: #8ab0ff;
  --danger: #ff9d94;
  --danger-subtle: #2a1614;
  --success: #86e0a4;
  --success-subtle: #10241a;
  --shadow-1: 0 1px 2px rgb(0 0 0 / 40%);
  --shadow-2: 0 6px 18px rgb(0 0 0 / 45%);

  /* Type. A fluid scale via clamp(): no breakpoint jumps, no media queries
     for font size. Each step is min / preferred(vw-based) / max. */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --step--1: clamp(0.82rem, 0.8rem + 0.1vw, 0.88rem);
  --step-0: clamp(1rem, 0.96rem + 0.18vw, 1.08rem);
  --step-1: clamp(1.18rem, 1.1rem + 0.35vw, 1.35rem);
  --step-2: clamp(1.4rem, 1.25rem + 0.7vw, 1.75rem);
  --step-3: clamp(1.7rem, 1.4rem + 1.3vw, 2.35rem);
  --step-4: clamp(2.1rem, 1.6rem + 2.4vw, 3.4rem);

  /* Space, on a 4px base. Using a scale instead of arbitrary numbers is what
     makes unrelated parts of a page feel like they belong together. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4.5rem;

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;

  /* Line length. Prose wider than ~70 characters is measurably harder to read
     because the eye loses its place on the return sweep. */
  --measure: 68ch;
  --container: 68rem;
  --container-narrow: 46rem;
}

/* ── 2. Theming: dark by default ───────────────────────────────────────── */

/*
  DARK IS THE DEFAULT, DELIBERATELY.

  The bare :root block above holds the dark palette, so that is what a first
  visit gets. Light is opt-in, via the header toggle, and the choice persists in
  localStorage.

  WHAT THIS GIVES UP, since it is a real trade and not a free win:
  the site no longer follows `prefers-color-scheme`. Someone whose OS is set to
  light — often a deliberate accessibility choice, and the default on most
  managed work machines — now lands on a dark page and has to toggle. That is a
  worse first impression for them than it is a better one for everyone else.

  To follow the OS again while keeping dark as the fallback, add:

      @media (prefers-color-scheme: light) {
        :root:not([data-theme='dark']) { ...light tokens... }
      }

  The inline script in views/partials/head.ejs applies a stored choice before
  first paint, so switching themes never flashes the wrong colours.
*/

:root[data-theme='light'] {
  --bg: #ffffff;
  --bg-subtle: #f6f7f9;
  --surface: #ffffff;
  --surface-raised: #ffffff;
  --text: #14171f;
  --text-muted: #5b6172;
  --border: #e2e5eb;
  --border-strong: #c9ced9;
  --accent: #1d4ed8;
  --accent-hover: #163fb0;
  --accent-contrast: #ffffff;
  --accent-subtle: #eef2ff;
  --focus: #1d4ed8;
  --danger: #b3261e;
  --danger-subtle: #fdecea;
  --success: #14622f;
  --success-subtle: #e7f6ec;
  --shadow-1: 0 1px 2px rgb(16 20 30 / 6%), 0 1px 3px rgb(16 20 30 / 4%);
  --shadow-2: 0 4px 12px rgb(16 20 30 / 8%), 0 2px 4px rgb(16 20 30 / 4%);
}

/* Tells the browser which palette is in play, so form controls, scrollbars and
   the overscroll area match instead of staying stubbornly light. */
:root { color-scheme: dark; }
:root[data-theme='light'] { color-scheme: light; }

/* ── 3. Reset & base ──────────────────────────────────────────────────── */

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

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

/* Anchor links land below the sticky header instead of underneath it. */
:target { scroll-margin-top: 5rem; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { line-height: 1.2; margin: 0; text-wrap: balance; letter-spacing: -0.015em; }
p, ul, ol { margin: 0; }
img, svg, video { max-width: 100%; height: auto; display: block; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }

/*
  NEVER `outline: none`. Removing the focus ring makes a site unusable by
  keyboard while looking fine to a mouse user, so the breakage is invisible to
  whoever removed it. :focus-visible is the modern answer: the ring shows for
  keyboard navigation and stays out of the way on click.
*/
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 3px;
}

code, pre, kbd { font-family: var(--font-mono); font-size: 0.92em; }

/* ── 4. Layout ────────────────────────────────────────────────────────── */

.container {
  width: 100%;
  max-inline-size: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-4);
}
.container--narrow { max-inline-size: var(--container-narrow); }

.section { padding-block: var(--space-7); }
.section + .section { border-top: 1px solid var(--border); }
.section--hero { padding-block: var(--space-8) var(--space-7); border-top: 0; }

.section__header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: baseline;
  justify-content: space-between;
}
.section__heading { font-size: var(--step-3); }
.section__subheading {
  font-size: var(--step-1);
  margin-block: var(--space-6) var(--space-4);
  color: var(--text);
}
.section__intro {
  color: var(--text-muted);
  max-inline-size: var(--measure);
  margin-block-start: var(--space-3);
  font-size: var(--step-1);
}
.section__action { font-size: var(--step--1); white-space: nowrap; }
.section__footnote { margin-block-start: var(--space-5); color: var(--text-muted); font-size: var(--step--1); }

/* auto-fit + minmax is the whole responsive story for the card grids: columns
   are added when there is room and removed when there is not, with no
   breakpoints to maintain. */
.grid { display: grid; gap: var(--space-5); margin-block-start: var(--space-5); }
.grid--cards { grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }

.stack { display: flex; flex-direction: column; gap: var(--space-5); margin-block-start: var(--space-5); }

.empty { color: var(--text-muted); font-style: italic; margin-block-start: var(--space-4); }

/* ── 5. Components ────────────────────────────────────────────────────── */

/* Skip link — the first focusable element on every page. */
.skip-link {
  position: absolute;
  inset-inline-start: var(--space-4);
  inset-block-start: -100%;
  z-index: 100;
  padding: var(--space-2) var(--space-4);
  background: var(--accent);
  color: var(--accent-contrast);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  text-decoration: none;
  font-weight: 600;
}
.skip-link:focus { inset-block-start: 0; color: var(--accent-contrast); }

/* Header */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
.site-header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  padding-block: var(--space-3);
}
.site-header__brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  margin-inline-start: -0.4rem;
  border-radius: var(--radius-md);
  color: var(--text-muted);
  text-decoration: none;
  transition: color 180ms ease, background-color 180ms ease;
}
.site-header__brand:hover { color: var(--accent); background: var(--bg-subtle); }
.site-header__brand[aria-current='page'] { color: var(--text); }
.site-header__home { display: block; }

/* A button that reads as a link — for controls that sit in a list of links,
   like the copy-address button in the footer. */
.linklike {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.linklike:hover { color: var(--accent-hover); }
.site-nav { margin-inline-end: auto; }
.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  list-style: none;
  padding: 0;
  font-size: var(--step--1);
}
.site-nav__list a { color: var(--text-muted); text-decoration: none; }
.site-nav__list a:hover { color: var(--accent); text-decoration: underline; }
.site-nav__list a[aria-current='page'] { color: var(--text); font-weight: 600; }

.theme-toggle {
  font: inherit;
  font-size: var(--step--1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
}
.theme-toggle:hover { color: var(--text); border-color: var(--accent); }

/* Buttons */
.button {
  display: inline-block;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
}
.button:hover { border-color: var(--accent); color: var(--accent); transform: translateY(-1px); }
.button--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.button--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-contrast); }

/* Hero */
.hero {
  display: grid;
  gap: var(--space-6);
  align-items: center;
  grid-template-columns: 1fr;
}
.hero__eyebrow { color: var(--text-muted); font-size: var(--step--1); text-transform: uppercase; letter-spacing: 0.08em; }
.hero__name { font-size: var(--step-4); margin-block: var(--space-3); }
.hero__tagline { font-size: var(--step-2); color: var(--text); max-inline-size: var(--measure); }
.hero__location { color: var(--text-muted); font-size: var(--step--1); margin-block-start: var(--space-3); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-block-start: var(--space-5); }
.hero__socials {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  list-style: none; padding: 0; margin-block-start: var(--space-5);
  font-size: var(--step--1);
}
.hero__portrait { justify-self: start; }
.hero__initials {
  display: grid;
  place-items: center;
  inline-size: 8.5rem;
  block-size: 8.5rem;
  border-radius: var(--radius-lg);
  background: var(--accent-subtle);
  color: var(--accent);
  font-size: var(--step-4);
  font-weight: 700;
  border: 1px solid var(--border);
}
.hero__portrait img { border-radius: var(--radius-lg); border: 1px solid var(--border); }

/* Cards */
.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  transition: box-shadow 140ms ease, border-color 140ms ease;
}
.card:hover { box-shadow: var(--shadow-2); border-color: var(--border-strong); }
.card--plain { box-shadow: none; background: var(--bg-subtle); }
.card__meta {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  color: var(--text-muted); font-size: var(--step--1);
}
.card__dot { color: var(--border-strong); }
.card__title { font-size: var(--step-1); }
.card__title a { color: var(--text); text-decoration: none; }
.card__title a:hover { color: var(--accent); text-decoration: underline; }
.card__external { font-size: 0.75em; margin-inline-start: 0.25em; color: var(--text-muted); }
.card__body { color: var(--text-muted); }
.card__list { margin: 0; padding-inline-start: var(--space-5); color: var(--text-muted); font-size: var(--step--1); }
.card__list li + li { margin-block-start: var(--space-1); }
.card__links {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  list-style: none; padding: 0; margin-block-start: auto;
  font-size: var(--step--1); font-weight: 600;
}

/* Chips & badges */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; padding: 0; }
.chip {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  font-size: var(--step--1);
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg-subtle);
  color: var(--text-muted);
  text-decoration: none;
}
.chips--links .chip:hover { border-color: var(--accent); color: var(--accent); }
.chip--active { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast); }
.chip__count { opacity: 0.65; font-variant-numeric: tabular-nums; }

.badge {
  display: inline-block;
  padding: 0 var(--space-2);
  font-size: var(--step--1);
  font-weight: 600;
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  color: var(--text-muted);
  border: 1px solid var(--border);
  text-transform: capitalize;
}
.badge--live, .badge--finished { background: var(--success-subtle); color: var(--success); border-color: transparent; }
.badge--wip, .badge--reading { background: var(--accent-subtle); color: var(--accent); border-color: transparent; }

.skills { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); margin-block-start: var(--space-6); }
.skills__title { font-size: var(--step--1); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin-block-end: var(--space-2); }

/* Timeline. The rule is a border on the list and a dot per item, so it grows
   with the content — no absolute positioning to go wrong at odd widths. */
.timeline { list-style: none; padding: 0; margin-block-start: var(--space-5); }
.timeline__item {
  position: relative;
  padding-inline-start: var(--space-6);
  padding-block-end: var(--space-6);
  border-inline-start: 2px solid var(--border);
}
.timeline__item:last-child { border-inline-start-color: transparent; padding-block-end: 0; }
.timeline__item::before {
  content: '';
  position: absolute;
  inset-inline-start: -6px;
  inset-block-start: 0.55rem;
  inline-size: 10px;
  block-size: 10px;
  border-radius: 50%;
  background: var(--accent);
}
.timeline__dates {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  color: var(--text-muted); font-size: var(--step--1); font-variant-numeric: tabular-nums;
}
.timeline__title { font-size: var(--step-1); margin-block: var(--space-2); }
.timeline__at { color: var(--text-muted); font-weight: 400; }
.timeline__meta { color: var(--text-muted); font-size: var(--step--1); }
.timeline__summary { margin-block: var(--space-3); }
.timeline__list { margin: 0 0 var(--space-3); padding-inline-start: var(--space-5); color: var(--text-muted); }
.timeline__list li + li { margin-block-start: var(--space-1); }

/* Reading list */
.reading { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--space-5); }
.reading__item { padding-inline-start: var(--space-4); border-inline-start: 3px solid var(--border); }
.reading__title { font-size: var(--step-1); }
.reading__title a { color: var(--text); text-decoration: none; }
.reading__title a:hover { color: var(--accent); text-decoration: underline; }
.reading__byline {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  color: var(--text-muted); font-size: var(--step--1); margin-block: var(--space-1) var(--space-2);
}
.reading__rating { color: var(--accent); letter-spacing: 0.1em; }
.reading__takeaway { max-inline-size: var(--measure); margin-block-end: var(--space-2); }

/* Tag bar */
.tagbar { margin-block: var(--space-5); }

/* Prose — the rendered Markdown of an article, and the About paragraphs. */
.prose { max-inline-size: var(--measure); }
.prose--narrow { max-inline-size: var(--measure); margin-block-start: var(--space-4); }
.prose > * + * { margin-block-start: var(--space-4); }
.prose h2 { font-size: var(--step-2); margin-block-start: var(--space-7); }
.prose h3 { font-size: var(--step-1); margin-block-start: var(--space-6); }
.prose ul, .prose ol { padding-inline-start: var(--space-5); }
.prose li + li { margin-block-start: var(--space-2); }
.prose a { text-decoration: underline; }
.prose strong { font-weight: 650; }
.prose blockquote {
  margin: 0;
  padding: var(--space-2) var(--space-5);
  border-inline-start: 3px solid var(--accent);
  background: var(--bg-subtle);
  color: var(--text-muted);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.prose code {
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1em 0.35em;
}
/* Code blocks scroll horizontally inside their own box, so a long line can
   never make the whole page scroll sideways on a phone. */
.prose pre {
  overflow-x: auto;
  padding: var(--space-4);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  line-height: 1.55;
}
.prose pre code { background: none; border: 0; padding: 0; }
/* Same treatment for tables. */
.prose table { width: 100%; border-collapse: collapse; font-size: var(--step--1); display: block; overflow-x: auto; }
.prose th, .prose td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); }
.prose th { color: var(--text-muted); font-weight: 600; }
.prose hr { border: 0; border-top: 1px solid var(--border); }

/* Article page */
.article__header { padding-block-end: var(--space-5); border-bottom: 1px solid var(--border); margin-block-end: var(--space-6); }
.article__meta { display: flex; flex-wrap: wrap; gap: var(--space-2); color: var(--text-muted); font-size: var(--step--1); }
.article__title { font-size: var(--step-3); margin-block: var(--space-3); }
.article__summary { font-size: var(--step-1); color: var(--text-muted); max-inline-size: var(--measure); margin-block-end: var(--space-4); }
.article__crosspost { font-size: var(--step--1); color: var(--text-muted); margin-block-start: var(--space-3); }
.article__footer { margin-block-start: var(--space-7); padding-block-start: var(--space-5); border-top: 1px solid var(--border); }

.toc {
  padding: var(--space-4) var(--space-5);
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-block-end: var(--space-6);
}
.toc__heading { font-size: var(--step--1); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.toc__list { list-style: none; padding: 0; margin-block-start: var(--space-2); font-size: var(--step--1); }
.toc__item + .toc__item { margin-block-start: var(--space-1); }
.toc__item--3 { padding-inline-start: var(--space-4); }

/* Share */
.share__heading { font-size: var(--step--1); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
.share__list { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; padding: 0; margin-block-start: var(--space-3); }
.share__link {
  display: inline-block;
  font: inherit;
  font-size: var(--step--1);
  font-weight: 600;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
}
.share__link:hover { border-color: var(--accent); color: var(--accent); }
.share__status { min-block-size: 1.5rem; font-size: var(--step--1); color: var(--success); margin-block-start: var(--space-2); }

/* Forms */
.form { margin-block-start: var(--space-5); display: flex; flex-direction: column; gap: var(--space-5); }
.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field__label { font-weight: 600; font-size: var(--step--1); }
.field__optional { font-weight: 400; color: var(--text-muted); }
.field__hint { font-size: var(--step--1); color: var(--text-muted); }
.field__control {
  font: inherit;
  padding: var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  inline-size: 100%;
  resize: vertical;
}
.field__control:hover { border-color: var(--text-muted); }
.field--invalid .field__control { border-color: var(--danger); }
.field__error { color: var(--danger); font-size: var(--step--1); font-weight: 600; }
.form__note { font-size: var(--step--1); color: var(--text-muted); }
.contact__direct { margin-block-start: var(--space-4); }

.notice {
  padding: var(--space-4);
  border-radius: var(--radius-md);
  margin-block-start: var(--space-5);
  border: 1px solid transparent;
}
.notice ul { margin-block-start: var(--space-2); padding-inline-start: var(--space-5); }
.notice--info { background: var(--accent-subtle); color: var(--text); border-color: var(--border-strong); }
.notice--info a { color: var(--accent); }
.notice--success { background: var(--success-subtle); color: var(--success); border-color: currentcolor; }
.notice--error { background: var(--danger-subtle); color: var(--danger); border-color: currentcolor; }
.notice--error a { color: inherit; }

/* Error page */
.error-page { text-align: center; padding-block: var(--space-7); }
.error-page__code { font-size: var(--step-4); font-weight: 700; color: var(--border-strong); line-height: 1; }
.error-page .hero__actions { justify-content: center; }

/* Footer */
.site-footer {
  border-top: 1px solid var(--border);
  background: var(--bg-subtle);
  padding-block: var(--space-6);
  margin-block-start: var(--space-7);
  font-size: var(--step--1);
  color: var(--text-muted);
}
.site-footer__links { display: flex; flex-wrap: wrap; gap: var(--space-4); list-style: none; padding: 0; margin-block: var(--space-3); }
.site-footer__meta { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); justify-content: space-between; }

/* ── 6. Utilities ─────────────────────────────────────────────────────── */

/*
  Visually hidden but still in the accessibility tree — used by the skip link's
  siblings and by the contact form honeypot.

  Note it is NOT `display: none` and NOT `visibility: hidden`: both of those
  remove the element from the accessibility tree too, which would defeat the
  point for a label and would let a bot skip the honeypot.
*/
.visually-hidden {
  position: absolute !important;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── 7. Breakpoints ───────────────────────────────────────────────────── */

/* Only two, and both mobile-first. The fluid type scale and auto-fit grids do
   most of the work, so there is very little left to do at a breakpoint. */

@media (min-width: 40rem) {
  .section { padding-block: var(--space-8); }
  .hero { grid-template-columns: 1fr auto; gap: var(--space-7); }
  .hero__initials { inline-size: 11rem; block-size: 11rem; }
}

@media (min-width: 64rem) {
  .container { padding-inline: var(--space-6); }
}

/* ── 8. Motion ────────────────────────────────────────────────────────── */

/*
  THE RULES THIS SECTION FOLLOWS
  -----------------------------
  1. Nothing starts invisible unless JavaScript is running. Every "hidden until
     revealed" state is behind `.has-js`, which the inline script in
     views/partials/head.ejs sets on <html> BEFORE first paint. With scripts
     blocked, the page is simply all there — no blank sections, no flash.

  2. Nothing starts invisible if the visitor asked for less motion. The whole
     section sits inside `prefers-reduced-motion: no-preference`, so a reduced-
     motion visitor never even gets the starting state. Section 9 then flattens
     any transition that slipped through.

  3. No inline styles. The CSP is `style-src 'self'`, which blocks style
     attributes as well as <style> blocks — so stagger delays are done with
     :nth-child, not with a style attribute written by a template.

  4. Transform and opacity only. Both are composited by the GPU, so they animate
     without triggering layout. Animating height/top/width instead is how
     scroll animations end up janky.
*/

/* Cross-document view transitions: a soft crossfade when you navigate between
   pages instead of a hard white flash. Browsers without support ignore this
   entirely, which is why it needs no fallback. */
@view-transition { navigation: auto; }

/* Colour changes when the theme flips, rather than an instant jump. Kept to the
   properties that actually change, so it cannot accidentally animate layout. */
body,
.card,
.site-header,
.site-footer,
.field__control,
.chip,
.button {
  transition: background-color 220ms ease, border-color 220ms ease, color 220ms ease;
}

@media (prefers-reduced-motion: no-preference) {

  /* ── Hero entrance ──────────────────────────────────────────────────
     Plays once on load. `both` fill mode holds the from-state before the
     delay and the to-state after, so nothing pops. */
  @keyframes rise-in {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: none; }
  }

  @keyframes scale-in {
    from { opacity: 0; transform: scale(0.94); }
    to   { opacity: 1; transform: none; }
  }

  .has-js .hero__eyebrow,
  .has-js .hero__name,
  .has-js .hero__tagline,
  .has-js .hero__location,
  .has-js .hero__actions,
  .has-js .hero__socials {
    animation: rise-in 700ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
  }

  .has-js .hero__eyebrow  { animation-delay: 40ms; }
  .has-js .hero__name     { animation-delay: 120ms; }
  .has-js .hero__tagline  { animation-delay: 200ms; }
  .has-js .hero__location { animation-delay: 260ms; }
  .has-js .hero__actions  { animation-delay: 320ms; }
  .has-js .hero__socials  { animation-delay: 380ms; }

  .has-js .hero__portrait {
    animation: scale-in 800ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
    animation-delay: 220ms;
  }

  /* A slow, very low-contrast glow behind the hero. Subtle enough that you
     notice the page feels alive rather than noticing the effect. */
  .section--hero { position: relative; overflow: hidden; }

  .section--hero::before {
    content: '';
    position: absolute;
    inset-block-start: -30%;
    inset-inline-start: 50%;
    inline-size: 42rem;
    block-size: 42rem;
    translate: -50% 0;
    border-radius: 50%;
    background: radial-gradient(circle, color-mix(in srgb, var(--accent) 12%, transparent) 0%, transparent 68%);
    animation: drift 22s ease-in-out infinite alternate;
    pointer-events: none;
    z-index: -1;
  }

  @keyframes drift {
    from { translate: -60% -4%; scale: 1; }
    to   { translate: -40%  4%; scale: 1.12; }
  }

  /* ── Reveal on scroll ───────────────────────────────────────────────
     The starting state is declared here, in CSS, so it applies before the
     first paint. public/js/motion.js only ever ADDS .is-visible — it never
     hides anything, which is what keeps the no-JS path correct. */
  .has-js .section__heading,
  .has-js .section__intro,
  .has-js .section__subheading,
  .has-js .card,
  .has-js .timeline__item,
  .has-js .reading__item,
  .has-js .skills__group,
  .has-js .article__header,
  .has-js .toc,
  .has-js .share {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 640ms cubic-bezier(0.22, 0.61, 0.36, 1),
                transform 640ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  .has-js .is-visible {
    opacity: 1;
    transform: none;
  }

  /* Stagger, by position rather than by a delay written into the markup.
     Capped at six: past that the last card would sit there long enough to
     read as broken rather than as choreography. */
  .has-js .grid > *:nth-child(2),
  .has-js .stack > *:nth-child(2),
  .has-js .timeline > *:nth-child(2),
  .has-js .reading > *:nth-child(2),
  .has-js .skills > *:nth-child(2) { transition-delay: 70ms; }

  .has-js .grid > *:nth-child(3),
  .has-js .stack > *:nth-child(3),
  .has-js .timeline > *:nth-child(3),
  .has-js .reading > *:nth-child(3),
  .has-js .skills > *:nth-child(3) { transition-delay: 140ms; }

  .has-js .grid > *:nth-child(4),
  .has-js .stack > *:nth-child(4),
  .has-js .timeline > *:nth-child(4),
  .has-js .reading > *:nth-child(4),
  .has-js .skills > *:nth-child(4) { transition-delay: 210ms; }

  .has-js .grid > *:nth-child(n + 5),
  .has-js .stack > *:nth-child(n + 5),
  .has-js .timeline > *:nth-child(n + 5),
  .has-js .reading > *:nth-child(n + 5),
  .has-js .skills > *:nth-child(n + 5) { transition-delay: 270ms; }
}

/* ── Micro-interactions ───────────────────────────────────────────────
   These are NOT inside the no-preference block: they are all triggered by
   the visitor's own hover or click, so they are feedback rather than motion
   happening at them. Section 9 still shortens them for reduced motion. */

/* Nav underline that grows from the left instead of appearing all at once. */
.site-nav__list a {
  position: relative;
  padding-block-end: 2px;
}

.site-nav__list a::after {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block-end: 0;
  inline-size: 100%;
  block-size: 1.5px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.site-nav__list a:hover::after,
.site-nav__list a:focus-visible::after,
.site-nav__list a[aria-current='page']::after,
.site-nav__list a.is-current::after {
  transform: scaleX(1);
}

.site-nav__list a:hover { text-decoration: none; }
.site-nav__list a.is-current { color: var(--text); }

/* Cards lift slightly and their title takes the accent colour together, so the
   whole card reads as one hoverable object. */
.card {
  transition: box-shadow 240ms ease, border-color 240ms ease,
              transform 240ms cubic-bezier(0.22, 0.61, 0.36, 1),
              background-color 220ms ease;
}
.card:hover { transform: translateY(-3px); }

/* Buttons press in on click — a small thing that makes a page feel responsive
   rather than like a document. */
.button:active { transform: translateY(0) scale(0.985); }
.share__link:active { transform: scale(0.985); }

.chip { transition: transform 180ms ease, background-color 180ms ease, border-color 180ms ease, color 180ms ease; }
.chips--links .chip:hover { transform: translateY(-2px); }

/* Prose links: underline sweeps in rather than being permanently drawn. */
.prose a {
  text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
  transition: text-decoration-color 200ms ease;
}
.prose a:hover { text-decoration-color: var(--accent); }

/* Anchor targets flash briefly so a jump from the table of contents is
   obvious rather than leaving you wondering whether anything happened. */
@media (prefers-reduced-motion: no-preference) {
  .prose :target {
    animation: target-flash 1.6s ease-out 1;
  }
  @keyframes target-flash {
    from { background: color-mix(in srgb, var(--accent) 16%, transparent); }
    to   { background: transparent; }
  }
}

/* ── Reading progress bar (article pages) ─────────────────────────────
   Ships hidden; public/js/motion.js reveals it, so there is no dead 0%-wide
   bar sitting at the top of the page without JavaScript. */
.reading-progress {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 3px;
  z-index: 30;
  background: transparent;
  pointer-events: none;
}

.reading-progress__bar {
  display: block;
  block-size: 100%;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 55%, var(--text)));
  transform: scaleX(var(--progress, 0));
  transform-origin: left;
}

/* ── 9. Motion preference ─────────────────────────────────────────────── */

/*
  Respecting this is not a nicety: for people with vestibular disorders,
  animation can cause actual nausea. The !important is one of the few
  legitimate uses — it has to beat every animation on the page, including
  future ones nobody has written yet.
*/
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── 10. Print ────────────────────────────────────────────────────────── */

/*
  Cmd-P on the home page produces a usable one-page resume, and on an article
  produces just the prose. Almost free, and genuinely useful — a recruiter
  printing your page to PDF is a real thing that happens.
*/
@media print {
  :root {
    --bg: #fff; --bg-subtle: #fff; --surface: #fff;
    --text: #000; --text-muted: #333;
    --border: #bbb; --border-strong: #999;
    --accent: #000; --accent-contrast: #fff; --accent-subtle: #fff;
    --shadow-1: none; --shadow-2: none;
  }

  .site-header,
  .site-footer,
  .theme-toggle,
  .skip-link,
  .reading-progress,
  .share,
  .toc,
  .tagbar,
  .hero__actions,
  .hero__socials,
  #contact,
  .form { display: none !important; }

  body { font-size: 11pt; line-height: 1.4; }
  /* Never print a decorative glow, and never print a half-faded section. */
  .section--hero::before { display: none !important; }
  .card, .timeline__item, .reading__item, .section__heading,
  .section__intro, .skills__group, .article__header {
    opacity: 1 !important;
    transform: none !important;
  }
  .section { padding-block: var(--space-4); border-top: 0; }
  .card { break-inside: avoid; box-shadow: none; border: 1px solid var(--border); }
  .timeline__item { break-inside: avoid; }
  .prose { max-inline-size: none; }

  /* Print the destination of a link, since you cannot click paper. */
  a[href^='http']::after { content: ' (' attr(href) ')'; font-size: 0.85em; color: #555; word-break: break-all; }
}
