/* Self-hosted, latin subset only.
   
   These used to come from fonts.googleapis.com, which cost two DNS lookups
   and two TLS handshakes to a third party before a single glyph could paint,
   sent every visitor's IP to Google on a site that otherwise tracks nobody,
   and — the reason that actually forced this — silently fell back to Georgia
   and the system sans on any network that blocks Google Fonts. Corporate
   networks do block it. This container blocks it. A site whose typography is
   most of its argument should not lose that typography to a proxy rule.

   font-display: swap so text is readable immediately and reflows once. */
@font-face {
  font-family: "Instrument Serif";
  src: url("../assets/fonts/InstrumentSerif-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../assets/fonts/IBMPlexSans-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("../assets/fonts/IBMPlexSans-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("../assets/fonts/IBMPlexMono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("../assets/fonts/IBMPlexMono-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   Tokens
   ========================================================================== */

:root {
  --navy-900: #070c16;
  --navy-800: #0b1220;
  --navy-700: #101a2c;
  --navy-600: #16223a;

  --bone: #e9e7e1;
  --bone-dim: #a6aebe;
  --muted: #6f7b8f;

  --accent: #c2a878;
  --accent-soft: rgba(194, 168, 120, 0.22);

  --line: rgba(233, 231, 225, 0.1);
  --line-strong: rgba(233, 231, 225, 0.22);

  --serif: "Instrument Serif", Georgia, "Times New Roman", serif;
  --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --shell: 1240px;
  --gutter: clamp(1.35rem, 5vw, 4.5rem);
  --rail: 190px;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --slow: 1000ms;
  --mid: 620ms;
  --fast: 280ms;
}

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

/* The site carried no images at all until the portrait, so it never needed
   this — and without `height: auto` the width/height attributes act as
   presentational hints that beat aspect-ratio, which rendered the portrait
   at its literal pixel height. Keep the attributes: they reserve the right
   space before the file loads, so the section does not jump. */
img {
  max-width: 100%;
  height: auto;
}

html {
  scroll-behavior: smooth;
  /* Clears the fixed 74px header, which was otherwise covering the top of
     whatever an in-page anchor jumped to. */
  scroll-padding-top: 110px;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--navy-900);
  color: var(--bone);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.7;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 400;
  line-height: 1.05;
  letter-spacing: -0.015em;
}

p { margin: 0; }

ul, ol { margin: 0; padding: 0; list-style: none; }

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

::selection {
  background: var(--accent-soft);
  color: var(--bone);
}

:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 4px;
}

/* Fine film grain — keeps large flat navy fields from reading as plastic. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.skip-link {
  position: fixed;
  left: 1rem;
  top: -4rem;
  z-index: 100;
  background: var(--bone);
  color: var(--navy-900);
  padding: 0.6rem 1rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  transition: top var(--fast) var(--ease);
}

.skip-link:focus { top: 1rem; }

/* ==========================================================================
   Type utilities
   ========================================================================== */

.mono {
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.serif { font-family: var(--serif); }

.dim { color: var(--bone-dim); }
.faint { color: var(--muted); }

/* ==========================================================================
   Shell
   ========================================================================== */

.shell {
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* ==========================================================================
   Top bar
   ========================================================================== */

.topbar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  transition: background var(--mid) ease, border-color var(--mid) ease;
  border-bottom: 1px solid transparent;
}

.topbar.is-stuck {
  background: rgba(7, 12, 22, 0.78);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom-color: var(--line);
}

.topbar .shell {
  height: 74px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.mark {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bone);
}

.topnav { display: none; }

.topnav ul {
  display: flex;
  gap: clamp(1.1rem, 2.4vw, 2.1rem);
}

.topnav a {
  position: relative;
  display: block;
  padding: 0.4rem 0;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color var(--fast) ease;
}

.topnav a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 100% 50%;
  transition: transform var(--mid) var(--ease);
}

.topnav a:hover,
.topnav a.is-active { color: var(--bone); }

.topnav a:hover::after,
.topnav a.is-active::after {
  transform: scaleX(1);
  transform-origin: 0 50%;
}

.menu-toggle {
  position: relative;
  width: 44px;
  height: 44px;
  /* The bars are positioned from the top-left, so widening the button moves
     them; this keeps the glyph centred at the new size. */
  margin-right: -5px;
}

/* Both bars share one centre point so they cross into an X when open. */
.menu-toggle span {
  position: absolute;
  left: 12px;
  top: 17px;
  width: 20px;
  height: 1px;
  background: var(--bone);
  transition: transform var(--mid) var(--ease);
}

.menu-toggle span:first-child { transform: translateY(-4px); }
.menu-toggle span:last-child { transform: translateY(4px); }

.menu-toggle[aria-expanded="true"] span:first-child { transform: rotate(45deg); }
.menu-toggle[aria-expanded="true"] span:last-child { transform: rotate(-45deg); }

.drawer {
  position: fixed;
  inset: 74px 0 auto 0;
  z-index: 39;
  background: var(--navy-800);
  border-bottom: 1px solid var(--line);
  clip-path: inset(0 0 100% 0);
  /* A landscape phone is about 360px tall; seven links plus padding do not
     fit, and without this the last ones are simply unreachable. */
  max-height: calc(100dvh - 74px);
  overflow-y: auto;
  overscroll-behavior: contain;
  visibility: hidden;
  transition: clip-path var(--mid) var(--ease), visibility 0s linear var(--mid);
}

.drawer.is-open {
  clip-path: inset(0 0 0 0);
  visibility: visible;
  transition: clip-path var(--mid) var(--ease), visibility 0s;
}

/* No JavaScript, no way to open the drawer — and the top nav is hidden at
   this width, so the page had no navigation at all. Show it as a plain list
   instead of a panel. */
html:not(.js) .drawer {
  position: static;
  clip-path: none;
  visibility: visible;
  max-height: none;
  inset: auto;
}

html:not(.js) .menu-toggle { display: none; }

.drawer ul { padding: 0.5rem var(--gutter) 1.5rem; }

.drawer a {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 1.05rem;
}

.drawer a span {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  color: var(--muted);
}

@media (min-width: 880px) {
  .topnav { display: block; }
  .menu-toggle { display: none; }
  .drawer { display: none; }
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  /* Centred rather than bottom-anchored: on a tall display, anchoring to the
     bottom left a dead half-screen of navy above the greeting. */
  justify-content: center;
  padding-block: 6rem clamp(3rem, 8vw, 5rem);
  position: relative;
}

.hero-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  color: var(--muted);
  margin-bottom: clamp(1.75rem, 4vw, 2.75rem);
}

