/* =========================================================================
   Navlo — foundations
   A warm, quiet, editorial surface. Restraint is the brand: one background,
   one ink, hairline rules, and type doing nearly all of the work.
   ====================================================================== */

:root {
  /* Ground */
  --bg: #ebe8e4;
  /* One step deeper than the page, for ground that is not the page — the
     territory after the profile ends. Same warmth, so the shift reads as a
     change of place rather than a change of site. */
  --bg-deep: #e0dcd6;
  --surface: #ffffff;

  /* Ink */
  --ink: #0a0a0a;
  --ink-muted: #59544f;
  --ink-faint: #a9a29a;

  /* Valence — the only colour on the site, used where work intensity has a
     direction that grey cannot express. Chosen by measuring rather than by
     eye: green/red is the worst possible pair for colour blindness, and this
     one still clears OKLab ΔE 8.0 under protanopia and 8.6 under
     deuteranopia against the surface. Height remains the primary encoding,
     so colour is reinforcement and never the only channel. */
  --good: #2e7d57;
  --intense: #b3402b;

  /* Lines */
  --hairline: rgba(10, 10, 10, 0.12);
  --hairline-strong: rgba(10, 10, 10, 0.22);

  /* Shape */
  --radius-pill: 999px;
  --radius-card: 24px;
  /* Between the two: the card's radius at a text field's height reads as a
     lozenge, and the pill's turns one into a search box. */
  --radius-field: 12px;

  /* Rhythm */
  --gutter: clamp(1.5rem, 5vw, 5rem);
  --measure: 78rem;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

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

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

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 1rem;
  line-height: 1.5;
  font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Content is written by JS; hold it back for one frame to avoid a flash of
   unstyled placeholders, but never trap it if the script fails to run. */
body[data-state='loading'] .reveal {
  opacity: 0;
}

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

/* =========================================================================
   Masthead
   ====================================================================== */

.masthead {
  padding-block: 1.5rem;
}

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.9375rem;
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--ink);
}

.wordmark b {
  font-weight: 600;
}

.wordmark span {
  color: var(--ink-muted);
  font-weight: 400;
}

.masthead__nav {
  display: flex;
  gap: 1.75rem;
  font-size: 0.9375rem;
}

.masthead__nav a {
  color: var(--ink-muted);
  text-decoration: none;
  transition: color 0.2s var(--ease);
}

.masthead__nav a:hover,
.masthead__nav a[aria-current='page'] {
  color: var(--ink);
}

@media (max-width: 34rem) {
  .masthead__nav {
    display: none;
  }
}

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

.hero {
  padding-block: clamp(3rem, 9vw, 7.5rem) clamp(2.5rem, 6vw, 5rem);
  /* One source for the display size. The title sets its font-size from this
     and the mark sizes itself against it, so the two can never drift apart
     the way two hand-matched clamps eventually would. */
  --hero-type: clamp(3.25rem, 11.5vw, 9.5rem);
}

/* --- Eyebrow ------------------------------------------------------------ */

.hero__family {
  margin: 0 0 clamp(1.25rem, 3vw, 2rem);
  font-size: 0.875rem;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

/* --- Title -------------------------------------------------------------- */

.hero__name {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: var(--hero-type);
  font-weight: 300;
  line-height: 0.92;
  letter-spacing: -0.045em;
  text-wrap: balance;
}

/* Each word is its own line so multi-word titles stack like a masthead
   rather than reflowing unpredictably at 100+ different title lengths. */
.hero__word {
  display: block;
}

/* --- Tagline ------------------------------------------------------------ */

.hero__tagline {
  margin: clamp(1.5rem, 3.5vw, 2.5rem) 0 0;
  max-width: 26ch;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.375rem, 3vw, 2.125rem);
  font-weight: 300;
  line-height: 1.2;
  letter-spacing: -0.025em;
  color: var(--ink-muted);
  text-wrap: balance;
}

/* --- Tags --------------------------------------------------------------- */

.hero__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: clamp(2rem, 5vw, 3.25rem) 0 0;
  padding: 0;
  list-style: none;
}

/* Themes are peers — no tag outranks another, so they share one treatment.
   Author order in the data decides reading order, nothing else. */
.tag {
  display: inline-flex;
  align-items: center;
  height: 2.375rem;
  padding-inline: 1.0625rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  font-size: 0.9375rem;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
}

.tag--link {
  text-decoration: none;
  background: var(--surface);
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}

.tag--link:hover {
  color: var(--ink);
  border-color: var(--hairline-strong);
}

/* =========================================================================
   Sections
   ====================================================================== */

.section {
  padding-block: clamp(2.5rem, 6vw, 4.5rem);
}

.section[hidden] {
  display: none;
}

.section__title {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.75rem, 3.5vw, 2.5rem);
  font-weight: 300;
  letter-spacing: -0.03em;
}

.section__lede {
  margin: 0.625rem 0 clamp(1.5rem, 3vw, 2.25rem);
  max-width: 46ch;
  color: var(--ink-muted);
}

/* A lede slot with nothing in it must not leave its margins behind. */
.section__lede[hidden],
.section__lede:empty {
  display: none;
}

/* Sections that carry no lede: the gap the lede would have supplied, so a
   title never sits flush on its own content. */
.section__title + .card,
.section__title + .ledger,
.section__title + .path,
.section__title + .degrees,
.section__title + .day,
.section__title + .skills,
.section__title + .hours,
.section__lede[hidden] + .card {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
}

/* =========================================================================
   Card
   ====================================================================== */

.card {
  background: var(--surface);
  border-radius: var(--radius-card);
  padding: clamp(1rem, 2.5vw, 1.75rem);
}

.card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--hairline);
}

.card__note {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

.card__toggle {
  flex: none;
  height: 2rem;
  padding-inline: 0.875rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}

.card__toggle:hover {
  color: var(--ink);
  border-color: var(--hairline-strong);
}

/* --- Table view: the chart's accessible twin ---------------------------- */

.card__table {
  margin-top: 1rem;
  overflow-x: auto;
}

.card__table table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

.card__table th,
.card__table td {
  padding: 0.5rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--hairline);
}

.card__table thead th {
  font-weight: 500;
  color: var(--ink-muted);
}

.card__table tbody th {
  font-weight: 400;
  color: var(--ink-muted);
}

/* =========================================================================
   Overview — prose beside a strip of facts
   ====================================================================== */

/* Prose leads and the facts sit beside it, close enough that the eye can
   run down either column alone. On narrow screens the facts come second:
   a number without the sentence around it anchors nothing. The top margin
   is the section lede's, inherited from a section that doesn't carry one —
   an overview with its own introduction would be a summary of a summary. */
.overview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(16rem, 30vw, 21rem);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
}

.overview__prose {
  max-width: 58ch;
}

.overview__p {
  margin: 0 0 1em;
  color: var(--ink-muted);
}

.overview__p:last-child {
  margin-bottom: 0;
}

/* One colour for the running text; the **anchors** alone carry full ink.
   Three short paragraphs are not enough room for a second hierarchy. */
.overview__p strong {
  font-weight: 600;
  color: var(--ink);
}

/* --- The figures --------------------------------------------------------- */

.facts {
  margin: 0;
  padding: 0.375rem 1.25rem;
  list-style: none;
  background: var(--surface);
  border-radius: var(--radius-card);
}

.facts[hidden] {
  display: none;
}

.facts__row + .facts__row {
  border-top: 1px solid var(--hairline);
}

/* Each row is label on the left, figure on the right — usually a link to
   the section the figure was read from, the whole width tappable, but a
   plain span where there is no section to send the reader to. The figure
   is the content, so it keeps full ink; the label only rises to meet it on
   hover, which is all the affordance a row this plain needs — and only on
   rows that actually go somewhere. */
.facts__link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.8125rem;
  text-decoration: none;
  color: inherit;
}

.facts__label {
  flex: none;
  font-size: 0.8125rem;
  color: var(--ink-muted);
  transition: color 0.2s var(--ease);
}

a.facts__link:hover .facts__label {
  color: var(--ink);
}

.facts__value {
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.125rem;
  font-weight: 400;
  letter-spacing: -0.015em;
  font-variant-numeric: tabular-nums;
  text-align: right;
  text-wrap: balance;
}



@media (max-width: 52rem) {
  .overview {
    grid-template-columns: 1fr;
    gap: 1.75rem;
  }
}

/* =========================================================================
   Salary chart
   ====================================================================== */

.chart {
  position: relative;
  width: 100%;
  /* Height is set by the script and includes the x-axis band, so the axis
     labels are never cropped into a nested scrollbar. */
}

.chart:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
  border-radius: 8px;
}

.chart__svg {
  display: block;
  overflow: visible;
}

