/* ---------------------------------------------------------------------------
   raffaelchoi.com

   The through-line of this work is systems under measurement: latency budgets,
   VRAM budgets, concurrent players, watchdog timers, commit counts. So the page
   is built to read like an instrument rather than a brochure - warm near-black,
   a single amber signal colour, and numbers treated as the largest objects on
   the page.

   Amber rather than blue on purpose. Frostcord, Frostphone and Frostify all use
   the same cool blue-violet; a portfolio that links to all three should not look
   like a fourth product page.

   Motion is CSS-only and scroll-driven, so the Content-Security-Policy can keep
   script-src 'none'. Everything animated is fully legible with the animation
   removed - the effects are enhancement, never the thing carrying the meaning.
--------------------------------------------------------------------------- */

@font-face {
  font-family: 'Bricolage';
  src: url('/data/fonts/bricolage-latin.woff2') format('woff2');
  font-weight: 200 800;
  font-display: swap;
}

@font-face {
  font-family: 'Inter';
  src: url('/data/fonts/inter-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --font-display: 'Bricolage', 'Inter', system-ui, sans-serif;
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;

  --bg: #0a0908;
  --bg-deep: #060505;
  --surface: #131110;
  --surface-hi: #1b1817;
  --line: #262220;
  --line-hi: #3a3431;

  --text: #f3efe8;
  --text-dim: #a49c93;
  /* 4.95:1 on the page background and 4.69:1 on a panel. The previous value
     sat at 3.48:1 and carried every mono label on the site, all of it small
     text, so the whole telemetry layer failed WCAG AA. */
  --text-faint: #847e76;

  --signal: #ffb020;
  --signal-hot: #ffc44d;
  --signal-wash: rgba(255, 176, 32, 0.1);

  /* One type scale. Every heading on the site picks a step from this list;
     before it existed there were thirteen display sizes with no ratio between
     them, which is most of why the pages read as separate pieces of work. */
  --t-hero: clamp(3.4rem, 12vw, 8.5rem);
  --t-display: clamp(2.5rem, 7vw, 4.6rem);
  --t-title: clamp(1.95rem, 4.4vw, 2.9rem);
  --t-step: clamp(1.55rem, 3.4vw, 2.3rem);
  --t-head: clamp(1.35rem, 2.4vw, 1.6rem);
  --t-sub: 1.3rem;
  --t-lead: clamp(1.08rem, 1.6vw, 1.28rem);
  --t-small: 0.94rem;
  --t-micro: 0.83rem;
  --t-label: 0.72rem;

  /* And one spacing scale, for the same reason. */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2rem;
  --s7: 3rem;
  --s8: 4rem;
  --section: clamp(3.75rem, 7.5vw, 6rem);

  --radius: 4px;
  --measure: 68ch;
  --container: 1180px;

  color-scheme: dark;
}

* { box-sizing: border-box; }

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

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

h1, h2, h3, h4 { margin: 0; font-weight: 500; }

p { margin: 0; }

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

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

a { color: var(--signal); text-decoration-color: rgba(255, 176, 32, 0.3); text-underline-offset: 3px; }
a:hover { color: var(--signal-hot); text-decoration-color: currentColor; }

:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--signal); color: #100c04; font-weight: 600;
}
.skip-link:focus { left: 0; }

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding-inline: clamp(1.25rem, 5vw, 3rem);
}

/* --- Telemetry label: the mono voice used for every piece of metadata -----
   One rule, one size, one tracking. This used to be thirteen near-identical
   declarations at 0.68, 0.70, 0.72, 0.74, 0.75 and 0.78rem with five different
   letter-spacings, so no two labels on a page ever agreed - the kind of drift
   nobody can name but everybody can see. */

.tele,
.hero-role,
.meta-label,
.project-metric-label,
.pipe-label,
.card-metric-label,
.feature-metric-label,
.readout-note,
.job-period,
.footer-note,
.back-link,
.nav-links a,
.button,
.link-row .label {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  line-height: 1.5;
}

/* Two variants, because a label either names a thing or announces one. */
.hero-role { color: var(--signal); }
.job-period, .footer-note, .stack-line { text-transform: none; letter-spacing: 0.05em; }

