/* ============================================================================
   Tamryn Osler — ePortfolio
   ----------------------------------------------------------------------------
   DIRECTION: a warm paper page under a dark green header.

   The palette is the client's, supplied as five families. Roles were assigned
   from measured contrast, not by eye:

     HONEY   — the identity. Links, filled controls, the thesis line, rules,
               markers, the point being made.
     CORAL   — the second accent. Used sparingly: one pill variant, the current
               role, the marked peak in the header figure. Never structural.
     BLUE    — informational only. Eyebrows, evidence labels, footer headings,
               info badges.
     GREEN   — feature accent, and the header's ground. green-800 (#26361A) is
               a SURFACE, not an accent; green-600 is the detail mark.
     NEUTRALS— canvas, linen, border, ink-2, ink.

   Note on the surfaces: an earlier build of this site was rejected for having
   a warm off-white background. This palette is warm on purpose and by request,
   which is a different thing — but it is why the accents are kept disciplined
   and the register stays flat. Warm surface plus loose accents is the failure
   mode; warm surface plus strict roles is a house style.

   Register: flat and hairline-driven. A white panel on this canvas is only
   1.06:1, so separation is a 1px hairline plus a very light shadow — never a
   heavy drop shadow and never a glow. The one exception is the header, which
   is a photograph, and where depth is the entire point.

   Type: Instrument Serif (display, 400 only — hierarchy comes from size and
   italic, not weight) · Instrument Sans (body) · Geist Mono (data, metadata).

   ---------------------------------------------------------------------------
   CONTRAST — every pair computed, not estimated.
   Canvas #FCF8F1, linen #F5EDE0, panel #FFFFFF, forest #26361A.

                              canvas  linen  panel  forest
     ink        #221F1A        15.51  14.13  16.42     -     body text
     text-2     #55524A         7.37   6.71   7.80     -     secondary
     text-3     #6A665C         5.41   4.93    -       -     metadata
     honey-deep #7A4A16         7.04   6.41   7.45     -     PRIMARY text/fill
     honey      #AC6F2A         3.92   3.57   4.15     -     rules, borders
     honey-soft #D69A52         2.31   2.10   2.44   5.29    fills only
     coral-deep #8A3B27         7.25   6.61   7.68     -     accent text
     coral      #C25B41         4.08   3.71   4.32     -     accent marks
     blue-deep  #2E6D93         5.33   4.85   5.64     -     informational
     blue       #6BA3C4         2.59   2.36   2.74   4.71    rules only
     green-deep #4F7130         5.31   4.84   5.62     -     detail text/marks
     green      #8CAE55         2.39   2.18   2.53   5.11    fills only
     line       #E7DBC8         1.29   1.18   1.37     -     decorative hairline

   On the forest surfaces (the header, the CTA band):
     on-forest       #FCF8F1   12.21   primary text
     on-forest-2     #CFC7B6    7.69   secondary text
     on-forest-honey #F0CE9C    8.62   the accent, and the display line
     on-forest-info  #B8D4E6    8.37   informational
     on-forest-mark  #E89178    5.39   the one saturated mark

   HARD RULES:
     - Filled controls are --honey-deep with WHITE: 7.45:1.
       NEVER white on --honey (#AC6F2A) - that is 4.15 and misses AA.
       A brighter honey fill takes --ink instead (6.73 on honey-soft).
     - --honey-soft, --blue and --green are NON-TEXT on the light surfaces.
       Honey text is --honey-deep; blue text is --blue-deep; green text is
       --green-deep.
     - On forest, every light token above passes. --honey-deep (1.73),
       --coral-deep (1.68) and --ink (1.27) all FAIL there, so the dark accents
       never appear on the dark surface.
     - Meaningful borders use --text-2 (7.37); --line is decorative.
     - The lightbox backdrop is #1A1712 and its control borders are #787264
       (3.74) - a control boundary needs 3:1.
   ========================================================================= */

/* ---------- 1. Tokens --------------------------------------------------- */
:root {
  /* Surfaces - the client's neutrals. Warm, and deliberately so. */
  --paper:   #FCF8F1;   /* canvas - page */
  --band:    #F5EDE0;   /* linen  - alternating section */
  --panel:   #FFFFFF;   /* raised cards */
  --line:    #E7DBC8;   /* border - decorative hairline */

  /* Ink */
  --ink:       #221F1A;
  --text-2:    #55524A;
  --text-3:    #6A665C;

  /* HONEY - the primary. Two working values: the deep one carries text and
     fills, the mid one carries rules and control borders (3.92 clears the 3:1
     a boundary needs; it would not clear 4.5 as text, so it never is). */
  --honey-deep: #7A4A16;   /* honey-800 - links, filled controls, honey text */
  --honey:      #AC6F2A;   /* honey-600 - rules, markers, borders */
  --honey-soft: #D69A52;   /* honey-400 - decorative fills only */
  --honey-pale: #FDF3E7;   /* honey-50  - tint blocks */

  /* CORAL - the second accent, on a short leash. Three places on the whole
     site: the current role, one pill variant, and the marked peak. */
  --coral-deep: #86342F;
  --coral:      #BF5551;
  --coral-soft: #E88B84;
  --coral-pale: #FDEFEF;

  /* BLUE - informational only. Eyebrows, evidence labels, footer headings. */
  --blue-ink:  #1A4560;
  --blue-deep: #2E6D93;
  --blue:      #6BA3C4;   /* rules - non-text */
  --blue-pale: #EDF4F9;

  /* GREEN - feature accent, and the dark surfaces. --forest is a SURFACE: the
     header and the CTA band stand on it. It is never used as an accent. */
  --green-deep:  #3D6B52;
  --green:       #8CAE55;   /* fills - non-text */
  --green-pale:  #E9F2EC;
  --forest:      #1E3529;   /* green-800 */
  --forest-lift: #2A483D;   /* one step up, for gradients on the header */

  /* Text and marks that sit ON the forest surfaces. Every one measured above;
     the dark accents are absent from this list because they fail there. */
  --on-forest:       #FCF8F1;
  --on-forest-2:     #CFC7B6;
  --on-forest-honey: #F0CE9C;   /* honey-200 */
  --on-forest-info:  #B8D4E6;   /* blue-200 */
  --on-forest-mark:  #E89178;   /* coral-400 - the one saturated mark */
  --ink-muted:       #CFC7B6;   /* kept: older rules still name it */

  /* Sequential ramp in the honey family, light to dark. Monotonic in lightness
     (verified). For charts and heatmaps; nothing consumes it yet. */
  --r1:  #FCF8F1; --r2:  #FBF1E2; --r3:  #F7E6CE; --r4:  #F2D8B6;
  --r5:  #E9C596; --r6:  #DFAF74; --r7:  #D19855; --r8:  #BC8039;
  --r9:  #A76B2B; --r10: #8E5722; --r11: #7A4A16; --r12: #5C3610;

  /* Semantic roles */
  --bg:            var(--paper);
  --bg-alt:        var(--band);
  --surface:       var(--panel);
  --text:          var(--ink);
  --border:        var(--line);        /* decorative */
  --border-strong: var(--text-2);      /* meaningful - 7.37:1 */
  --link:          var(--honey-deep);
  --ring:          var(--ink);         /* 15.51:1 */

  /* Type */
  --font-display: 'Instrument Serif', 'Times New Roman', serif;
  --font-body:    'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono:    'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Display face is 400-only, so the scale runs large to compensate for the
     absence of a bold weight. */
  --step--2: clamp(0.72rem, 0.70rem + 0.10vw, 0.78rem);
  --step--1: clamp(0.86rem, 0.83rem + 0.15vw, 0.94rem);
  --step-0:  clamp(1.02rem, 0.99rem + 0.17vw, 1.11rem);
  --step-1:  clamp(1.22rem, 1.15rem + 0.32vw, 1.42rem);
  --step-2:  clamp(1.46rem, 1.34rem + 0.58vw, 1.84rem);
  --step-3:  clamp(1.75rem, 1.54rem + 1.03vw, 2.38rem);
  --step-4:  clamp(2.10rem, 1.75rem + 1.72vw, 3.08rem);
  --step-5:  clamp(2.52rem, 1.97rem + 2.75vw, 3.98rem);
  --step-6:  clamp(3.02rem, 2.18rem + 4.20vw, 5.15rem);

  /* Spacing — 4px rhythm */
  --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: 4rem;     --space-9: 6rem;

  /* Section padding. Was clamp(3.5rem, 2.2rem + 5vw, 6.5rem), which put 13rem
     of empty page between two sections on a desktop — enough that readers were
     stopping partway down, believing they had reached the end. Cut by roughly a
     third. The band alternation, not the gap, is what separates sections. */
  --section-y: clamp(2.5rem, 1.85rem + 2.6vw, 4.25rem);
  --shell-max: 1180px;
  --gutter:    clamp(1.25rem, 0.8rem + 2.2vw, 2.5rem);

  --r-sm: 2px;  --r-md: 3px;  --r-lg: 4px;

  /* Elevation. Very light: a white panel is only 1.06:1 against the paper,
     so the hairline does most of the work. */
  --shadow-1: 0 1px 2px rgba(33, 29, 34, 0.04);
  --shadow-2: 0 4px 14px rgba(33, 29, 34, 0.07);
  --shadow-3: 0 18px 50px rgba(33, 29, 34, 0.13);

  /* Motion */
  --dur-fast:  120ms;
  --dur-mid:   240ms;
  --dur-slow:  460ms;
  --ease-out:  cubic-bezier(0.22, 0.85, 0.3, 1);

  --z-base: 0; --z-raised: 10; --z-sticky: 40; --z-overlay: 100; --z-modal: 1000;
}