/* Recessive chrome — hairline, solid, one step off the surface. */
.chart__grid {
  stroke: rgba(10, 10, 10, 0.09);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

.chart__tick {
  font-size: 0.75rem;
  fill: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* The band — a wash, never a block. One shape, so it reads as a single
   range rather than as two more series. */
.chart__band {
  fill: var(--family-wash);
}

/* Edges at a third the median's weight: enough to see where the top and
   bottom 10% begin, never enough to compete with the typical earner. */
.chart__band-edge {
  fill: none;
  stroke: var(--family-edge);
  stroke-width: 1;
  stroke-linejoin: round;
}

.chart__line {
  fill: none;
  stroke: var(--family-ink);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Rings are surface-coloured, so markers stay legible over the line. */
.chart__end-ring,
.chart__focus-ring {
  fill: var(--surface);
}

.chart__end-dot,
.chart__focus-dot {
  fill: var(--family-ink);
}

.chart__focus-band {
  fill: var(--ink-muted);
}

/* Endpoints are the only directly-labelled values; the axis carries the rest. */
.chart__end-label {
  font-size: 0.875rem;
  font-weight: 500;
  fill: var(--ink);
}

.chart__end-label--band {
  font-size: 0.8125rem;
  font-weight: 400;
  fill: var(--ink-muted);
}

/* Names the band edge it sits against, so "top of the shading" needs no
   cross-reference to the legend. */
.chart__end-caption {
  font-size: 0.6875rem;
  font-weight: 400;
  fill: var(--ink-faint);
  letter-spacing: 0.01em;
}

.chart__crosshair {
  position: absolute;
  top: var(--plot-top, 0);
  height: var(--plot-height, 100%);
  width: 1px;
  background: var(--hairline-strong);
  pointer-events: none;
  transform: translateX(-0.5px);
}

.chart__tooltip {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 8.5rem;
  padding: 0.5rem 0.75rem;
  border-radius: 12px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(10, 10, 10, 0.08), 0 8px 24px rgba(10, 10, 10, 0.1);
  pointer-events: none;
  transform: translate(-50%, calc(-100% - 1rem));
  white-space: nowrap;
}

.chart__tooltip[hidden],
.chart__crosshair[hidden] {
  display: none;
}

/* Value leads, label follows — the reader has the series and wants the number. */
.chart__tooltip strong {
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}

.chart__tooltip-when {
  font-size: 0.75rem;
  color: var(--ink-muted);
}

.chart__tooltip-band {
  display: grid;
  gap: 0.1875rem;
  margin-top: 0.4375rem;
  padding-top: 0.4375rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.75rem;
}

.chart__tooltip-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  color: var(--ink-muted);
}

.chart__tooltip-row b {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* =========================================================================
   Chart summary — narrow screens only
   ====================================================================== */

.chart-summary[hidden] {
  display: none;
}

.chart-summary {
  margin-top: 1.25rem;
}

.chart-summary__caption {
  margin: 0 0 0.5rem;
  font-size: 0.75rem;
  color: var(--ink-faint);
}

.chart-summary__list {
  margin: 0;
  display: grid;
  gap: 0.375rem;
}

.chart-summary__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.875rem;
  color: var(--ink-muted);
}

.chart-summary__row dt,
.chart-summary__row dd {
  margin: 0;
}

.chart-summary__row dd {
  font-variant-numeric: tabular-nums;
}

/* The typical earner is the line on the chart, so it leads here too. */
.chart-summary__row[data-emphasis] {
  color: var(--ink);
}

.chart-summary__row[data-emphasis] dd {
  font-weight: 600;
}

/* =========================================================================
   Legend
   ====================================================================== */

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin: 0 0 0.5rem;
  padding: 0 0 0 0.25rem;
  list-style: none;
  font-size: 0.8125rem;
  color: var(--ink-muted);
}

.legend__item {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* The key mirrors the mark: a line for the line, a filled swatch for the band. */
.legend__key {
  flex: none;
}

.legend__key--line {
  width: 1rem;
  height: 2px;
  border-radius: 1px;
  background: var(--family-ink);
}

.legend__key--band {
  width: 1rem;
  height: 0.75rem;
  border-radius: 2px;
  background: var(--family-wash);
  box-shadow: inset 0 1px 0 var(--family-edge),
    inset 0 -1px 0 var(--family-edge);
}

/* =========================================================================
   The good and the bad — two columns
   ====================================================================== */

.ledger {
  display: grid;
  gap: clamp(2.5rem, 5vw, 3rem);
}

/* Equal columns: the symmetry is the argument. One side wider than the other
   would quietly rank them. */
@media (min-width: 52rem) {
  .ledger {
    grid-template-columns: 1fr 1fr;
    column-gap: clamp(2.5rem, 5vw, 4.5rem);
  }
}

.ledger__title {
  margin: 0 0 1.5rem;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--hairline-strong);
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.125rem;
  font-weight: 400;
  letter-spacing: -0.01em;
}

.ledger__items {
  display: grid;
  gap: clamp(1.75rem, 3vw, 2.25rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ledger__item {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 0.75rem;
  align-items: baseline;
}

/* Filled for an upside, hollow for a trade-off: an item stays legible if it
   is read on its own, away from its column heading. */
.ledger__marker {
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: 50%;
  border: 1px solid var(--ink);
  transform: translateY(-0.15em);
}

.ledger__marker[data-kind='upside'] {
  background: var(--ink);
}

.ledger__headline {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.125rem, 2vw, 1.375rem);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.ledger__detail {
  grid-column: 2;
  margin: 0.5rem 0 0;
  color: var(--ink-muted);
}

.ledger__detail strong {
  font-weight: 600;
  color: var(--ink);
}

/* =========================================================================
   Progression — a timeline on a rail
   ====================================================================== */

.path {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Rail on the left, body beside it. The stage's own padding lengthens the
   run below it, so time spent shows up as distance travelled. */
.stage {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: clamp(1rem, 3vw, 1.75rem);
  padding-bottom: calc(1.5rem + var(--span, 3) * 1.5rem);
}

.stage[data-last='true'] {
  padding-bottom: 0;
}

/* --- Rail --------------------------------------------------------------- */

/* Built per stage: dot, line, duration, line. The rail is a grid item, so it
   spans the stage's content box and the padding below it stays clear —
   each rung gets its own length of line rather than one rule running the
   whole section. */
.stage__rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  grid-row: 1 / -1;
  min-height: 100%;
}

.stage__dot {
  flex: none;
  width: 0.5625rem;
  height: 0.5625rem;
  margin-top: 0.5rem;
  border-radius: 50%;
  background: var(--family-ink);
}

/* The rail carries the career's hue too. It sits a little stronger than the
   neutral hairline it replaces — at the same 22% a colour reads as a smudge
   rather than as a drawn line. */
.stage__line {
  flex: 1;
  width: 1px;
  min-height: 1.5rem;
  background: oklch(0.48 0.125 var(--h) / 0.4);
}

/* The final rung has nowhere further to go, so its rail fades out rather
   than stopping dead. */
.stage[data-last='true'] .stage__line:last-child {
  flex: none;
  height: 3.5rem;
  background: linear-gradient(
    oklch(0.48 0.125 var(--h) / 0.4),
    transparent
  );
}

/* --- The duration ------------------------------------------------------- */

/* Sits on the rail and breaks the line, level with the rung it belongs to —
   the two flex lines either side of it divide the remaining rail equally, so
   it lands alongside that seniority level's own title and text. */
.stage__stay {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.125rem;
  padding: 0.5rem 0;
  background: var(--bg);
}

/* The duration is the one number on the rail, and it is display-sized, so it
   can carry the hue at full strength without the page turning into a colour
   chart. */
.stage__years {
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.75rem, 4vw, 2.75rem);
  font-weight: 300;
  line-height: 0.9;
  letter-spacing: -0.04em;
  font-feature-settings: 'tnum' 0;
  color: var(--family-ink);
}

/* Stays grey. It is the same word under every number on the page, so giving
   it the hue as well would double the colour without adding anything. */
.stage__unit {
  font-size: 0.6875rem;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
}

/* --- Body --------------------------------------------------------------- */

.stage__body {
  padding-top: 0.125rem;
  max-width: 42ch;
}

.stage__role {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.25rem, 2.6vw, 1.75rem);
  font-weight: 300;
  line-height: 1.15;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

/* The rail is aria-hidden, so this carries the duration for screen readers.
   Always visually hidden — on screen the bead already says it, and showing
   both just repeats the same number twice. */
.stage__stay-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.stage__detail {
  margin: 0.75rem 0 0;
  color: var(--ink-muted);
}

.stage__detail strong {
  font-weight: 600;
  color: var(--ink);
}

/* --- Wide: alternate sides of a centred rail ---------------------------- */

@media (min-width: 62rem) {
  .stage {
    grid-template-columns: 1fr 8.5rem 1fr;
    column-gap: 0;
  }

  .stage__rail {
    grid-column: 2;
  }

  .stage__body {
    grid-row: 1;
    max-width: none;
  }

  .stage[data-side='left'] .stage__body {
    grid-column: 1;
    text-align: right;
    justify-self: end;
    padding-right: clamp(1rem, 2vw, 2rem);
  }

  .stage[data-side='right'] .stage__body {
    grid-column: 3;
    padding-left: clamp(1rem, 2vw, 2rem);
  }
}

/* =========================================================================
   Common degrees studied — donut
   ====================================================================== */

.degrees {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: center;
  margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
}

@media (min-width: 46rem) {
  .degrees {
    grid-template-columns: auto 1fr;
  }
}

.donut {
  position: relative;
  width: min(19rem, 100%);
  justify-self: center;
}

.donut__svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Exactly one slice is inked; the rest share a single wash. Colour is never
   asked to distinguish six things, so it never has to. */
.donut__slice {
  stroke: var(--bg);
  stroke-width: 1.5;
  cursor: pointer;
  transition: fill 0.25s var(--ease);
}

.donut__slice[data-state='inked'] {
  fill: var(--family-ink);
}

.donut__slice[data-state='washed'] {
  fill: var(--family-faint);
}

/* --- Centre readout ----------------------------------------------------- */

/* The hole is where the reading happens — the only place a figure appears. */
.donut__readout {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3125rem;
  /* Keeps text inside the hole's chord at the label's height, not just
     inside its widest diameter — see MAX_SUBJECT_CHARS in careers.js. */
  padding: 0 22%;
  text-align: center;
  pointer-events: none;
}

.donut__share {
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(2rem, 7vw, 2.75rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.04em;
}

/* The subject is the answer to "what am I looking at" — it carries as much
   weight as the figure above it rather than sitting under it as a caption. */
.donut__label {
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-wrap: balance;
}

/* --- The list: the exact figures a circle can't give you ---------------- */

.degrees__list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

.degree {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  width: 100%;
  padding: 0.6875rem 0;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  background: none;
  font: inherit;
  text-align: left;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.2s var(--ease);
}

.degrees__list li:last-child .degree {
  border-bottom: 0;
}

.degree[data-state='inked'] {
  color: var(--ink);
}

.degree[data-state='inked'] .degree__name {
  font-weight: 500;
}

.degree__share {
  flex: none;
  font-variant-numeric: tabular-nums;
}

.degree[data-state='inked'] .degree__share {
  font-weight: 600;
}

.degree:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* --- What the segment pays out ------------------------------------------ */

.degrees__detail {
  max-width: 62ch;
}

/* Reserved height, so moving between segments doesn't shunt the page around
   as the text length changes. */
.degrees__why {
  margin: 0;
  min-height: 6rem;
  min-height: 4lh;
  color: var(--ink-muted);
}

.degrees__hint {
  margin: 0.75rem 0 0;
  font-size: 0.8125rem;
  color: var(--ink-faint);
  transition: opacity 0.4s var(--ease);
}

/* Once they've found the interaction they don't need telling about it. */
.degrees__detail[data-touched] .degrees__hint {
  opacity: 0;
}

.degrees__hint-tap {
  display: none;
}

@media (hover: none) {
  .degrees__hint-hover {
    display: none;
  }

  .degrees__hint-tap {
    display: inline;
  }
}

/* =========================================================================
   A day in the life — the working day as a calendar
   ====================================================================== */

/* --- The band ----------------------------------------------------------- */

/* A day read top to bottom is a calendar, and everyone can already read one.
   Time is the axis, so no separate axis is drawn — each block states its
   own start. */
.day__band {
  display: flex;
  flex-direction: column;
  gap: 2px; /* surface gap, not a border — see the marks spec */
  min-height: 32rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* flex-basis 0, so a block's whole height is proportional to its duration.
   With the default `auto` basis only the leftover space gets shared out and
   a fifteen-minute standup ends up nearly as tall as a ninety-minute
   stretch. The min-height on the block is the floor that keeps short blocks
   readable and clickable. */
.day__band > li {
  display: flex;
  flex: var(--share, 1) 1 0;
  /* The floor lives on the flex child. On the button instead, the item could
     shrink beneath it and the button would overflow onto the block below. */
  min-height: 2rem;
}

/* One block inked at a time, the same language as the degrees donut. Blocks
   are sized by duration, so the shape of the day reads before any label. */
.day__block {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  width: 100%;
  height: 100%;
  min-width: 0;
  padding: 0 0.875rem;
  border: 0;
  border-radius: 2px;
  overflow: hidden;
  background: var(--family-faint);
  font: inherit;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
  transition: background-color 0.2s var(--ease), color 0.2s var(--ease);
}

.day__block[data-state='current'] {
  background: var(--family-ink);
  color: var(--bg);
}

.day__block:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Each block carries its own start time and name — the calendar is readable
   at rest, and hovering pays out the detail rather than the label. */
.day__block-time,
.day__block-name {
  font-size: 0.875rem;
  line-height: 1.2;
}

.day__block-time {
  flex: none;
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

.day__block-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Readout ------------------------------------------------------------ */

.day__readout {
  margin-top: clamp(1.25rem, 3vw, 1.75rem);
  max-width: 54ch;
}

.day__now {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  margin: 0;
}

.day__clock {
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.5rem, 3vw, 2rem);
  font-weight: 300;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.day__activity {
  font-size: clamp(1rem, 2vw, 1.125rem);
  font-weight: 500;
  letter-spacing: -0.01em;
}

/* Reserved height so moving between blocks doesn't shunt the page around. */
.day__detail {
  margin: 0.5rem 0 0;
  min-height: 7.5rem;
  min-height: 5lh;
  color: var(--ink-muted);
}


.day__detail strong {
  font-weight: 600;
  color: var(--ink);
}

/* --- Wide: calendar on the left, what you're doing beside it ------------ */

@media (min-width: 46rem) {
  .day {
    display: grid;
    grid-template-columns: minmax(15rem, 21rem) 1fr;
    grid-template-rows: auto 1fr;
    column-gap: clamp(2rem, 5vw, 4rem);
    align-items: start;
  }

  .day__band {
    grid-column: 1;
    grid-row: 1 / span 2;
    min-height: 38rem;
  }

  /* The readout sits level with the top of the calendar rather than below
     it, so the block you're pointing at and its description are in view
     together. */
  .day__readout {
    grid-column: 2;
    grid-row: 1;
    margin-top: 0;
  }

}

/* =========================================================================
   Hours — one linear scale
   ====================================================================== */

.hours {
  --track-h: 2.75rem;
}

/* --- Headline ----------------------------------------------------------- */

.hours__headline {
  display: flex;
  align-items: baseline;
  gap: 0.625rem;
}

.hours__figure {
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(3rem, 8vw, 4.5rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.04em;
}

.hours__unit {
  font-size: clamp(1rem, 2vw, 1.125rem);
  color: var(--ink-muted);
}

.hours__context {
  margin: 0.625rem 0 clamp(2.75rem, 6vw, 3.75rem);
  max-width: 46ch;
  color: var(--ink-muted);
}

/* Signed, and coloured with the same valence as the scale. The sign already
   says the direction, so the colour is reinforcement rather than the
   message — which is what keeps a red/green pair defensible. */
.hours__delta {
  font-weight: 600;
  color: var(--ink);
}

.hours__delta[data-direction='more'] {
  color: var(--intense);
}

.hours__delta[data-direction='less'] {
  color: var(--good);
}

/* --- The scale ---------------------------------------------------------- */

.hours__scale {
  position: relative;
  max-width: 44rem;
  height: var(--track-h);
}

/* The gradient's neutral is pinned to the graduate average, so left of it
   reads as lighter than most graduate jobs and right of it as heavier.
   Position on the axis is the encoding; colour only reinforces it. */
.hours__track,
.hours__band {
  position: absolute;
  inset: 0;
  border-radius: 3px;
  background: linear-gradient(
    90deg,
    var(--good) 0%,
    #9c958c var(--neutral-at),
    var(--intense) var(--intense-at),
    var(--intense) 100%
  );
}

/* Outside the band the scale is present but recessive — context, not data. */
.hours__track {
  opacity: 0.16;
}

/* Clipping the same gradient keeps the band's colour honest: it is the
   scale's colour at that position, not a second encoding laid on top. */
.hours__band {
  clip-path: inset(0 calc(100% - var(--to)) 0 var(--from) round 3px);
}

.hours__median {
  position: absolute;
  top: -0.375rem;
  bottom: -0.375rem;
  left: var(--at);
  width: 2px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--ink);
}

/* --- Hover readout ------------------------------------------------------ */

.hours__band {
  padding: 0;
  border: 0;
  cursor: pointer;
}

.hours__band:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 6px;
  border-radius: 5px;
}

.hours__tooltip[hidden] {
  display: none;
}

.hours__tooltip {
  position: absolute;
  z-index: 2;
  /* Below the scale, not above it: hovering must not cover the headline
     figure and the comparison, which are the things being explained. */
  top: calc(100% + 0.625rem);
  left: var(--at);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 13.5rem;
  padding: 0.625rem 0.75rem;
  border-radius: 12px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(10, 10, 10, 0.08), 0 8px 24px rgba(10, 10, 10, 0.1);
  pointer-events: none;
  white-space: nowrap;
}

.hours__tooltip-title {
  font-size: 0.75rem;
  color: var(--ink-faint);
}

.hours__tooltip-rows {
  display: grid;
  gap: 0.1875rem;
  margin-top: 0.375rem;
  font-size: 0.75rem;
}

.hours__tooltip-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  color: var(--ink-muted);
}

.hours__tooltip-row b {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Centring on the band leaves the tooltip a pixel from the edge on a phone,
   and a career with a lighter week would push it off entirely. Below this
   width it anchors to the scale instead, where it cannot overflow. */
@media (max-width: 30rem) {
  .hours__tooltip {
    left: 0;
    transform: none;
    min-width: 0;
    width: 100%;
  }
}

/* --- The graduate average ----------------------------------------------- */

.hours__average {
  position: absolute;
  left: var(--at);
  bottom: calc(100% + 0.5rem);
  width: 1px;
  height: 0.625rem;
  background: var(--hairline-strong);
}

.hours__average span {
  position: absolute;
  bottom: calc(100% + 0.25rem);
  left: 0;
  transform: translateX(-50%);
  font-size: 0.75rem;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* --- Axis --------------------------------------------------------------- */

.hours__axis {
  position: relative;
  max-width: 44rem;
  height: 1.5rem;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

.hours__tick {
  position: absolute;
  top: 0;
  left: var(--at);
  transform: translateX(-50%);
}

/* The first and last ticks are pulled inside the track so they can't
   overhang the section's gutter. */
.hours__tick:first-child {
  transform: none;
}

.hours__tick:last-child {
  transform: translateX(-100%);
}

/* =========================================================================
   Skills — panels that open in place
   ====================================================================== */

/* Equal divisions, deliberately. There is no honest number behind "how much
   of this job is communication", so sizing the panels by a weighting would
   look like data and be invention. This is a layout, not a chart. */
.skills {
  display: flex;
  gap: 2px;
  /* Tall enough for the wordiest skill: every body is laid out at the open
     panel's width whether or not its panel is open, so the row takes the
     height of the longest text and each card gets the same height. The
     floor stops a terse career collapsing into a slot. */
  min-height: clamp(21rem, 32vw, 26rem);
  /* So the bodies can size themselves from this block's width (cqw) rather
     than from their own panel's, which is narrow while closed. */
  container-type: inline-size;
  margin: 0;
  padding: 0;
  list-style: none;
  border-radius: 32px;
  overflow: hidden;
}

.skill {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  transition: flex-grow 0.45s var(--ease);
}

/* The opened panel takes most of the room; the others stay wide enough to
   read their spine, which is the whole point of opening in place. */
.skill[data-state='open'] {
  flex-grow: 3.8;
}

.skill__panel {
  position: relative;
  display: block;
  width: 100%;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  border: 0;
  /* The closed panels are most of this section's area, so colouring them is
     what makes the block read as the career's rather than as a grey shelf.
     Every closed panel gets the same tint: the file's own reasoning for equal
     widths applies just as well to colour — there is no honest number behind
     "how much of this job is communication", and a ramp across the row would
     look like a ranking that does not exist. */
  background: var(--family-vivid);
  font: inherit;
  text-align: left;
  /* Stays ink. The only thing visible on a closed panel is the spine, which
     sets its own white; everything else in here is the body, which is only
     ever seen once the panel has opened and turned white behind it. Putting
     white on the panel instead loses the open panel's heading entirely. */
  color: var(--ink);
  cursor: pointer;
  overflow: hidden;
  /* Closed panels sit back, so the open one reads as lifted out of the block
     without needing a shadow the container would clip anyway. Deeper than the
     old inset, because the same 5% black that read as a recess on a pale grey
     panel is invisible on a saturated one. */
  box-shadow: inset 0 1px 4px rgba(10, 10, 10, 0.18);
  transition: background-color 0.35s var(--ease), box-shadow 0.35s var(--ease);
}

/* Lightens rather than deepens: on a dark panel, coming forward is the thing
   the pointer should mean. */
.skill__panel:hover {
  background: var(--family-vivid-lift);
}

.skill[data-state='open'] .skill__panel,
.skill[data-state='open'] .skill__panel:hover {
  background: var(--surface);
  box-shadow: none;
  /* Still a pointer: a card opens as soon as it is hovered, so the open one
     is the only state a pointer ever rests on. An arrow there would say the
     block does nothing. */
  cursor: pointer;
}

.skill__panel:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -4px;
}

/* --- Spine: the compressed label ---------------------------------------- */

/* Vertical, so a narrow panel still says which skill it is. It cross-fades
   with the body rather than one element changing writing-mode, which would
   snap rather than transition. Laid over the whole panel and centred, so it
   takes no part in the panel's height. */
.skill__spine {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1rem, 1.7vw, 1.25rem);
  /* Heavier than the display weight used elsewhere. A closed panel is a label
     to be picked out of a row at a glance, not a line to be read, and 300 on
     a saturated ground goes thin and apologetic. */
  font-weight: 500;
  letter-spacing: -0.01em;
  white-space: nowrap;
  writing-mode: vertical-rl;
  color: #fff;
  opacity: 1;
  transition: opacity 0.25s var(--ease), color 0.25s var(--ease);
}

.skill__panel:hover .skill__spine {
  color: #fff;
}

.skill[data-state='open'] .skill__spine {
  opacity: 0;
}

/* --- Body: the opened content -------------------------------------------- */

/* In flow, at a fixed width, so the text does not reflow line by line while
   the panel animates — it is revealed rather than re-wrapped. The width is
   what the open panel offers: three equal shares with the open one grown to
   3.8, so it takes 3.8/5.8 of the block. A closed panel is narrower than its
   body and simply clips it, while still standing as tall as it. */
.skill__body {
  position: relative;
  display: block;
  width: min(30rem, calc(100cqw * 0.655 - 2 * clamp(1.5rem, 3vw, 2.5rem)));
  opacity: 0;
  transition: opacity 0.25s var(--ease);
  pointer-events: none;
}

.skill[data-state='open'] .skill__body {
  opacity: 1;
  transition-delay: 0.15s;
}

.skill__name {
  display: block;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(1.75rem, 3.4vw, 2.5rem);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.skill__rule {
  display: block;
  width: 2.5rem;
  height: 1px;
  margin: 1.125rem 0;
  background: var(--hairline-strong);
}

.skill__detail {
  display: block;
}

.skill__detail p {
  margin: 0 0 0.75rem;
  color: var(--ink-muted);
}

.skill__detail p:last-child {
  margin-bottom: 0;
}

/* The opening line is set in full ink: somewhere for the eye to land before
   it commits to the rest. Weight and colour do that on their own — setting
   it larger as well made the panel read as having two type sizes in three
   lines, which is a lot of hierarchy for one short paragraph. */
.skill__detail p.skill__lede {
  color: var(--ink);
}

.skill__detail strong {
  font-weight: 600;
  color: var(--ink);
}

/* --- Narrow: stack, and show everything --------------------------------- */

/* Panels this thin cannot hold three paragraphs, and there is no hover to
   open them with. They become a plain stack with every skill already open —
   the same content, without an interaction that would not work. */
@media (max-width: 52rem) {
  .skills {
    flex-direction: column;
    min-height: 0;
    border-radius: 22px;
  }

  .skill,
  .skill[data-state='open'] {
    flex: none;
  }

  .skill__panel,
  .skill[data-state='open'] .skill__panel {
    display: block;
    background: var(--surface);
    box-shadow: none;
    cursor: default;
  }

  .skill__spine {
    display: none;
  }

  .skill__body,
  .skill[data-state='open'] .skill__body {
    position: static;
    width: auto;
    opacity: 1;
    transition: none;
  }
}

/* Nothing here is load-bearing, so with reduced motion the section simply
   arrives: no rise, no stagger, no scripted opening. */
@media (prefers-reduced-motion: reduce) {
  .skills,
  .skill,
  .skill__panel,
  .skill__spine,
  .skill__body {
    transition: none;
  }
}

/* =========================================================================
   Related careers — the page after the profile
   ====================================================================== */

/* Related careers sit on their own ground, full-bleed and running to the
   end of the document — the page closes the way it opened, on a band. The
   profile happens on --bg; the deepened tone is what says that part is
   over, and the white cards read even better here than on the page. */
.related-band {
  margin-top: clamp(2rem, 5vw, 4rem);
  background: var(--bg-deep);
}

.related-band .section {
  padding-block: clamp(3rem, 7vw, 5rem) clamp(3.5rem, 8vw, 6rem);
}

/* A profile section announces itself at reading scale; this heading opens
   a different part of the page altogether, so it speaks in the display
   register the hero does — a clear step above the section titles, and a
   clear step below the career's own name, which stays the largest thing
   on the page. */
/* One shared rule for both halves of the line, so the link can never drift
   away from the heading it is set to match. */
.related-band .section__title,
.related-head__link {
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(2.5rem, 6vw, 4.25rem);
  font-weight: 300;
  letter-spacing: -0.04em;
  line-height: 1;
}

/* Heading on the left, the way out on the right, as two halves of one
   display line — the same register, so the pair reads as a single
   statement: related careers, or view all. Wrapping lets the link drop to
   its own line on screens too narrow to hold both. */
.related-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
}

.related-head__link {
  flex: none;
  color: var(--ink);
  text-decoration: none;
}

/* The arrow is the affordance: it leads the way on hover so the type can
   stay still — an underline at this size would be a second horizon line. */
.related-head__arrow {
  display: inline-block;
  transition: transform 0.25s var(--ease);
}

.related-head__link:hover .related-head__arrow {
  transform: translateX(0.12em);
}

.related-head__link:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .related-head__arrow {
    transition: none;
  }
}

/* The heading stands alone — no lede — so the gap the lede used to supply
   is the grid's to carry, sized up to hold its own against the larger
   heading. */
.related-band .grid {
  margin-top: clamp(2rem, 4vw, 3rem);
}

/* =========================================================================
   Table of contents — rail on wide screens, sticky bar on narrow
   ====================================================================== */

/* Anchors would otherwise land under the sticky bar. */
.section__title {
  scroll-margin-top: 4.5rem;
}

.section__title:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 6px;
}

/* --- The rail ----------------------------------------------------------- */

/* A narrow column of marks in the viewport's edge that widens into a panel on
   hover. The rail is fixed, so animating its width costs the page no layout
   at all — which is what lets the labels live in normal flow rather than
   being positioned out of it. One container means one background and one
   shadow, instead of six chips with six seams.

   The open state borrows the surface and shadow the tooltips elsewhere use,
   so it reads as part of the same system rather than a new kind of object. */
.contents {
  position: fixed;
  top: 50%;
  right: 1.25rem;
  width: 3.25rem;
  padding: 0.5rem 0.375rem;
  border-radius: 12px;
  background: transparent;
  box-shadow: 0 0 0 0 transparent;
  overflow: hidden;
  transform: translateY(-50%);
  z-index: 3;
  transition: width 0.28s var(--ease), background-color 0.28s var(--ease),
    box-shadow 0.28s var(--ease), opacity 0.25s var(--ease),
    visibility 0.25s var(--ease);
}

.contents:hover,
.contents:focus-within,
.contents[data-open='true'] {
  /* Measured from the longest label by the contents module; the fallback
     only applies if that has not run yet. */
  width: var(--open-width, 13.25rem);
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(10, 10, 10, 0.08), 0 8px 24px rgba(10, 10, 10, 0.1);
}

.contents[hidden],
.contents-bar[hidden],
.contents-bar__panel[hidden] {
  display: none;
}

/* The contents map the profile, so they leave the page with the reader:
   once the scroll reaches related careers the map has nothing left to
   point at, and both presentations fade out rather than trailing the
   reader into territory they don't cover. Visibility rides along so the
   faded rail is out of the tab order and the accessibility tree too. */
.contents[data-stopped='true'],
.contents-bar[data-stopped='true'] {
  opacity: 0;
  visibility: hidden;
}

.contents__list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Grid items default to min-width: auto, which would hold each row as wide as
   its nowrap label — pushing the marks outside the closed panel and clipping
   them away entirely. */
.contents__list > li {
  min-width: 0;
}

.contents__link {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  height: 1.875rem;
  /* The row's own inset, so the hover fill clears the dash rather than
     stopping flush against it. */
  padding-inline: 0.625rem 0.5rem;
  border-radius: 6px;
  text-decoration: none;
}

.contents__link:hover {
  background: rgba(10, 10, 10, 0.04);
}

/* Drawn inside the row, since the panel clips anything that overflows it. */
.contents__link:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/* --- Labels ------------------------------------------------------------- */

/* Right-aligned so they sit flush against the marks: the marks stay put as
   the panel opens, and the text arrives beside them rather than pushing
   them around. */
.contents__label {
  flex: 1;
  min-width: 0;
  font-size: 0.8125rem;
  line-height: 1.25;
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  color: var(--ink-muted);
  opacity: 0;
  transition: opacity 0.2s var(--ease), color 0.2s var(--ease);
}

.contents:hover .contents__label,
.contents:focus-within .contents__label,
.contents[data-open='true'] .contents__label {
  opacity: 1;
}

.contents__link[data-current='true'] .contents__label {
  color: var(--ink);
  font-weight: 500;
}

.contents__link:hover .contents__label {
  color: var(--ink);
}

/* --- Marks --------------------------------------------------------------- */

/* The box is a constant width and the dash inside it varies. Sizing the box
   itself would change how much room each label had, leaving the current
   row's text a few pixels out of line with the rest. */
.contents__mark {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  width: 1.25rem;
  height: 0.75rem;
}

.contents__mark::after {
  content: '';
  width: 0.75rem;
  height: 1px;
  border-radius: 1px;
  background: var(--hairline-strong);
  transition: width 0.25s var(--ease), height 0.25s var(--ease),
    background-color 0.25s var(--ease);
}

.contents__link:hover .contents__mark::after {
  background: var(--ink-muted);
}

/* Current section: longer and heavier, so position is legible from the marks
   alone while the rail is closed. */
.contents__link[data-current='true'] .contents__mark::after {
  width: 1.25rem;
  height: 2px;
  background: var(--ink);
}

@media (prefers-reduced-motion: reduce) {
  .contents,
  .contents__mark::after,
  .contents__label {
    transition: none;
  }
}

/* --- The bar ------------------------------------------------------------ */

.contents-bar {
  position: sticky;
  top: 0;
  z-index: 4;
  background: var(--bg);
  border-bottom: 1px solid var(--hairline);
  transition: opacity 0.25s var(--ease), visibility 0.25s var(--ease);
}

.contents-bar__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  width: 100%;
  height: 3rem;
  padding-inline: var(--gutter);
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.875rem;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
}

.contents-bar__chevron {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.25s var(--ease);
}

.contents-bar__toggle[aria-expanded='true'] .contents-bar__chevron {
  transform: translateY(1px) rotate(-135deg);
}

.contents-bar__panel {
  margin: 0;
  padding: 0 0 0.5rem;
  list-style: none;
  border-top: 1px solid var(--hairline);
}

.contents-bar__panel .contents__link {
  display: block;
  width: auto;
  height: auto;
  padding: 0.625rem var(--gutter);
  border-inline-start: 2px solid transparent;
  font-size: 0.875rem;
  color: var(--ink-faint);
}

.contents-bar__panel .contents__link[data-current='true'] {
  color: var(--ink);
  font-weight: 500;
}

.contents-bar__panel .contents__link[data-current='true'] {
  border-inline-start-color: var(--ink);
}

/* =========================================================================
   Browse — every career, as a filterable grid
   ====================================================================== */

/* Only the h1 is missing from view, and only because the grid says what the
   page is faster than a sentence about the grid would. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- The category row ----------------------------------------------------
   The coarsest cut on the site, so it sits above everything and stays put:
   in a grid this long, the row is the answer to "what am I looking at" as
   much as it is a control. Opaque, because cards scroll under it. */

.categories {
  position: sticky;
  top: 0;
  z-index: 5;
  margin-top: clamp(1rem, 3vw, 2rem);
  background: var(--bg);
  border-bottom: 1px solid var(--hairline);
}

/* Scrolls sideways rather than stacking, and the negative margin lets the
   categories run to the edge of the screen while the first one still lines
   up with the page's own content edge. */
.categories__row {
  display: flex;
  gap: 0.5rem;
  margin-inline: calc(-1 * var(--gutter));
  padding: 0.875rem var(--gutter);
  overflow-x: auto;
  scrollbar-width: none;
}

.categories__row::-webkit-scrollbar {
  display: none;
}

/* Where there is room to wrap, wrap. A sideways scroll with no scrollbar
   hides whatever is off the right edge behind a gesture nobody was told
   about, and on a wide screen the only cost of showing every category at
   once is a second line. Narrow screens keep the scroll, where that second
   line would be a fifth one. */
@media (min-width: 62rem) {
  .categories__row {
    flex-wrap: wrap;
    overflow-x: visible;
  }
}

.category {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4375rem;
  flex: none;
  padding: 0.4375rem 0.875rem;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: 0.875rem;
  color: var(--ink-muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease),
    background-color 0.2s var(--ease);
}

/* Explicitly not the pressed one. Hovering ink-on-ink would print black
   text on a black fill and take the category off the screen entirely.

   Hovering a category previews its colour before you commit to it, which is
   the quietest possible version of this: one chip, one hue, and only while
   the pointer is on it. */
.category:hover:not(:disabled):not([aria-pressed='true']) {
  color: var(--family-ink);
  border-color: var(--family-ink);
}

/* The selected chip was already the heaviest thing in the row; this changes
   what colour that weight is, not how much of it there is. Only ever one at a
   time, and it is the same hue the cards below it are about to be labelled
   in. White on it measures 5.8–7.0, so every family clears 4.5:1 at 14px. */
.category[aria-pressed='true'] {
  background: var(--family-fill);
  border-color: var(--family-fill);
  color: var(--family-on-fill);
}

/* Pressing the active category clears it, so it still needs to answer the
   pointer. It softens rather than changing colour — anything else would read
   as a third state on a control that only has two. */
.category[aria-pressed='true']:hover {
  opacity: 0.84;
}

/* Still readable, still in place: a category that has nothing in it under
   the current filters is information, and hiding it would reshuffle the row
   under the reader's hand. */
.category:disabled {
  opacity: 0.4;
  cursor: default;
}

.category:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* --- Layout -------------------------------------------------------------- */

.browse {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  padding-block: clamp(1.75rem, 4vw, 2.75rem) clamp(4rem, 9vw, 7rem);
  align-items: start;
}

@media (min-width: 62rem) {
  .browse {
    grid-template-columns: 15rem 1fr;
  }
}

/* --- The filter panel ---------------------------------------------------- */

/* Below the breakpoint the panel is worth more than the screen it would
   cost, so it folds behind a toggle and the grid starts at the top. */
.filters__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  justify-self: start;
  padding: 0.5rem 1rem;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  font: inherit;
  font-size: 0.875rem;
  color: var(--ink);
  cursor: pointer;
}