/* --- Navigation --------------------------------------------------------- */

.nav {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}

.nav-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.5rem; min-height: 62px;
}

.nav-brand {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  color: var(--text);
  text-decoration: none;
}
.nav-brand-mark { color: var(--signal); }
.nav-brand:hover { color: var(--signal-hot); }

.nav-links { display: flex; gap: clamp(1.1rem, 3vw, 2.2rem); }

.nav-links a { text-decoration: none; }
.nav-links a:hover { color: var(--text); }
.nav-links a[aria-current='page'] { color: var(--signal); }

/* --- Hero --------------------------------------------------------------- */

.hero {
  position: relative;
  padding-block: clamp(3.5rem, 11vw, 7.5rem) clamp(2rem, 5vw, 3.5rem);
  overflow: hidden;
}

/* A slow amber wash, off to one side so the name is never centred in it. */
.hero::before {
  content: '';
  position: absolute;
  inset: -30% -10% auto -25%;
  height: 130%;
  background: radial-gradient(45% 40% at 22% 42%, rgba(255, 176, 32, 0.14), transparent 72%);
  pointer-events: none;
}

.hero > * { position: relative; }

.hero-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-hero);
  line-height: 0.86;
  letter-spacing: -0.045em;
  margin-block: 1.1rem 0;
  text-wrap: balance;
}

/* The signal line: a waveform that runs under the name and keeps moving, the
   way a level meter does when nobody is talking. It carries no information, so
   it is aria-hidden and disappears entirely under reduced-motion. */
.signal-line {
  display: block;
  width: 100%;
  height: clamp(40px, 5.5vw, 68px);
  margin-top: 0.2rem;
  overflow: visible;
}

.signal-path {
  fill: none;
  stroke: var(--signal);
  stroke-width: 1.6;
  stroke-linecap: round;
  opacity: 0.85;
}

.signal-path-echo {
  stroke: var(--signal);
  opacity: 0.22;
}

@media (prefers-reduced-motion: no-preference) {
  .signal-path { animation: drift 7s ease-in-out infinite alternate; }
  .signal-path-echo { animation: drift 7s ease-in-out infinite alternate-reverse; }
}

@keyframes drift {
  from { transform: translateX(0) scaleY(1); }
  to   { transform: translateX(-36px) scaleY(0.55); }
}

.hero-headline {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-step);
  line-height: 1.24;
  letter-spacing: -0.025em;
  max-width: 34ch;
  margin-top: 1.8rem;
  color: var(--text);
  text-wrap: balance;
}

.hero-status {
  display: inline-flex; align-items: center; gap: 0.6rem;
  margin-top: 2rem;
}

.pulse {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--signal);
  box-shadow: 0 0 0 0 var(--signal-wash);
}
@media (prefers-reduced-motion: no-preference) {
  .pulse { animation: pulse 2.4s ease-out infinite; }
}
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(255, 176, 32, 0.55); }
  70%  { box-shadow: 0 0 0 11px rgba(255, 176, 32, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 176, 32, 0); }
}

.button-row { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2.2rem; }

.button {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 0.7rem 1.4rem;
  border: 1px solid var(--line-hi);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.16s, background-color 0.16s, color 0.16s;
}
.button:hover { border-color: var(--signal); background: var(--signal-wash); color: var(--text); }

.button-primary {
  background: var(--signal);
  border-color: var(--signal);
  color: #140f04;
  font-weight: 600;
}
.button-primary:hover { background: var(--signal-hot); border-color: var(--signal-hot); color: #140f04; }

/* --- Scroll-driven reveal ------------------------------------------------
   A scroll timeline rather than an intersection observer, so no script runs
   and the CSP can keep script-src 'none'. Guarded twice: browsers without
   animation-timeline never enter the block, and the keyframes end at the
   element's resting state, so a failure mid-animation still leaves it legible. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .rise {
      animation: rise-in linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 26%;
    }
  }
}

@keyframes rise-in {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: none; }
}

/* --- Sections ----------------------------------------------------------- */

/* Two adjacent sections each contributing a full block of padding produced
   fourteen rems of nothing between them, which is where the page felt empty.
   The first one pays; the second starts where the first left off. */
section { padding-block: var(--section); }
section + section { padding-top: 0; }

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

.section-head { margin-bottom: clamp(1.75rem, 4vw, 2.75rem); }


.section-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-title);
  line-height: 1;
  letter-spacing: -0.035em;
  margin-top: 0.9rem;
  text-wrap: balance;
}