/* ---------- 2. Reset ---------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
  background: var(--bg);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Display face: 400 only. Size and italic carry the hierarchy. */
h1, h2 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.04;
  letter-spacing: -0.015em;
  margin: 0;
  text-wrap: balance;
  color: var(--text);
}
h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 0;
  text-wrap: balance;
  color: var(--text);
}

h1 { font-size: var(--step-6); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

/* Instrument Serif's italic is the emphasis device, in place of a bold weight. */
em, .em { font-style: italic; color: var(--text); }

p { margin: 0; text-wrap: pretty; }
p + p { margin-top: var(--space-4); }

/* Dates, counts and figures are compared vertically all over this site
   (timeline years, the spec list, stat cells), so they are tabular
   everywhere the mono face is used as data rather than as a label. */
.meta, .tl-item__when, .spec dd, .edu-card__when, .stat__num {
  font-variant-numeric: tabular-nums;
}

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

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

a {
  color: var(--link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  text-decoration-color: color-mix(in srgb, var(--link) 40%, transparent);
  transition: color var(--dur-fast) var(--ease-out),
              text-decoration-color var(--dur-fast) var(--ease-out);
}
a:hover { color: var(--ink); text-decoration-color: currentColor; }

button { font: inherit; color: inherit; cursor: pointer; }

:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--honey-pale); color: var(--ink); }

/* ---------- 3. Layout primitives --------------------------------------- */

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

.section { padding-block: var(--section-y); }
.section--tight { padding-block: calc(var(--section-y) * 0.6); }
/* The contact page is one short section that must fit above the fold. Less air
   at the top, and none wasted at the bottom where the footer follows. */
.section--contact { padding-block: calc(var(--section-y) * 0.62) var(--section-y); }

.band {
  background: var(--bg-alt);
  border-block: 1px solid var(--line);
}

.split {
  display: grid;
  gap: var(--space-5) var(--space-8);
  grid-template-columns: 1fr;
}
@media (min-width: 860px) {
  .split { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); }
}

.stack > * + * { margin-top: var(--space-4); }
.stack-lg > * + * { margin-top: var(--space-6); }

.prose { max-width: 64ch; }
.prose p { color: var(--text-2); }

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: var(--z-modal);
  padding: var(--space-3) var(--space-4);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  border-radius: var(--r-sm);
  transform: translateY(-180%);
  transition: transform var(--dur-mid) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); color: var(--paper); }

/* ---------- 4. Type helpers -------------------------------------------- */

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* Blue: an eyebrow is a factual label, which is blue's role. 6.95:1 on
     canvas, 4.85 on linen. The rule beside it stays honey. */
  color: var(--blue-deep);
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}
/* Honey rule — non-text use, so the mid honey is correct here. */
.eyebrow::before {
  content: '';
  width: 1.5rem;
  height: 2px;
  background: var(--honey);
  flex: none;
  margin-top: 0.6em;
}
.eyebrow--plain::before { display: none; }

.lede {
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-2);
  max-width: 56ch;
}

.meta {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  color: var(--text-3);
}

.section-head { margin-bottom: var(--space-6); }
.section-head .eyebrow { margin-bottom: var(--space-4); }

/* Emphasis in prose: a honey rule under the phrase. A filled highlight on a
   light page needs a tint pale enough to keep ink legible; the rule is quieter
   and does the same job. */
.mark {
  color: var(--ink);
  border-bottom: 2px solid var(--honey);
  padding-bottom: 0.06em;
}

/* ---------- 5. Buttons -------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 48px;
  padding: var(--space-3) var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  line-height: 1.2;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out);
}

/* Filled: honey-deep + white = 7.45:1.
   Never the mid --honey with white — that is 4.15 and misses AA. A brighter
   honey fill takes --ink instead (6.73 on honey-soft). */
.btn--primary {
  background: var(--honey-deep);
  color: #FFFFFF;
  border-color: var(--honey-deep);
  box-shadow: var(--shadow-1);
}
.btn--primary:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: #FFFFFF;
  box-shadow: var(--shadow-2);
}

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--border-strong);   /* 7.49:1 */
}
.btn--ghost:hover {
  background: var(--panel);
  border-color: var(--ink);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}

.btn--dark {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
.btn--dark:hover { background: #000; color: var(--paper); }

.btn--sm { min-height: 40px; padding: var(--space-2) var(--space-4); }

.btn__icon { width: 1em; height: 1em; flex: none; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--honey-deep);
  min-height: 44px;
}
.link-arrow::after {
  content: '';
  width: 1.1em; height: 1.1em;
  background: currentColor;
  -webkit-mask: var(--arrow-mask) center / contain no-repeat;
  mask: var(--arrow-mask) center / contain no-repeat;
  transition: transform var(--dur-mid) var(--ease-out);
}
.link-arrow:hover { color: var(--ink); }
.link-arrow:hover::after { transform: translateX(4px); }

:root {
  --arrow-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E");
}

/* ---------- 6. Header / nav -------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-mid) var(--ease-out);
}
.site-header[data-scrolled='true'] { border-bottom-color: var(--line); }

/* Read-position rail. It sits ON the header's bottom hairline, so it reads as
   that rule filling in rather than as a separate bar. main.js drives the width;
   with no JS it stays at 0 and never appears. Honey is correct here — this is
   a non-text mark, and progress through the page is the site's own voice. */
.site-header::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: var(--progress, 0%);
  max-width: 100%;
  background: var(--honey);
  pointer-events: none;
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 4.5rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--step-1);
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  color: var(--ink);
}
.brand:hover { color: var(--ink); }
.brand__mark {
  display: grid;
  place-items: center;
  width: 2rem; height: 2rem;
  background: transparent;
  border: 1px solid var(--honey);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--honey-deep);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.brand:hover .brand__mark { background: var(--honey-pale); }

.nav { display: flex; align-items: center; gap: var(--space-2); }
.nav__actions { display: flex; align-items: center; gap: var(--space-3); }
.nav__list { display: flex; align-items: center; gap: 0; }

.nav__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-2) 0.6rem;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-2);
  text-decoration: none;
  position: relative;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav__link:hover { color: var(--ink); }

/* Active: colour AND a honey rule, so it never depends on colour alone. */
.nav__link[aria-current] { color: var(--ink); }
.nav__link[aria-current]::after {
  content: '';
  position: absolute;
  left: 0.6rem; right: 0.6rem;
  bottom: 0.6rem;
  height: 2px;
  background: var(--honey);
}

.nav__cta { margin-left: 0; }

/* The current page, on a control that has no underline to carry the state.
   A tint plus a stronger border, so it does not rely on colour alone. */
.nav__cta[aria-current] {
  background: var(--honey-pale);
  border-color: var(--honey);
  color: var(--honey-deep);
}

/* The CV is a FILE, not a page, so it is deliberately not a button. Mono,
   an icon that points down into a tray, and a rule that appears on hover —
   the vocabulary of a download rather than of a destination. */
.nav__download {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-2);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.nav__download svg { width: 0.95rem; height: 0.95rem; flex: none; }
.nav__download:hover { color: var(--honey-deep); border-bottom-color: var(--honey); }

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 48px; height: 48px;
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  color: var(--ink);
  touch-action: manipulation;
}
.nav-toggle:hover { background: var(--panel); }
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block;
  width: 17px; height: 1.5px;
  background: currentColor;
  transition: transform var(--dur-mid) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.nav-toggle__bars { position: relative; }
.nav-toggle__bars::before,
.nav-toggle__bars::after { content: ''; position: absolute; left: 0; }
.nav-toggle__bars::before { transform: translateY(-5px); }
.nav-toggle__bars::after  { transform: translateY(5px); }
.nav-toggle[aria-expanded='true'] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded='true'] .nav-toggle__bars::before { transform: rotate(45deg); }
.nav-toggle[aria-expanded='true'] .nav-toggle__bars::after  { transform: rotate(-45deg); }

/* Six destinations plus a pill CTA stop fitting on one line below 1024px:
   at 900 the brand and the button both wrapped to two lines. The nav collapses
   to the panel earlier than the hero does, which is fine — they are answering
   different questions about the same width. */