.hero-meta .sep { color: var(--line-strong); }

.hero-meta time { font-variant-numeric: tabular-nums; }

.display {
  font-family: var(--serif);
  font-size: clamp(3.6rem, 14vw, 10rem);
  line-height: 0.88;
  letter-spacing: -0.025em;
}

.mask {
  display: block;
  overflow: hidden;
  /* Room for descenders: line-height is 0.88, so glyph tails fall outside
     the line box and would otherwise be clipped by overflow:hidden. */
  padding-bottom: 0.2em;
}

.js .mask > span {
  display: block;
  /* Must exceed (lineHeight + padding) / lineHeight = ~123%, or the text
     peeks into the padding while it's supposed to be hidden. */
  transform: translateY(128%);
  transition: transform var(--slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

.mask > span { display: block; }

.js .is-visible .mask > span,
.js .mask.is-visible > span { transform: none; }

.hero-lede {
  margin-top: clamp(1.75rem, 4vw, 2.5rem);
  max-width: 46ch;
  font-size: clamp(1.02rem, 1.6vw, 1.2rem);
  line-height: 1.65;
  color: var(--bone-dim);
}

.hero-links {
  margin-top: clamp(2rem, 4vw, 2.75rem);
  display: flex;
  flex-wrap: wrap;
  gap: 1.75rem;
}

/* Underline wipes out to the right, then back in from the left. */
.wipe {
  position: relative;
  display: inline-block;
  padding-bottom: 3px;
  color: var(--bone);
}

.wipe::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform-origin: 0 50%;
  transition: transform var(--mid) var(--ease);
}

.wipe:hover { color: var(--accent); }

.wipe:hover::after {
  animation: wipe var(--mid) var(--ease);
}

@keyframes wipe {
  0%   { transform: scaleX(1); transform-origin: 100% 50%; }
  49%  { transform: scaleX(0); transform-origin: 100% 50%; }
  51%  { transform: scaleX(0); transform-origin: 0 50%; }
  100% { transform: scaleX(1); transform-origin: 0 50%; }
}

.hero-scroll {
  position: absolute;
  right: var(--gutter);
  bottom: clamp(3rem, 8vw, 5.5rem);
  display: none;
  align-items: center;
  gap: 0.75rem;
  color: var(--muted);
}

.hero-scroll .track {
  width: 1px;
  height: 54px;
  background: var(--line);
  position: relative;
  overflow: hidden;
}

.hero-scroll .track::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 18px;
  background: var(--accent);
  animation: track 2.6s var(--ease) infinite;
}

@keyframes track {
  0%   { transform: translateY(-100%); }
  60%  { transform: translateY(300%); }
  100% { transform: translateY(300%); }
}

@media (min-width: 880px) {
  .hero-scroll { display: flex; }
}

@media (max-width: 879px) {
  .hero { padding-block: 6rem 4rem; }
}

/* ==========================================================================
   Bands (section = sticky rail + body)
   ========================================================================== */

.band { padding-block: clamp(3.25rem, 6.5vw, 5.75rem); }

.band-grid {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
}

.rail { color: var(--muted); }

.rail .idx { color: var(--accent); }