.lead {
  font-size: var(--t-lead);
  color: var(--text-dim);
  max-width: var(--measure);
}

.prose { max-width: var(--measure); color: var(--text-dim); display: grid; gap: 1.1rem; }
.prose strong { color: var(--text); font-weight: 600; }

/* --- Readout: the numbers, treated as the largest objects on the page ---- */

.readout {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.readout-cell {
  padding: clamp(1.5rem, 3vw, 2.25rem) clamp(1rem, 2vw, 1.75rem);
  border-bottom: 1px solid var(--line);
}
.readout-cell:last-child { border-bottom: 0; }

/* Side by side only once there is room: stacked, a right-hand border draws a
   stray vertical line down the edge of the column. */
@media (min-width: 720px) {
  .readout-cell { border-bottom: 0; border-right: 1px solid var(--line); }
  .readout-cell:last-child { border-right: 0; }
}

.readout-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.4rem, 6vw, 4rem);
  line-height: 0.9;
  letter-spacing: -0.05em;
  color: var(--signal);
}

.readout-label { margin-top: 0.75rem; font-size: 0.92rem; color: var(--text); }
.readout-note { margin-top: 0.3rem; }

/* --- Feature block: one project gets the whole width --------------------- */

.feature {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding: clamp(1.75rem, 4vw, 3rem);
  background: linear-gradient(160deg, var(--surface) 0%, var(--bg-deep) 100%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  position: relative;
  overflow: hidden;
  /* Grid and flex items default to min-width: auto, so the diagram's 620px
     min-width would push the whole block wider than the viewport instead of
     scrolling inside its own container. */
  min-width: 0;
}

.feature > * { min-width: 0; }
.feature-main { display: flow-root; }

.feature::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 2px;
  background: linear-gradient(180deg, var(--signal), transparent 70%);
}

.feature-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-display);
  line-height: 0.94;
  letter-spacing: -0.04em;
}

.feature-title a { color: var(--text); text-decoration: none; }
.feature-title a:hover { color: var(--signal-hot); }

.feature-tagline {
  margin-top: 0.9rem;
  font-size: var(--t-lead);
  color: var(--text-dim);
  max-width: 46ch;
}

.feature-body { color: var(--text-dim); margin-top: 1.1rem; max-width: var(--measure); }

@media (min-width: 900px) {
  .feature { grid-template-columns: minmax(0, 1fr) minmax(0, 14rem); }
  /* Readings down the side of the panel they belong to, rather than a row of
     numbers under a paragraph that already stopped short of the edge. */
  .feature-metrics {
    border-top: 0;
    border-left: 1px solid var(--line);
    padding: 0 0 0 clamp(1.5rem, 3vw, 2.5rem);
    margin: 0;
    align-content: start;
  }
}

.feature-metrics {
  display: grid; gap: var(--s5);
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
}
@media (max-width: 899px) {
  .feature-metrics { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
}

.feature-metric-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.7rem, 3.5vw, 2.4rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--signal);
}
.feature-metric-label { margin-top: 0.4rem; }

/* --- Project cards ------------------------------------------------------- */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1rem;
}

/* Cards carry their own border rather than being separated by a 1px gap over a
   coloured grid background. That technique draws neat rules right up until the
   card count does not fill the last row, and then the empty cell renders as a
   visible block. */
.card {
  position: relative;
  display: flex; flex-direction: column; gap: 0.7rem;
  padding: clamp(1.4rem, 2.5vw, 1.9rem);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: background-color 0.18s, border-color 0.18s;
}
.card:hover { background: var(--surface); border-color: var(--line-hi); }
.card:focus-within { background: var(--surface); border-color: var(--signal); }