@media (max-width: 1023px) {
  .nav-toggle { display: inline-flex; }
  .nav {
    position: fixed;
    inset: 4.5rem 0 auto 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--space-4) var(--gutter) var(--space-7);
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    max-height: calc(100dvh - 4.5rem);
    overflow-y: auto;
    transform: translateY(-8px);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-mid) var(--ease-out),
                transform var(--dur-mid) var(--ease-out),
                visibility var(--dur-mid) linear;
  }
  .nav[data-open='true'] { transform: translateY(0); opacity: 1; visibility: visible; }
  .nav__list { flex-direction: column; align-items: stretch; }
  .nav__link {
    /* flex, not inline-flex: the row must fill the panel so the whole width is
       tappable and the divider runs edge to edge. */
    display: flex;
    min-height: 52px;
    padding-inline: 0;
    font-size: var(--step--1);
    border-bottom: 1px solid var(--line);
  }
  .nav__link[aria-current]::after { left: 0; right: auto; width: 1.5rem; bottom: 0.9rem; }
  /* In the panel the actions become full-width rows, like the tabs above. */
  .nav__actions {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
    margin-top: var(--space-5);
  }
  .nav__cta { margin: 0; }
  .nav__download {
    justify-content: center;
    min-height: 44px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--r-sm);
  }
  .nav__download:hover { border-style: dashed; border-color: var(--honey); }
}

/* --- Header over the dark hero -------------------------------------------
   The home page opens on green-800, so the sticky header has to invert while
   it is over it and change back once it is not. `.has-dark-hero` on <body>
   scopes this to that page; the other two never see it.

   The DARK treatment is the default and the light one is the exception,
   because the page loads at the top: with JavaScript off, the header stays
   dark, which is the state that is actually correct there. main.js sets
   data-on-dark="false" once the hero has scrolled past.

   The switch is deliberately NOT tied to data-scrolled. That flips after 8px,
   which would turn the header light while it is still sitting on the
   photograph.
   ------------------------------------------------------------------------ */

.has-dark-hero .site-header {
  /* Fixed rather than sticky ONLY here. Sticky keeps its place in the flow, so
     the header sat as a light strip above the photograph instead of on it.
     Taking it out of the flow lets the hero start at the top of the page; the
     hero's own top padding is what keeps the copy clear of it. */
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  background: transparent;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border-bottom-color: transparent;
}
.has-dark-hero .site-header .brand,
.has-dark-hero .site-header .brand:hover { color: var(--on-forest); }
.has-dark-hero .site-header .brand__mark {
  border-color: color-mix(in srgb, var(--on-forest-honey) 70%, transparent);
  color: var(--on-forest-honey);
}
.has-dark-hero .site-header .brand:hover .brand__mark {
  background: color-mix(in srgb, var(--on-forest) 12%, transparent);
}
.has-dark-hero .site-header .nav__link { color: var(--on-forest-2); }
.has-dark-hero .site-header .nav__link:hover,
.has-dark-hero .site-header .nav__link[aria-current] { color: var(--on-forest); }
.has-dark-hero .site-header .nav__link[aria-current]::after {
  background: var(--on-forest-honey);
}
.has-dark-hero .site-header .nav__cta {
  color: var(--on-forest);
  border-color: color-mix(in srgb, var(--on-forest) 52%, transparent);
}
.has-dark-hero .site-header .nav__cta[aria-current] {
  background: color-mix(in srgb, var(--on-forest-honey) 22%, transparent);
  border-color: var(--on-forest-honey);
  color: var(--on-forest);
}
.has-dark-hero .site-header .nav__download { color: var(--on-forest-2); }
.has-dark-hero .site-header .nav__download:hover {
  color: var(--on-forest);
  border-bottom-color: var(--on-forest-honey);
}
.has-dark-hero .site-header .nav__cta:hover {
  background: color-mix(in srgb, var(--on-forest) 14%, transparent);
  border-color: var(--on-forest);
  color: var(--on-forest);
}
.has-dark-hero .site-header .nav-toggle {
  color: var(--on-forest);
  border-color: color-mix(in srgb, var(--on-forest) 52%, transparent);
}
.has-dark-hero .site-header .nav-toggle:hover {
  background: color-mix(in srgb, var(--on-forest) 12%, transparent);
}
.has-dark-hero .site-header :focus-visible { outline-color: var(--on-forest); }

/* Past the hero: the ordinary light header, restored in one block. */
.has-dark-hero .site-header[data-on-dark='false'] {
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
}
.has-dark-hero .site-header[data-on-dark='false'][data-scrolled='true'] {
  border-bottom-color: var(--line);
}
.has-dark-hero .site-header[data-on-dark='false'] .brand,
.has-dark-hero .site-header[data-on-dark='false'] .brand:hover { color: var(--ink); }
.has-dark-hero .site-header[data-on-dark='false'] .brand__mark {
  border-color: var(--honey);
  color: var(--honey-deep);
}
.has-dark-hero .site-header[data-on-dark='false'] .brand:hover .brand__mark {
  background: var(--honey-pale);
}
.has-dark-hero .site-header[data-on-dark='false'] .nav__link { color: var(--text-2); }
.has-dark-hero .site-header[data-on-dark='false'] .nav__link:hover,
.has-dark-hero .site-header[data-on-dark='false'] .nav__link[aria-current] { color: var(--ink); }
.has-dark-hero .site-header[data-on-dark='false'] .nav__link[aria-current]::after {
  background: var(--honey);
}
.has-dark-hero .site-header[data-on-dark='false'] .nav__cta,
.has-dark-hero .site-header[data-on-dark='false'] .nav-toggle {
  color: var(--ink);
  border-color: var(--border-strong);
}
.has-dark-hero .site-header[data-on-dark='false'] .nav__cta[aria-current] {
  background: var(--honey-pale);
  border-color: var(--honey);
  color: var(--honey-deep);
}
.has-dark-hero .site-header[data-on-dark='false'] .nav__download { color: var(--text-2); }
.has-dark-hero .site-header[data-on-dark='false'] .nav__download:hover {
  color: var(--honey-deep);
  border-bottom-color: var(--honey);
}
.has-dark-hero .site-header[data-on-dark='false'] .nav__cta:hover,
.has-dark-hero .site-header[data-on-dark='false'] .nav-toggle:hover {
  background: var(--panel);
  border-color: var(--ink);
  color: var(--ink);
}
.has-dark-hero .site-header[data-on-dark='false'] :focus-visible { outline-color: var(--ring); }

@media (max-width: 1023px) {
  /* The open nav panel is its own light surface, wherever the header is, so
     its contents go back to the light palette regardless. */
  .has-dark-hero .site-header .nav .nav__link { color: var(--text-2); }
  .has-dark-hero .site-header .nav .nav__link:hover,
  .has-dark-hero .site-header .nav .nav__link[aria-current] { color: var(--ink); }
  .has-dark-hero .site-header .nav .nav__link[aria-current]::after { background: var(--honey); }
  .has-dark-hero .site-header .nav .nav__cta { color: var(--ink); border-color: var(--border-strong); }
  .has-dark-hero .site-header .nav .nav__cta:hover {
    background: var(--panel); border-color: var(--ink); color: var(--ink);
  }
  .has-dark-hero .site-header .nav :focus-visible { outline-color: var(--ring); }
}

/* ---------- 7. Hero (the dark header) ----------------------------------
   The page opens on green-800 with a photograph dissolved into it. The
   dissolve is composited into the JPEG by tools/build-hero.py, not laid over
   it here — see that file for why. All this stylesheet has to do is stand the
   copy on the ground colour and get out of the way.

   Contrast on forest, all measured (see the table at the top of this file):
     on-forest       #FCF8F1  12.21   the name, and body-weight copy
     on-forest-2     #CFC7B6   7.69   the lede, metadata
     on-forest-honey #F0CE9C   8.62   the thesis line, the filled button
     on-forest-info  #B8D4E6   8.37   the eyebrow
     green           #8CAE55   5.11   the availability marker
   The dark accents — honey-deep, coral-deep, ink — all fail here and never
   appear on this surface.
   ---------------------------------------------------------------------- */

.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--forest);
  color: var(--on-forest);
  display: grid;
  align-items: center;
  /* The sticky header floats over this, so the top padding has to clear it. */
  min-height: min(94vh, 880px);
  min-height: min(94svh, 880px);
  padding-block: clamp(7rem, 5rem + 8vw, 10rem) clamp(3.5rem, 2.5rem + 5vw, 6rem);
}

.hero__bg { position: absolute; inset: 0; z-index: 0; }
.hero__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* She is in the right-hand third of the file, so `cover` is told to trim
     from the RIGHT (a low percentage keeps the left edge). That keeps the whole
     dissolve on screen and pushes her further out to the side. */
  object-position: 45% 34%;
}

/* Fades the scene into solid ground at the top and bottom edges: the sticky
   header gets a settled surface, and nothing is sliced off against the rail
   below. Sits above the field so it hems that too. */
.hero__hem {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(to bottom, var(--forest) 0%,
      color-mix(in srgb, var(--forest) 55%, transparent) 6%, transparent 15%),
    linear-gradient(to top, var(--forest) 0%,
      color-mix(in srgb, var(--forest) 55%, transparent) 5%, transparent 14%);
}

.hero__grid { position: relative; z-index: 3; }
.hero__copy { max-width: min(100%, 34rem); }

.hero__eyebrow { margin-bottom: var(--space-6); }
.hero__title { margin-bottom: var(--space-5); color: var(--on-forest); }

/* The thesis line. Serif italic in the accent — the emphasis device in a
   400-only face, and the primary colour's most prominent appearance. */