.rail-label {
  display: flex;
  align-items: baseline;
  gap: 0.8rem;
  /* This is an <h2> for the document outline, so it has to be pulled back
     to the rail's mono metrics: the UA sheet sizes h2 at 1.5em, and the
     shared h1-h4 rule sets letter-spacing on the element directly, which
     beats the .mono values it would otherwise inherit from the parent. */
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}

@media (min-width: 880px) {
  .band-grid {
    grid-template-columns: var(--rail) 1fr;
  }
  .rail {
    position: sticky;
    top: 130px;
    align-self: start;
  }
  .rail-label { flex-direction: column; gap: 0.55rem; }
}

/* ==========================================================================
   About
   ========================================================================== */

/* The About rail holds a label and then a column of empty space. The
   portrait fills it without touching the reading column, so the measure
   and rhythm of the page are unchanged.

   A colour studio photograph would be the only full-colour object on the
   site and would pull every eye on the page. Desaturating it and letting
   the navy through folds it into the two colours already here; it lifts to
   full colour on hover, so the real photograph is one gesture away. */
.portrait {
  display: block;
  width: 100%;
  max-width: 190px;
  margin-top: 1.6rem;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  object-position: 50% 8%;
  filter: grayscale(1) contrast(1.05) brightness(0.9);
  mix-blend-mode: luminosity;
  transition: filter var(--mid) ease;
}

.portrait:hover { filter: none; }

@media (max-width: 879px) {
  /* The rail stacks above the text on narrow screens, so a full-width
     portrait would push the section's first sentence off the fold. */
  .portrait { max-width: 118px; margin-top: 1.2rem; }
}

.about-lede {
  font-family: var(--serif);
  font-size: clamp(1.6rem, 3.4vw, 2.6rem);
  line-height: 1.22;
  letter-spacing: -0.015em;
  max-width: 22ch;
}

.about-body {
  margin-top: clamp(2rem, 4vw, 2.75rem);
  display: grid;
  gap: 1.35rem;
  max-width: 58ch;
  color: var(--bone-dim);
}

/* No max-width: every hairline on the page aligns to the same right edge. */
.about-facts {
  margin-top: clamp(2.25rem, 5vw, 3.25rem);
  display: grid;
  gap: 0;
}

.fact {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
  justify-content: space-between;
  padding: 0.85rem 0;
  border-top: 1px solid var(--line);
  color: var(--muted);
}

.fact:last-child { border-bottom: 1px solid var(--line); }

.fact dd { margin: 0; color: var(--bone-dim); }


/* ==========================================================================
   Rows — experience, leadership, certifications
   ========================================================================== */

.rows { display: grid; }

.row {
  position: relative;
  display: grid;
  gap: 0.6rem 2.5rem;
  padding: clamp(1.6rem, 3vw, 2.25rem) 0;
  transition: background var(--mid) ease;
}

.row::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--slow) var(--ease);
}

.row.is-visible::before { transform: scaleX(1); }

.rows .row:last-child::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
}

.row-when {
  color: var(--muted);
  transition: color var(--mid) ease;
}

.row:hover .row-when { color: var(--accent); }

.row-role {
  font-size: clamp(1.08rem, 2vw, 1.3rem);
  font-weight: 500;
  letter-spacing: -0.01em;
}

.row-org {
  margin-top: 0.3rem;
  color: var(--bone-dim);
  font-size: 0.95rem;
}

.row-org .at { color: var(--muted); }

.row-notes {
  margin-top: 1.15rem;
  display: grid;
  gap: 0.7rem;
  max-width: 62ch;
  color: var(--bone-dim);
  font-size: 0.95rem;
}

.row-notes li {
  position: relative;
  padding-left: 1.6rem;
}

.row-notes li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.82em;
  width: 11px;
  height: 1px;
  background: var(--line-strong);
}

@media (min-width: 700px) {
  /* Wide enough that a full "May 2026 — Aug 2026" stays on one line. */
  .row { grid-template-columns: 172px 1fr; }
}

/* ==========================================================================
   Projects
   ========================================================================== */

.projects { display: grid; }

/* Work was the tallest section on the page (2134px) for 203 words, because
   all six entries carried the same display-sized title and padding. The four
   ordinary entries are now compact; the two featured ones keep their weight
   below, which sharpens the distinction rather than flattening it. */
.project {
  position: relative;
  padding: clamp(1.2rem, 2.4vw, 1.7rem) 0;
}

.project--featured { padding: clamp(1.9rem, 3.8vw, 2.7rem) 0; }

.project::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--slow) var(--ease);
}

.project.is-visible::before { transform: scaleX(1); }

.projects .project:last-child::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
}

.project-top {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 2rem;
  color: var(--muted);
}

.project h3 {
  margin-top: 0.75rem;
  font-family: var(--serif);
  font-size: clamp(1.25rem, 2.4vw, 1.7rem);
  line-height: 1.12;
  max-width: 20ch;
  transition: color var(--mid) ease;
}

.project:hover h3 { color: var(--accent); }

.project-note {
  margin-top: 0.9rem;
  max-width: 56ch;
  color: var(--bone-dim);
}