.filters__toggle-count[hidden] {
  display: none;
}

.filters__toggle-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.3125rem;
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: var(--bg);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

.filters {
  display: none;
}

.filters[data-open='true'] {
  display: block;
}

@media (min-width: 62rem) {
  .filters__toggle {
    display: none;
  }

  /* Scrolls with the page rather than pinning.

     Sticky was the obvious thing and it does not survive contact with the
     panel's real height: taller than the window, a pinned panel puts its own
     bottom below the fold and keeps it there, because pinning is precisely
     what stops it moving when you scroll for it. The only way to reach the
     hours slider again is to give the panel its own scrollbar inside a page
     that already has one — two scrollbars, one of them permanently visible,
     for a control the reader touches a handful of times.

     So it stays in the flow. The category row above is sticky and carries
     the coarse cut, which is the one worth having always to hand. */
  .filters {
    display: block;
  }
}

.filters__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--hairline-strong);
}

.filters__heading {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.125rem;
  font-weight: 400;
  letter-spacing: -0.01em;
}

.filters__clear {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--ink-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color 0.2s var(--ease);
}

.filters__clear:hover {
  color: var(--ink);
}

/* --- A facet ------------------------------------------------------------- */

.facet {
  padding-block: 1.25rem;
  border-bottom: 1px solid var(--hairline);
}

.facet__title {
  display: block;
  margin: 0 0 0.75rem;
  font-size: 0.875rem;
  font-weight: 500;
}

.facet__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each item is only a wrapper: contents lets every chip share the one
   wrapped row and the list's own gap. */
.facet__list > li {
  display: contents;
}

.facet__list > li[data-folded] {
  display: none;
}

.facet__list[data-expanded='true'] > li[data-folded] {
  display: contents;
}

/* The chip the category row wears, one step down: smaller type, tighter
   padding, and a lighter border at rest, because category is the cut that
   changes what the page is about and these only narrow it — the two rows
   should read as the same kind of control at different ranks. Neutral ink
   rather than a family colour: a theme cuts across families. The control
   underneath is still a checkbox — only the clothes changed. */