.hero__role {
  font-family: var(--font-display);
  font-style: italic;
  font-size: var(--step-3);
  line-height: 1.15;
  color: var(--on-forest-honey);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid color-mix(in srgb, var(--on-forest) 24%, transparent);
}

.hero .eyebrow { color: var(--on-forest-info); }
.hero .eyebrow::before { background: var(--on-forest-honey); }
.hero .lede { color: var(--on-forest-2); }
.hero .mark { color: var(--on-forest); border-bottom-color: var(--on-forest-honey); }

.hero__ctas { margin-top: var(--space-6); }

/* Filled control on the dark surface: honey-200 ground with forest type is
   8.62:1. The light-surface pairing (honey-deep + white) would be 1.73 here. */
.hero .btn--primary {
  background: var(--on-forest-honey);
  color: var(--forest);
  border-color: var(--on-forest-honey);
  box-shadow: none;
}
.hero .btn--primary:hover {
  background: var(--on-forest);
  border-color: var(--on-forest);
  color: var(--forest);
}
.hero .btn--ghost {
  color: var(--on-forest);
  background: color-mix(in srgb, var(--forest) 45%, transparent);
  border-color: color-mix(in srgb, var(--on-forest) 52%, transparent);
}
.hero .btn--ghost:hover {
  background: color-mix(in srgb, var(--on-forest) 14%, transparent);
  border-color: var(--on-forest);
  color: var(--on-forest);
}
/* --ring is ink, which is 1.27 here and would be an invisible focus ring. */
.hero :focus-visible { outline-color: var(--on-forest); }

.hero__status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-6);
  margin-top: var(--space-7);
}
.status-dot {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  color: var(--text-2);
}
/* Green as detail: an availability marker. The words carry the meaning, and
   the dot takes --green-deep — --green is 2.39 on canvas and would vanish. */
.status-dot::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--green-deep);
  flex: none;
  transform: translateY(-0.15em);
}
/* On forest the reverse holds: green-deep is 2.30 and green-400 is 5.11. */
.hero .status-dot { color: var(--on-forest-2); }
.hero .status-dot::before { background: var(--green); }

@media (max-width: 899px) {
  /* Stacked, there is no room beside her, so the layout changes rather than
     just narrowing: the copy drops to the BOTTOM of the header and the
     photograph keeps the top. Centring it instead put the first paragraph
     across her face, which is the one thing a portrait cannot survive. */
  /* On a portrait viewport `cover` scales the master to the header's HEIGHT,
     so her chin always lands at ~30% of it however tall the header is. The
     copy is bottom-aligned, so it starts at (height - its own height). Keeping
     those apart is arithmetic, not taste: the copy has to stay under ~70% of
     the header, which is why it is tightened below rather than the header
     being made taller. A header over 100svh hides its own call to action. */
  .hero {
    align-items: end;
    min-height: min(94vh, 800px);
    min-height: min(94svh, 800px);
    padding-block: clamp(4.5rem, 4rem + 5vw, 6rem) clamp(2.25rem, 1.75rem + 3vw, 3.5rem);
  }
  /* Portrait viewport: `cover` scales to the width and there is over a
     thousand pixels of horizontal overflow, so a low percentage here would
     crop her out of the frame entirely. 84% keeps her roughly centred in the
     band above the copy. */
  .hero__photo { object-position: 84% 8%; }
  /* The bottom ramp is doing real work now: it is the surface the copy stands
     on. It reaches solid ground well before the text starts. */
  .hero__hem {
    background:
      linear-gradient(to bottom, var(--forest) 0%,
        color-mix(in srgb, var(--forest) 58%, transparent) 7%, transparent 18%),
      linear-gradient(to top, var(--forest) 0%, var(--forest) 30%,
        color-mix(in srgb, var(--forest) 82%, transparent) 46%,
        color-mix(in srgb, var(--forest) 34%, transparent) 68%, transparent 88%);
  }
  /* The copy is tightened rather than the header being made taller. A header
     over 100svh hides its own call to action, which is worse than smaller
     type: everything here still clears the 16px floor comfortably. */
  .hero__title { font-size: var(--step-5); margin-bottom: var(--space-4); }
  .hero__eyebrow { margin-bottom: var(--space-4); }
  .hero__role {
    font-size: var(--step-2);
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-4);
  }
  .hero .lede { font-size: var(--step--1); }
  .hero__ctas { margin-top: var(--space-5); }
  .hero__status { margin-top: var(--space-4); gap: var(--space-2) var(--space-5); }
}

/* ---------- 7b. Credential rail ----------------------------------------
   Three internships is the strongest single fact on this page, and it was
   previously buried two thousand pixels down in the timeline. The rail states
   it immediately, in the form a recruiter scans for.

   It stays on --paper: it reads as a rule-delimited rail closing the hero
   rather than as another banded section. Hairlines above and below separate
   it, which is the whole treatment.
   ---------------------------------------------------------------------- */

.cred {
  border-block: 1px solid var(--line);
  background: var(--paper);
}
.cred__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-7);
  padding-block: var(--space-5);
}
.cred__label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--blue-deep);      /* a factual label — blue's role */
  flex: none;
}
.cred__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-7);
}
.cred__item { display: inline-flex; align-items: center; gap: var(--space-3); }

/* Square tiles, matching the timeline: the marks are supplied on their own
   backgrounds and a tile is the only treatment that keeps all three correct. */