/* The two write-ups with a document attached are the strongest thing in
   this section, and they used to look identical to a dashboard entry. They
   are marked in the one accent the site already uses rather than with a
   card or a border, which the rest of the page does not have: the tag goes
   brass, the title sits a size larger, and the standing hairline above the
   entry is drawn in accent instead of rule. */
.project--featured .project-tag { color: var(--accent); }

.project--featured h3 {
  margin-top: 0.9rem;
  font-size: clamp(1.75rem, 4vw, 2.7rem);
  max-width: 22ch;
}

.project--featured::before { background: var(--accent-soft); }

.project--featured .project-note { color: var(--bone); }

/* Featured links sit outside the collapsed detail, so they need the space
   the detail block would otherwise have given them. */
.project--featured > .project-link { margin-top: 1.25rem; }

.project details { margin-top: 1.35rem; }

/* One line of hard facts above the fold. The hero is a greeting and a
   sentence naming the degree; a reader who never scrolls used to leave
   knowing nothing they could act on. Same mono register as the links under
   it, so it reads as metadata rather than as a second headline. */
.hero-facts {
  margin: 1.4rem 0 0;
  color: var(--muted);
}

/* What he is recruiting for and when. The most useful line on a recruiting
   site, and Contact used to carry only a location.
   It appears twice on purpose: a recruiter who lands in the hero and one who
   scrolls to the foot both get the answer without hunting for it. */
.contact-status { color: var(--bone-dim); }

/* The hero copy of it. Two mono lines of equal weight under the lede read as
   one block of metadata and neither gets looked at, so the status line sits
   tighter to the facts above it and leads in the accent, with the three
   answers themselves dimmed. */
.hero-status {
  margin: 0.55rem 0 0;
  color: var(--accent);
}

/* ==========================================================================
   Interactive DCF
   ========================================================================== */

/* The deck's model, live, inside the detail of the entry that links to the
   deck. It was above the fold of that entry first, which put two sliders and
   an unexplained number in front of every reader before anything had set
   them up — it read as noise. Down here it costs nothing and is found by the
   reader who opened the entry, which is the reader who wants it.

   Hairlines and mono labels like everything else; the only new object is the
   slider, drawn out of the same two colours. */
.dcf {
  margin-top: 1.6rem;
  padding-top: 1.35rem;
  border-top: 1px solid var(--line);
  max-width: 34rem;
}

.dcf-kicker { margin: 0 0 0.9rem; }

.dcf-readout {
  display: flex;
  align-items: baseline;
  gap: 0.8rem;
  margin: 0;
}

.dcf-value {
  font-family: var(--serif);
  font-size: clamp(2.1rem, 5vw, 3rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.dcf-delta { font-size: 0.72rem; letter-spacing: 0.14em; }
.dcf-delta.is-up { color: var(--accent); }
.dcf-delta.is-down { color: var(--muted); }

.dcf-against { margin: 0.6rem 0 0; }

.dcf-controls {
  display: grid;
  gap: 1rem;
  margin-top: 1.5rem;
}

.dcf-control { display: grid; gap: 0.5rem; cursor: pointer; }

.dcf-legend {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  color: var(--muted);
}

.dcf-legend span { color: var(--bone); }

/* One track, drawn by hand so it looks the same in every browser. --fill is
   set from JS because no engine exposes the filled portion to CSS. */
.dcf input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 22px;
  background: transparent;
  cursor: grab;
}

.dcf input[type="range"]:active { cursor: grabbing; }

.dcf input[type="range"]::-webkit-slider-runnable-track {
  height: 1px;
  background: linear-gradient(
    to right,
    var(--accent) 0 var(--fill, 50%),
    var(--line-strong) var(--fill, 50%) 100%
  );
}

.dcf input[type="range"]::-moz-range-track {
  height: 1px;
  background: linear-gradient(
    to right,
    var(--accent) 0 var(--fill, 50%),
    var(--line-strong) var(--fill, 50%) 100%
  );
}

.dcf input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--accent);
  transition: transform var(--fast) ease;
}

.dcf input[type="range"]::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}

.dcf input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.25); }

.dcf input[type="range"]:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 4px;
}

/* Engine-specific thumb halo on top of the outline, not instead of it. */
.dcf input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.dcf input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.dcf-cases {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.4rem;
}

.dcf-cases button {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line);
  padding: 0.5rem 0.8rem;
  cursor: pointer;
  transition: color var(--fast) ease, border-color var(--fast) ease;
}

.dcf-cases button:hover {
  color: var(--bone);
  border-color: var(--line-strong);
}

.dcf-note {
  margin: 1.3rem 0 0;
  font-size: 0.82rem;
  line-height: 1.55;
  max-width: 46ch;
}

/* One disclosure control, shared by Work, Experience and Capabilities. */
.project summary,
.row-more summary,
.cap-group summary {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  list-style: none;
  color: var(--muted);
  transition: color var(--fast) ease;
}

.project summary::-webkit-details-marker,
.row-more summary::-webkit-details-marker,
.cap-group summary::-webkit-details-marker { display: none; }