.card-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-sub);
  letter-spacing: -0.025em;
  line-height: 1.05;
}
.card-title a { color: var(--text); text-decoration: none; }
.card-title a::after { content: ''; position: absolute; inset: 0; }
.card:hover .card-title a { color: var(--signal-hot); }

.card-tagline { color: var(--text-dim); font-size: 0.94rem; }

.card-metrics {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.9rem 1.25rem;
  margin-top: auto;
  padding-top: 1rem;
}

.card-metric-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.35rem;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--signal);
}
.card-metric-label { margin-top: 0.25rem; }

.stack-line { margin-top: 0.9rem; color: var(--text-faint); }

/* --- Skills -------------------------------------------------------------- */

/* Seven groups of very different heights. As a grid this reads as debris: every
   row grows to its tallest cell, so a five-chip group sat above 340px of nothing
   and the seventh group landed alone in a row of three. Columns pack by height
   rather than by row, which is what a list of unequal lists wants.

   Two columns rather than three on purpose. The chips are labels, not words -
   "MediaSession & foreground services" is one unbreakable line - so a narrow
   column puts every chip on its own row and the tallest group ends up three
   times the height of the shortest, which no balancing algorithm can rescue.
   Wider columns let the chips pack, and the groups come out close enough in
   height to balance. */
.skill-grid { columns: 2 320px; column-gap: 3rem; }
.skill-group { break-inside: avoid; margin-bottom: var(--s6); padding-top: var(--s4); border-top: 1px solid var(--line); }
.skill-group h3 { margin-bottom: var(--s3); }
.skill-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.chip {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--text-dim);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 0.24rem 0.55rem;
}

.skill-note { margin-top: 0.8rem; font-size: 0.83rem; color: var(--text-faint); max-width: 40ch; line-height: 1.55; }

/* --- Contact panel ------------------------------------------------------- */

.panel {
  padding: clamp(2rem, 5vw, 3.5rem);
  background: linear-gradient(160deg, var(--surface) 0%, var(--bg-deep) 100%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.link-list { margin-top: 2rem; }

.link-row {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: 1rem; padding-block: 1rem;
  border-top: 1px solid var(--line);
  text-decoration: none;
}
.link-row:last-child { border-bottom: 1px solid var(--line); }
.link-row .handle { color: var(--text); font-size: 1.05rem; }
.link-row:hover .handle { color: var(--signal-hot); }

/* --- Footer -------------------------------------------------------------- */

.footer { border-top: 1px solid var(--line); padding-block: 3rem; }
.footer-inner { display: flex; flex-wrap: wrap; gap: 1.5rem; justify-content: space-between; }
.footer-links { display: flex; flex-wrap: wrap; gap: 1.25rem; }
.footer a { color: var(--text-dim); text-decoration: none; font-size: 0.85rem; }
.footer a:hover { color: var(--signal-hot); }

/* --- Architecture diagrams -----------------------------------------------
   Colours live here rather than on the SVG elements: the CSP sets
   style-src-attr 'none', so a style attribute inside the markup is dropped. */

.diagram { margin: 0; }
.rail > .diagram, .rail > .decisions, .rail > .prose { grid-column: 2; }
@media (max-width: 860px) { .rail > .diagram, .rail > .decisions, .rail > .prose { grid-column: 1; } }

.diagram-scroll {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-deep);
  padding: clamp(0.9rem, 2vw, 1.5rem);
}

.diagram-svg { display: block; width: 100%; min-width: 620px; height: auto; }

.diagram-caption {
  margin-top: 0.85rem;
  font-size: 0.83rem;
  color: var(--text-faint);
  max-width: var(--measure);
  line-height: 1.55;
}

.d-zone {
  fill: rgba(255, 176, 32, 0.05);
  stroke: var(--signal);
  stroke-width: 1;
  stroke-dasharray: 5 4;
}

.d-zone-label {
  fill: var(--signal);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
}

.d-box { fill: var(--surface-hi); stroke: var(--line-hi); stroke-width: 1; }
.d-box-gpu { stroke: var(--signal); }

.d-box-title { fill: var(--text); font-family: var(--font-sans); font-size: 14px; font-weight: 600; }
/* --text-faint on --surface-hi is 4.39:1, just under AA - and this is the
   smallest type on the site at 10px, inside a filled box. --text-dim clears it
   at 6.5:1 and still sits well below the box title at 15.4:1. */
.d-box-sub { fill: var(--text-dim); font-family: var(--font-mono); font-size: 10px; }

.d-line { stroke: var(--line-hi); stroke-width: 1.5; fill: none; }
.d-line-return { stroke: var(--signal); stroke-dasharray: 4 3; }
.d-arrowhead { fill: var(--line-hi); }
.d-edge { fill: var(--text-faint); font-family: var(--font-mono); font-size: 10px; }
.d-clash { fill: var(--signal); opacity: 0.14; }
.d-strong { fill: var(--signal); font-family: var(--font-mono); font-size: 10px; }
.d-profile { stroke: var(--text); stroke-width: 2; fill: none; }
.d-profile-alt { stroke: var(--signal); stroke-width: 2; stroke-dasharray: 5 4; fill: none; }
.d-tick { stroke: var(--signal); stroke-width: 1; }

/* --- Carried over from the earlier pages, restyled ----------------------- */

/* `eyebrow` predates `tele` and means the same thing. Aliased rather than
   renamed across every page, since the two are genuinely interchangeable. */

.hero-role { margin-bottom: var(--s4); }

.hero-summary { margin-top: 1.4rem; max-width: var(--measure); color: var(--text-dim); }

.hero-meta {
  display: flex; flex-wrap: wrap; gap: 0.6rem 1.5rem; margin-top: 1.6rem;
  font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-faint);
}
.hero-meta span { display: inline-flex; align-items: flex-start; gap: 0.5rem; }