.cred__tile {
  display: grid;
  place-items: center;
  width: 2.25rem; height: 2.25rem;
  flex: none;
  padding: 0.3rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.cred__tile--light { background: var(--panel); border-color: var(--line); padding: 0.25rem; }
/* WER1's mark is white on an opaque near-black plate, so it needs an ink tile
   or it prints a dark square onto the paper. */
.cred__tile--dark { background: var(--ink); border-color: var(--ink); padding: 0; }
.cred__tile img { width: 100%; height: 100%; object-fit: contain; }

.cred__name { display: block; font-size: var(--step--1); line-height: 1.25; color: var(--ink); }
.cred__what {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-top: 0.15em;
}

@media (max-width: 599px) {
  /* Below this the row wraps to one company per line and the label reads as a
     heading for the list, which is what it is. */
  .cred__inner { gap: var(--space-4); }
  .cred__list  { gap: var(--space-4); flex-direction: column; align-items: flex-start; }
}

/* ---------- 8. The field (the signature) --------------------------------
   The header's depth, and the reason it reads as a space rather than a picture
   with things on it. A real CSS 3D scene: `perspective` on the frame,
   `preserve-3d` on the stage, every object at its own Z. Only depth is
   animated — the drift across, the rise and the shrinking all fall out of the
   projection, which is why the motion reads as travel through a space instead
   of things being slid around a picture.

   SIX KINDS OF FIGURE, so the vocabulary is wide enough not to repeat itself
   across a 96-second cycle, and every one is a real reading rather than an
   impression of one: spectrum traces, FFT butterflies, pole–zero plots,
   clocked digital lines, QAM constellations, resident cache lines. All the
   geometry is generated from the maths — see tools/ — never eyeballed. That is
   the difference between a diagram and a drawing of one, and it is visible.

   HOW IT STAYS QUIET:
     · Hairline stroke only. The light on dark does the work; there is no glow.
     · Weighted away from the copy, but never to zero — it is a header-wide
       field, not a panel behind the photograph. At 15% a hairline this pale is
     texture; the name sits on it and is never read through it.
     · Objects fade to nothing at both ends of their travel: no seam, no pop.
     · One saturated mark per trace — the coral peak. Everything else is pale.
       The eye needs somewhere to land or the whole field reads as noise.
   ---------------------------------------------------------------------- */

.field {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;

  perspective: 1250px;
  /* The vanishing point, up in the top-right corner. Objects are anchored low
     and centre-left, so everything recedes UP AND ACROSS the whole header. */
  perspective-origin: 88% 22%;

  /* TWO masks, intersected.

     The first thins the field over the copy. It is not taken to zero: this is
     a header-wide field, not a panel behind the photograph. At 15% a hairline
     this pale is texture, and the name sits on it without being read through.

     The second is a keep-out over her face. A trace crossing a portrait is the
     one thing this backdrop must never do, and no arrangement of the objects
     can guarantee it when they are all in motion — so the hole is cut in the
     mask instead, where it holds for every frame of the cycle. Its centre
     tracks where `object-position` puts her head.

     `mask-composite: intersect` is what makes them cut rather than add. The
     -webkit- line is the same operation under the legacy keyword. Without
     either the two would union and the field would get MORE visible in exactly
     the two places it is being kept out of, so both are stated. */
  -webkit-mask-image:
    linear-gradient(to right,
      rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.22) 28%, rgba(0, 0, 0, 0.6) 50%, #000 72%),
    radial-gradient(24% 30% at 83% 26%,
      transparent 0%, transparent 44%, rgba(0, 0, 0, 0.55) 72%, #000 100%);
  -webkit-mask-composite: source-in;
          mask-image:
    linear-gradient(to right,
      rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.22) 28%, rgba(0, 0, 0, 0.6) 50%, #000 72%),
    radial-gradient(24% 30% at 83% 26%,
      transparent 0%, transparent 44%, rgba(0, 0, 0, 0.55) 72%, #000 100%);
          mask-composite: intersect;
}

.field__scene {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/* --dx/--dy fix each object's place in WORLD space and never change; the
   perspective divide spreads them apart near the viewer and draws them
   together as they recede. A single shared ray was tried first and put every
   object in the same narrow cone, where they piled up on each other.
   --zs is where an object rests when the drift is switched off. */
.field__obj {
  position: absolute;
  left: 46%;
  top: 56%;
  width: var(--w);
  transform: translate(-50%, -50%)
             translate3d(var(--dx, 0px), var(--dy, 0px), var(--zs, 0px));
  animation: field-drift 96s linear var(--delay, 0s) infinite;
}
.field__obj svg { display: block; width: 100%; height: auto; overflow: visible; }

@keyframes field-drift {
  0%   { transform: translate(-50%, -50%)
                    translate3d(var(--dx, 0px), var(--dy, 0px), 240px);
         opacity: 0; }
  12%  { opacity: 1; }
  76%  { opacity: 1; }
  100% { transform: translate(-50%, -50%)
                    translate3d(var(--dx, 0px), var(--dy, 0px), -2160px);
         opacity: 0; }
}

/* Instrument chrome — the axes a reading is taken against. Neutral rather than
   the figure's own colour, because the axis belongs to the instrument. */
.fx-axis, .fx-tick, .fx-axis2, .fx-clk {
  stroke: color-mix(in srgb, var(--on-forest) 26%, transparent);
  stroke-width: 1.2;
}
.fx-clk { stroke-dasharray: 2 6; }

.fx-trace, .fx-wave {
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* The marked peak — the one place in the field at full strength. Coral, the
   site's second accent, spent here and in two other places on the whole page. */
.fx-cap  { stroke: var(--on-forest-mark); stroke-width: 2; stroke-linecap: round; }
.fx-drop { stroke: color-mix(in srgb, var(--on-forest-mark) 62%, transparent);
           stroke-width: 1.2; stroke-dasharray: 4 5; }
.fx-peak { fill: var(--on-forest-mark); }

.fx-edge { stroke: currentColor; stroke-width: 1.4; }
.fx-node { fill: currentColor; }

.fx-cell { stroke: currentColor; stroke-width: 1.3; }
.fx-cell-on {
  fill: color-mix(in srgb, var(--on-forest-honey) 26%, transparent);
  stroke: var(--on-forest-honey);
  stroke-width: 1.3;
}
.fx-brace { stroke: var(--on-forest-mark); stroke-width: 1.6; }

.fx-unit { stroke: currentColor; stroke-width: 1.5; }
.fx-pole { stroke: var(--on-forest-mark); stroke-width: 2; stroke-linecap: round; }
.fx-zero { stroke: currentColor; stroke-width: 1.8; }

.fx-sym   { fill: currentColor; }
.fx-ideal { stroke: var(--on-forest-honey); stroke-width: 1.4; }

/* Colour follows the site's hierarchy: blue carries the informational
   majority, honey is the accent, neutral draws structure. */
.field__obj--trace { color: color-mix(in srgb, var(--on-forest-info) 82%, transparent); }
.field__obj--fft   { color: color-mix(in srgb, var(--on-forest) 40%, transparent); }
.field__obj--cache { color: color-mix(in srgb, var(--on-forest) 34%, transparent); }
.field__obj--pz    { color: color-mix(in srgb, var(--on-forest-honey) 74%, transparent); }
.field__obj--clk   { color: color-mix(in srgb, var(--green) 88%, transparent); }
.field__obj--con   { color: color-mix(in srgb, var(--on-forest-info) 78%, transparent); }

@media (max-width: 899px) {
  /* Stacked, the copy sits over the photograph and the field has to give way
     to it. Four of the sixteen objects go: at this width the scene reads as
     clutter long before it reads as depth. */
  .field {
    perspective-origin: 62% 18%;
    /* Same pair, re-aimed: the copy is below her now rather than beside her,
       so the thinning runs top-to-bottom and the keep-out moves to where the
       portrait viewport puts her face. */
    -webkit-mask-image:
      linear-gradient(to bottom,
        rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.3) 46%, rgba(0, 0, 0, 0.75) 78%, #000 100%),
      radial-gradient(34% 17% at 56% 24%,
        transparent 0%, transparent 44%, rgba(0, 0, 0, 0.55) 72%, #000 100%);
    -webkit-mask-composite: source-in;
            mask-image:
      linear-gradient(to bottom,
        rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.3) 46%, rgba(0, 0, 0, 0.75) 78%, #000 100%),
      radial-gradient(34% 17% at 56% 24%,
        transparent 0%, transparent 44%, rgba(0, 0, 0, 0.55) 72%, #000 100%);
            mask-composite: intersect;
  }
  .field__obj--cache,
  .field__obj--con { display: none; }
}

/* ---------- 9. Pills --------------------------------------------------- */

.pills { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.pill {
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  padding: 0 var(--space-3);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  color: var(--text-2);
  white-space: nowrap;
}
/* Emphasis pills. --accent is HONEY (primary); --orange is CORAL, a class
   name that finally matches its colour; --info is BLUE; --leaf is GREEN. */
.pill--accent { background: var(--honey-pale); border-color: var(--honey);      color: var(--honey-deep); }
.pill--orange { background: var(--coral-pale); border-color: var(--coral);      color: var(--coral-deep); }
.pill--info   { background: var(--blue-pale);  border-color: var(--blue);       color: var(--blue-deep); }
.pill--leaf   { background: var(--green-pale); border-color: var(--green-deep); color: var(--green-deep); }

/* ---------- 9b. Spec list ----------------------------------------------
   The at-a-glance block in the About column. Every long-scroll CV needs one
   place where the hard facts sit together in scannable form, and this column
   previously held nothing but an eyebrow — 15rem of empty page beside the
   section people read most closely.

   Set as a specification table, not a card: label above value, hairline
   between rows, mono labels in blue. Same register as the skill-group
   evidence list, deliberately, because it does the same job.
   ---------------------------------------------------------------------- */

.spec {
  margin: 0;
  border-top: 1px solid var(--line);
}
.spec__row {
  display: grid;
  gap: 0.15rem;
  padding-block: var(--space-3);
  border-bottom: 1px solid var(--line);
}
.spec dt {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blue-deep);
}
.spec dd {
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--ink);
}
/* Horizontal variant, used on the contact page. Four short facts read perfectly
   well as a row, and as a vertical stack they made that column 336px tall — tall
   enough to push the fourth contact card off the bottom of a laptop screen. The
   top rule is kept as the strip's boundary; the per-row rules would just be
   noise side by side. */
@media (min-width: 760px) {
  .spec--strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .spec--strip .spec__row { border-bottom: 0; padding-block: var(--space-4) 0; padding-inline-end: var(--space-5); }
}

/* Green as detail, exactly as the availability dots use it. */
.spec dd.is-open {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  color: var(--green-deep);
}
.spec dd.is-open::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--green-deep);
  flex: none;
  transform: translateY(-0.15em);
}

/* ---------- 10. Skill groups ------------------------------------------- */

/* Hairline-separated cells rather than four floating cards: one surface
   divided, not four objects. */
.skills-grid { display: grid; gap: 1px; grid-template-columns: 1fr; background: var(--line); }
@media (min-width: 700px) { .skills-grid { grid-template-columns: repeat(2, 1fr); } }
/* With FIVE cards the two-column grid leaves a dead cell showing the hairline
   colour, exactly as the projects grid did. The last card spans instead. */
@media (min-width: 700px) { .skill-group--wide { grid-column: 1 / -1; } }

.skill-group {
  padding: var(--space-6);
  background: var(--paper);
  transition: background-color var(--dur-mid) var(--ease-out);
}
.band .skill-group { background: var(--paper); }
.skill-group:hover { background: var(--panel); }

.skill-group__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.skill-group__icon {
  display: grid;
  place-items: center;
  width: 1.6rem; height: 1.6rem;
  flex: none;
  color: var(--honey-deep);
  transform: translateY(0.25rem);
}
.skill-group__icon svg { width: 100%; height: 100%; }
.skill-group h3 { font-size: var(--step-2); margin-bottom: var(--space-1); }
.skill-group__head .meta { display: block; }
.skill-group__tags { margin-bottom: var(--space-5); }

.skill-group__evidence {
  display: grid;
  gap: var(--space-1);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
  font-size: var(--step--1);
  color: var(--text-2);
}
/* Blue as the informational label — 6.95:1 on paper. "WHERE" is a factual
   annotation, so it takes blue rather than the green detail colour. */
.skill-group__evidence dt {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blue-deep);
}
.skill-group__evidence dd { margin: 0; }

/* ---------- 10c. Education record --------------------------------------
   A CV for a final-year student that never states the degree, the institution
   and the graduation date in one place is missing its most-checked field. It
   was previously only inferable from the caption under the photograph.

   Built as one record rather than a list of cards, because there is one
   degree. The head is the field a reader scans; everything below it is
   supporting detail behind a hairline.
   ---------------------------------------------------------------------- */

.edu-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
  padding: clamp(1.35rem, 1rem + 1.6vw, 2.25rem);
}

.edu-card__head {
  display: grid;
  gap: var(--space-4);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--line);
}
@media (min-width: 780px) {
  .edu-card__head {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--space-6);
  }
}
.edu-card__degree { font-size: var(--step-2); margin-bottom: var(--space-2); }
.edu-card__org {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--honey-deep);      /* 6.38:1 on the white panel */
}
.edu-card__when {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
@media (min-width: 780px) { .edu-card__when { text-align: right; } }

.edu-rows { display: grid; gap: var(--space-6); margin-top: var(--space-5); }
@media (min-width: 880px) {
  .edu-rows { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); gap: var(--space-8); }
}
.edu-row__label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blue-deep);
  margin-bottom: var(--space-3);
}
.edu-row p { color: var(--text-2); font-size: var(--step--1); line-height: 1.65; }