.project summary:hover,
.row-more summary:hover,
.cap-group summary:hover { color: var(--bone); }

.project summary .plus,
.row-more summary .plus,
.cap-group summary .plus {
  display: inline-block;
  width: 11px;
  height: 11px;
  position: relative;
}

.project summary .plus::before,
.project summary .plus::after,
.row-more summary .plus::before,
.row-more summary .plus::after,
.cap-group summary .plus::before,
.cap-group summary .plus::after {
  content: "";
  position: absolute;
  background: currentColor;
  transition: transform var(--mid) var(--ease);
}

.project summary .plus::before,
.row-more summary .plus::before,
.cap-group summary .plus::before {
  inset: 5px 0 5px 0;
  height: 1px;
}

.project summary .plus::after,
.row-more summary .plus::after {
  inset: 0 5px 0 5px;
  width: 1px;
}

.cap-group summary .plus::after {
  inset: 0 5px 0 5px;
  width: 1px;
}

.project details[open] summary .plus::after,
.row-more[open] summary .plus::after,
.cap-group[open] summary .plus::after { transform: scaleY(0); }

.project-detail {
  margin-top: 1.25rem;
  display: grid;
  gap: 0.7rem;
  max-width: 62ch;
  color: var(--bone-dim);
  font-size: 0.95rem;
}

.project-detail li {
  position: relative;
  padding-left: 1.6rem;
}

.project-detail li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.82em;
  width: 11px;
  height: 1px;
  background: var(--line-strong);
}

/* ==========================================================================
   Certifications + leadership lists
   ========================================================================== */

/* Certifications and Involvement were two full-width sections carrying 92
   words between them across 1488px — 18% of the front page's height for 10%
   of its words. They are now one section, side by side, with nothing cut. */
.two-up {
  display: grid;
  gap: clamp(2rem, 4vw, 3.25rem);
}

@media (min-width: 880px) {
  .two-up { grid-template-columns: 1fr 1fr; align-items: start; }
}

/* An <h3> for the document outline, so it needs the same pull-back to mono
   metrics that .rail-label needs: the shared h1-h4 rule sets letter-spacing
   on the element, which beats the .mono value it would otherwise inherit. */
.col-label {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  margin-bottom: 0.4rem;
}

.list { display: grid; }

/* Two columns, not wrapping flex.
   As flex, the value was shrink-to-fit, so where it landed depended on how
   long it happened to be: beside the name it went right; wrapped to its own
   line it sat at flex-start (left) because space-between has nothing to
   space; wrapped and wider than the row it filled the width and text-align
   finally applied, ragged-left. Three placements, one rule. A grid column
   puts it in the same place every time and lets a long name wrap inside its
   own column instead of pushing the value off the line. */
.list-item {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.35rem 2rem;
  padding: 1.1rem 0;
}

/* A value too long for a column takes the full width underneath instead,
   where it reads left-to-right as a continuation of the name. Set from the
   data in js/templates.js, because CSS cannot measure text. */
.list-item--stacked { grid-template-columns: 1fr; }

.list-item::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--slow) var(--ease);
}

.list-item.is-visible::before { transform: scaleX(1); }

.list .list-item:last-child::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
}

.list-item dt,
.list-item .name {
  font-size: 1rem;
  font-weight: 400;
}

.list-item dd,
.list-item .by {
  margin: 0;
  color: var(--muted);
  text-align: right;
}

/* Same specificity as the rule above, so this has to follow it. */
.list-item--stacked .by { text-align: left; }

/* The collapsed tail of the involvement list. It is one label and a run of
   names, so it has to read as a line of prose rather than as a right-aligned
   value: the label shrinks to a rail marker and the names take the width. */
.list-more {
  grid-template-columns: auto minmax(0, 1fr);
}

.list-more .name { color: var(--muted); }

.list-more .by {
  text-align: left;
  color: var(--bone-dim);
  font-size: 0.95rem;
}

/* Narrow screens: let label/value stack cleanly instead of half-wrapping
   whenever a value happens to be long. */
@media (max-width: 620px) {
  .fact {
    flex-direction: column;
    align-items: stretch;
    gap: 0.3rem;
  }

  .list-item,
  .list-more {
    grid-template-columns: 1fr;
    align-items: stretch;
    gap: 0.3rem;
  }

  .fact dd,
  .list-item dd,
  .list-item .by {
    text-align: left;
  }
}

/* ==========================================================================
   Contact
   ========================================================================== */