.dot {
  width: 6px; height: 6px; margin-top: 0.5em;
  border-radius: 50%; background: var(--signal); flex: none;
}


/* --- Experience list ----------------------------------------------------- */

/* --- The rail ------------------------------------------------------------
   A label column and a content column. The experience list was the only place
   on the site using it, and the only place that looked composed rather than
   stacked - everything else was one left-aligned column with the right half of
   the page empty. So it becomes the spine instead of a one-off. */

.prose-rails { display: grid; gap: clamp(2rem, 4vw, 3rem); }
.prose-rails .rail { padding-top: clamp(1.5rem, 3vw, 2rem); border-top: 1px solid var(--line); }
.rail-label { padding-top: 0.2rem; }
@media (min-width: 861px) { .rail-label { position: sticky; top: 6.5rem; } }

/* The opening paragraph of a page is the one that has to land. */
.lead-strong {
  margin-top: var(--s5);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--t-step);
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--text);
  max-width: 34ch;
}
@media (min-width: 861px) { .lead-strong { max-width: 44ch; } }

.prose-emphatic {
  font-family: var(--font-display);
  font-size: var(--t-lead);
  letter-spacing: -0.015em;
  color: var(--text);
}

.rail {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: var(--s3) clamp(2rem, 4vw, 3.5rem);
  align-items: start;
}
@media (max-width: 860px) { .rail { grid-template-columns: 1fr; gap: var(--s3); } }

.experience { display: grid; gap: var(--s7); }

.job { display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); gap: var(--s2) clamp(2rem, 4vw, 3.5rem); }
@media (max-width: 860px) { .job { grid-template-columns: 1fr; gap: var(--s3); } }

.job-period { padding-top: 0.45rem; }

.job-role { font-family: var(--font-display); font-weight: 600; font-size: var(--t-sub); letter-spacing: -0.025em; }
.job-org { color: var(--text-dim); font-size: 0.94rem; margin-top: 0.25rem; }

.job-bullets { display: grid; gap: 0.7rem; margin-top: 1rem; }
.job-bullets li { position: relative; padding-left: 1.1rem; color: var(--text-dim); font-size: 0.95rem; }
.job-bullets li::before {
  content: ''; position: absolute; left: 0; top: 0.78em;
  width: 6px; height: 1px; background: var(--signal);
}

/* --- Case study pages ---------------------------------------------------- */



.project-header { padding-block: clamp(2.5rem, 7vw, 4.5rem) 2.5rem; border-bottom: 1px solid var(--line); }

.back-link { text-decoration: none; }
.back-link:hover { color: var(--signal); }