/* Awards. Same honey dash as the timeline bullets, because they are the same
   kind of thing: a claim with evidence attached. */
.edu-row__list { margin-top: var(--space-2); }
.edu-row__list li {
  position: relative;
  padding-left: var(--space-5);
  color: var(--text-2);
  font-size: var(--step--1);
  line-height: 1.65;
}
.edu-row__list li + li { margin-top: var(--space-3); }
.edu-row__list li::before {
  content: '';
  position: absolute;
  left: 0; top: 0.72em;
  width: 7px; height: 2px;
  background: var(--honey);
}
/* The honour itself, in the display face. NOT a bold weight — the house rule
   is that size and face carry hierarchy, and no body bold is loaded for it. */
.award { font-family: var(--font-display); font-size: var(--step-0); color: var(--ink); }

/* An inline figure inside prose — a GPA, an hour count. Honey-deep matches the
   result figures on the project cards, so a number reads the same everywhere. */
.figure {
  font-family: var(--font-display);
  font-size: 1.2em;
  line-height: 1;
  color: var(--honey-deep);
  font-variant-numeric: tabular-nums;
}
/* The one figure on the page that should stop a skim-reader. Size and a honey
   tint block do the work; the colour role is unchanged, so it gets louder
   without the palette gaining a fifth meaning. */
.figure--lead {
  font-size: 1.9em;
  padding: 0.04em 0.26em 0.1em;
  /* --r4 from the honey ramp, not --honey-pale: the pale tint is barely a shade
     off the white panel it sits on, so the chip did not read as a chip.
     --honey-deep on --r4 measures 5.37:1, so it still clears AA as body text
     even though this is set large. */
  background: var(--r4);
  border-radius: var(--r-sm);
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* A second label inside the same cell needs the gap the first one gets from
   the row above it. */
.edu-row__label--stacked { margin-top: var(--space-5); }

/* ---------- 11. Timeline ----------------------------------------------- */

.timeline { position: relative; }
.timeline::before {
  content: '';
  position: absolute;
  left: 1.5rem; top: 0.5rem; bottom: 0.5rem;
  width: 1px;
  background: var(--line);
}
@media (min-width: 860px) { .timeline::before { left: 12rem; } }

.tl-item { position: relative; padding-left: 4rem; padding-bottom: var(--space-8); }
.tl-item:last-child { padding-bottom: 0; }
@media (min-width: 860px) {
  .tl-item { display: grid; grid-template-columns: 9rem 1fr; gap: var(--space-9); padding-left: 0; }
}

.tl-item__when {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  color: var(--text-3);
  text-transform: uppercase;
  margin-bottom: var(--space-3);
}
@media (min-width: 860px) {
  .tl-item__when { text-align: right; margin-bottom: 0; padding-top: 0.55rem; }
}

.tl-item__body {
  position: relative;
  padding: var(--space-5) var(--space-6);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-mid) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out);
}
.tl-item__body:hover { border-color: var(--honey); box-shadow: var(--shadow-2); }

/* The one role still running gets a coral edge. This marks a fact — which
   position is current — rather than decorating the first card, so it moves
   down the list as roles end. The date text says the same thing, so the
   colour is never the only signal. */
.tl-item--current .tl-item__body {
  border-left: 2px solid var(--honey);
  padding-left: calc(var(--space-6) - 2px);   /* keeps the text column aligned */
}

/* Brand marks are supplied on white, so a white tile keeps them correct.
   WER1's mark is dark-on-dark and gets an ink tile instead. */
.tl-item__logo {
  position: absolute;
  left: -4rem; top: var(--space-5);
  display: grid;
  place-items: center;
  width: 2.75rem; height: 2.75rem;
  padding: 0.3rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.cred__tile--light,
.tl-item__logo--light { background: var(--panel); border-color: var(--line); padding: 0.25rem; }
@media (min-width: 860px) { .tl-item__logo { left: -4.4rem; } }
.tl-item__logo img { width: 100%; height: 100%; object-fit: contain; }
.tl-item__logo--dark { background: var(--ink); border-color: var(--ink); padding: 0; }

/* Role and engagement type sit on one row: a reader scanning job titles
   should not have to also parse "was this an internship?" from prose. */
.tl-item__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
}
.tl-item__role { font-size: var(--step-2); margin-bottom: var(--space-2); }
.tl-item__org {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--honey-deep);      /* 6.01 on paper, 6.38 on the white panel */
}
/* Location and engagement type. Standard CV furniture, and it keeps the
   organisation line from having to carry three facts at once. */
.tl-item__meta {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin-top: var(--space-2);
  margin-bottom: var(--space-4);
}
.tl-item__summary {
  color: var(--text-2);
  font-size: var(--step--1);
  max-width: 60ch;
}
.tl-item__list { margin-top: var(--space-4); }

/* Tools used, per role. Employers filter on these words, so they get their own
   row behind a hairline rather than being hidden inside the bullets. */
.tl-item__stack {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
.tl-item__stack-label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--blue-deep);
  flex: none;
}
.tl-item__list li {
  position: relative;
  padding-left: var(--space-5);
  color: var(--text-2);
  font-size: var(--step--1);
  line-height: 1.65;
}
.tl-item__list li + li { margin-top: var(--space-3); }
.tl-item__list li::before {
  content: '';
  position: absolute;
  left: 0; top: 0.72em;
  width: 7px; height: 2px;
  background: var(--honey);
}

/* ---------- 12. Project cards ------------------------------------------ */

/* Two across, not three. The cell background is the hairline colour, so any
   cell the cards don't fill shows as a solid block rather than as whitespace.
   With FOUR projects, two columns tile exactly; three columns leave two dead
   cells on the second row. Re-check this if a project is added or removed. */
.projects { display: grid; gap: 1px; grid-template-columns: 1fr; background: var(--line); }
@media (min-width: 760px)  { .projects { grid-template-columns: repeat(2, 1fr); } }

.project {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-6);
  background: var(--paper);
  transition: background-color var(--dur-mid) var(--ease-out);
}
.band .project { background: var(--paper); }
.project:hover { background: var(--panel); }

/* No 01/02/03 markers: these projects are not a sequence, so numbering them
   would encode nothing. Only the tag row remains. */
.project__top { display: flex; align-items: center; gap: var(--space-3); }
.project h3 { font-size: var(--step-2); }
.project__desc { color: var(--text-2); font-size: var(--step--1); flex: 1; }
/* The result, stated as a figure. Their own copy in the timeline says "a
   number is worth ten adjectives" — this is the slot that holds one, set in
   the display face so it carries weight without needing a bold. */
.project__result {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
.project__num {
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1;
  color: var(--honey-deep);
  font-variant-numeric: tabular-nums;
}
.project__num-label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
  line-height: 1.35;
}

.project__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
.project__links { display: flex; flex-wrap: wrap; gap: var(--space-4); }
.project__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--honey-deep);
}
.project__link svg { width: 0.95em; height: 0.95em; flex: none; }
.project__link:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 0.3em; }
.project code { font-family: var(--font-mono); font-size: 0.9em; color: var(--honey-deep); }

/* Photographs of the thing itself, behind a hairline at the foot of the card.
   Not a header image: only three of the four projects have photographs, and a
   banner on some cards and not others reads as a broken layout. As a footer
   strip an absent one reads as "no photos", which is the truth.

   Each thumbnail is a real link to the full-size file, so the strip still works
   with JavaScript off; main.js upgrades it to the lightbox. */
.project__shots {
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--space-2);
}
.shot {
  display: block;
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--band);
  transition: border-color var(--dur-mid) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out);
}
.shot img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
.shot:hover { border-color: var(--honey); box-shadow: var(--shadow-2); }

/* The unfinished project has no photographs, so it carries a position instead.
   Same top hairline and same place in the card as .project__shots, so the four
   cards still read as one set. */
.project__progress {
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}
.progress__rail {
  position: relative;
  height: 6px;
  border-radius: 999px;
  background: var(--band);
  border: 1px solid var(--line);
  overflow: hidden;
}
.progress__fill {
  display: block;
  position: relative;
  height: 100%;
  width: 45%;
  border-radius: 999px;
  background: var(--honey);   /* non-text use, so the mid honey is correct */
  overflow: hidden;
}
/* A pale highlight travelling along the FILLED portion — the affordance that
   reads as "running" rather than "stalled". Deliberately not over the empty
   part of the track: nothing is happening there yet, and a sweep across the
   whole rail darkened the fill mid-pass and looked like a colour bug. */