.contact-mail {
  display: inline-block;
  margin-top: 1.5rem;
  font-family: var(--serif);
  font-size: clamp(1.8rem, 5.5vw, 3.6rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
  word-break: break-word;
}

.contact-aside {
  margin-top: clamp(2.25rem, 5vw, 3rem);
  display: flex;
  flex-wrap: wrap;
  gap: 1.75rem;
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-foot {
  border-top: 1px solid var(--line);
  padding-block: 2.25rem 2.75rem;
}

.site-foot .shell {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 2rem;
  justify-content: space-between;
  align-items: baseline;
  color: var(--muted);
}

/* Its own line in the footer's flex row, above the copyright and the clocks.
   Everything else — mono, 0.7rem, the letter-spacing, uppercase, --muted — is
   inherited from .site-foot .shell, so the line matches its neighbours. */
.foot-quote { flex: 0 0 100%; }

.site-foot a { transition: color var(--fast) ease; }
.site-foot a:hover { color: var(--accent); }


/* The hairlines above each row draw themselves in on reveal, from scaleX(0).
   Without JavaScript nothing ever scales them up, so the section dividers
   would simply be missing. Same inversion as the reveal rules above. */
html:not(.js) .row::before,
html:not(.js) .project::before,
html:not(.js) .list-item::before { transform: scaleX(1); }

/* ==========================================================================
   Reveal
   ========================================================================== */

/* Everything here is scoped under html.js, which an inline script in <head>
   adds and a watchdog in that same script removes.

   The default used to be hidden, with JavaScript revealing content. That is
   the wrong way round for a page whose content ships in the HTML. <noscript>
   only fires when scripts are *disabled*; it does nothing when scripts are
   enabled and the module fails — a CSP, a blocked import, a proxy that
   mangles type="module". In that case nothing added is-visible, nothing
   applied the noscript style, and the reader got a blank page with the whole
   record sitting in it, unreadable. That is the exact failure the prerender
   exists to prevent, reached through a different door.

   Hidden is now a thing JavaScript opts into and gives up if it dies. */
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--slow) var(--ease), transform var(--slow) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

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

/* Children of a revealed row fade slightly behind the rule that draws above them. */
.js .row .row-when,
.js .row .row-body,
.js .project > *:not(::before),
.js .list-item > * {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity var(--slow) var(--ease), transform var(--slow) var(--ease);
  transition-delay: 120ms;
}

.row.is-visible .row-when,
.row.is-visible .row-body,
.project.is-visible > *,
.list-item.is-visible > * {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .reveal,
  .row .row-when,
  .row .row-body,
  .project > *,
  .list-item > * {
    opacity: 1;
    transform: none;
  }

  .mask > span { transform: none; }

  .row::before,
  .row:last-child::after,
  .project::before,
  .list-item::before { transform: scaleX(1); }
}

/* ==========================================================================
   Hero greeting, scroll progress, market clocks
   ========================================================================== */

/* Parallax layer — transformed on scroll by initScrollMotion(). */
.hero-inner { will-change: transform, opacity; }

/* A greeting is a longer string than a name, so it sets its own scale. */
.display-greet {
  font-size: clamp(2.2rem, 8vw, 6.5rem);
}

.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 45;
  height: 1px;
  pointer-events: none;
}

.scroll-progress span {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
}

.clocks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.5rem;
}

.clocks li {
  display: inline-flex;
  gap: 0.5rem;
}

.clocks time { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   About page, brand glyphs
   ========================================================================== */

/* Monochrome so brand marks inherit the palette instead of fighting it. */
.glyph {
  width: 0.95em;
  height: 0.95em;
  display: inline-block;
  vertical-align: -0.13em;
  margin-right: 0.15em;
}

.hero-links .lead-link { color: var(--accent); }

.about-more {
  margin-top: clamp(2rem, 4vw, 2.75rem);
}

.chapter-body {
  display: grid;
  gap: 1.35rem;
  max-width: 58ch;
  font-size: 1.05rem;
  color: var(--bone-dim);
}

/* Chapters sit closer together than top-level sections — it's one story,
   not six separate parts of a page. */
.chapter { padding-block: clamp(2rem, 4vw, 3.25rem); }

.caps { display: grid; }

.cap-group {
  padding: 0.8rem 0;
  border-top: 1px solid var(--line);
}

.caps .cap-group:last-child { border-bottom: 1px solid var(--line); }

/* The count sits hard right, so the column of numbers reads as a shape:
   which groups are deep and which are thin, before opening any of them. */
.cap-group summary {
  display: flex;
  width: 100%;
  color: var(--bone-dim);
}

.cap-count { margin-left: auto; color: var(--muted); }

.cap-items {
  margin-top: 0.8rem;
  color: var(--bone-dim);
  max-width: 62ch;
}

/* Experience prose + the artifact block beneath it */
.row-summary {
  margin-top: 1rem;
  max-width: 60ch;
  color: var(--bone-dim);
}

/* Each role's artifact list collapses behind its own label, which was
   already written per role ("What I built", "The call", "The numbers") and
   now does double duty as the toggle. Five roles of prose plus five lists
   was more than anyone scrolls; this keeps the voice visible and puts the
   specifics one click away, the same way Work already behaves. */
.row-more {
  margin-top: 1.4rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--line);
  max-width: 62ch;
}

.row-more > .row-notes { margin-top: 0.9rem; }

.operating-intro {
  max-width: 54ch;
  margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
  color: var(--bone-dim);
}