.project-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-display);
  line-height: 0.92;
  letter-spacing: -0.045em;
  margin-block: 1.4rem 0.9rem;
  text-wrap: balance;
}

.project-tagline { font-size: var(--t-lead); color: var(--text-dim); max-width: var(--measure); }

.meta-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 1.5rem 2rem; margin-top: 2.5rem; padding-top: 2rem; border-top: 1px solid var(--line);
}
@media (min-width: 900px) { .meta-grid { grid-template-columns: 1.7fr 0.7fr 0.8fr 1.2fr; } }

.meta-label { margin-bottom: var(--s2); }
.meta-value { color: var(--text); font-size: 0.94rem; line-height: 1.5; }

.project-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--s5) var(--s7); margin-top: var(--s6); }
.project-metric-value {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.9rem, 4.5vw, 2.8rem); line-height: 0.95;
  letter-spacing: -0.045em; color: var(--signal);
}
.project-metric-label { margin-top: var(--s2); }

.project-body { display: grid; gap: clamp(2.5rem, 6vw, 3.75rem); padding-block: clamp(2.5rem, 7vw, 4rem); }

/* The case studies carry the substance of this site and were laid out as one
   grey column down the left with the other half of the page empty for the whole
   scroll. The heading moves into the rail and stays there while its own section
   is read, so the page has a spine and the prose gets the width. */
.project-section { padding-block: 0; }
.project-section > h2 {
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--t-head); letter-spacing: -0.03em;
  line-height: 1.15;
  color: var(--text);
  text-wrap: balance;
}
@media (min-width: 861px) {
  .project-section > h2 { position: sticky; top: 6.5rem; }
}

.project-section-n {
  display: block;
  margin-bottom: var(--s2);
  color: var(--signal);
}

/* The lead is the one paragraph everybody reads. It was the same colour and
   nearly the same size as the body it introduces. */
.project-body > .lead {
  font-size: var(--t-step);
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--text);
  max-width: 28ch;
}
@media (min-width: 861px) { .project-body > .lead { max-width: 38ch; } }

.decisions { display: grid; gap: 1rem; }
.decision {
  padding: 1.5rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 2px solid var(--signal);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.decision h3 { font-family: var(--font-display); font-weight: 600; font-size: 1.08rem; margin-bottom: 0.6rem; letter-spacing: -0.02em; }
.decision p { color: var(--text-dim); font-size: 0.95rem; }

.project-nav { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem; padding-block: 2.5rem; border-top: 1px solid var(--line); }

/* --- 404 and legal ------------------------------------------------------- */

.notfound { min-height: 55vh; display: grid; align-content: center; gap: 1rem; padding-block: 5rem; }
.notfound-code {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(4rem, 14vw, 9rem); line-height: 0.85;
  letter-spacing: -0.05em; color: var(--signal);
}

.legal { padding-block: clamp(3rem, 8vw, 5rem); }
.legal h1 { font-family: var(--font-display); font-weight: 700; font-size: var(--t-title); letter-spacing: -0.04em; margin-bottom: 2rem; }
.legal h2 { font-family: var(--font-display); font-weight: 600; font-size: 1.15rem; letter-spacing: -0.02em; margin-block: 2.25rem 0.75rem; }
.legal p, .legal li { color: var(--text-dim); max-width: var(--measure); }
.legal ul { display: grid; gap: 0.5rem; padding-left: 1.2rem; list-style: disc; }

/* --- Print --------------------------------------------------------------- */

@media print {
  .nav, .footer, .button-row, .signal-line { display: none; }
  body { background: #fff; color: #000; }
  a { color: #000; }
}

/* --- The call film ------------------------------------------------------
   A pinned diagram with the five stages scrolling past it. Everything below is
   legible with every animation removed: the diagram is sticky (a layout
   feature, not an animated one) and the stages are an ordinary list.

   The scroll timeline only lights up the matching node, and it is applied to a
   property nothing depends on for reading. */

.film { padding-block: var(--section) 0; }

.film-intro { margin-bottom: clamp(2rem, 5vw, 3.5rem); }

.film-track { display: grid; gap: clamp(2rem, 5vw, 4rem); }

/* Same two columns as everything else on the site. It used to be its own
   width, which is one more reason the page read as separate pieces. */
@media (max-width: 860px) { .film-outro { grid-column: 1; } }

@media (min-width: 861px) {
  .film-track { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); align-items: start; }
  .film-visual { position: sticky; top: 6.5rem; }
}

/* The pipeline: five nodes down a line, with a bracket marking the GPU. */

.pipe { display: grid; gap: 1.15rem; position: relative; padding-left: 1.35rem; }

.pipe-n { color: var(--signal); }

.pipe::before {
  content: '';
  position: absolute;
  left: 4px; top: 6px; bottom: 6px;
  width: 1px;
  background: linear-gradient(180deg, var(--line-hi), var(--signal) 45%, var(--line-hi));
}

.pipe-node { position: relative; display: flex; align-items: baseline; gap: var(--s2); font-family: var(--font-mono); font-size: var(--t-label); letter-spacing: 0.14em; text-transform: uppercase; }

.pipe-dot {
  position: absolute; left: -1.35rem;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--bg); border: 1px solid var(--line-hi);
}

.pipe-label { color: var(--text-faint); }

.pipe-boundary {
  margin-top: var(--s5);
  padding: var(--s4);
  border: 1px dashed var(--line-hi);
  border-radius: var(--radius);
  color: var(--signal);
  line-height: 1.7;
}

/* The stages. Tall enough that one occupies the screen beside the pinned
   diagram, but in normal flow - nothing is hidden and nothing overlaps. */

.film-steps { display: grid; gap: clamp(2.5rem, 8vh, 5.5rem); list-style: none; margin: 0; padding: 0; }

.film-step { max-width: 54ch; }

.film-n { font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.2em; color: var(--signal); }

.film-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-step);
  line-height: 1.05;
  letter-spacing: -0.035em;
  margin-top: 0.6rem;
  text-wrap: balance;
}