.progress__fill::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
              transparent 0%,
              color-mix(in srgb, var(--paper) 60%, transparent) 50%,
              transparent 100%);
  transform: translateX(-100%);
  animation: rail-sweep 2.4s var(--ease-out) infinite;
}
@keyframes rail-sweep { to { transform: translateX(100%); } }

.progress__steps {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}
.progress__arrow { width: 0.85rem; height: 0.85rem; flex: none; }
/* The current phase carries a dot as well as a colour, so it never depends on
   colour alone — the same rule the active nav link follows. */
.progress__steps .is-on { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--honey-deep); }
.progress__steps .is-on::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--honey-deep);
  flex: none;
}

/* .project--empty is gone: it styled a dashed "Room for the next one" card that
   printed build instructions on the public page. Adding a project is now a
   comment in the markup instead. */

/* ---------- 13. Beyond-engineering teaser ------------------------------ */

.beyond { display: grid; gap: var(--space-8); grid-template-columns: 1fr; align-items: center; }
@media (min-width: 900px) { .beyond { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); } }

/* Three equal thumbnails read as a contact sheet — fine for an archive, wrong
   for a teaser, and they left the column shorter than the text beside it. One
   lead image at full height with two stacked beside it fills the column and
   gives the group a subject. */
.beyond__fan {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}
.beyond__fan figure { margin: 0; display: grid; min-height: 0; }
.beyond__fan figure:first-child { grid-row: 1 / span 2; }
.beyond__fan figure:first-child a,
.beyond__fan figure:first-child img { height: 100%; }
.beyond__fan figure:first-child img { aspect-ratio: auto; }
.beyond__fan img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--band);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-mid) var(--ease-out),
              transform var(--dur-mid) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out);
}
.beyond__fan a:hover img {
  border-color: var(--honey);
  transform: translateY(-3px);
  box-shadow: var(--shadow-2);
}

.chip-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-5); }

/* ---------- 14. Gallery ------------------------------------------------ */

.gallery { display: grid; gap: var(--space-6); grid-template-columns: 1fr; align-items: start; }
@media (min-width: 640px)  { .gallery { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .gallery { grid-template-columns: repeat(3, 1fr); } }
/* Four across, for a group that is meant to read as a single row rather than as
   a grid. Only worth using when the group holds exactly four. */
@media (min-width: 1000px) { .gallery--quad { grid-template-columns: repeat(4, 1fr); } }

.art { margin: 0; }
/* An <a> to the full-size image, not a <button>, so the gallery degrades to
   plain links with JS disabled. main.js intercepts the click. */
.art__btn {
  display: block;
  width: 100%;
  padding: var(--space-3);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
  position: relative;
  text-decoration: none;
  cursor: pointer;
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-mid) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out);
}
.art__btn:hover { border-color: var(--honey); box-shadow: var(--shadow-2); }
/* Square thumbnails: the works mix landscape and portrait, and any single
   non-square ratio leaves ragged rows and misaligned captions. */
.art__btn img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  aspect-ratio: 1 / 1;
  border-radius: 1px;
  background: var(--band);
}
.art--tall .art__btn img { aspect-ratio: 3 / 4; }
/* The photographs are all one landscape frame, and squaring them would crop the
   subject out of half of them. Climbing shots are all portrait, so they take
   the tall ratio. */
.art--wide .art__btn img { aspect-ratio: 3 / 2; }

.art__zoom {
  position: absolute;
  right: var(--space-4); bottom: var(--space-4);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  background: var(--paper);
  border: 1px solid var(--honey);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--honey-deep);
  opacity: 0;
  transition: opacity var(--dur-mid) var(--ease-out);
}
.art__zoom svg { width: 0.85rem; height: 0.85rem; }
.art__btn:hover .art__zoom,
.art__btn:focus-visible .art__zoom { opacity: 1; }
/* Touch devices get no hover, so the affordance is always visible there. */
@media (hover: none) { .art__zoom { opacity: 1; } }

.art figcaption { margin-top: var(--space-3); }
.art__title { font-family: var(--font-display); font-size: var(--step-1); color: var(--ink); }
.art__meta {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin-top: var(--space-1);
}

.art--slot .art__ph {
  display: grid;
  place-items: center;
  gap: var(--space-2);
  aspect-ratio: 1 / 1;   /* matches the real tiles so rows stay even */
  background: transparent;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm);
  color: var(--text-2);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  padding: var(--space-4);
}
.art--slot .art__ph svg { width: 1.25rem; height: 1.25rem; }
.art--tall.art--slot .art__ph { aspect-ratio: 3 / 4; }
.art--wide.art--slot .art__ph { aspect-ratio: 3 / 2; }

/* ---------- 15. Lightbox ----------------------------------------------- */

/* Deliberately DARK on a light site. This is a modal for viewing artwork, and
   graphite drawings and paintings read far better isolated on near-black than
   on paper. It is the only dark surface besides the CTA band. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  /* minmax(0, 1fr), not 1fr: a bare 1fr is minmax(auto, 1fr), so a large image
     grows the track and pushes the caption and controls off-screen. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  row-gap: var(--space-4);
  background: #1A1712;   /* opaque: isolating the work is the point */
  padding: var(--space-4);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-mid) var(--ease-out), visibility var(--dur-mid) linear;
}
.lightbox[data-open='true'] { opacity: 1; visibility: visible; }
.lightbox__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}
.lightbox__count {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}
.lightbox__stage { position: relative; min-height: 0; }
/* An absolutely positioned <img> with auto width/height falls back to its
   intrinsic size, so both must be explicit for object-fit to constrain it. */
.lightbox__stage img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity var(--dur-mid) var(--ease-out);
}
.lightbox[data-open='true'] .lightbox__stage img { opacity: 1; }
.lightbox__cap { text-align: center; padding-block: var(--space-3); min-height: 3.5rem; }
.lightbox__cap strong {
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--step-1);
  color: var(--paper);
}
.lightbox__cap span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  margin-top: var(--space-1);
}

.lb-btn {
  display: inline-grid;
  place-items: center;
  width: 48px; height: 48px;
  background: transparent;
  /* A control boundary needs 3:1 against the backdrop. Verified 3.74. */
  border: 1px solid #787264;
  border-radius: var(--r-sm);
  color: var(--paper);
  touch-action: manipulation;
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.lb-btn:hover { border-color: var(--paper); background: #2B261E; }
.lb-btn svg { width: 1.1rem; height: 1.1rem; }
.lb-btn:focus-visible { outline-color: var(--on-forest-honey); }
.lightbox__nav { display: flex; gap: var(--space-3); align-items: center; justify-content: center; }

/* ---------- 16. Contact ------------------------------------------------ */

.contact-grid { display: grid; gap: 1px; grid-template-columns: 1fr; background: var(--line); }
@media (min-width: 700px) { .contact-grid { grid-template-columns: repeat(2, 1fr); } }

.contact-card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
  background: var(--paper);
  text-decoration: none;
  color: var(--ink);
  min-height: 80px;
  transition: background-color var(--dur-mid) var(--ease-out);
}
.contact-card:hover { background: var(--panel); color: var(--ink); }
.contact-card__icon {
  display: grid;
  place-items: center;
  width: 1.5rem; height: 1.5rem;
  flex: none;
  color: var(--honey-deep);
}
.contact-card__icon svg { width: 100%; height: 100%; }
/* These are spans, so they need an explicit block display to stack. */
.contact-card__label,
.contact-card__value { display: block; }
.contact-card__label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: var(--space-1);
}
.contact-card__value {
  font-family: var(--font-display);
  font-size: var(--step-2);
  color: var(--ink);
  overflow-wrap: anywhere;
  min-width: 0;
}
.contact-card__body { min-width: 0; }

/* ---------- 17. CTA band ----------------------------------------------- */

/* The page's other forest surface. It matches the header deliberately: the
   site opens and closes on the same green, and everything between is paper.
   Same contrast rules as the header — the dark accents fail here too. */
.cta-band {
  background: var(--forest);
  color: var(--on-forest);
  border-radius: var(--r-sm);
  padding: clamp(2rem, 1.2rem + 3vw, 3.5rem);
  display: grid;
  gap: var(--space-6);
  align-items: center;
}
@media (min-width: 820px) { .cta-band { grid-template-columns: minmax(0, 1fr) auto; } }
.cta-band h2 { color: var(--on-forest); margin-bottom: var(--space-4); }
.cta-band p { color: var(--on-forest-2); max-width: 46ch; }
.cta-band .eyebrow { color: var(--on-forest-info); margin-bottom: var(--space-4); }
.cta-band .eyebrow::before { background: var(--on-forest-honey); }
.cta-band .btn--primary {
  background: var(--on-forest-honey);
  color: var(--forest);        /* 8.62:1 */
  border-color: var(--on-forest-honey);
}
.cta-band .btn--primary:hover {
  background: var(--on-forest);
  border-color: var(--on-forest);
  color: var(--forest);        /* 12.21:1 */
}
.cta-band :focus-visible { outline-color: var(--on-forest); }