/* "How I got here" leaves the page, so it reads differently to the anchors. */
.topnav .nav-page,
.drawer .nav-page { color: var(--accent); }

.topnav .nav-page::after { background: var(--accent); }

.project-link { margin-top: 1.1rem; }

/* The location is the second half of the greeting sentence, so it sits
   under it at reduced scale rather than floating above as its own label. */
.display-greet .from {
  font-size: 0.46em;
  color: var(--bone-dim);
}

/* ==========================================================================
   About page: narrative with a chapter navigator, and counted stats
   ========================================================================== */

.narrative { padding-block: clamp(2rem, 5vw, 4rem); }

.narrative-grid { display: grid; gap: clamp(2rem, 5vw, 4.5rem); }

.chapter-nav { display: none; }

@media (min-width: 880px) {
  .narrative-grid { grid-template-columns: var(--rail) 1fr; }
  .chapter-nav {
    display: block;
    position: sticky;
    top: 130px;
    align-self: start;
    padding-left: 1.1rem;
  }
}

/* The spine sits behind the dots and fills as you move through the story. */
.chapter-nav .spine {
  position: absolute;
  left: 0;
  top: 0.55em;
  bottom: 0.55em;
  width: 1px;
  background: var(--line);
}

.chapter-nav .spine-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: 50% 0;
}

.chapter-nav ol { position: relative; display: grid; gap: 0.9rem; }

.chapter-link {
  position: relative;
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: baseline;
  gap: 0 0.55rem;
  color: var(--muted);
  transition: color var(--fast) ease;
}

.chapter-link .dot {
  position: absolute;
  left: -1.1rem;
  top: 0.42em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--line-strong);
  transform: translateX(-2px);
  transition: background var(--fast) ease, transform var(--fast) var(--ease);
}

.chapter-link:hover { color: var(--bone-dim); }

.chapter-link.is-active { color: var(--bone); }

.chapter-link.is-active .dot {
  background: var(--accent);
  transform: translateX(-2px) scale(1.6);
}

.chapter-link.is-active .idx { color: var(--accent); }

/* Chapter bodies */
.chapter { padding-block: clamp(1.5rem, 3.5vw, 2.75rem); }

.chapter-head {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  margin-bottom: 1.1rem;
}

.chapter-head .idx { color: var(--accent); }

.chapter-title {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Counted figures */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: clamp(1.5rem, 3vw, 2.25rem);
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
  padding-bottom: clamp(2rem, 4vw, 2.75rem);
  border-bottom: 1px solid var(--line);
}