.film-body { margin-top: 0.9rem; color: var(--text-dim); }

/* It closes the list of stages, so it belongs in the column the stages are in.
   As a .container with its own max-width it was centring itself instead. */
.film-outro {
  grid-column: 2;
  margin-top: clamp(2.5rem, 6vw, 4rem);
  font-family: var(--font-display);
  font-size: var(--t-lead);
  letter-spacing: -0.02em;
  color: var(--text-dim);
  max-width: 46ch;
}

/* Enhancement only: each stage brightens as it passes the middle of the screen.
   If the timeline never runs, every stage simply stays at full strength. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .film-step {
      animation: film-focus linear both;
      animation-timeline: view();
      animation-range: cover 15% cover 85%;
    }
  }
}

@keyframes film-focus {
  0%   { opacity: 0.35; }
  35%  { opacity: 1; }
  65%  { opacity: 1; }
  100% { opacity: 0.35; }
}

/* --- Layout utilities ----------------------------------------------------
   These exist because the Content-Security-Policy sets style-src-attr 'none':
   a style attribute in the markup is dropped in production while still working
   in dev, so the deployed page would quietly differ from the reviewed one.
   tests/build.test.mjs fails if a style attribute reappears. */

.hero-compact { padding-block: clamp(2.5rem, 8vw, 4.5rem) 1rem; }

.page-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--t-display);
  line-height: 0.92;
  letter-spacing: -0.045em;
  margin-block: 1.1rem 0;
  text-wrap: balance;
}

/* What used to live here: eight single-use spacing classes, one per place the
   rhythm had gone wrong - section-tight, section-flush, section-title-spaced,
   grid-spaced, lead-spaced, prose-spaced, section-head-spaced. Each was a
   correct local fix and together they were the reason the pages read as
   separately assembled. The rhythm rule at the top of Sections replaces them. */
.panel-lead { margin-top: var(--s4); }
.prose-block { margin-top: var(--s7); }

/* --- Footer -------------------------------------------------------------- */

.footer-note { line-height: 1.7; }



/* A second head inside one section - "Other work", under the featured project
   and its walkthrough. It needs air above it, but not a section's worth: that
   plus the film's own bottom padding was where the page went quiet. */
.section-head-inner { margin-top: var(--section); }