.facet__option {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: 0.375rem;
  padding: 0.3125rem 0.6875rem;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  font-size: 0.8125rem;
  color: var(--ink-muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease),
    background-color 0.2s var(--ease), opacity 0.2s var(--ease);
}

.facet__option:hover:not(:has(input:disabled)):not(:has(input:checked)) {
  color: var(--ink);
  border-color: var(--ink);
}

/* Visually absorbed into the chip, functionally still a checkbox: present
   and focusable, so keyboard and screen readers keep the real control. */
.facet__option input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

/* Ticked wears the fill the pressed "All sectors" chip does — ink, not a
   family colour, because several can be on at once and a row of competing
   hues would out-shout the category that actually sets the page. */
.facet__option:has(input:checked) {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bg);
}

/* Unticking still answers the pointer — mirrors the pressed category. */
.facet__option:has(input:checked):hover {
  opacity: 0.84;
}

.facet__option:has(input:disabled) {
  opacity: 0.4;
  cursor: default;
}

.facet__option:has(input:focus-visible) {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.facet__count {
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

/* Inside a chip the count dims with the chip's own colour instead of
   holding the panel's faint grey — same treatment as the category row,
   scaled down with the chip. */
.facet__option .facet__count {
  font-size: 0.6875rem;
  color: inherit;
  opacity: 0.6;
}

.category .facet__count {
  color: inherit;
  opacity: 0.6;
}

/* 0.6 was set against the ink fill, where a dimmed greige still has plenty of
   room. Over a mid-tone family colour the same dimming measures 3.1–3.6:1 and
   fails; 0.88 brings the worst hue to 4.9:1. It reads as barely dimmed, which
   is the price of the count staying legible on ten different grounds. */
.category[aria-pressed='true'][data-value]:not([data-value='']) .facet__count {
  opacity: 0.88;
}

.facet__more {
  margin-top: 0.5rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--ink-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.facet__more:hover {
  color: var(--ink);
}

/* --- A range ------------------------------------------------------------- */

/* One thumb. The readout carries the value in words, so the thumb never has
   to be read as a position on an unlabelled line. */
.facet__readout {
  display: block;
  margin-bottom: 0.5rem;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.125rem;
  letter-spacing: -0.01em;
}

.facet__slider {
  width: 100%;
  margin: 0;
  accent-color: var(--ink);
  cursor: pointer;
}

.facet__slider:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
  border-radius: var(--radius-pill);
}

.facet__scale {
  display: flex;
  justify-content: space-between;
  margin-top: 0.25rem;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

/* --- Results ------------------------------------------------------------- */

.results__count {
  margin: 0 0 1.25rem;
  font-size: 0.875rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
  gap: 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.grid[hidden] {
  display: none;
}

.career-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 1.5rem;
  /* Transparent at rest so there is no reflow when hover fills it in. The
     resting card is defined by its shadow alone: a coloured edge on twenty
     cards at once turned the grid into a set of tagged boxes, and the family
     colour is already being carried by the label. */
  border: 1px solid transparent;
  border-radius: var(--radius-card);
  background: var(--surface);
  text-decoration: none;
  color: var(--ink);
  /* Two layers, both warm. A neutral black shadow at low alpha goes grey and
     dead against a warm ground; tinting it toward the paper keeps it reading
     as a shadow rather than as dirt. The tight layer is contact — it is what
     makes the card sit on the page rather than hover above it — and the wide
     one is the diffuse fall, pulled back with a negative spread so it stays
     under the card instead of haloing around it. */
  box-shadow:
    0 1px 3px rgba(60, 48, 38, 0.06),
    0 10px 28px -14px rgba(60, 48, 38, 0.13);
  transition: border-color 0.25s var(--ease), transform 0.25s var(--ease),
    box-shadow 0.25s var(--ease);
}

/* The lift was already here and was almost invisible: two pixels of movement
   with nothing underneath it to register against. Growing the shadow is what
   makes the rise legible — the card leaves the page rather than just shifting
   up it. */
.career-card:hover {
  border-color: var(--family-line);
  transform: translateY(-2px);
  box-shadow:
    0 2px 4px rgba(60, 48, 38, 0.05),
    0 18px 40px -16px rgba(60, 48, 38, 0.16);
}

.career-card:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* The family label is the one place category colour can be spent without a
   legend: it is the word for the category, printed in the category's colour.
   Measured at 5.8–7.0 against the white card and 4.7–5.7 against the greige,
   so every hue clears 4.5:1 at this size. */
.career-card__family {
  /* A row rather than a line, so the "Coming soon" badge on a listed card can
     sit at the far end of the eyebrow without being part of the family word. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 1.25rem;
  /* A step down from 12px, because caps read optically larger than the same
     size set in lower case — held at 12 they would start competing with the
     career name below them. */
  font-size: 0.6875rem;
  /* Caps set tight are a solid block with no word shape to read; the tracking
     is what puts the letters back into words. The extra weight is the same
     correction in the other direction — at 11px, caps in a mid-tone colour go
     thin and dusty at 400. */
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--family-ink);
}

.career-card__name {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

.career-card__tagline {
  margin: 0.5rem 0 1.5rem;
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

/* Pushed to the foot, so cards of different name lengths still line their
   tags up with each other across a row. */
.career-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin: auto 0 0;
  padding: 0;
  list-style: none;
}

/* The base tag is sized as something to be pressed — 38px tall, because on
   the profile it sits under the title as a target. On a card it is a label
   and nothing else, so it loses the height it was keeping for a thumb and
   the row of them closes up. */
.tag--small {
  height: 1.625rem;
  padding-inline: 0.625rem;
  font-size: 0.75rem;
}

/* Listed-but-unprofiled cards behave exactly like the rest, and now honestly
   so: they are anchors like every other card and lead to a page carrying the
   career's hero. The rule that used to force a pointer on them is gone —
   an <a href> brings its own — but data-listed is still written, because it
   is the one hook that says how much of a career has been written. */

/* The badge a listed career wears until its profile is written. Set in the
   same caps as the family label so it reads as a second piece of eyebrow
   information rather than as a button, and in the quietest ink on the page:
   twenty cards saying "coming soon" should be a fact the eye can check, not
   a chorus. The outline is what keeps it from reading as a tag. */
.soon {
  display: inline-block;
  flex: none;
  padding: 0.2em 0.6em 0.15em;
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* On the hero band the eyebrow is a sentence-case line on a dark ground, so
   the badge sits after the family word and inverts with the rest of the
   band. Alpha rather than a fixed grey, so it holds up on every family hue. */
.hero__family .soon {
  margin-left: 0.75rem;
  vertical-align: 0.1em;
  border-color: rgba(255, 255, 255, 0.4);
  color: rgba(255, 255, 255, 0.78);
}

/* --- Nothing matched ----------------------------------------------------- */

.results__empty {
  max-width: 26rem;
  padding: 3rem 0;
}

.results__empty[hidden] {
  display: none;
}

.results__empty-title {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.5rem;
  font-weight: 400;
  letter-spacing: -0.02em;
}

.results__empty-help {
  margin: 0.75rem 0 1.5rem;
  color: var(--ink-muted);
}

/* =========================================================================
   Contact — the one page that asks

   The site's only form, so it borrows rather than invents: fields are ruled
   underneath in the hairline every table and figure strip uses, and the send
   button wears the fill a ticked filter chip does. A form styled as its own
   little product would read as a different site pasted into this one.
   ====================================================================== */

.contact {
  padding-block: clamp(2.5rem, 7vw, 5rem) clamp(3rem, 8vw, 6rem);
}

.contact__title {
  margin: 0 0 clamp(2rem, 5vw, 3rem);
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: clamp(2.5rem, 7vw, 4.5rem);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.04em;
}

/* --- The form ------------------------------------------------------------
   On the white surface every other page keeps for the thing being read,
   because on this page the form is that thing. */

/* Held to the width of a form rather than the width of the page: nothing
   here is read across, so the measure that suits prose would only push the
   fields apart. Left, with the masthead and the footer, because a column
   that centres itself reads as a different site. */
.contact__form,
.contact__sent {
  max-width: 34rem;
  background: var(--surface);
  border-radius: var(--radius-card);
  padding: clamp(1.5rem, 3.5vw, 2.5rem);
}

.field {
  display: block;
  margin: 0 0 clamp(1.5rem, 3vw, 2rem);
  padding: 0;
  border: 0;
}

/* Full ink and a little weight, so the label reads as the name of the thing
   below it rather than as text sitting inside an empty box. Everywhere else
   on the site a small muted line is a caption; here it had to stop looking
   like one. */
.field__label {
  display: block;
  margin-bottom: 0.4375rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ink);
}

/* A filled well rather than a rule underneath. The underline was quieter and
   more in keeping with the rest of the site, and it left the reader guessing
   which part of the white card was the part to click. The ground is the page
   colour, so the field reads as a hole cut in the card — no new hue, and the
   shape alone does the work. */
.field__input {
  display: block;
  width: 100%;
  padding: 0.6875rem 0.875rem;
  border: 1px solid transparent;
  border-radius: var(--radius-field);
  background: var(--bg);
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  transition: background-color 0.2s var(--ease), border-color 0.2s var(--ease);
}

.field__input:hover {
  background: var(--bg-deep);
}

/* Focus lifts the field off the ground it was cut into and draws its edge.
   Two changes at once, both small: a browser puts a text field in the
   focus-visible state on a click as readily as on a tab, so anything ringed
   and offset here would be worn every time somebody simply starts typing. */
.field__input:focus {
  background: var(--surface);
  border-color: var(--hairline-strong);
  outline: none;
}

.field__input--area {
  resize: vertical;
  min-height: 9rem;
  line-height: 1.5;
}

/* Red is the site's intensity colour, borrowed here for the same reason it
   is used on the hours scale: it is the one hue that already means "look at
   this". The message carries the meaning; the colour only repeats it. */
.field__error {
  margin: 0.5rem 0 0;
  font-size: 0.8125rem;
  color: var(--intense);
}

.field__input[aria-invalid='true'] {
  border-color: var(--intense);
}

/* --- Sending ------------------------------------------------------------- */

.contact__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
}

/* The one filled control on the site. Everything else is an outline, so the
   fill is available to mean "this is the action". */
.button {
  flex: none;
  height: 2.75rem;
  padding-inline: 1.5rem;
  border: 1px solid var(--ink);
  border-radius: var(--radius-pill);
  background: var(--ink);
  font: inherit;
  font-size: 0.9375rem;
  color: var(--bg);
  cursor: pointer;
  transition: opacity 0.2s var(--ease);
}

.button:hover {
  opacity: 0.84;
}

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

.button:disabled {
  opacity: 0.4;
  cursor: default;
}

.contact__status {
  margin: 0;
  font-size: 0.875rem;
  color: var(--ink-muted);
}

.contact__status:empty {
  display: none;
}

.contact__status[data-tone='bad'] {
  color: var(--intense);
}

/* --- Sent ---------------------------------------------------------------- */

.contact__sent {
  margin: 0;
  font-family: 'Inter Tight', Inter, ui-sans-serif, system-ui, sans-serif;
  font-size: 1.5rem;
  font-weight: 400;
  letter-spacing: -0.02em;
}

/* --- Not connected yet, and the trap -------------------------------------
   The notice is only ever on screen while ENDPOINT is unset. Deliberately
   not styled as an error: nothing has gone wrong, the page is simply not
   finished. The fields stay live underneath it — a form nobody can put a
   cursor in tells the reader nothing about what it will be. */

.contact__notice {
  margin: 0 0 clamp(1.5rem, 3vw, 2rem);
  padding-bottom: clamp(1.25rem, 2.5vw, 1.5rem);
  border-bottom: 1px solid var(--hairline);
  font-size: 0.875rem;
  color: var(--ink-muted);
}


/* Off the page rather than display:none — a field that is not rendered at
   all is one some robots know to skip. */
.contact__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

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

.reveal {
  opacity: 1;
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}

body[data-state='ready'] .reveal {
  animation: rise 0.8s var(--ease) both;
  animation-delay: calc(var(--d, 0) * 90ms + var(--i, 0) * 70ms);
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(0.75rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Title words stagger within the h1 rather than as one block. */
body[data-state='ready'] .hero__word {
  animation: rise 0.9s var(--ease) both;
  animation-delay: calc(120ms + var(--i, 0) * 80ms);
}


/* =========================================================================
   Scroll reveal
   Each section arrives as you reach it, and the marks inside it are drawn
   along the axis they are meant to be read along: earnings left to right,
   the ladder top to bottom, the day from morning down to evening. The motion
   is saying which way to read the thing before the lede has been.

   Two rules govern everything below.

   Nothing is hidden by CSS alone. `data-scroll="pending"` is written by
   reveal.js a moment before it starts watching an element, so a page whose
   scripts failed is a static page rather than a blank one.

   Every hidden starting state lives inside this no-preference block. A rule
   that hid an element outside it would leave a reduced-motion reader looking
   at nothing at all, since the animation that was going to bring it back is
   exactly what they have asked not to have.
   ====================================================================== */

@media (prefers-reduced-motion: no-preference) {
  /* --- The common entrance ----------------------------------------------
     A section's own children — heading, lede, the block itself — lift into
     place in reading order. Containers whose contents stagger internally opt
     out with data-self-reveal: an element fading in inside a parent that is
     itself fading in reads as a wash rather than as a sequence. */

  .section[data-scroll='pending'] > *:not([data-self-reveal]) {
    opacity: 0;
    transform: translateY(0.9rem);
  }

  .section[data-scroll='in'] > *:not([data-self-reveal]) {
    transition: opacity 0.75s var(--ease), transform 0.75s var(--ease);
    transition-delay: calc(var(--i, 0) * 90ms);
  }

  /* --- Salary — the line draws itself -----------------------------------
     Ten years of earnings drawn left to right, which is the direction the
     axis already runs in. The band is wiped rather than faded so its leading
     edge travels with the line, and the endpoint callouts hold back until
     the line has arrived under them — they label a point that does not exist
     until then. */

  .chart[data-scroll='pending'] .chart__grid,
  .chart[data-scroll='pending'] .chart__tick {
    opacity: 0;
  }

  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__grid,
  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__tick {
    animation: fade-in 0.5s var(--ease) 0.15s both;
  }

  .chart[data-scroll='pending'] .chart__band {
    clip-path: inset(0 100% 0 0);
  }

  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__band {
    /* Same duration and easing as the strokes it sits under, so the wash's
       leading edge travels with the line rather than running ahead of it. */
    animation: wipe-right 1.3s var(--ease) 0.3s both;
  }

  /* Dash length is measured off the drawn path and set as --len, so the
     stroke is hidden by exactly its own length however wide the plot is. */
  .chart[data-scroll='pending'] .chart__line,
  .chart[data-scroll='pending'] .chart__band-edge {
    stroke-dasharray: var(--len);
    stroke-dashoffset: var(--len);
  }

  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__line,
  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__band-edge {
    stroke-dasharray: var(--len);
    animation: draw 1.3s var(--ease) 0.3s both;
  }

  .chart[data-scroll='pending'] .chart__end-ring,
  .chart[data-scroll='pending'] .chart__end-dot,
  .chart[data-scroll='pending'] .chart__end-label,
  .chart[data-scroll='pending'] .chart__end-caption {
    opacity: 0;
  }

  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__end-ring,
  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__end-dot,
  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__end-label,
  .chart[data-scroll='in'] .chart__svg[data-entrance='first'] .chart__end-caption {
    animation: fade-in 0.55s var(--ease) 1.25s both;
  }

  /* --- The good and the bad ---------------------------------------------
     Both columns run at once. Staggering them one after the other would
     make the second column read as the consequence of the first, and the
     whole point of the section is that neither side comes first. */

  .ledger[data-scroll='pending'] .ledger__title,
  .ledger[data-scroll='pending'] .ledger__item {
    opacity: 0;
    transform: translateY(0.75rem);
  }

  .ledger[data-scroll='in'] .ledger__title,
  .ledger[data-scroll='in'] .ledger__item {
    transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
    transition-delay: calc(var(--i, 0) * 100ms);
  }

  /* --- Progression — the rail grows -------------------------------------
     Watched a rung at a time rather than as one block. The section is
     several screens tall, so a single entrance would run itself out on the
     rungs nobody has scrolled to yet, and they would be sitting there
     already finished by the time they were reached.

     The line grows downward from the dot, which is the direction time runs
     in here: the length of that line is how long people stay at this stage. */

  .stage[data-scroll='pending'] .stage__dot {
    transform: scale(0);
  }

  .stage[data-scroll='in'] .stage__dot {
    transition: transform 0.45s var(--ease);
  }

  .stage[data-scroll='pending'] .stage__line {
    transform: scaleY(0);
  }

  .stage[data-scroll='in'] .stage__line {
    transition: transform 0.7s var(--ease) 0.12s;
  }

  .stage__line {
    transform-origin: top center;
  }

  .stage[data-scroll='pending'] .stage__stay,
  .stage[data-scroll='pending'] .stage__body {
    opacity: 0;
  }

  .stage[data-scroll='pending'] .stage__body {
    /* Each rung enters from its own side of the rail, so the zigzag the
       layout already draws is the path the content travels along. */
    transform: translateX(calc(var(--from, 1) * 0.75rem));
  }

  .stage[data-scroll='in'] .stage__stay {
    transition: opacity 0.5s var(--ease) 0.35s;
  }

  .stage[data-scroll='in'] .stage__body {
    transition: opacity 0.7s var(--ease) 0.1s, transform 0.7s var(--ease) 0.1s;
  }

  @media (min-width: 62rem) {
    /* Only above this width do the stages alternate sides. Below it every
       body sits to the right of the rail, so every rung arrives from the
       right and the default holds. */
    .stage[data-side='left'] {
      --from: -1;
    }
  }

  /* --- Degrees — the ring fills in --------------------------------------
     Slices arrive clockwise from twelve o'clock, in the order the data is
     drawn in, which is also the order of the list beside it: the biggest
     subject lands first, and how gated the career is reads before the ring
     has finished closing. */

  .degrees[data-scroll='pending'] .donut__slice,
  .degrees[data-scroll='pending'] .donut__readout,
  .degrees[data-scroll='pending'] .degree {
    opacity: 0;
  }

  .degrees[data-scroll='pending'] .donut__svg {
    transform: scale(0.96);
  }

  .degrees[data-scroll='in'] .donut__svg {
    transition: transform 0.9s var(--ease);
  }

  .degrees[data-scroll='in'] .donut__slice {
    animation: fade-in 0.5s var(--ease) both;
    animation-delay: calc(var(--i, 0) * 90ms);
  }

  .degrees[data-scroll='in'] .donut__readout {
    transition: opacity 0.6s var(--ease) 0.55s;
  }

  .degrees[data-scroll='in'] .degree {
    transition: opacity 0.6s var(--ease);
    transition-delay: calc(0.1s + var(--i, 0) * 90ms);
  }

  /* --- The day fills in from the morning --------------------------------
     Blocks are clipped open downward, in clock order. The band is a day read
     top to bottom, so it is drawn the way it is lived. */

  .day[data-scroll='pending'] .day__band > li {
    clip-path: inset(0 0 100% 0);
  }

  .day[data-scroll='in'] .day__band > li {
    animation: unroll 0.5s var(--ease) both;
    animation-delay: calc(0.2s + var(--i, 0) * 70ms);
  }

  /* --- Hours — the band opens out from the median -----------------------
     Not a left-to-right wipe: the median is the figure the section leads
     with, and the spread either side of it is the thing being shown. Growing
     it outward from that point says so; sweeping it in from the left would
     make the good weeks look like they come first. */

  .hours[data-scroll='pending'] .hours__band {
    clip-path: inset(0 calc(100% - var(--mid)) 0 var(--mid) round 3px);
  }

  .hours[data-scroll='in'] .hours__band {
    animation: spread 0.9s var(--ease) 0.25s both;
  }

  .hours[data-scroll='pending'] .hours__median {
    transform: scaleY(0);
  }

  .hours[data-scroll='in'] .hours__median {
    transition: transform 0.45s var(--ease) 0.85s;
  }

  .hours[data-scroll='pending'] .hours__average,
  .hours[data-scroll='pending'] .hours__tick {
    opacity: 0;
  }

  .hours[data-scroll='in'] .hours__average {
    transition: opacity 0.5s var(--ease) 1s;
  }

  .hours[data-scroll='in'] .hours__tick {
    transition: opacity 0.4s var(--ease);
    transition-delay: calc(0.3s + var(--i, 0) * 60ms);
  }

  /* --- Skills -----------------------------------------------------------
     The block lifts as one and the panels fade in across it. The rise is on
     the container rather than on the panels because the container clips:
     panels translating inside it would open a gap at the top and read as a
     rendering fault rather than as motion.

     The panels' stagger is on the entrance only. Leaving it on the base rule
     would make every later hover wait out a delay meant for the arrival. */

  .skills[data-scroll='pending'] {
    opacity: 0;
    transform: translateY(20px);
  }

  .skills[data-scroll='in'] {
    transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
  }

  .skills[data-scroll='pending'] .skill__panel {
    opacity: 0;
  }

  .skills[data-scroll='in'] .skill__panel {
    transition: background-color 0.35s var(--ease), box-shadow 0.35s var(--ease),
      opacity 0.45s var(--ease) calc(var(--i, 0) * 110ms);
  }

  @media (max-width: 52rem) {
    /* Stacked, the stagger would run top to bottom past the fold — the last
       panel would be waiting on a delay from an entrance the reader has
       already scrolled through. The block arrives as one instead. */
    .skills[data-scroll='pending'] .skill__panel,
    .skills[data-scroll='in'] .skill__panel {
      opacity: 1;
      transition: background-color 0.35s var(--ease), box-shadow 0.35s var(--ease);
    }
  }

  /* --- The browse grid ---------------------------------------------------
     Cards rise as they are scrolled to, staggered across a row rather than
     down the whole grid — the delay is counted from the start of each row,
     so the hundredth card is not waiting out a queue of ninety-nine.

     Refiltering is a different event and gets a different treatment. The
     reader is looking straight at the grid and has just asked it to change,
     so the answer is one quick settle rather than a fresh entrance per card
     that would have to be scrolled through to be seen. */

  .grid__cell[data-scroll='pending'] {
    opacity: 0;
    transform: translateY(0.9rem);
  }

  .grid__cell[data-scroll='in'] {
    transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
    transition-delay: calc(var(--i, 0) * 70ms);
  }

  .grid[data-refiltered='true'] .grid__cell {
    animation: fade-in 0.35s var(--ease) both;
    animation-delay: calc(var(--i, 0) * 35ms);
  }

  /* --- The hero, on the way out -----------------------------------------
     Scroll-driven rather than triggered: the title tracks the scroll exactly
     as far as it is pushed, which is the one thing an entrance animation
     cannot do. Progressive enhancement — where the timeline is unsupported
     the hero simply scrolls off. */

  @supports (animation-timeline: view()) {
    .hero {
      animation: lift-away linear both;
      animation-timeline: view();
      /* Only the last stretch of the exit. Fading in proportion to how much
         of the hero has left would have the tagline sitting at half opacity
         while it is still in the middle of the screen and perfectly
         readable; held back to here, the fade only ever touches the strip
         still on screen as it slides under the masthead. */
      animation-range: exit-crossing 55% exit-crossing 100%;
    }
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes draw {
  from {
    stroke-dashoffset: var(--len);
  }
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes wipe-right {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes unroll {
  from {
    clip-path: inset(0 0 100% 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes spread {
  from {
    clip-path: inset(0 calc(100% - var(--mid)) 0 var(--mid) round 3px);
  }
  to {
    clip-path: inset(0 calc(100% - var(--to)) 0 var(--from) round 3px);
  }
}

@keyframes lift-away {
  to {
    opacity: 0;
    transform: translateY(-1.5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  body[data-state='ready'] .reveal,
  body[data-state='ready'] .hero__word {
    animation: none;
  }
  .reveal {
    opacity: 1;
  }
  * {
    transition-duration: 0.01ms !important;
  }
}

/* =========================================================================
   Family colour

   Eleven hues, one lightness, one chroma. Hue is the only thing that varies
   between families, which is what stops any one category shouting louder
   than the rest — a palette picked by eye always has a dominant member.

   They sit ~30° apart across the two arcs left over once the chart colours
   are excluded: --good (#2e7d57, ~157°) and --intense (#b3402b, ~35°) already
   mean something on the salary and hours charts, and these keep ±15° clear of
   both. A reader who has learned that green marks the gentler end of a scale
   should never meet a green that just means "Environment".

   Declared on .career-card rather than on :root on purpose. A custom property
   is substituted where it is DECLARED, so deriving these at :root would
   resolve them against :root's hue and every card in the grid would come out
   the same colour whatever --h it set for itself.

   Colour is never the only channel here: the family is written out in full
   next to its own colour, which is what makes ~30° spacing legible.
   ====================================================================== */

/* The fallback hue, for anything whose family is missing or not yet mapped.
   It has to sit BEFORE the map below: these selectors and the [data-family]
   ones weigh the same, so source order is the only thing separating them and
   a default declared afterwards would quietly win every time. */
body,
.career-card,
.category {
  --h: 250;
}

/* The one map, shared by the cards and by the filter row above them. The two
   selectors are the two places a family gets named in markup: cards carry it
   as data-family, category buttons already carried it as data-value for the
   filter itself. Matching both is cheaper than putting a second attribute on
   the buttons that says the same word twice. */
[data-family='Politics'],
.category[data-value='Politics'] { --h: 10; }
[data-family='Finance'],
.category[data-value='Finance'] { --h: 65; }
[data-family='Property & Construction'],
.category[data-value='Property & Construction'] { --h: 95; }
[data-family='Environment'],
.category[data-value='Environment'] { --h: 128; }
[data-family='Transport'],
.category[data-value='Transport'] { --h: 158; }
[data-family='Healthcare'],
.category[data-value='Healthcare'] { --h: 190; }
[data-family='Science & Engineering'],
.category[data-value='Science & Engineering'] { --h: 220; }
[data-family='Technology'],
.category[data-value='Technology'] { --h: 250; }
[data-family='Education'],
.category[data-value='Education'] { --h: 280; }
[data-family='Law'],
.category[data-value='Law'] { --h: 310; }
[data-family='Creative'],
.category[data-value='Creative'] { --h: 340; }

.career-card {
  --family-ink: oklch(0.48 0.125 var(--h));
  --family-line: oklch(0.62 0.15 var(--h));
}

/* A profile is about exactly one career, so the whole page can carry its
   family hue and every chart on it can be drawn in the career's own ink
   rather than in undifferentiated black.

   The line this does not cross: colour here is identity, never encoding.
   Anything that means something — the good/intense valence on the hours
   scale — keeps the colour it was measured for. A reader who has learned
   that green marks the gentler end of a week must not find the same green
   used because a career happens to be filed under Environment. */
body {
  --family-ink: oklch(0.48 0.125 var(--h));
  --family-edge: oklch(0.48 0.125 var(--h) / 0.3);
  --family-wash: oklch(0.48 0.125 var(--h) / 0.12);
  --family-faint: oklch(0.48 0.125 var(--h) / 0.1);
  /* The saturated end of the family, for large areas that carry white type:
     the hero band and the closed skills panels. Lightness is set by the
     quietest thing that sits on it, not by how vivid it could be — see the
     note on .hero-band. */
  --family-vivid: oklch(0.43 0.17 var(--h));
  --family-vivid-lift: oklch(0.49 0.17 var(--h));
}

/* The full-bleed band behind a career profile. Higher chroma than anything on
   the browse grid, because a profile shows one career and has nothing to
   clash with — and dark enough that everything on it inverts to white, which
   is what makes it read as a place rather than as a tinted panel. */
/* Lightness on --family-vivid is set by the quietest thing that has to sit on
   it, not by how vivid it could be. At 0.5 the hero eyebrow at 78% white
   measures 3.6:1 and fails at 14px; 0.43 brings it to 4.7 while solid white
   lands at 6.6 and the tagline at 5.3, so all three tiers clear for every hue
   in the map. */
.hero-band {
  background: var(--family-vivid);
}

/* Neutral by default, because "All sectors" is not a family and should not
   borrow one's colour. It keeps the ink fill it always had, and only the eleven
   real categories derive from a hue. */
.category {
  --family-ink: var(--ink);
  --family-fill: var(--ink);
  --family-on-fill: var(--bg);
}

.category[data-value]:not([data-value='']) {
  --family-ink: oklch(0.48 0.125 var(--h));
  --family-fill: oklch(0.48 0.125 var(--h));
  --family-on-fill: #fff;
}

/* =========================================================================
   Occupational marks
   Abstract marks, one per career, drawn from a shared kit of primitives.
   See assets/marks.js for the drawing contract.
   ====================================================================== */

.career-card__mark {
  display: inline-block;
  width: 1.55em;
  height: 1.55em;
  /* Sized in em so the mark tracks the heading rather than being pinned to a
     pixel value the heading can grow away from. */
  margin-left: 0.32em;
  color: var(--ink);
  /* vertical-align:middle centres on the x-height band, which leaves the mark
     sitting slightly low against a cap-height word. This lifts it onto the
     cap band, where the eye expects it. */
  vertical-align: middle;
  position: relative;
  top: -0.08em;
}

/* -------------------------------------------------------------------------
   Mark animation

   Bespoke per mark, never shared. A generic wobble applied to twenty drawings
   is decoration; a movement that belongs to the specific thing being drawn is
   part of the meaning. Each one gets its own rule, or none at all.

   Rules are keyed to the base `.mark` class rather than to where the mark
   happens to sit, so a drawing behaves the same on a browse card as it does
   at the top of a profile. Only the trigger differs: on the grid it is the
   whole card, on the profile it is the mark itself.
   ---------------------------------------------------------------------- */

/* Paramedic — the trace sweeps in the way a monitor draws it.
   The reveal is clipped in X rather than run along the path, and that is the
   whole point: a monitor advances at a constant horizontal rate, so the flat
   run takes most of the time and the deflection happens almost instantly. A
   path-length animation would draw the spike at the same speed as the flat,
   which is the one thing an ECG never does. */
@keyframes mark-sweep {
  /* Starts at 84% rather than 100%: the mark is optically centred in a
     48-unit box, so the first sixth of a left-to-right wipe crosses empty
     margin. Beginning past it means the trace is moving on the first frame
     instead of after a beat of nothing. */
  from {
    clip-path: inset(0 84% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

.career-card:hover .mark[data-mark='paramedic'],
.career-card:focus-visible .mark[data-mark='paramedic'] {
  animation: mark-sweep 640ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='paramedic'] {
  animation: mark-sweep 640ms linear var(--mark-entrance) both;
}

/* Software Engineer — the mark types itself.

   `</>` is not a shape that opens or rotates; it is three characters someone
   typed. So the characters arrive in the order they would be typed, each as a
   hard cut rather than a fade — a keystroke either happened or it did not —
   with a caret stepping along in front of them and vanishing once the last
   one lands. The intervals are deliberately uneven, because evenly spaced
   keystrokes read as a machine rather than as typing. */
@keyframes mark-type-1 {
  0%,
  21.3% {
    opacity: 0;
  }
  21.4%,
  100% {
    opacity: 1;
  }
}

@keyframes mark-type-2 {
  0%,
  47.5% {
    opacity: 0;
  }
  47.6%,
  100% {
    opacity: 1;
  }
}

@keyframes mark-type-3 {
  0%,
  68.9% {
    opacity: 0;
  }
  69%,
  100% {
    opacity: 1;
  }
}

@keyframes mark-caret {
  0%,
  21.3% {
    transform: translateX(-17.5px);
    opacity: 1;
  }
  21.4%,
  47.5% {
    /* Held 1.5 units further right than a caret would strictly sit. Directly
       against the chevron's open edge it closes it into a solid triangle for
       the tenth of a second before the slash lands. */
    transform: translateX(-1.5px);
    opacity: 1;
  }
  47.6%,
  68.9% {
    transform: translateX(4.5px);
    opacity: 1;
  }
  69%,
  90.4% {
    transform: translateX(17px);
    opacity: 1;
  }
  90.5%,
  100% {
    transform: translateX(17px);
    opacity: 0;
  }
}

/* The caret only exists while the mark is being typed. */
.mark[data-mark='software-engineer'] .mark__caret {
  opacity: 0;
}

.career-card:hover .mark[data-mark='software-engineer'] .mark__type-1,
.career-card:focus-visible .mark[data-mark='software-engineer'] .mark__type-1 {
  animation: mark-type-1 420ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='software-engineer'] .mark__type-1 {
  animation: mark-type-1 420ms linear var(--mark-entrance) both;
}

.career-card:hover .mark[data-mark='software-engineer'] .mark__type-2,
.career-card:focus-visible .mark[data-mark='software-engineer'] .mark__type-2 {
  animation: mark-type-2 420ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='software-engineer'] .mark__type-2 {
  animation: mark-type-2 420ms linear var(--mark-entrance) both;
}

.career-card:hover .mark[data-mark='software-engineer'] .mark__type-3,
.career-card:focus-visible .mark[data-mark='software-engineer'] .mark__type-3 {
  animation: mark-type-3 420ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='software-engineer'] .mark__type-3 {
  animation: mark-type-3 420ms linear var(--mark-entrance) both;
}

.career-card:hover .mark[data-mark='software-engineer'] .mark__caret,
.career-card:focus-visible .mark[data-mark='software-engineer'] .mark__caret {
  animation: mark-caret 420ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='software-engineer'] .mark__caret {
  animation: mark-caret 420ms linear var(--mark-entrance) both;
}

@media (prefers-reduced-motion: reduce) {
  .career-card:hover .mark,
  .career-card:focus-visible .mark,
  .hero__mark,
  body[data-state='ready'] .hero__mark {
    animation: none;
  }

  .career-card:hover .mark *,
  .career-card:focus-visible .mark *,
  .hero__mark *,
  body[data-state='ready'] .hero__mark * {
    animation: none;
  }
}

/* -------------------------------------------------------------------------
   Inverting the hero onto the band

   Everything in here was drawn for ink on paper. On the band the relationship
   flips, and the three tiers of emphasis have to flip with it: the name goes
   to solid white, the tagline and the eyebrow sit back off it, and the tags
   become an outline of the ground rather than a fill on it.
   ---------------------------------------------------------------------- */

.hero-band .hero__name {
  color: #fff;
}

.hero-band .hero__tagline {
  color: rgba(255, 255, 255, 0.86);
}

.hero-band .hero__family {
  color: rgba(255, 255, 255, 0.78);
}

.hero-band .hero__mark {
  color: #fff;
}

/* Borrowed from the band rather than painted on top of it. A solid white pill
   here would out-shout the title, and the site's hairline is drawn for a
   greige ground and disappears entirely on a saturated one. */
.hero-band .tag {
  border-color: rgba(255, 255, 255, 0.32);
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
}

/* -------------------------------------------------------------------------
   The mark on a career profile

   Beside the name, not above it. Set on its own line over the family label it
   read as a separate object that happened to be on the same page; sharing a
   row with the title makes it part of the name, which is what it is.

   It is much larger here than on a browse card — roughly the height of one
   line of the display type — because it is sitting next to letterforms that
   run to 9.5rem, and anything smaller looks like a footnote to them.

   Vertical centring is against the whole title block rather than the first
   line: names here stack a word per line, and aligning to the top would leave
   the mark stranded beside "Software" with "Engineer" hanging below it.
   ---------------------------------------------------------------------- */

/* When the mark performs on a profile.

   The hero's own entrance is already choreographed: the title's words rise
   from 120ms at 80ms apart, and the mark's slot rises from 180ms. Starting
   the drawing's own animation at 520ms puts it just past the point where its
   slot has faded up, so the performance is seen rather than half-hidden, and
   it runs alongside the last of the title landing rather than after it.

   `both` fill matters here: through the delay the mark holds its first frame,
   so it is clipped or untyped from the outset instead of appearing whole and
   then being wiped — which would read as a glitch rather than an entrance. */
.hero {
  --mark-entrance: 520ms;
}

.hero__title {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 3.5vw, 3rem);
  /* Safety valve: the title is shrink-to-fit on its longest word, so on a
     narrow screen a long name plus a large mark can outrun the line. Wrapping
     is a worse layout than sitting side by side, but a better one than
     overflowing. */
  flex-wrap: wrap;
}

.hero__mark-slot {
  flex: none;
}

/* Careers whose mark has not been drawn yet leave the slot empty; without
   this the flex gap would still open a hole beside the title. */
.hero__mark-slot:empty {
  display: none;
}

/* Sized against one line of the display type, not against the viewport. A
   viewport-relative mark looks right beside a one-word name and undersized
   beside a two-word one, because the title block doubles in height while the
   mark does not. Pegging it to the type keeps it the height of a line, which
   is the relationship the eye is actually reading. */
.hero__mark {
  display: block;
  width: calc(var(--hero-type) * 1.28);
  height: calc(var(--hero-type) * 1.28);
  color: var(--ink);
}

/* Policy Adviser — the door opens, and shuts again.

   Scaling the leaf toward its hinge is the flat way to swing a door, and it
   works here because the doorway behind it is drawn: as the leaf narrows, the
   jambs and the threshold are what is left, so it reads as an opening rather
   than as a shape shrinking. It closes again at the end — a mark left as an
   empty doorway is a mark that has lost its subject. */
@keyframes mark-door {
  0% {
    transform: scaleX(1);
  }
  32%,
  62% {
    transform: scaleX(0.08);
  }
  100% {
    transform: scaleX(1);
  }
}

.mark[data-mark='policy-adviser'] .mark__door {
  /* view-box, not the default fill-box, so the origin is the hinge in the
     drawing's own coordinates rather than an edge of the leaf's bounding box
     — which moves as the leaf scales. */
  transform-box: view-box;
  transform-origin: 17px 30px;
}

.career-card:hover .mark[data-mark='policy-adviser'] .mark__door,
.career-card:focus-visible .mark[data-mark='policy-adviser'] .mark__door {
  animation: mark-door 900ms var(--ease);
}

body[data-state='ready'] .hero__mark[data-mark='policy-adviser'] .mark__door {
  animation: mark-door 900ms var(--ease) var(--mark-entrance) both;
}

/* Barrister — the scales find balance.

   A damped swing: each overshoot is roughly a third of the last, and it lands
   on level rather than easing into it. Scales settle by running out of energy,
   so the amplitude has to decay while the rhythm stays even. The pans ride the
   beam rather than hanging plumb, which is wrong in physics and right at 34px
   — pans that stayed level while the beam tilted would read as a mistake. */
@keyframes mark-balance {
  0% {
    transform: rotate(-18deg);
  }
  30% {
    transform: rotate(11deg);
  }
  52% {
    transform: rotate(-6deg);
  }
  72% {
    transform: rotate(3deg);
  }
  88% {
    transform: rotate(-1.3deg);
  }
  100% {
    transform: rotate(0deg);
  }
}

.mark[data-mark='barrister'] .mark__beam {
  transform-box: view-box;
  /* The pivot, where the beam crosses the post. */
  transform-origin: 24px 14px;
}

.career-card:hover .mark[data-mark='barrister'] .mark__beam,
.career-card:focus-visible .mark[data-mark='barrister'] .mark__beam {
  animation: mark-balance 1100ms var(--ease);
}

body[data-state='ready'] .hero__mark[data-mark='barrister'] .mark__beam {
  animation: mark-balance 1100ms var(--ease) var(--mark-entrance) both;
}

/* Product Manager — the road is laid out, and the flag goes in last.

   The stroke draws the route, and each milestone lands AT THE MOMENT THE
   ROAD REACHES IT. That is the whole trick and it is arithmetic rather than
   taste: the track is 82.56 units long — two straight runs of 20 and 12, a
   third of 16, and two semicircular bends of pi times 5.5 — so a stop 12
   units along the top run is 14.5% of the way through, and at a 900ms draw
   that is 131ms. The four delays below are those figures. Guess them instead
   and the dots arrive next to a road that has not got there yet, which reads
   as a fault rather than as a sequence.

   Each delay is carried on a custom property rather than set in its own
   rule, which is the same trap the data scientist's dots documented: the
   trigger uses the `animation` shorthand, that shorthand resets
   animation-delay to zero, and it outranks any per-dot rule because it
   carries an extra class and a pseudo-class. Set the delays separately and
   all four stops arrive together.

   The flag plants rather than draws. It scales up from its own foot — the
   point where the pole meets the road — on a curve that overshoots very
   slightly and settles, which is what a pole pushed into the ground does.
   Drawing it stroke-first would have made it grow like a plant.

   The road is drawn at a constant rate, because a roadmap is not a
   performance: it is laid out at whatever pace the planning takes, and
   easing it would put a flourish on the one mark in the set that is about
   deciding things in order. */
@keyframes mark-stop {
  from {
    opacity: 0;
    transform: scale(0.2);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes mark-plant {
  from {
    transform: scale(0);
  }
  to {
    transform: scale(1);
  }
}

.mark[data-mark='product-manager'] .mark__track {
  /* One dash as long as the whole path, so the offset can pull it off the
     end and back on again. */
  stroke-dasharray: 100 100;
}

.mark[data-mark='product-manager'] .mark__stops circle {
  /* fill-box, not view-box: each stop scales about its own centre, so the
     reference box has to be the dot rather than the drawing. */
  transform-box: fill-box;
  transform-origin: center;
}

/* Where the drawing stroke actually reaches each stop, as a share of 900ms. */
.mark[data-mark='product-manager'] .mark__stops circle:nth-child(1) {
  --stop-delay: 0ms;
}
.mark[data-mark='product-manager'] .mark__stops circle:nth-child(2) {
  --stop-delay: 131ms;
}
.mark[data-mark='product-manager'] .mark__stops circle:nth-child(3) {
  --stop-delay: 483ms;
}
.mark[data-mark='product-manager'] .mark__stops circle:nth-child(4) {
  --stop-delay: 791ms;
}

.mark[data-mark='product-manager'] .mark__flag {
  transform-box: view-box;
  /* The foot of the pole, where it meets the road. */
  transform-origin: 33px 35px;
}

.career-card:hover .mark[data-mark='product-manager'] .mark__track,
.career-card:focus-visible .mark[data-mark='product-manager'] .mark__track {
  animation: mark-draw 900ms linear;
}

.career-card:hover .mark[data-mark='product-manager'] .mark__stops circle,
.career-card:focus-visible
  .mark[data-mark='product-manager']
  .mark__stops
  circle {
  animation: mark-stop 180ms var(--ease) var(--stop-delay) backwards;
}

.career-card:hover .mark[data-mark='product-manager'] .mark__flag,
.career-card:focus-visible .mark[data-mark='product-manager'] .mark__flag {
  animation: mark-plant 260ms cubic-bezier(0.34, 1.42, 0.64, 1) 880ms backwards;
}

body[data-state='ready'] .hero__mark[data-mark='product-manager'] .mark__track {
  animation: mark-draw 900ms linear var(--mark-entrance) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='product-manager']
  .mark__stops
  circle {
  animation: mark-stop 180ms var(--ease)
    calc(var(--mark-entrance) + var(--stop-delay)) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='product-manager'] .mark__flag {
  animation: mark-plant 260ms cubic-bezier(0.34, 1.42, 0.64, 1)
    calc(var(--mark-entrance) + 880ms) backwards;
}

/* Policy Researcher — the filament takes the current, then the bulb lights.

   Two stages and they are in the right order. The filament draws first,
   from the foot of one leg up over the coil and down the other, which is
   the path the current actually takes; only once it is complete does the
   glass answer. Lighting the glass first would say the idea arrived and the
   work followed, which is the exact reading the drawing was built to avoid.

   The bloom is fast up and slow down — a fifth of the run to rise and the
   rest to fall away. That is the shape a filament actually flashes with when
   it is switched on: it surges past its working temperature and settles
   back, and eyes know the asymmetry even when they cannot name it. A
   symmetrical fade would read as a pulsing notification dot.

   It settles to nothing rather than staying lit, which is the one liberty.
   A bulb left glowing on the card would mean the resting mark is a lit bulb,
   and a permanent grey wash inside the envelope reads as a printing fault
   rather than as light. Ending at zero also keeps the rest state exactly the
   drawing that was there before any of this was added.

   Peak opacity is 0.22. Enough to be unmistakable against a hairline
   envelope, and short of the point where the filament stops being the
   darkest thing inside the glass. */
@keyframes mark-glow {
  from {
    opacity: 0;
  }
  22% {
    opacity: 0.22;
  }
  to {
    opacity: 0;
  }
}

.mark[data-mark='policy-researcher'] .mark__filament {
  /* One dash as long as the whole path, so the offset can pull it off the
     end and back on again. */
  stroke-dasharray: 100 100;
}

.career-card:hover .mark[data-mark='policy-researcher'] .mark__filament,
.career-card:focus-visible
  .mark[data-mark='policy-researcher']
  .mark__filament {
  animation: mark-draw 420ms linear;
}

.career-card:hover .mark[data-mark='policy-researcher'] .mark__glow,
.career-card:focus-visible .mark[data-mark='policy-researcher'] .mark__glow {
  animation: mark-glow 820ms var(--ease) 340ms backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='policy-researcher']
  .mark__filament {
  animation: mark-draw 420ms linear var(--mark-entrance) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='policy-researcher'] .mark__glow {
  animation: mark-glow 820ms var(--ease) calc(var(--mark-entrance) + 340ms)
    backwards;
}

/* Narrative Designer — two thumbs on the pad.

   The whole point of this one is that the sticks and the buttons move in
   DIFFERENT registers, because they are different kinds of input. A stick is
   analogue: it reports every position between centre and its stop, so it
   travels continuously and eases, and being spring-centred it always comes
   home. A face button is digital: it is down or it is up and there is no
   value in between, so it snaps — the keyframes sit two percent apart, which
   is a hard cut in everything but name. Easing a button press would be a lie
   about the hardware, and the accountant's cell cursor snaps for the same
   reason.

   The two thumbs are deliberately out of phase. Real hands do not move in
   lockstep, and two sticks tracing the same path at the same moment reads as
   a mechanism rather than as someone playing. The amplitudes are held to 1.5
   units, which is a pixel at card size and four on a hero: enough to see,
   and short of the point where the two sticks would touch at full deflection
   toward each other — they clear by about 1.6 units at the worst frame,
   which was checked rather than hoped for.

   The buttons pressed are cross and circle, in that order. On a PlayStation
   pad that is confirm and then back, which is the single most common pair of
   inputs anyone makes, and it is exactly what someone testing a dialogue
   choice would do: take the branch, then back out and try the other one.
   That is the job.

   The d-pad and the other two buttons never move. A hand does not use
   everything at once, and a mark where every part is animated reads as a
   toy. */
@keyframes mark-stick-l {
  0% {
    transform: translate(0, 0);
  }
  20% {
    transform: translate(-1.5px, 1px);
  }
  42% {
    transform: translate(-0.4px, -1.5px);
  }
  64% {
    transform: translate(1px, 0.5px);
  }
  84%,
  100% {
    transform: translate(0, 0);
  }
}

@keyframes mark-stick-r {
  0%,
  12% {
    transform: translate(0, 0);
  }
  32% {
    transform: translate(1.4px, -0.9px);
  }
  54% {
    transform: translate(0.2px, 1.4px);
  }
  74% {
    transform: translate(-1px, -0.3px);
  }
  90%,
  100% {
    transform: translate(0, 0);
  }
}

/* Down, held, up. The two-percent gaps are the cut. */
@keyframes mark-press {
  0%,
  22% {
    transform: scale(1);
  }
  24%,
  76% {
    transform: scale(0.5);
  }
  78%,
  100% {
    transform: scale(1);
  }
}

.mark[data-mark='narrative-designer'] .mark__stick-l,
.mark[data-mark='narrative-designer'] .mark__stick-r {
  transform-box: view-box;
}

/* fill-box so `center` is each dot's own centre rather than the whole mark's,
   which is what lets one keyframe serve both buttons. */
.mark[data-mark='narrative-designer'] .mark__btn-x,
.mark[data-mark='narrative-designer'] .mark__btn-o {
  transform-box: fill-box;
  transform-origin: center;
}

.career-card:hover .mark[data-mark='narrative-designer'] .mark__stick-l,
.career-card:focus-visible
  .mark[data-mark='narrative-designer']
  .mark__stick-l {
  animation: mark-stick-l 1500ms var(--ease);
}

.career-card:hover .mark[data-mark='narrative-designer'] .mark__stick-r,
.career-card:focus-visible
  .mark[data-mark='narrative-designer']
  .mark__stick-r {
  animation: mark-stick-r 1500ms var(--ease);
}

.career-card:hover .mark[data-mark='narrative-designer'] .mark__btn-x,
.career-card:focus-visible .mark[data-mark='narrative-designer'] .mark__btn-x {
  animation: mark-press 300ms linear 460ms backwards;
}

.career-card:hover .mark[data-mark='narrative-designer'] .mark__btn-o,
.career-card:focus-visible .mark[data-mark='narrative-designer'] .mark__btn-o {
  animation: mark-press 300ms linear 900ms backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='narrative-designer']
  .mark__stick-l {
  animation: mark-stick-l 1500ms var(--ease) var(--mark-entrance) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='narrative-designer']
  .mark__stick-r {
  animation: mark-stick-r 1500ms var(--ease) var(--mark-entrance) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='narrative-designer']
  .mark__btn-x {
  animation: mark-press 300ms linear calc(var(--mark-entrance) + 460ms) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='narrative-designer']
  .mark__btn-o {
  animation: mark-press 300ms linear calc(var(--mark-entrance) + 900ms) backwards;
}

/* Police Officer — the cuffs ratchet shut.

   A handcuff does not close by swinging shut like a gate. The bow rotates
   THROUGH the body, tooth by tooth, and the body stays where it is — which
   is why this is a rotation about each ring's own centre rather than a hinge
   somewhere on the rim. Both ends of the bow live under the block, so it can
   turn 50 degrees without either end ever showing daylight: the gap opens on
   one side of the body and closes again as the bow comes round.

   In steps, not smoothly. It is a ratchet — the sound everyone knows is the
   pawl going over the teeth — so the bow advances in seven discrete clicks
   rather than gliding. Seven over fifty degrees is about seven degrees a
   click, which is roughly what the real tooth pitch gives you. At 34px each
   click moves the free end half a pixel and the whole thing reads as one
   quick movement; on a hero it reads as what it is.

   One cuff then the other, because that is an arrest and not a magic trick.
   The bodies and the bar do not move at all: the hardware is already there,
   and only the bows travel.

   One keyframe serves both bows — they turn through the same angle — and
   only transform-origin differs, which is each ring's own centre. */
@keyframes mark-ratchet {
  from {
    transform: rotate(-50deg);
  }
  to {
    transform: rotate(0deg);
  }
}

.mark[data-mark='police-officer'] .mark__bow-a,
.mark[data-mark='police-officer'] .mark__bow-b {
  transform-box: view-box;
}

.mark[data-mark='police-officer'] .mark__bow-a {
  transform-origin: 15.5px 16.5px;
}

.mark[data-mark='police-officer'] .mark__bow-b {
  transform-origin: 32.5px 31.5px;
}

.career-card:hover .mark[data-mark='police-officer'] .mark__bow-a,
.career-card:focus-visible .mark[data-mark='police-officer'] .mark__bow-a {
  animation: mark-ratchet 460ms steps(7, end);
}

.career-card:hover .mark[data-mark='police-officer'] .mark__bow-b,
.career-card:focus-visible .mark[data-mark='police-officer'] .mark__bow-b {
  animation: mark-ratchet 460ms steps(7, end) 320ms backwards;
}

body[data-state='ready'] .hero__mark[data-mark='police-officer'] .mark__bow-a {
  animation: mark-ratchet 460ms steps(7, end) var(--mark-entrance) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='police-officer'] .mark__bow-b {
  animation: mark-ratchet 460ms steps(7, end) calc(var(--mark-entrance) + 320ms)
    backwards;
}

/* Environmental Consultant — the earth turns.

   A translation, not a squash. The land strip scrolls behind the circular
   mask by two globe-widths, which walks the near hemisphere off one limb,
   brings the far one round, and lands on a frame identical to the first.

   It runs from -52 to 0 rather than 0 to -52, so the land travels left to
   right. On a globe facing you, east is to the right and the earth turns
   eastward, so features cross toward the right limb and new ones arrive at
   the left. Scrolling the other way is the same sequence of continents played
   on a planet spinning backwards. Ending at 0 also means the resting mark —
   what sits on the card when nobody is hovering — is the near side: Europe,
   Africa and Asia, which the strip carries twice so the turn can both start
   and finish on it.

   A translation and not a scaleX. An earlier version projected the land the
   way a flat disc pivoting edge-on projects — cos of the angle — and it
   squashed the continents toward the centre and made the planet look like a
   coin being flipped. A sphere's land keeps its size and travels.

   Linear, and slow. Constant angular speed is what a planet has, and easing
   would put an acceleration on something that has not changed speed in four
   and a half billion years. */
@keyframes mark-globe {
  from {
    transform: translateX(-52px);
  }
  to {
    transform: translateX(0);
  }
}

.mark[data-mark='environmental-consultant'] .mark__land {
  transform-box: view-box;
}

.career-card:hover .mark[data-mark='environmental-consultant'] .mark__land,
.career-card:focus-visible
  .mark[data-mark='environmental-consultant']
  .mark__land {
  animation: mark-globe 3200ms linear;
}

body[data-state='ready']
  .hero__mark[data-mark='environmental-consultant']
  .mark__land {
  animation: mark-globe 3200ms linear var(--mark-entrance) both;
}

/* Data Scientist — the scatter plots, then the fit is drawn.

   The order is the argument. Points first and left to right, because that is
   how a reading is taken; the line only afterwards, because a line of best
   fit is a conclusion drawn from data that already exists. Drawing them at
   once, or the line first, would say something false about where it came
   from.

   Each dot's delay is carried on a custom property rather than set by a
   later rule. The trigger below uses the `animation` shorthand, which resets
   animation-delay to 0 — and it outranks any per-dot rule, because it has a
   whole extra class and a pseudo-class on it. The first version of this set
   the delays in their own rules and lost every one of them, so the six dots
   arrived together and the stagger existed only in the stylesheet.

   Each dot also finishes before the next begins. Overlapping them by even a
   little turns six arrivals into one ripple, which is the thing being
   avoided.

   pathLength normalises the diagonal to 100 units so the dash maths does not
   have to know its real length — which is just as well, since the stroke is
   non-scaling and the rendered length changes with the mark's size. */
@keyframes mark-plot {
  from {
    opacity: 0;
    transform: scale(0.3);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes mark-fit {
  from {
    stroke-dashoffset: 100;
  }
  to {
    stroke-dashoffset: 0;
  }
}

.mark[data-mark='data-scientist'] .mark__dots circle {
  /* fill-box, not view-box: each dot scales about its own centre, so the
     reference box has to be the dot rather than the drawing. */
  transform-box: fill-box;
  transform-origin: center;
}

.mark[data-mark='data-scientist'] .mark__dots circle:nth-child(1) {
  --dot-delay: 0ms;
}
.mark[data-mark='data-scientist'] .mark__dots circle:nth-child(2) {
  --dot-delay: 130ms;
}
.mark[data-mark='data-scientist'] .mark__dots circle:nth-child(3) {
  --dot-delay: 260ms;
}
.mark[data-mark='data-scientist'] .mark__dots circle:nth-child(4) {
  --dot-delay: 390ms;
}
.mark[data-mark='data-scientist'] .mark__dots circle:nth-child(5) {
  --dot-delay: 520ms;
}
.mark[data-mark='data-scientist'] .mark__dots circle:nth-child(6) {
  --dot-delay: 650ms;
}

.mark[data-mark='data-scientist'] .mark__fit {
  stroke-dasharray: 100 100;
}

.career-card:hover .mark[data-mark='data-scientist'] .mark__dots circle,
.career-card:focus-visible .mark[data-mark='data-scientist'] .mark__dots circle {
  animation: mark-plot 130ms var(--ease) var(--dot-delay, 0ms) both;
}

.career-card:hover .mark[data-mark='data-scientist'] .mark__fit,
.career-card:focus-visible .mark[data-mark='data-scientist'] .mark__fit {
  animation: mark-fit 520ms var(--ease) 850ms both;
}

body[data-state='ready']
  .hero__mark[data-mark='data-scientist']
  .mark__dots
  circle {
  animation: mark-plot 130ms var(--ease)
    calc(var(--mark-entrance) + var(--dot-delay, 0ms)) both;
}

body[data-state='ready'] .hero__mark[data-mark='data-scientist'] .mark__fit {
  animation: mark-fit 520ms var(--ease) calc(var(--mark-entrance) + 850ms) both;
}

/* Pilot — the aircraft taxis: it comes round to the right, lines up, and
   rolls away off the edge of the card.

   Deliberately not the rocket's move, and the difference is the point. The
   rocket goes straight up out of a standing start because that is the only
   thing a rocket does; an aeroplane's own movement is a heading change, and
   plan view is the one view that can show it — a turn here is a rotation of
   the drawing itself.

   It turns about a point at 24,28: the main gear, aft of the wing root,
   which is what an aircraft on the ground actually pivots around. Turn it
   about the middle of the picture instead and the tail swings the wrong way,
   which anyone who has watched an aircraft push back will read as wrong
   without being able to say why. The nose is the furthest ink from that
   pivot at 19 units, so the sweep stays inside the box — measured, not hoped
   for: at 5..43 across and 9..47 down, nothing clips while it comes round.

   Three phases, three curves. The turn is eased in and out, because it
   starts from a standstill and stops on a heading. Then a beat of nothing,
   holding on the new heading — that pause is the whole difference between
   taxiing and skidding, and it is where the drawing says the aircraft is
   waiting to be released. Then the run, eased IN and never out, because it
   is still gathering speed when it leaves the frame.

   The run is -46 units in the aircraft's own frame, which after the rotation
   is straight off to the right. Squaring that against the geometry: the tail
   sits at x=13 once it has come round, and it needs 35 to clear the right
   edge; the rest is margin for the stroke. Then the frame is empty for the
   last tenth before it cuts back to the stand. */
@keyframes mark-taxi {
  0%,
  10% {
    transform: rotate(0deg) translateY(0);
    animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
  }
  48%,
  58% {
    /* Round onto the new heading, creeping forward as it comes — an aircraft
       turns by rolling, never by pivoting on the spot. */
    transform: rotate(90deg) translateY(-3px);
    animation-timing-function: cubic-bezier(0.55, 0, 1, 1);
  }
  90%,
  100% {
    transform: rotate(90deg) translateY(-46px);
  }
}

.mark[data-mark='pilot'] .mark__aircraft {
  transform-box: view-box;
  /* The main gear. */
  transform-origin: 24px 28px;
}

.career-card:hover .mark[data-mark='pilot'] .mark__aircraft,
.career-card:focus-visible .mark[data-mark='pilot'] .mark__aircraft {
  animation: mark-taxi 2200ms var(--ease);
}

/* `backwards` rather than `both`, for the reason the rocket gives below:
   this one ends with the aircraft off the side of the box. */
body[data-state='ready'] .hero__mark[data-mark='pilot'] .mark__aircraft {
  animation: mark-taxi 2200ms var(--ease) var(--mark-entrance) backwards;
}

/* Graphic Designer — the two primitives draw themselves, the square leading.

   200ms of stagger against a 700ms stroke, so the square is a third of the
   way round before the circle starts and the two are drawing together for
   the rest of it. Both extremes were tried and both were worse: dead in
   unison read as one mechanism with two arms, and a full sequence turned a
   mark about two shapes overlapping into a mark about one shape waiting for
   another. The overlap is the whole point of the drawing, so it had better
   be the whole point of the movement.

   The curves are not the same either. The square is RULED — a pen
   accelerates off a corner and is decelerating into the next one, so it
   eases. The circle is STRUCK, with a compass, and a compass turns at one
   rate for the whole revolution, so it is linear.

   The circle's fill-mode is doing real work: with a delay and no fill it
   would sit there finished for 200ms and then blink back to nothing the
   instant its animation started. `backwards` holds it undrawn through the
   delay instead. It ends where it rests, so nothing needs holding after.

   mark-draw is the same two lines as mark-fit further down. They are kept
   apart on purpose: mark-fit is named for a line of best fit and its comment
   argues about what that line means, so borrowing it here would cost the
   data scientist's mark its own idea for the sake of saving four lines. */
@keyframes mark-draw {
  from {
    stroke-dashoffset: 100;
  }
  to {
    stroke-dashoffset: 0;
  }
}

.mark[data-mark='graphic-designer'] .mark__square,
.mark[data-mark='graphic-designer'] .mark__circle {
  /* One dash as long as the whole outline, so the offset can pull it off the
     end and back on again. */
  stroke-dasharray: 100 100;
}

.career-card:hover .mark[data-mark='graphic-designer'] .mark__square,
.career-card:focus-visible .mark[data-mark='graphic-designer'] .mark__square {
  animation: mark-draw 700ms var(--ease);
}

.career-card:hover .mark[data-mark='graphic-designer'] .mark__circle,
.career-card:focus-visible .mark[data-mark='graphic-designer'] .mark__circle {
  animation: mark-draw 700ms linear 200ms backwards;
}

body[data-state='ready'] .hero__mark[data-mark='graphic-designer'] .mark__square {
  animation: mark-draw 700ms var(--ease) var(--mark-entrance) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='graphic-designer'] .mark__circle {
  animation: mark-draw 700ms linear calc(var(--mark-entrance) + 200ms) backwards;
}

/* Accountant — the cursor steps down a column.

   A cell cursor does not travel. It is in one cell or it is in the next one,
   and the jump between them is instant — which is why this is a hard cut
   rather than a movement, and why the two positions are held either side of
   it rather than eased through. Every other mark in this set that moves,
   moves continuously; this one is the exception because the thing being
   drawn is the exception.

   It steps DOWN rather than across, because entering figures is a column
   job: the cursor starts in the top cell of the right-hand column and works
   the whole column, one figure per cell, landing on the cell the mark rests
   on — so the drawing at rest is where the animation finishes rather than
   where it started. 7 units is exactly one cell.

   The holds are deliberately unequal. A figure takes as long to type as it
   takes, and evenly spaced entries read as a machine rather than as entry —
   the same reasoning as the software engineer's keystrokes. */
@keyframes mark-cell-step {
  0%,
  38% {
    transform: translateY(-14px);
  }
  39%,
  71% {
    transform: translateY(-7px);
  }
  72%,
  100% {
    transform: translateY(0);
  }
}

.mark[data-mark='accountant'] .mark__cell {
  transform-box: view-box;
}

.career-card:hover .mark[data-mark='accountant'] .mark__cell,
.career-card:focus-visible .mark[data-mark='accountant'] .mark__cell {
  animation: mark-cell-step 940ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='accountant'] .mark__cell {
  animation: mark-cell-step 940ms linear var(--mark-entrance) backwards;
}

/* Civil Engineer — the leaves lower into place.

   A bascule closing is hydraulic: it starts slow, runs steady, and eases
   into its seat, because two hundred tonnes of counterweighted deck is not
   dropped. So the movement is one decelerating rotation per leaf, held open
   for a beat first so the eye finds the raised deck before it starts to
   come down.

   Each leaf rotates about its own pier, because that is what the real ones
   do — a bascule is not a drawbridge on one hinge but two half-decks, each
   counterweighted at its own tower. Same angles mirrored, same clock, the
   way the bridge master runs them.

   55 degrees open is the working angle of the real leaves, and it is also
   the pose from which the drawing reads as "lifting bridge" rather than
   "broken deck" — lower and the raised leaves look like a drawing error,
   higher and they vanish against the towers. */
@keyframes mark-bascule-l {
  0%,
  18% {
    transform: rotate(-55deg);
    animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1);
  }
  92%,
  100% {
    transform: rotate(0deg);
  }
}

@keyframes mark-bascule-r {
  0%,
  18% {
    transform: rotate(55deg);
    animation-timing-function: cubic-bezier(0.45, 0, 0.25, 1);
  }
  92%,
  100% {
    transform: rotate(0deg);
  }
}

.mark[data-mark='civil-engineer'] .mark__leaf-l,
.mark[data-mark='civil-engineer'] .mark__leaf-r {
  transform-box: view-box;
}

.mark[data-mark='civil-engineer'] .mark__leaf-l {
  transform-origin: 16.5px 29px;
}

.mark[data-mark='civil-engineer'] .mark__leaf-r {
  transform-origin: 31.5px 29px;
}

.career-card:hover .mark[data-mark='civil-engineer'] .mark__leaf-l,
.career-card:focus-visible .mark[data-mark='civil-engineer'] .mark__leaf-l {
  animation: mark-bascule-l 1100ms linear;
}

.career-card:hover .mark[data-mark='civil-engineer'] .mark__leaf-r,
.career-card:focus-visible .mark[data-mark='civil-engineer'] .mark__leaf-r {
  animation: mark-bascule-r 1100ms linear;
}

body[data-state='ready'] .hero__mark[data-mark='civil-engineer'] .mark__leaf-l {
  animation: mark-bascule-l 1100ms linear var(--mark-entrance) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='civil-engineer'] .mark__leaf-r {
  animation: mark-bascule-r 1100ms linear var(--mark-entrance) backwards;
}

/* Parliamentary Assistant — Westminster goes up, and the clock goes in last.

   Same register as the architect's Shard: a constant-rate draw, no easing,
   because masonry does not accelerate. What is different is that this is a
   drawing with parts, so it gets a build order instead of two stages, and
   the order is the real one. Elizabeth Tower first, one stroke up the west
   face, over the spire and down the east. Then the Palace runs out from its
   foot and Victoria Tower rises at the far end. Then the windows are cut.
   Then the clock face. Then the hands.

   The clock last is not just staging. The Great Clock went into a tower that
   was already standing — it started in May 1859, after the tower had been
   topped out — and it is also the only order that reads: a clock face
   drawn before the tower it sits in looks like a coin with a building
   growing out of it.

   The stages overlap by design. Each one starts before the one before it has
   finished, so the mark reads as a single build rather than five things
   queueing; the Shard does the same with its arris. The whole run is 1480ms,
   which is long for a card, but this is a drawing with twelve strokes in it
   and hurrying it turns the sequence into a flicker.

   The four window slots share one path, so a single offset cuts them in
   sequence — the tower's window, then the three along the Palace, left to
   right, which is the direction the building is being drawn in anyway.

   The ground rule is not animated. It is there before the building is. */
.mark[data-mark='parliamentary-assistant'] .mark__tower,
.mark[data-mark='parliamentary-assistant'] .mark__palace,
.mark[data-mark='parliamentary-assistant'] .mark__slots,
.mark[data-mark='parliamentary-assistant'] .mark__clock,
.mark[data-mark='parliamentary-assistant'] .mark__hand {
  /* One dash as long as the whole path, so the offset can pull it off the
     end and back on again. */
  stroke-dasharray: 100 100;
}

.career-card:hover .mark[data-mark='parliamentary-assistant'] .mark__tower,
.career-card:focus-visible
  .mark[data-mark='parliamentary-assistant']
  .mark__tower {
  animation: mark-draw 620ms linear;
}

.career-card:hover .mark[data-mark='parliamentary-assistant'] .mark__palace,
.career-card:focus-visible
  .mark[data-mark='parliamentary-assistant']
  .mark__palace {
  animation: mark-draw 460ms linear 480ms backwards;
}

.career-card:hover .mark[data-mark='parliamentary-assistant'] .mark__slots,
.career-card:focus-visible
  .mark[data-mark='parliamentary-assistant']
  .mark__slots {
  animation: mark-draw 260ms linear 860ms backwards;
}

.career-card:hover .mark[data-mark='parliamentary-assistant'] .mark__clock,
.career-card:focus-visible
  .mark[data-mark='parliamentary-assistant']
  .mark__clock {
  animation: mark-draw 360ms linear 940ms backwards;
}

.career-card:hover .mark[data-mark='parliamentary-assistant'] .mark__hand,
.career-card:focus-visible
  .mark[data-mark='parliamentary-assistant']
  .mark__hand {
  animation: mark-draw 240ms linear 1240ms backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='parliamentary-assistant']
  .mark__tower {
  animation: mark-draw 620ms linear var(--mark-entrance) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='parliamentary-assistant']
  .mark__palace {
  animation: mark-draw 460ms linear calc(var(--mark-entrance) + 480ms) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='parliamentary-assistant']
  .mark__slots {
  animation: mark-draw 260ms linear calc(var(--mark-entrance) + 860ms) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='parliamentary-assistant']
  .mark__clock {
  animation: mark-draw 360ms linear calc(var(--mark-entrance) + 940ms) backwards;
}

body[data-state='ready']
  .hero__mark[data-mark='parliamentary-assistant']
  .mark__hand {
  animation: mark-draw 240ms linear calc(var(--mark-entrance) + 1240ms) backwards;
}

/* Architect — the tower goes up.

   The tagline is "designs buildings and sees them built", so the movement is
   the building being built: the silhouette draws from the foot of the left
   plane, up over the fork, and back down to the foot of the right, and the
   arris arrives afterwards. Frame first, then the line where two planes
   meet — the order it actually happens in on site.

   The draw runs at a constant rate rather than eased. A tower does not
   accelerate; it goes up floor by floor at whatever rate the crane manages,
   and easing the outline made it read as a swipe. The one liberty is that
   the offset carries the stroke over the fork without pausing, which is the
   moment the crown fails to close — the whole point of the drawing, and
   worth arriving at mid-movement rather than at the end.

   The ground rule is not animated. It is there before the building is. */
.mark[data-mark='architect'] .mark__tower,
.mark[data-mark='architect'] .mark__arris {
  /* One dash as long as the whole path, so the offset can pull it off the
     end and back on again. */
  stroke-dasharray: 100 100;
}

.career-card:hover .mark[data-mark='architect'] .mark__tower,
.career-card:focus-visible .mark[data-mark='architect'] .mark__tower {
  animation: mark-draw 760ms linear;
}

.career-card:hover .mark[data-mark='architect'] .mark__arris,
.career-card:focus-visible .mark[data-mark='architect'] .mark__arris {
  animation: mark-draw 260ms linear 620ms backwards;
}

body[data-state='ready'] .hero__mark[data-mark='architect'] .mark__tower {
  animation: mark-draw 760ms linear var(--mark-entrance) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='architect'] .mark__arris {
  animation: mark-draw 260ms linear calc(var(--mark-entrance) + 620ms) backwards;
}

/* Librarian — the last book is placed on the shelf.

   Not dropped. A book does not arrive vertically and it does not arrive
   level: it comes in leaning, one corner touches down first, and the hand
   lets it tip upright into the row. So the movement is two phases about one
   fixed point — the book's bottom right corner, which is the corner it
   actually stands on while it is still leaning.

   Phase one is the approach, eased OUT: the corner is being aimed at the
   board, and aiming decelerates. Phase two is the tip, eased IN, because
   from the moment the hand lets go the book is falling the last few degrees
   and it stops dead against the board. Reversing those two curves is what
   made the first version read as a drop.

   The lean is 16 degrees. Much less and it reads as a drop with a wobble;
   much more and the book fouls its neighbour on the way in — at 16 the top
   corner passes 4 units clear of the book beside it, which was measured
   rather than eyeballed.

   The book stands 15 units tall with its top edge at y=19 and the window
   starts at y=1, so -38 puts it clear of the top before it starts, with the
   6-unit offset to the right putting it outside the row rather than directly
   above the slot it is going into. */
@keyframes mark-shelve {
  0%,
  10% {
    transform: translate(6px, -38px) rotate(16deg);
  }
  55% {
    /* Touchdown: the corner is on the board, the book is still leaning. */
    transform: translate(0, 0) rotate(16deg);
    animation-timing-function: cubic-bezier(0.6, 0, 0.9, 1);
  }
  82%,
  100% {
    transform: translate(0, 0) rotate(0deg);
  }
}

.mark[data-mark='librarian'] .mark__book {
  transform-box: view-box;
  /* The corner the book stands on while it is leaning, and turns about as it
     comes upright. */
  transform-origin: 36px 34px;
}

.career-card:hover .mark[data-mark='librarian'] .mark__book,
.career-card:focus-visible .mark[data-mark='librarian'] .mark__book {
  animation: mark-shelve 1100ms var(--ease);
}

body[data-state='ready'] .hero__mark[data-mark='librarian'] .mark__book {
  animation: mark-shelve 1100ms var(--ease) var(--mark-entrance) both;
}

/* Venture Capitalist — the rocket lights, strains, and goes.

   The sequence is the argument, and it is the same one the job runs on:
   nothing happens for a beat, then everything happens at once. Ignition is a
   hard cut rather than a fade, because a motor is either lit or it is not.
   The airframe settles a fraction of a unit under thrust before it moves —
   the hold-downs are still on — and that settle is what makes the climb
   afterwards read as thrust rather than as something drifting upward.

   The climb is eased IN, never out. A launch is constant thrust against a
   falling mass, so it is still accelerating when it leaves the frame; easing
   out would have it arriving somewhere, and it is not arriving anywhere the
   card can show.

   Then the frame is empty for half a second before the rocket is back on the
   pad. That pause is doing real work: without it the reset lands on the same
   frame as the departure and reads as a bounce. With it, the mark says the
   thing left, and the pad is ready for the next one — which is the actual
   shape of the business.

   Travel is -58 units, and it has to be more than the box is tall — which is
   not obvious, and is why the first version left a wedge of flame hanging at
   the top of the card. The rocket's own height is only about 24 units, but
   the plume is stretched to 3.4x by then, which puts its tip at y=49: the
   very bottom edge of the window. Clearing the top therefore means moving
   the whole 48-unit height of the viewBox, not the height of the drawing.
   58 leaves ten units of margin for the stroke width and for any later
   change to how far the flame draws out. Overshooting costs nothing, since
   everything past the edge is clipped. */
@keyframes mark-launch {
  0%,
  8% {
    transform: translateY(0);
  }
  20% {
    transform: translateY(0.8px);
  }
  30% {
    transform: translateY(0);
    /* Ease-in applies from here to the next stop: this is the climb. */
    animation-timing-function: cubic-bezier(0.55, 0, 1, 1);
  }
  72%,
  100% {
    transform: translateY(-58px);
  }
}

/* The plume: lit on a cut, unsteady while the thrust builds, then drawn out
   as the rocket accelerates away from its own exhaust. The flicker stops
   once it is climbing — a motor at full thrust is the steadiest thing in the
   sequence, and it is the ignition that is violent. */
@keyframes mark-burn {
  0%,
  7.9% {
    opacity: 0;
    transform: scaleY(0.2);
  }
  8% {
    opacity: 1;
    transform: scaleY(0.8);
  }
  13% {
    transform: scaleY(1.5);
  }
  17% {
    transform: scaleY(1.05);
  }
  21% {
    transform: scaleY(1.75);
  }
  25% {
    transform: scaleY(1.3);
  }
  30% {
    transform: scaleY(1.9);
  }
  72%,
  100% {
    opacity: 1;
    transform: scaleY(3.4);
  }
}

.mark[data-mark='venture-capitalist'] .mark__rocket,
.mark[data-mark='venture-capitalist'] .mark__plume {
  transform-box: view-box;
}

.mark[data-mark='venture-capitalist'] .mark__plume {
  /* The nozzle: the plume grows downward out of the base, not out of its own
     middle. */
  transform-origin: 24px 32px;
  /* Unlit at rest — and hidden rather than scaled to nothing, because a
     non-scaling stroke still draws a line at zero height. */
  opacity: 0;
}

.career-card:hover .mark[data-mark='venture-capitalist'] .mark__rocket,
.career-card:focus-visible .mark[data-mark='venture-capitalist'] .mark__rocket {
  animation: mark-launch 1800ms var(--ease);
}

.career-card:hover .mark[data-mark='venture-capitalist'] .mark__plume,
.career-card:focus-visible .mark[data-mark='venture-capitalist'] .mark__plume {
  animation: mark-burn 1800ms linear;
}

/* `backwards`, not `both`, and that difference matters here more than
   anywhere else in this file. Every other entrance ends where the mark
   rests, so holding the last frame is free; this one ends with the rocket
   off the top of the box, and `both` would leave a profile hero permanently
   empty. Backwards holds the pad still through the delay and lets the mark
   drop back to its resting state at the end. */
body[data-state='ready'] .hero__mark[data-mark='venture-capitalist'] .mark__rocket {
  animation: mark-launch 1800ms var(--ease) var(--mark-entrance) backwards;
}

body[data-state='ready'] .hero__mark[data-mark='venture-capitalist'] .mark__plume {
  animation: mark-burn 1800ms linear var(--mark-entrance) backwards;
}

@media (prefers-reduced-motion: reduce) {
  .career-card:hover .mark *,
  .career-card:focus-visible .mark *,
  .hero__mark * {
    animation: none;
  }
}

/* =========================================================================
   Footer
   The same deepened ground the profile's end matter sits on, so "the
   content is over" looks the same everywhere: on a career page it
   continues the related band under a hairline seam, on the browse page it
   is the one deep band the page has.
   ====================================================================== */

.footer {
  margin-top: clamp(3rem, 8vw, 6rem);
  background: var(--bg-deep);
  border-top: 1px solid var(--hairline);
}

/* On pages that already end on the deepened ground, a gap would put a
   strip of page between two pieces of the same territory. */
.footer--flush {
  margin-top: 0;
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem 1.75rem;
  padding-top: clamp(2rem, 5vw, 3rem);
}

/* The masthead's nav, mirrored — same size, same quietness. Unlike the
   masthead's it never hides on narrow screens: the foot of the page is
   where a phone reader expects to find their way onward. */
.footer__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.75rem;
  font-size: 0.9375rem;
}

.footer__nav a {
  color: var(--ink-muted);
  text-decoration: none;
  transition: color 0.2s var(--ease);
}

.footer__nav a:hover {
  color: var(--ink);
}

.footer__nav a:focus-visible,
.footer .wordmark:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.footer__meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  margin-top: clamp(1.75rem, 4vw, 2.5rem);
  padding-block: 1.25rem clamp(1.5rem, 4vw, 2.25rem);
  border-top: 1px solid var(--hairline);
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

.footer__meta p {
  margin: 0;
}