.stat-value {
  font-family: var(--serif);
  font-size: clamp(2rem, 4vw, 3rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.stat-label {
  margin: 0.6rem 0 0;
  color: var(--muted);
  line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
  .chapter-nav .spine-fill { transform: scaleY(1); }
}

/* ==========================================================================
   Touch targets
   ==========================================================================
   Measured on a 390px viewport, the disclosure toggles were 19px tall and
   most inline links 22px. WCAG 2.2 asks for 24px minimum and Apple
   recommends 44; more to the point, those toggles are the primary
   interaction on this site — every artifact list and every capability group
   is behind one — and most visitors arrive from a LinkedIn link on a phone.

   Scoped to coarse pointers so the desktop layout, which is tuned tightly,
   is not touched at all. Padding only grows the hit area; nothing moves
   visually except the spacing between rows. */

@media (pointer: coarse) {
  .project summary,
  .row-more summary,
  .cap-group summary {
    padding-block: 0.6rem;
  }

  .topnav a,
  .hero-links a,
  .contact-aside a,
  .about-more a,
  .project-link a,
  .chapter-link,
  .site-foot a {
    padding-block: 0.45rem;
  }

  /* The wordmark is a link back to the top and was 20px tall. */
  .mark { padding-block: 0.5rem; }

  /* The slider track is drawn at 1px, so a taller input changes nothing
     visually and gives the thumb a real area to be grabbed by. */
  .dcf input[type="range"] { height: 44px; }
}

@media (pointer: coarse) and (hover: none) {
  /* The portrait lifts to colour on hover, which a touch device cannot do
     and which a tap would otherwise latch on. Leave it monochrome. */
  .portrait:hover { filter: grayscale(1) contrast(1.05) brightness(0.9); }
}

/* ==========================================================================
   Print
   ==========================================================================
   Recruiters print candidate material, or save it to PDF to read later,
   attach to a req, or forward to a hiring manager. Without this the site
   prints navy pages with invisible hairlines and every <details> shut, which
   silently drops every artifact list on the page.

   The rule that matters most is `details { display: block }` with
   `::details-content` unhidden: on paper there is nothing to click, so
   everything has to be open. What comes out is a clean document from content
   that is already maintained — a second artifact nobody has to write. */

@media print {
  :root {
    --bone: #111;
    --bone-dim: #333;
    --muted: #555;
    --line: rgba(0, 0, 0, 0.22);
    --line-strong: rgba(0, 0, 0, 0.35);
    --accent: #6b5836;
  }

  /* Every size and gap in this stylesheet is in rem, which roots at html and
     not at body — so shrinking body font-size did nothing. Scaling the root
     scales the whole layout proportionally, which is the single biggest
     lever on page count here. */
  html { font-size: 10.5px; }

  html, body { background: #fff !important; color: #111; }

  /* Chrome, the drawer, and everything that only makes sense on a screen. */
  .topbar, .drawer, .menu-toggle, .grain, .scroll-progress,
  .hero-scroll, .skip-link, .clocks, .dcf-controls, .dcf-cases,
  .chapter-nav, .portrait { display: none !important; }

  /* Nothing may be hidden behind a toggle on paper. */
  details { display: block; }
  details > summary { list-style: none; font-weight: 500; }
  details::details-content { content-visibility: visible !important; }
  .plus { display: none !important; }

  /* Reveal animations never run without script or motion, so make sure the
     print path does not inherit an opacity:0 mid-state. */
  .reveal, .row > *, .project > *, .list-item > *, .chapter-body, .stat {
    opacity: 1 !important;
    transform: none !important;
  }
  .row::before, .project::before, .list-item::before { transform: scaleX(1) !important; }
  .mask > span { transform: none !important; }

  /* Give the reader the destination, since a printed link is otherwise a
     dead end — but only where it earns its place. Printing it after every
     link put the whole LinkedIn URL inside the hero's icon row and broke
     the line. The two documents and one profile link are what matter. */
  .project-link a::after,
  .contact-aside a::after {
    content: " (" attr(href) ")";
    font-size: 0.72em;
    color: #555;
    word-break: break-all;
  }

  /* Only units that fit on a page may ask to be kept whole. A .band is
     taller than A4, so asking to keep it intact cannot be honoured and just
     wastes the page before it — that alone was two pages. */
  .project, .row, .chapter, .list-item, .stat { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }

  .shell { max-width: none; padding: 0; }
  body { font-size: 10pt; line-height: 1.45; }

  /* Everything below exists because the first print came out at fifteen A4
     pages, which is worse than no print stylesheet at all — nobody reads a
     fifteen-page candidate handout. The screen layout is built out of
     viewport-relative padding and display-sized type, both of which are
     wrong on paper. */

  /* The hero is a full viewport on screen and a title block on paper. The
     greeting is atmosphere; the lede carries the name, so it leads. */
  .hero { min-height: 0 !important; padding: 0 0 0.6rem !important; display: block; }
  .display-greet { display: none !important; }
  .hero-inner {
    /* initScrollMotion writes transform and opacity inline, so a page
       printed after scrolling would otherwise come out blank or displaced. */
    transform: none !important;
    opacity: 1 !important;
  }
  .hero-lede {
    font-family: var(--serif);
    font-size: 17pt;
    line-height: 1.25;
    max-width: none;
  }
  .hero-facts { margin-top: 0.5rem; }
  .hero-status { margin-top: 0.25rem; }
  .hero-links { margin-top: 0.5rem; }

  /* Bands are spaced for scrolling, not for reading a stack of pages. */
  .band { padding: 0.9rem 0 !important; }
  .band-grid { display: block !important; gap: 0 !important; }
  .rail { position: static !important; margin-bottom: 0.35rem; }
  .rail-label { flex-direction: row !important; }

  .about-lede { font-size: 13pt; max-width: none; margin-bottom: 0.5rem; }
  .about-body, .about-body p { max-width: none; }
  .about-body p { margin: 0.35rem 0; }
  /* The measure that reads well on screen leaves half the sheet blank on
     A4, where the column is wider and the type smaller. */
  .row-summary, .project-note, .row-notes, .project-detail,
  .hero-lede, .cap-items, .chapter-body p { max-width: none; }

  .row, .project { padding: 0.55rem 0 !important; }
  .row { display: grid !important; grid-template-columns: 7.5rem 1fr; gap: 0 1rem; }
  .row-role, .project h3, .project--featured h3 {
    font-size: 12pt !important;
    max-width: none;
    margin-top: 0.15rem;
  }
  .row-summary, .project-note { margin-top: 0.2rem; max-width: none; }
  .row-notes, .project-detail { margin-top: 0.3rem; gap: 0.15rem; max-width: none; }
  .row-more { margin-top: 0.4rem; padding-top: 0.3rem; }

  /* The DCF is a tool. On paper it is a dead control panel, and the deck it
     comes from is linked two lines above with its URL printed. */
  .dcf { display: none !important; }

  /* The story page prints as prose; its navigator is a screen affordance. */
  .narrative-grid { display: block !important; }
  .chapter { padding: 0.5rem 0 !important; }
  .chapter-body p { margin: 0.3rem 0; max-width: none; }
  .stats { gap: 0.5rem !important; }

  .list-item { padding: 0.3rem 0 !important; }
  .two-up { gap: 1.2rem !important; }

  @page { margin: 14mm 13mm; }
}