/* The availability lines repeat here because this is the last thing read
   before the footer and the point at which someone decides to write. On
   forest, --text-2 is unreadable and --on-forest-2 is 7.69:1; the marker
   switches to green-400 for the same reason it does in the header. */
.cta-band .hero__status { margin-top: var(--space-5); }
.cta-band .status-dot { color: var(--on-forest-2); }
.cta-band .status-dot::before { background: var(--green); }

/* ---------- 18. Footer ------------------------------------------------- */

.site-footer {
  border-top: 1px solid var(--line);
  background: var(--band);
  padding-block: var(--space-8) var(--space-6);
  margin-top: var(--section-y);
}
.site-footer__top {
  display: grid;
  gap: var(--space-7);
  grid-template-columns: 1fr;
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--line);
}
@media (min-width: 760px) { .site-footer__top { grid-template-columns: 1.5fr 1fr 1fr; } }
.site-footer h2 { font-size: var(--step-2); margin-bottom: var(--space-2); }
.site-footer__heading {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--blue-deep);    /* blue = informational heading */
  margin-bottom: var(--space-3);
}
.site-footer__links a {
  display: inline-flex;
  align-items: center;
  min-height: 38px;
  font-size: var(--step--1);
  color: var(--text-2);
  text-decoration: none;
}
.site-footer__links a:hover { color: var(--honey-deep); text-decoration: underline; }
/* Icon row under the name block. Icons alone are ambiguous, so each keeps a
   visible text label; the icon is the scanning aid, not the label. */
.site-footer__social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
}
.site-footer__social a {
  display: inline-grid;
  place-items: center;
  width: 40px; height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--paper);
  color: var(--text-2);
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.site-footer__social a:hover { border-color: var(--honey); color: var(--honey-deep); }
.site-footer__social svg { width: 1.05rem; height: 1.05rem; }

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-5);
  align-items: center;
  justify-content: space-between;
  padding-top: var(--space-5);
}

.to-top {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-2);
  min-height: 40px;
}
.to-top svg { width: 0.9rem; height: 0.9rem; }
.to-top:hover { color: var(--honey-deep); }

/* ---------- 19. Utilities ---------------------------------------------- */

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* Shown only on paper — the contact block a printed CV must carry, which the
   screen version puts in the header and footer instead. */
.print-only { display: none; }

.text-center { text-align: center; }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mt-7 { margin-top: var(--space-7); }

body.is-locked { overflow: hidden; }

/* ---------- 20. Motion -------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}
.reveal.is-in { opacity: 1; transform: none; }
.reveal[data-delay='1'] { transition-delay: 70ms; }
.reveal[data-delay='2'] { transition-delay: 140ms; }
.reveal[data-delay='3'] { transition-delay: 210ms; }
.reveal[data-delay='4'] { transition-delay: 280ms; }

/* No JS? Everything must be visible. main.js removes this class immediately.
   The field needs no fallback of its own: it is pure CSS, so it runs, and its
   static state is the same 3D composition with the drift stopped. */
.no-js .reveal { opacity: 1; transform: none; }

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

  /* The blanket rule above would collapse the field's drift to its final
     keyframe, which is fully transparent — the backdrop would simply vanish.
     Turning the animation off instead leaves every object at its own resting
     depth, so the scene is still there: composed, and still. */
  .field__obj { animation: none !important; }

  /* The sweep's final keyframe is off the right edge of the rail, so the
     blanket rule above would simply hide it. Say so explicitly rather than
     rely on that: the rail keeps its fill, and stops moving. */
  .progress__fill::after { animation: none !important; opacity: 0; }

  .beyond__fan a:hover img,
  .tl-item__body:hover { transform: none !important; }
}

/* ---------- 21. Print -------------------------------------------------- */

/* Printing this page produces a CV, not a screenshot of a website.

   Anyone reviewing a candidate will at some point hit Ctrl+P — on a shortlist,
   into a PDF, or onto paper for a panel. The screen layout does not survive
   that: the hero is a full viewport tall, the figure and the gallery carry no
   information a reader can act on, and the contact details live in a footer
   that gets dropped. So print gets its own document.

   What is kept: name, discipline, contact line, summary, skills, education,
   experience, projects. What goes: navigation, the drifting field, the art
   teaser, the CTA band, every image that is decoration rather than evidence.
   ---------------------------------------------------------------------- */
@media print {
  @page { margin: 15mm 14mm; }

  .print-only { display: block; }

  .site-header, .nav, .nav-toggle, .skip-link, .lightbox,
  .cta-band, .art__zoom, .cred, .field, .beyond, .hero__bg, .hero__hem,
  .hero__ctas, .hero__status, .to-top,
  .site-footer__social, .site-footer__links, .project__links,
  .beyond__fan { display: none !important; }

  html, body { background: #fff; color: #000; font-size: 10.5pt; line-height: 1.42; }
  .reveal { opacity: 1 !important; transform: none !important; }

  /* Sections stop being full-bleed bands and become blocks of a document.
     The gap is set on every section after the first rather than with a
     `.section + .section` pair, because the hidden figure and rail sit between
     them in the source and would break the adjacency. */
  .section, .section--tight { padding-block: 0 0 !important; }
  main > section + section { margin-top: 7mm; }
  .section-head { margin-bottom: 3mm; }
  .band { background: none !important; border: 0 !important; }
  .shell { max-width: none; padding-inline: 0; }
  /* The header is a dark photograph on screen. On paper it is a name and a
     line of contact details, in black, with no ground at all. */
  .hero {
    padding-block: 0 !important;
    min-height: 0 !important;
    background: none !important;
    color: #000 !important;
    display: block !important;
  }
  .hero__copy { max-width: none; }
  .hero__title, .hero .lede, .hero .eyebrow, .hero .mark, .hero .status-dot,
  .hero__role { color: #000 !important; }
  .site-footer { margin-top: 6mm; padding-block: 3mm 0; background: none; }

  /* One column. The split and the hero grid both collapse. */
  .hero__grid, .split, .beyond, .skills-grid, .projects,
  .edu-rows, .site-footer__top {
    display: block !important;
  }
  /* Collapsed grids lose their gap along with their tracks, so the two halves
     of a split would butt straight into each other. */
  .split > * + * { margin-top: 5mm; }

  /* The CTA band's wrapper would otherwise leave its section gap as a blank
     strip above the footer. Scoped with :has so the contact page's own
     .section--tight, which holds real content, is untouched. Browsers without
     :has simply keep the gap. */
  section:has(> .shell > .cta-band) { display: none; }
  .hero__title { font-size: 24pt; }
  .hero__role { font-size: 13pt; padding-bottom: 2mm; margin-bottom: 3mm; }
  h2 { font-size: 15pt; }
  h3 { font-size: 12pt; }
  .lede { font-size: 11pt; }

  /* The at-a-glance block becomes the profile header a CV opens with: label
     and value on one line, ruled, rather than the tall stacked rows the narrow
     screen column needs. */
  .spec__row {
    grid-template-columns: 34mm minmax(0, 1fr);
    gap: 0 4mm;
    align-items: baseline;
    padding-block: 1.1mm;
  }
  .spec dd.is-open::before { display: none; }

  .edu-row__label { margin-bottom: 1.5mm; }
  .edu-row + .edu-row, .edu-rows > * + * { margin-top: 4mm; }
  .tl-item__stack { margin-top: 3mm; padding-top: 2mm; }

  /* The contact line a printed CV must carry. */
  .print-contact {
    font-family: var(--font-mono);
    font-size: 9pt;
    letter-spacing: 0.02em;
    color: #000;
    margin-bottom: 4mm;
  }
  .print-contact span + span::before { content: '  ·  '; }

  /* Panels flatten: ink on white, ruled, no shadows or fills. */
  .tl-item__body, .edu-card, .skill-group, .project, .contact-card {
    background: none !important;
    box-shadow: none !important;
    border: 0 !important;
    border-top: 0.5pt solid #999 !important;
    padding: 2mm 0 !important;
  }
  .tl-item--current .tl-item__body { border-left: 0 !important; }
  .skills-grid, .projects { background: none !important; gap: 0 !important; }
  .pill { border-color: #999 !important; background: none !important; color: #000 !important; }

  /* Never split a role, a project or a qualification across a page break. */
  .tl-item, .edu-card, .skill-group, .project, .section-head { break-inside: avoid; }
  h2, h3 { break-after: avoid; }
  .timeline::before { display: none; }
  .tl-item { padding-left: 0 !important; padding-bottom: 4mm !important; }
  .tl-item__logo { display: none !important; }
  .tl-item__when { text-align: left !important; padding-top: 0 !important; }

  /* Links: the URL has to be on the page, since paper has no hover. Internal
     anchors are not printed — they would resolve to nothing off the site. */
  a { color: #000 !important; text-decoration: none !important; }
  a[href^='http']::after,
  a[href^='mailto']::after { content: ' (' attr(href) ')'; font-size: 0.82em; word-break: break-all; }
}
