/* Tokens: the whole visual world lives here. Light is the default; dark is
   redefined under the OS preference (unless the visitor pinned light) and under
   an explicit pin.

   The palette is the blue and cyan system of the redesign style guide section 2.
   Navy is structure, cyan is action and signal, magenta is the one counterpoint
   so the page is not monochrome. Every pair that paints text clears 7:1 in both
   themes and the smallest is 7.60:1, which is headroom on purpose: the palette
   before this one shipped pairs in the low sixes and needed a second pass, and a
   pair at 7.05 is one rounding change away from red. */

:root {
  color-scheme: light dark;

  /* Surfaces and the type that sits on them */
  --ink: #0b1218;
  --ground: #e2e8ee;
  --field: #062033;
  --datum: #141c4a;
  --signal: #22b3d4;
  --annot: #7a1f57;

  /* Derived roles. Every one of these is contrast-tested in tools/check-contrast.mjs. */
  --ink-soft: #39434d; /* secondary type on --ground */
  --signal-ink: #0b4a63; /* cyan where it must read as a mark or a rule on --ground */
  --signal-text: #0b4a63; /* the same cyan where it must read as text on --ground */
  --signal-plate: #7de3f4; /* cyan where it must read as text on a plate */
  --rail: #2a3f57; /* structural rule on --ground */
  --leader-ink: #4a5b6e; /* the displaced-label leader, a meaningful graphic at 3:1 on --ground */
  --focus: #0f6f8f; /* focus ring on --ground */
  --focus-plate: #56d8f2; /* focus ring on the --field and --datum plates */
  --annot-plate: #ff9ec9; /* annotation marks on the --field and --datum plates */
  /* Filled cyan and filled magenta carry a fixed dark label in BOTH themes.
     Inheriting the dark theme's --ink onto cyan would give 2.12:1, so the label
     is pinned rather than inherited. */
  --on-annot: #fdf2f8; /* label inside an --annot mark */
  --on-annot-plate: #0b1218; /* label inside an --annot-plate mark */
  --on-signal: #0b1218; /* label on a filled --signal action */
  --on-field: #dbeaf5; /* type on the deep field plate */
  --on-field-soft: #c2d9ea; /* secondary type on the field */
  /* The switch's two rendered states, as opaque colours rather than a
     `color-mix` no gate row can see through. Style guide rule 2.3.1: every
     state that paints text is an opaque token and a row in the gate. The
     plates are the same colour in both themes, so these are too. */
  --switch-selected: #041824;
  --switch-hover: #16394f;
  --on-datum: #dfe6fb;
  --on-datum-soft: #c6cfef;
  --annot-ink: #7a1f57; /* annotation used as body-size type on --ground */
  --hairline: #0b1218;
  --hairline-alpha: 0.24;
  --field-hairline: #dbeaf5;
  --field-hairline-alpha: 0.28;
  --ground-raise: #eef2f6; /* raised surface inside the ground, e.g. cards */
  --selection-bg: #22b3d4;
  --selection-ink: #0b1218;

  /* Type */
  --face-display: Archivo, "Liberation Sans Narrow", "Arial Narrow", system-ui, sans-serif;
  --face-mono: "Martian Mono", "DejaVu Sans Mono", ui-monospace, monospace;
  --wdth-condensed: 72;
  --wdth-normal: 100;

  --step--2: clamp(0.66rem, 0.63rem + 0.14vw, 0.74rem);
  --step--1: clamp(0.81rem, 0.77rem + 0.18vw, 0.92rem);
  --step-0: clamp(1.0625rem, 1.02rem + 0.22vw, 1.1875rem);
  --step-1: clamp(1.33rem, 1.26rem + 0.33vw, 1.55rem);
  --step-2: clamp(1.66rem, 1.54rem + 0.58vw, 2.05rem);
  --step-3: clamp(2.07rem, 1.86rem + 1.03vw, 2.75rem);
  --step-4: clamp(2.59rem, 2.21rem + 1.85vw, 3.8rem);
  --step-5: clamp(3.2rem, 2.4rem + 4vw, 6rem);

  --measure: 66ch;

  /* Space: 4px base, geometric */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs: 0.75rem;
  --space-s: 1rem;
  --space-m: 1.5rem;
  --space-l: 2.25rem;
  --space-xl: 3.5rem;
  --space-2xl: 5.25rem;
  --space-3xl: 8rem;

  --gutter: clamp(1.25rem, 0.9rem + 1.6vw, 2.5rem);
  --chrome-inset: clamp(1rem, 0.6rem + 1.4vw, 3rem); /* the outer plate margin: never over 48px */
  --measure-page: 84rem;
  --axis-strip: 11rem; /* axis, compact selector and status line */

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast: 140ms;
  --dur-mid: 320ms;
  --dur-slow: 620ms;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #e8eef4;
    --ground: #080d13;
    --field: #062033;
    --datum: #141c4a;
    --signal: #22b3d4;
    --annot: #ff9ec9;

    --ink-soft: #b6c8d6;
    --signal-ink: #56d8f2;
    --signal-text: #7de3f4;
    --signal-plate: #7de3f4;
    --rail: #8fa6bb;
    --leader-ink: #6d8095;
    --focus: #56d8f2;
    --focus-plate: #56d8f2;
    --annot-plate: #ff9ec9;
    --on-annot: #0b1218;
    --on-annot-plate: #0b1218;
    --on-signal: #0b1218;
    --on-field: #dbeaf5;
    --on-field-soft: #c2d9ea;
    --switch-selected: #041824;
    --switch-hover: #16394f;
    --on-datum: #dfe6fb;
    --on-datum-soft: #c6cfef;
    --annot-ink: #ff9ec9;
    --hairline: #e8eef4;
    --hairline-alpha: 0.26;
    --field-hairline: #dbeaf5;
    --field-hairline-alpha: 0.3;
    --ground-raise: #111922;
    --selection-bg: #22b3d4;
    --selection-ink: #0b1218;
  }
}

:root[data-theme="dark"] {
  --ink: #e8eef4;
  --ground: #080d13;
  --field: #062033;
  --datum: #141c4a;
  --signal: #22b3d4;
  --annot: #ff9ec9;

  --ink-soft: #b6c8d6;
  --signal-ink: #56d8f2;
  --signal-text: #7de3f4;
  --signal-plate: #7de3f4;
  --rail: #8fa6bb;
  --leader-ink: #6d8095;
  --focus: #56d8f2;
  --focus-plate: #56d8f2;
  --annot-plate: #ff9ec9;
  --on-annot: #0b1218;
  --on-annot-plate: #0b1218;
  --on-signal: #0b1218;
  --on-field: #dbeaf5;
  --on-field-soft: #c2d9ea;
  --switch-selected: #041824;
  --switch-hover: #16394f;
  --on-datum: #dfe6fb;
  --on-datum-soft: #c6cfef;
  --annot-ink: #ff9ec9;
  --hairline: #e8eef4;
  --hairline-alpha: 0.26;
  --field-hairline: #dbeaf5;
  --field-hairline-alpha: 0.3;
  --ground-raise: #111922;
  --selection-bg: #22b3d4;
  --selection-ink: #0b1218;
}

/* Base: faces, reset, landmarks, and the browser surfaces the plate also owns. */

@font-face {
  font-family: Archivo;
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("/assets/fonts/archivo-latin.woff2") format("woff2");
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Martian Mono";
  font-style: normal;
  font-weight: 300 700;
  font-stretch: 75% 112.5%;
  font-display: swap;
  src: url("/assets/fonts/martian-mono-latin.woff2") format("woff2");
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scrollbar-color: var(--signal) var(--ground);
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--face-display);
  font-variation-settings: "wdth" var(--wdth-normal);
  font-size: var(--step-0);
  line-height: 1.55;
  text-rendering: optimizeLegibility;
  /* No `overflow-x: clip` here. It was concealing the enlarged-text overflow
     rather than resolving it, which is the remedy expert 18.4 forbids: the
     measurement improves and the reader still loses the content. The cause is
     fixed at `.plate` instead, and the page-level test asserts both that
     nothing scrolls and that nothing sits outside the viewport. */
}

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

:where(h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote) {
  margin: 0;
}

:where(ul, ol) {
  padding: 0;
  list-style: none;
}

h1,
h2,
h3 {
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

a {
  color: inherit;
  text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
}

a:hover {
  text-decoration-color: var(--focus);
  text-decoration-thickness: 2px;
}

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 1px;
}

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

img {
  height: auto;
}

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

[hidden] {
  display: none !important;
}

.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The mono voice carries measurement only: coordinates, years, chips, legend keys. */
.u-mono {
  font-family: var(--face-mono);
  font-size: var(--step--1);
  font-variation-settings: "wdth" 87.5;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.u-nowrap {
  white-space: nowrap;
}

.u-label {
  font-family: var(--face-mono);
  font-size: var(--step--2);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.skip-link {
  position: absolute;
  z-index: 20;
  top: var(--space-2xs);
  left: var(--space-2xs);
  /* 2.5.5 Target Size (Enhanced). The first control on the page is the one a
     keyboard visitor meets first, so it carries the same 44px floor as the
     rest, measured where it lands rather than where it parks. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-2xs) var(--space-s);
  background: var(--signal);
  color: var(--on-signal);
  font-family: var(--face-mono);
  font-size: var(--step--1);
  text-decoration: none;
  transform: translateY(-160%);
}

.skip-link:focus-visible {
  transform: none;
}

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

  .skip-link {
    transition: transform var(--dur-fast) var(--ease-out);
  }
}

/* Plates: the survey system: chrome, contour field, traverse rail, section plates. */

/* ── Page frame ─────────────────────────────────────────────────────────── */

.plate {
  position: relative;
  padding-block: var(--space-2xl);
  padding-inline: var(--chrome-inset);
  background: var(--ground);
  color: var(--ink);
  /* Defect L1, the same shape as R1 one level down. A grid or flex item
     defaults to a minimum of its own content, so an uppercase condensed title
     at a doubled text size set a floor far wider than the page and pushed the
     whole plate off screen. These may shrink, and the inherited break rule
     lets a long word break rather than widen the column it sits in. It covers
     the plate number, title, lead and anything else inside (expert 18.4). */
  min-width: 0;
  overflow-wrap: anywhere;
}

.plate__inner {
  max-width: var(--measure-page);
  margin-inline: auto;
  min-width: 0;
}

.plate--field {
  background: var(--field);
  color: var(--on-field);
  --ink: var(--on-field);
  --ink-soft: var(--on-field-soft);
  --hairline: var(--field-hairline);
  --hairline-alpha: var(--field-hairline-alpha);
  --focus: var(--focus-plate);
  --annot: var(--annot-plate);
  --on-annot: var(--on-annot-plate);
  --signal-ink: var(--signal-plate);
  --rail: var(--on-field-soft);
}

.plate--datum {
  background: var(--datum);
  color: var(--on-datum);
  --ink: var(--on-datum);
  --ink-soft: var(--on-datum-soft);
  --hairline: var(--on-datum);
  --hairline-alpha: 0.28;
  --focus: var(--focus-plate);
  --annot: var(--annot-plate);
  --on-annot: var(--on-annot-plate);
  --signal-ink: var(--signal-plate);
  --rail: var(--on-datum-soft);
}

.plate__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xs);
  min-width: 0;
  padding-block-end: var(--space-m);
  margin-block-end: var(--space-xl);
  border-block-end: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
}

.plate__num {
  color: var(--ink-soft);
}

.plate__title {
  font-size: var(--step-3);
  font-variation-settings: "wdth" 84;
  text-transform: uppercase;
  letter-spacing: -0.015em;
}

.plate__lead {
  max-width: 48ch;
  color: var(--ink-soft);
  font-size: var(--step-0);
}

/* ── Chrome: language + theme ───────────────────────────────────────────── */

.chrome {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-m);
  align-items: center;
  justify-content: space-between;
}

.chrome__mark {
  color: var(--ink-soft);
}

.chrome__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-m);
  align-items: center;
  justify-content: flex-end;
  min-width: 0;
  max-width: 100%;
  margin-inline-start: auto;
}

/* Below the wide plate the two control sets take their own row rather than
   pushing the inner box past the plate edge (designer finding D01). */
@media (max-width: 74.99rem) {
  .chrome__controls {
    flex: 1 1 100%;
    justify-content: flex-start;
    margin-inline-start: 0;
  }
}

.chrome__set {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

.chrome__legend {
  color: var(--ink-soft);
  margin-inline-end: var(--space-3xs);
}

@media (max-width: 767px) {
  .chrome__legend {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .switch__item {
    padding-inline: var(--space-2xs);
  }
}

.switch {
  display: flex;
  flex-wrap: wrap;
  min-width: 0;
  border: 1px solid var(--on-field-soft);
}

.switch__item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding-inline: var(--space-xs);
  font-family: var(--face-mono);
  font-size: var(--step--2);
  font-variation-settings: "wdth" 87.5;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  /* The pressed bar is reserved here and transparent, so becoming pressed
     changes colour rather than geometry and nothing shifts. */
  border-block-end: 3px solid transparent;
}

.switch__item + .switch__item {
  border-inline-start: 1px solid var(--on-field-soft);
}

.switch__item:hover {
  color: var(--ink);
  background: var(--switch-hover);
}

.switch__item[aria-current="true"],
.switch__item[aria-pressed="true"] {
  color: var(--ink);
  font-weight: 700;
  background: var(--switch-selected);
  /* A real border rather than a shadow, so the pressed bar survives forced
     colors, where a box-shadow does not. The rest of the group reserves the
     same 3px so nothing moves when a button becomes pressed. */
  border-block-end: 3px solid currentColor;
}

/* Icon-only theme buttons: the label is gone, so the box is the target. */
.switch__item .mark {
  width: 24px;
  height: 24px;
}

@media (forced-colors: active) {
  .switch {
    border-color: ButtonText;
  }

  .switch__item {
    forced-color-adjust: auto;
    background: ButtonFace;
    color: ButtonText;
    /* Forced colors repaints a transparent border rather than leaving it
       unseen, so the reserved bar appeared under every button and stopped
       meaning anything. ButtonFace is the button's own surface: the space
       stays reserved, nothing moves, and the bar belongs to the selected one
       again. */
    border-block-end-color: ButtonFace;
  }

  .switch__item + .switch__item {
    border-inline-start-color: ButtonText;
  }

  /* Selection is the bar, and only the bar.

     The design table asked for `Highlight` on `HighlightText`, and inverting
     the surface is the obvious repair when that does not render. Neither
     works, for the same reason: Chromium paints a legibility backplate behind
     text in this mode, in the `Canvas` colour, and any label coloured like
     `Canvas` is then drawn on top of itself. The selected `EN` came out as a
     solid block with no letters in it under `HighlightText` and again under
     `ButtonFace`. So the label keeps `ButtonText`, which the backplate is
     there to make legible, and selection moves entirely onto the 3px rule,
     which is a border and not text and so is painted as asked. Present on the
     selected control, absent on every other: a difference of structure, not
     of colour, which is what this mode leaves us. */
  .switch__item[aria-pressed="true"],
  .switch__item[aria-current="true"] {
    background: ButtonFace;
    color: ButtonText;
    border-block-end-color: Highlight;
  }

  .switch__item:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 2px;
  }
}

/* ── Hero ────────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-l);
  min-height: 100svh;
  padding-block: var(--space-s) var(--space-l);
  align-content: stretch;
  overflow: clip;
}

.hero__inner {
  position: relative;
  z-index: 2;
  min-width: 0;
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-m);
  align-content: stretch;
  max-width: var(--measure-page);
  width: 100%;
  margin-inline: auto;
  min-height: 0;
}

/* Coordinate ticks along the top and left edges of the plate. */
.hero::before,
.hero::after {
  content: "";
  position: absolute;
  z-index: 1;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    var(--tick-direction, to right),
    color-mix(in oklab, var(--field-hairline) 55%, transparent) 0 1px,
    transparent 1px var(--tick-step, 2.5rem)
  );
}

.hero::before {
  inset-block-start: 0;
  inset-inline: 0;
  height: 0.5rem;
}

.hero::after {
  --tick-direction: to bottom;
  inset-block: 0;
  inset-inline-start: 0;
  width: 0.5rem;
}
@media (min-width: 60rem) {
  /* Coordinate ticks along the top and left edges of the plate. */
  .hero::before,
  .hero::after {
    content: "";
    position: absolute;
    z-index: 1;
    pointer-events: none;
    background-image: repeating-linear-gradient(
      var(--tick-direction, to right),
      color-mix(in oklab, var(--field-hairline) 55%, transparent) 0 1px,
      transparent 1px var(--tick-step, 2.5rem)
    );
  }

  .hero::before {
    inset-block-start: 0;
    inset-inline: 0;
    height: 0.5rem;
  }

  .hero::after {
    --tick-direction: to bottom;
    inset-block: 0;
    inset-inline-start: 0;
    width: 0.5rem;
  }
}
.contour__line {
  fill: none;
  stroke: currentColor;
  stroke-width: 0.9;
  vector-effect: non-scaling-stroke;
}

.contour__line--major {
  stroke-width: 1.6;
}

.hero__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-l) var(--space-2xl);
  align-content: center;
  grid-column: 1 / -1;
}

@media (min-width: 60rem) {
  .hero__body {
    grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
    align-items: end;
  }
}

.hero__lede {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-m);
  align-content: end;
  min-width: 0;
}
@media (min-width: 60rem) {
  .hero__fact {
    display: grid;
  }
}
.hero__fact {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-2xs);
  min-width: 0;
}

.hero__fact dt {
  color: var(--on-field-soft);
  font-family: var(--face-mono);
  font-size: var(--step--2);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hero__fact dd {
  margin: 0;
  white-space: nowrap;
  color: var(--signal-plate);
  font-family: var(--face-mono);
  font-size: var(--step-0);
  font-variation-settings: "wdth" 87.5;
  font-variant-numeric: tabular-nums;
}

/* Where the plate is narrow for the text it carries, a long label and its
   value take their own lines instead of running off the edge. The threshold is
   in rem so it answers an enlarged text size as well as a small screen. This
   has to sit after the rules above: it was written before them at the same
   specificity and so never applied, which is why these values were still being
   clipped by the hero at a doubled root (expert 18.4 item 4). */
/* Releasing `nowrap` does not force a wrap, it only permits one, so this is
   safe to apply well above the width where it is strictly needed. It has to
   cover 390px at a 32px root, where a long measured value is wider than the
   screen; the old 22.5rem gate covered that by accident, because rem moved
   with the root, and the px gate that replaced it did not. */
@media (max-width: 767px) {
  .hero__fact {
    display: grid;
  }

  .hero__fact dd,
  .hero__role .u-nowrap {
    white-space: normal;
  }
}

.hero__name {
  font-size: var(--step-5);
  font-variation-settings: "wdth" 66;
  word-spacing: 0.1em;
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 0.88;
  text-transform: uppercase;
  margin-block-end: var(--space-3xs);
}

.hero__role {
  display: block;
  color: var(--on-field);
  font-family: var(--face-mono);
  font-size: var(--step--1);
  font-variation-settings: "wdth" 87.5;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.hero__summary {
  max-width: 54ch;
  color: var(--on-field-soft);
}

.hero__foot {
  display: grid;
  gap: var(--space-m);
  align-items: end;
}

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

@media (max-width: 767px) {
  .hero {
    padding-block: var(--space-2xs) var(--space-m);
  }

  .hero__inner {
    gap: var(--space-s);
  }

  .hero__name {
    /* The floor comes down so the surname fits the column: at a 32px root it
       was breaking into VALAREZ / O, which is the one thing a name must not
       do. The ceiling is untouched, so an enlarged setting still enlarges. */
    font-size: clamp(1.8rem, 1.45rem + 2.2vw, 3.4rem);
  }

  .hero__foot {
    gap: var(--space-s);
  }

  /* On a phone the action comes before the datum strip, so it lands on the first screen. */
  .hero__foot .actions {
    order: -1;
  }
}

.datum {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-s);
  align-items: center;
  padding-block-start: var(--space-xs);
  border-block-start: 1px solid color-mix(in oklab, var(--field-hairline) 28%, transparent);
  color: var(--on-field-soft);
}

.datum__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}

.datum__item + .datum__item::before {
  content: "";
  width: 4px;
  height: 4px;
  background: currentColor;
  transform: rotate(45deg);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s) var(--space-m);
  align-items: center;
}

.button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: 44px;
  padding: var(--space-2xs) var(--space-m);
  background: var(--signal);
  color: var(--on-signal);
  font-family: var(--face-mono);
  font-size: var(--step--1);
  font-weight: 700;
  font-variation-settings: "wdth" 87.5;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--signal);
}

.button:hover {
  background: var(--annot);
  border-color: var(--annot);
  color: var(--on-annot);
}

.link-mono {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--face-mono);
  font-size: var(--step--1);
  font-variation-settings: "wdth" 87.5;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-underline-offset: 0.3em;
}
@media (min-width: 60rem) {
}

/* ── Legend ──────────────────────────────────────────────────────────── */

.legend {
  display: grid;
  gap: var(--space-l);
}

@media (min-width: 960px) {
  .legend {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-xl);
  }
}

.legend__key {
  display: grid;
  gap: var(--space-xs);
  padding-block-start: var(--space-s);
  border-block-start: 2px solid var(--signal-ink);
}

.legend__title {
  font-size: var(--step-1);
  font-variation-settings: "wdth" 88;
  letter-spacing: -0.01em;
}

.legend__evidence {
  color: var(--ink-soft);
  font-size: var(--step--1);
  line-height: 1.6;
}

/* ── Traverse ────────────────────────────────────────────────────────── */

.traverse {
  display: grid;
  gap: 0;
}

.station {
  position: relative;
  display: grid;
  gap: var(--space-2xs) var(--space-m);
  padding-block: var(--space-m);
  /* The decoration offset and the text gutter are one length and a clearance
     derived from it, in the same unit. Written as two literals in different
     units they drifted apart under enlargement: a 0.5rem rail became 16px at a
     32px root while the gutter stayed near 13px, so the rail landed on the
     dates. Neither may go back to being a bare literal. */
  --rail-offset: 0.5rem;
  --rail-clearance: 1.75rem;
  padding-inline-start: calc(var(--rail-offset) + var(--rail-clearance));
  border-block-start: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
}

.station:last-child {
  border-block-end: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
}

@media (min-width: 60rem) {
  .station {
    grid-template-columns: 14rem 1fr;
  }
}

/* The rail runs through every station; the dot is the station mark. */
.station::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--rail-offset);
  width: 1px;
  background: var(--rail);
}

.station::after {
  content: "";
  position: absolute;
  inset-block-start: calc(var(--space-m) + 0.45em);
  inset-inline-start: var(--rail-offset);
  width: 9px;
  height: 9px;
  translate: -4px 0;
  background: var(--ground);
  border: 2px solid var(--rail);
  border-radius: 50%;
}

.station[data-current="true"]::after {
  background: var(--signal-ink);
  border-color: var(--signal-ink);
  scale: 1.45;
}

.station[data-marked="true"]::after {
  background: var(--annot);
  border-color: var(--annot);
  scale: 1.45;
}

.station__meta {
  display: grid;
  gap: var(--space-3xs);
  align-content: start;
  color: var(--ink-soft);
}

.station__dates {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.station__band {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  color: var(--ink-soft);
}

.station__band::before {
  content: "";
  width: 1.25rem;
  height: 1px;
  background: currentColor;
}

.station__now,
.station__mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  width: fit-content;
  padding: 2px var(--space-2xs);
  font-family: var(--face-mono);
  font-size: var(--step--2);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.station__now {
  background: var(--signal);
  color: var(--on-signal);
  font-weight: 700;
}

.station__mark {
  background: var(--annot);
  color: var(--on-annot);
  font-weight: 700;
}

.station__body {
  display: grid;
  gap: var(--space-2xs);
}

.station__org {
  font-size: var(--step-1);
  font-variation-settings: "wdth" 88;
}

.station__role {
  color: var(--ink-soft);
  font-family: var(--face-mono);
  font-size: var(--step--1);
  font-variation-settings: "wdth" 87.5;
  letter-spacing: 0.04em;
}

.plate--field .station__role,
.plate--datum .station__role {
  color: var(--signal-plate);
}

.station__scope {
  max-width: var(--measure);
  color: var(--ink-soft);
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-2xs);
  margin-block-start: var(--space-3xs);
}

.chip {
  padding: 3px var(--space-2xs);
  border: 1px solid color-mix(in oklab, var(--hairline) 38%, transparent);
  color: var(--ink-soft);
  font-family: var(--face-mono);
  font-size: var(--step--2);
  font-variation-settings: "wdth" 75;
  letter-spacing: 0.02em;
}

.traverse__more {
  display: flex;
  justify-content: flex-start;
  margin-block-start: var(--space-m);
}

.button--ghost {
  background: none;
  color: var(--ink);
  border: 1px solid color-mix(in oklab, var(--hairline) 55%, transparent);
}

.button--ghost:hover {
  background: var(--annot);
  border-color: var(--annot);
  color: var(--on-annot);
}

.button__count {
  font-weight: 400;
  opacity: 0.75;
  letter-spacing: 0.04em;
}

/* ── Plates (portfolio) ──────────────────────────────────────────────── */

.works {
  display: grid;
  gap: var(--space-l);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr));
  /* Cards take their own height and align at the top. Stretching them to the
     tallest in the row would pad the shorter one and drag its action down to
     meet the other, which is the forced alignment spec 7.9 refuses. */
  align-items: start;
}

.work {
  display: grid;
  gap: var(--space-s);
  align-content: start;
  /* The gutter is bounded in pixels rather than scaled with the text. A rem
     gutter takes 48px a side at a 32px root, which is 96px of a 256px card, so
     the container shrank exactly when its contents grew. 24px from 600px up,
     which is the composition already accepted at default text (expert R1). */
  padding: clamp(12px, 4vw, 24px);
  min-width: 0;
  /* Inherited, so the title, host, summary, mechanism and chips are all
     covered by one rule. `anywhere` rather than `break-word` because it also
     lowers the element's intrinsic minimum, so a long word can never widen the
     column it sits in; `break-word` hides the overflow and leaves that floor
     in place. A word is only broken when it would otherwise overrun, and no
     hyphen is introduced (expert 17.1). */
  overflow-wrap: anywhere;
  border: 1px solid color-mix(in oklab, var(--hairline) 45%, transparent);
}

.work__lede,
.work__body {
  display: grid;
  gap: var(--space-s);
  align-content: start;
  min-width: 0;
}

.work__evidence {
  min-width: 0;
}

/* The horizontal arrangement that hung on `.work:only-child` is gone with the
   second entry. Both cards read vertically in the same order, take their
   natural height and align at the top, and no rule tries to bring their two
   actions onto a shared baseline (spec 7.9). */

.work__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-s);
  align-items: baseline;
  justify-content: space-between;
  min-width: 0;
  color: var(--ink-soft);
}

.work__host {
  min-width: 0;
}

/* Status and year travel together, 8px apart, so the host is the only thing
   that wraps away from them when the line runs out. */
.work__meta {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs);
  min-width: 0;
}

.work__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
}

.work__status::before {
  content: "";
  width: 7px;
  height: 7px;
  background: var(--signal-ink);
  transform: rotate(45deg);
}

/* Live work carries a filled mark; a finished historical entry carries a
   hollow one. The distinction is the shape, and the label says it in words, so
   neither depends on colour. "Archived" describes this portfolio's shelf, and
   it neither dims the card nor disables its action. */
.work__status[data-status="archived"]::before {
  background: none;
  border: 1px solid var(--on-datum-soft);
}

.work__year {
  font-variant-numeric: tabular-nums;
}

.work__title {
  font-size: var(--step-2);
  font-variation-settings: "wdth" 84;
  text-transform: uppercase;
  min-width: 0;
}

.work__summary {
  max-width: var(--measure);
  min-width: 0;
}

.work__mechanism {
  max-width: var(--measure);
  padding-inline-start: var(--space-s);
  border-inline-start: 1px solid var(--signal-ink);
  color: var(--ink-soft);
  font-size: var(--step--1);
}

/* The action keeps its whole label. It wraps to a second line rather than
   setting a floor wider than the card, its gutters are bounded in pixels like
   the card's, and the 44px target is a floor rather than a fixed height, so
   growing to two lines makes it taller and never smaller. */
.work__link {
  justify-self: start;
  max-width: 100%;
  min-width: 0;
  flex-wrap: wrap;
  padding-inline: clamp(10px, 3vw, 24px);
  text-align: start;
}

.work__image {
  width: 100%;
  height: auto;
  border: 1px solid color-mix(in oklab, var(--hairline) 45%, transparent);
}

/* ── Instruments ─────────────────────────────────────────────────────── */

.instruments {
  columns: 1;
  column-gap: var(--space-xl);
}

@media (min-width: 48rem) {
  .instruments {
    columns: 2;
  }
}

@media (min-width: 75rem) {
  .instruments {
    columns: 3;
  }
}

.instrument {
  break-inside: avoid;
  margin-block-end: var(--space-l);
}

.instruments__key {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-block-start: var(--space-s);
  color: var(--ink-soft);
}

.chip[data-in-use="true"],
.instruments__key::before {
  border-color: var(--signal-ink);
  box-shadow: inset 2px 0 0 var(--signal-ink);
}

.instruments__key::before {
  content: "";
  width: 1.5rem;
  height: 1rem;
  border: 1px solid var(--signal-ink);
}

.instrument__title {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding-block-end: var(--space-2xs);
  margin-block-end: var(--space-s);
  border-block-end: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
  color: var(--ink);
}

.instrument__title::before {
  content: "";
  width: 10px;
  height: 10px;
  background: var(--rail);
}

.instrument:nth-child(2n) .instrument__title::before {
  background: var(--ink-soft);
}

.instrument:nth-child(3n) .instrument__title::before {
  background: var(--signal-ink);
}

/* ── Field notes ─────────────────────────────────────────────────────── */

.notes {
  display: grid;
  gap: var(--space-l);
}

@media (min-width: 48rem) {
  .notes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 75rem) {
  .notes {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.note__title {
  padding-block-end: var(--space-2xs);
  margin-block-end: var(--space-s);
  border-block-end: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
  color: var(--ink-soft);
}

.note__list {
  display: grid;
  gap: var(--space-s);
}

.note__item {
  display: grid;
  gap: 2px;
}

.note__detail {
  color: var(--ink-soft);
  font-size: var(--step--1);
}

/* ── Contact ─────────────────────────────────────────────────────────── */

.contact {
  display: grid;
  gap: var(--space-l);
}

.contact__email {
  /* The clickable box carries the project's 44px target without growing the
     type or reaching past the plate (designer finding DV6). */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-block: var(--space-2xs);
  max-width: 100%;
  font-size: clamp(1.6rem, 0.9rem + 3.6vw, 4.2rem);
  font-variation-settings: "wdth" 72;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
  /* No uppercase. All caps measurably slows reading and hurts dyslexic readers
     most, and this is the address a visitor may retype. It renders exactly as
     it is written (spec 12.4). */
  text-decoration: none;
  color: var(--signal-plate);
  word-break: break-word;
  justify-self: start;
  /* Opaque, not a mix: a rule under text is measured, and a mix is a colour no
     gate row can describe (style guide 2.3.1). */
  border-block-end: 2px solid var(--signal-plate);
}

/* An abbreviation offers its expansion; a dotted rule says so without being
   mistaken for the solid underline a link carries. */
.contact__location abbr[title],
abbr[title] {
  text-decoration: underline dotted currentColor;
  text-underline-offset: 0.2em;
  cursor: help;
}

.contact__email:hover {
  color: var(--annot);
  border-block-end-color: var(--annot);
}

.contact__rows {
  display: grid;
  gap: var(--space-s);
}

@media (min-width: 768px) {
  .contact__rows {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-xl);
  }
}

.contact__row {
  display: grid;
  gap: var(--space-3xs);
  padding-block-start: var(--space-2xs);
  border-block-start: 1px solid color-mix(in oklab, var(--field-hairline) 30%, transparent);
}

.contact__label {
  color: var(--on-field-soft);
}

/* ── Footer ─────────────────────────────────────────────────────────────── */

.footer {
  padding-block: var(--space-l);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-m);
  justify-content: space-between;
  max-width: var(--measure-page);
  margin-inline: auto;
  color: var(--on-field-soft);
}

/* Consent panel. A compact block in the footer's normal flow: not a modal, not
   an overlay, nothing that covers content or traps focus. */
.privacy {
  display: grid;
  gap: var(--space-2xs);
  max-width: var(--measure-page);
  margin-inline: auto;
  margin-block-end: var(--space-l);
  padding-block-end: var(--space-m);
  border-block-end: 1px solid color-mix(in oklab, var(--field-hairline) 34%, transparent);
  min-width: 0;
  overflow-wrap: anywhere;
}

.privacy__title {
  color: var(--on-field-soft);
  font-size: var(--step--2);
}

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

.privacy__body {
  max-width: var(--measure);
  color: var(--on-field);
  font-size: var(--step--1);
}

.privacy__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  min-width: 0;
}

/* The same outlined treatment and 44px floor as every other action here, and
   the label wraps rather than setting a floor wider than the plate. */
.privacy__button {
  max-width: 100%;
  min-width: 0;
  flex-wrap: wrap;
  padding-inline: clamp(10px, 3vw, 24px);
  text-align: start;
}

/* ── Root bounce + 404 ──────────────────────────────────────────────────── */

.bounce {
  display: grid;
  gap: var(--space-l);
  place-content: center;
  min-height: 100svh;
  padding: var(--gutter);
  text-align: start;
}

.bounce__name {
  font-size: var(--step-4);
  font-variation-settings: "wdth" 70;
  text-transform: uppercase;
  letter-spacing: -0.03em;
}

.bounce__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
}

/* ── Motion ─────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: no-preference) {
  /* Every isoline carries pathLength="100", so one rule draws them all and no
     path needs an inline style a `style-src 'self'` policy would block. */
  .contour__line {
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
    animation: draw 1200ms var(--ease-out) forwards;
  }

  .contour__line[data-level="1"] {
    animation-delay: 90ms;
  }

  .contour__line[data-level="2"] {
    animation-delay: 180ms;
  }

  .contour__line[data-level="3"] {
    animation-delay: 270ms;
  }

  .contour__line[data-level="4"] {
    animation-delay: 360ms;
  }

  .contour__line[data-level="5"] {
    animation-delay: 450ms;
  }

  .contour__line[data-level="6"] {
    animation-delay: 540ms;
  }
}

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}

/* ── R2 components ──────────────────────────────────────────────────────── */

/* The mark sprite is markup, not a picture: it takes no space itself. */
.u-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.mark {
  flex: none;
  color: currentColor;
  vertical-align: -0.12em;
}
/* Below the wide plate there is no open field to put it in. */
@media (max-width: 74.99rem) {
}
@media (max-width: 59.99rem) {
}

/* The capture sits in the plate's own frame, at its real 80:23 crop. */
.work__figure {
  display: grid;
  gap: var(--space-2xs);
  margin: 0;
}

.work__image {
  width: 100%;
  height: auto;
  /* The largest rendition is 1600px wide. At an enlarged text size the column
     can ask for more than that, which would upscale the capture and show it
     soft. Regenerating a larger one is not permitted, so the element stops at
     the source's own width instead. Below the cap nothing changes (17.3). */
  max-width: 1600px;
  aspect-ratio: 80 / 23;
  border: 1px solid color-mix(in oklab, var(--hairline) 45%, transparent);
  background: color-mix(in oklab, var(--datum) 60%, black);
}

.work__caption {
  color: var(--ink-soft);
}

/* ── Career by year ─────────────────────────────────────────────────────── */

.career {
  display: grid;
  /* Grid and flex items default to min-width: auto, which refuses to shrink
     below the widest word. At 320px with 200 percent text the Spanish strings
     set a minimum wider than the column, and the picker pushed out of the
     plate. Every item that can be sized by its content declares otherwise
     (expert finding H1). */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-s);
  margin-block-end: var(--space-xl);
  padding-block-end: var(--space-m);
  border-block-end: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
}

.career__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3xs);
  min-width: 0;
  max-width: min(var(--measure), 100%);
}

.career__heading {
  font-size: var(--step-1);
  font-variation-settings: "wdth" 88;
}

.career__explanation {
  color: var(--ink-soft);
}

.career__fallback,
.career__instruction {
  color: var(--ink-soft);
  font-family: var(--face-mono);
  font-size: var(--step--2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The rail. A passive overview reads as a chart; the interactive rail reads as
   a control, and the two never look alike. */
.career__rail {
  position: relative;
  min-height: 4rem;
  /* The reading measure is 84rem, which would cap a 30 year rail at 44.8px per
     year. The rail alone may use the plate's padded width so that the measured
     rule can reach a 44px cell on a wide screen. It never leaves the plate. */
  --career-plot: min(calc(100vw - 2 * var(--chrome-inset)), 100rem);
  width: var(--career-plot);
  margin-inline: calc((100% - var(--career-plot)) / 2);
  /* Two rows: the caption, then the strip of year points. A grid rather than a
     wrapping flex row, because zero-basis flex items do not force a wrap and
     would sit on the caption's line with no width at all. The column count is
     the surveyed span, which the build writes as --span. */
  display: grid;
  grid-template-columns: repeat(var(--span), minmax(0, 1fr));
  grid-template-rows: auto auto;
  align-items: stretch;
  padding-block-start: 0;
  border-block-end: 1px solid color-mix(in oklab, var(--hairline) 45%, transparent);
}

/* The caption takes a row of its own rather than lying over the strip. It is in
   the flow, so its height is whatever it renders at, including the Spanish
   string wrapping onto a second line at narrow widths. The indicator starts
   below the box this produces, measured after layout (expert finding G1). */
.career__overview {
  grid-column: 1 / -1;
  grid-row: 1;
  min-width: 0;
  padding-block: 0.25rem 1rem;
  color: var(--ink-soft);
}

/* Nothing is reserved above the strip when the caption is not rendered. */
.career[data-mode="interactive"] .career__rail {
  padding-block-start: 1.25rem;
}

/* The caption belongs to the passive overview and says what the rail is. It
   stays for as long as the rail is passive, selection or not: a rail that
   cannot be clicked still needs to say so (designer finding F5). */
.career[data-mode="interactive"] .career__overview {
  display: none;
}

.career__point {
  position: relative;
  grid-row: 2;
  min-width: 0;
  /* The box is the same in both modes, so the measured rule reads a real target
     rather than one that only exists once the mode has already switched. */
  min-height: 44px;
}

.career__point::before {
  content: "";
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 50%;
  width: 1px;
  height: 0.55rem;
  translate: -50% 0;
  background: var(--rail);
}

.career__label {
  position: absolute;
  inset-block-end: -1.35rem;
  inset-inline-start: 50%;
  translate: -50% 0;
  color: var(--ink-soft);
  font-family: var(--face-mono);
  font-size: var(--step--2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0;
}

/* The two ends are always named, so the range reads whatever the width is. */
.career__point[data-edge] .career__label {
  opacity: 1;
}

/* Every other label is thinned by how much room it has for its own size. The
   thresholds are in rem, so they answer the text size as well as the viewport:
   at 200% text the same query needs twice the pixels, and the labels thin out
   instead of running into each other (designer finding D4). */
@media (min-width: 20rem) {
  .career__point[data-tier="1"] .career__label {
    opacity: 1;
  }
}

/* The end labels are pulled inside the plot so neither is clipped. Their points
   and the indicator still sit on the true centre. */
.career__point[data-edge="start"] .career__label {
  inset-inline-start: 0;
  translate: none;
}

.career__point[data-edge="end"] .career__label {
  inset-inline-start: auto;
  inset-inline-end: 0;
  translate: none;
}

@media (min-width: 30rem) {
  .career__point[data-tier="2"] .career__label {
    opacity: 1;
  }
}

@media (min-width: 90rem) {
  .career__point[data-tier="3"] .career__label {
    opacity: 1;
  }
}

/* Once the script has measured the rendered labels it decides the set outright,
   and its answer outranks every tier rule above. The tiers stay as the no-JS
   baseline. The endpoints and the active year are always in the set. */
.career__point[data-show="false"] .career__label {
  opacity: 0;
}

.career__point[data-show="true"] .career__label {
  opacity: 1;
}

/* A label moved sideways to clear one already placed. The point stays where it
   is; only the box moves, and the connector below says which point it names. */
.career__point[data-shifted="true"] .career__label {
  translate: calc(-50% + var(--label-shift, 0px)) 0;
}

/* A label that could not sit over its own point gets a leader that reaches it:
   a drop at the true point and a run along to the near edge of the label box.
   Drawn with borders rather than a fill so it survives forced colors. */
.career__point[data-leader]::after {
  content: "";
  position: absolute;
  inset-block-start: 100%;
  width: var(--leader-width, 0px);
  height: 0.5rem;
  border-block-end: 1px solid var(--leader-ink);
}

.career__point[data-leader="right"]::after {
  inset-inline-start: 50%;
  border-inline-start: 1px solid var(--leader-ink);
}

.career__point[data-leader="left"]::after {
  inset-inline-end: 50%;
  border-inline-end: 1px solid var(--leader-ink);
}

/* Interactive rail: real targets, filled for the selection, hollow for a
   preview, with the state carried by shape as well as colour. */
.career[data-mode="interactive"] .career__point {
  cursor: pointer;
}

/* Cells are contiguous, so the ring is drawn inside its own cell. It keeps the
   2px weight and the focus colour, and it stays clear of the plot edges at both
   endpoints because it never reaches outside the box it belongs to. */
.career__point:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
  border-radius: 2px;
}

.career[data-mode="interactive"] .career__point::before {
  height: 0.9rem;
  width: 2px;
  background: var(--rail);
}

/* State outranks mode. These carry the `.career` ancestor so they are at least
   as specific as the interactive base rule above, which otherwise flattened
   both marks back to the plain one and left a selection with no shape change at
   all in the interactive rail. They apply in the passive overview too, where a
   year can still be chosen from the select (designer finding D2). */
/* Preview is hollow, the selection is filled, and both are taller than a plain
   year: three states told apart by shape before any colour is read. */
.career .career__point[data-preview="true"]::before {
  height: 1.4rem;
  width: 6px;
  background: transparent;
  border: 1px solid var(--annot-ink);
}

.career .career__point[aria-checked="true"]::before {
  height: 1.75rem;
  width: 6px;
  background: var(--signal-ink);
  border: 0;
}

/* The active year is always named, in either mode. Without script the thinning
   set does not exist, so this also covers the no-JS page; with script the year
   is already in the measured set and this only adds the emphasis. */
.career .career__point[data-preview="true"] .career__label,
.career .career__point[aria-checked="true"] .career__label {
  opacity: 1;
  color: var(--ink);
}

.career .career__point[aria-checked="true"] .career__label {
  font-weight: 600;
}

/* The indicator snaps: its position is never transitioned. */
.career__indicator {
  position: absolute;
  /* Starts below the caption's rendered bounds, which the script measures, so
     the rule never crosses its lettering in either locale. */
  inset-block: calc(var(--career-top, 0px) + 0.25rem) 0;
  inset-inline-start: 0;
  width: 2px;
  translate: calc(var(--career-x, 0px) - 1px) 0;
  background: var(--signal-ink);
  pointer-events: none;
}

.career__indicator[data-state="preview"] {
  background: repeating-linear-gradient(to bottom, var(--annot-ink) 0 4px, transparent 4px 8px);
}

/* Forced colors. Backgrounds are dropped there, and these marks are drawn with
   backgrounds, so the rail, the points, the selection and the indicator would
   all disappear (designer finding D3). They are redrawn with system colors and
   kept out of the forced-color substitution, and preview stays distinct from
   committed by border style as well as by its wording. */
@media (forced-colors: active) {
  /* Carrying the same ancestors as the rules these override, so the mode and
     state rules above cannot win on specificity and leave a mark unpainted. */
  .career .career__point::before,
  .career[data-mode="interactive"] .career__point::before,
  .career__indicator {
    forced-color-adjust: none;
    background: CanvasText;
  }

  /* Hollow for preview, filled for the selection, the same distinction the
     normal themes draw. A border survives here where a background does not. */
  .career .career__point[data-preview="true"]::before {
    background: transparent;
    border: 1px solid CanvasText;
  }

  .career .career__point[aria-checked="true"]::before,
  .career__indicator[data-state="committed"] {
    background: Highlight;
    border: 0;
  }

  /* The leader is already drawn with borders; it only needs a system colour. */
  .career__point[data-leader]::after {
    forced-color-adjust: none;
    --leader-ink: CanvasText;
  }

  /* A dashed rule rather than a gradient, because a gradient is not guaranteed
     to survive and a border style is. */
  .career__indicator[data-state="preview"] {
    background: none;
    width: 0;
    border-inline-start: 2px dashed CanvasText;
  }

  .career .career__point[aria-checked="true"] .career__label {
    forced-color-adjust: none;
    color: Highlight;
  }

  .career__rail {
    border-block-end-color: CanvasText;
  }

  .career__point:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }
}

.career__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  align-items: end;
  min-width: 0;
  max-width: 100%;
}

.career__field {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
  margin: 0;
  min-width: 0;
  /* The field takes the row to itself once the steppers no longer fit beside
     it, rather than forcing the row wider than the plate. */
  flex: 1 1 12rem;
}

.career__field-label {
  color: var(--ink-soft);
}

.career__select {
  min-height: 44px;
  min-width: 0;
  max-width: 100%;
  padding: var(--space-3xs) var(--space-2xs);
  color: var(--ink);
  background: var(--ground);
  border: 1px solid color-mix(in oklab, var(--hairline) 55%, transparent);
  border-radius: 0;
  font: inherit;
  font-family: var(--face-mono);
  font-size: var(--step--1);
}

.career__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  color: var(--ink);
  border: 1px solid color-mix(in oklab, var(--hairline) 55%, transparent);
}

.career__step:hover {
  color: var(--annot-ink);
  border-color: var(--annot-ink);
}

.career__chevron {
  width: 0.55rem;
  height: 0.55rem;
  border-block-start: 2px solid currentColor;
  border-inline-start: 2px solid currentColor;
}

.career__step[data-career-step="-1"] .career__chevron {
  rotate: -45deg;
  translate: 0.12rem 0;
}

.career__step[data-career-step="1"] .career__chevron {
  rotate: 135deg;
  translate: -0.12rem 0;
}

.career__clear {
  min-height: 44px;
  padding-inline: var(--space-xs);
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: 0.3em;
}

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

/* The result sits under the control in reserved space, so a commit never moves
   the control it came from. */
.career__result {
  display: grid;
  gap: var(--space-2xs);
  min-height: 7.5rem;
  align-content: start;
}

.career__preview {
  min-height: 1.4em;
  color: var(--annot-ink);
}

.career__figure {
  margin: 0;
  color: var(--signal-text);
  font-family: var(--face-mono);
  font-size: var(--step-2);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.career__status {
  margin: 0;
  min-height: 1.5em;
  color: var(--ink);
}

.career__matches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3xs) var(--space-s);
  margin: 0;
}

.career__matches a {
  font-family: var(--face-mono);
  font-size: var(--step--1);
}

.career__link {
  justify-self: start;
}

.traverse__back {
  margin-block-start: var(--space-s);
}

@media (max-width: 767px) {
  .career__result {
    min-height: 10rem;
  }
}

/* ── Plain language + glossary ──────────────────────────────────────────── */

/* Neither plate is part of the survey the rest of the page is drawing: they
   carry no plate number and they do not speak in the plate's uppercase. */
.plate--plain .plate__title,
.plate--reference .plate__title {
  text-transform: none;
}

/* V1. `.link-mono` is a flex box because the links wearing it carry a 44px
   target. The location is neither a link nor a target; it is a sentence, and
   in a flex box each side of the abbreviation became its own item, so at 320px
   with 200% text `UTC` collapsed into a 35 by 121px column of single letters.
   Page overflow stayed at zero the whole time, which is why only an eye caught
   it. It takes the typography and leaves the box. */
.contact__location {
  display: inline;
  min-height: 0;
}

/* 1.4.8 asks for paragraph spacing of at least 1.5 times the line spacing.
   The line spacing here is 1.55, so the floor is 2.325em, measured at the
   paragraph's own font size, which is what `em` on the paragraph gives. */
.prose {
  max-width: var(--measure);
  font-size: var(--step-0);
}

.prose > p + p {
  margin-block-start: 2.325em;
}

.glossary {
  max-width: var(--measure);
  font-size: var(--step-0);
}

.glossary__term {
  font-weight: 700;
  color: var(--ink);
  /* The destination of a first-use mark. It takes focus, so it must not land
     flush against the top of the viewport when it does. */
  scroll-margin-block: 1rem;
}

.glossary__definition {
  margin-block-start: 0.5rem;
  font-weight: 400;
  color: var(--ink);
}

.glossary__definition + .glossary__term {
  margin-block-start: 1.5rem;
}

/* The mark on a term's defining use. Upright, because a `dfn` is italic by
   default and the prose does not use italics for anything else, and underlined
   solid so it is not mistaken for the dotted rule that means an abbreviation.
   The 44px floor is bought with padding rather than with a larger font, so the
   word keeps the size of the sentence it sits in and the line grows instead. */
dfn {
  font-style: normal;
}

dfn > a,
.career__help {
  display: inline-block;
  min-height: 44px;
  padding-block: max(0px, calc((44px - 1.55em) / 2));
  color: inherit;
  text-decoration: underline solid currentColor;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

dfn > a:hover,
.career__help:hover {
  text-decoration-thickness: 2px;
}

/* The navigation links that reach the two plates, at the same target floor as
   every other control in the chrome. */
.chrome__privacy {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink-soft);
}

@media (forced-colors: active) {
  dfn > a,
  .career__help {
    text-decoration: underline solid LinkText;
  }
}

/* ── The coordinate field ───────────────────────────────────────────────── */

/* The rest state is the design. `--px` and `--py` default here so the lattice
   composes with no script at all, and the pointer module only ever overwrites
   two numbers it finds already set. */
.hero__lattice {
  --px: 50%;
  --py: 50%;
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: clip;
  pointer-events: none;
  color: var(--on-field-soft);
}

.lattice {
  width: 100%;
  height: 100%;
  display: block;
}

/* Each layer leans toward the pointer by a different amount, which is what
   reads as depth. The transform is the only thing that moves, and opacity is
   the only thing that changes with it: no filter, no blend mode, nothing that
   asks a compositor for a new layer per frame. */
.lattice__layer {
  transform-box: fill-box;
  transform-origin: center;
}

@media (prefers-reduced-motion: no-preference) {
  .lattice__layer {
    transition:
      transform var(--dur-mid) var(--ease-out),
      opacity var(--dur-mid) var(--ease-out);
  }

  .hero:hover .lattice__layer[data-depth="1"] {
    transform: translate(calc((var(--px) - 50%) * 0.012), calc((var(--py) - 50%) * 0.012));
    opacity: 0.9;
  }

  .hero:hover .lattice__layer[data-depth="2"] {
    transform: translate(calc((var(--px) - 50%) * 0.024), calc((var(--py) - 50%) * 0.024));
    opacity: 0.95;
  }

  .hero:hover .lattice__layer[data-depth="3"] {
    transform: translate(calc((var(--px) - 50%) * 0.036), calc((var(--py) - 50%) * 0.036));
  }

  .hero:hover .lattice__layer[data-depth="4"] {
    transform: translate(calc((var(--px) - 50%) * 0.05), calc((var(--py) - 50%) * 0.05));
  }

  .hero:hover .lattice__layer[data-depth="5"] {
    transform: translate(calc((var(--px) - 50%) * 0.07), calc((var(--py) - 50%) * 0.07));
    opacity: 1;
  }
}

/* It is decoration, and decoration repainted in system colours is noise over
   the text that matters. */
@media (forced-colors: active) {
  .hero__lattice {
    display: none;
  }
}

/* ── The clock ──────────────────────────────────────────────────────────── */

.clock {
  display: grid;
  gap: var(--space-2xs);
  padding-block-end: var(--space-m);
  margin-block-end: var(--space-m);
  border-block-end: 1px solid
    color-mix(in oklab, var(--hairline) calc(var(--hairline-alpha) * 100%), transparent);
}

.clock__title {
  color: var(--ink-soft);
}

.clock__rows {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-xl);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Label, time, offset, zone, in that order, and wrapping rather than clipping
   when a long zone name meets a 320px screen at 200% text. */
.clock__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-s);
  min-width: 0;
  font-family: var(--face-mono);
  font-size: var(--step--1);
  color: var(--on-field-soft);
}

.clock__label {
  color: var(--on-field);
  font-weight: 700;
}

.clock__time {
  color: var(--on-field);
  font-variant-numeric: tabular-nums;
}

.clock__time:empty {
  display: none;
}

.clock__zone {
  overflow-wrap: anywhere;
}

/* ── Text sits on the field, never on the lattice ───────────────────────── */

/* The lattice is behind everything in the hero, and thin lines crossing a
   sentence are exactly the kind of decoration that makes text harder to read
   while looking like it was meant. Each block that carries words gets an
   opaque backing in the field colour, bled out by the gap so the edge of the
   backing is not a visible box, and the lattice keeps the whitespace between
   the blocks, which is where it was always supposed to be seen. */
.hero__lede,
.hero__marginalia,
.hero .chrome,
.hero__foot {
  position: relative;
  z-index: 1;
  background: var(--field);
  /* Bleed matches the gap between hero rows, so neighbouring backings meet the
     lattice at the same distance on every edge. */
  padding: var(--space-2xs) var(--space-xs);
  margin: calc(var(--space-2xs) * -1) calc(var(--space-xs) * -1);
}

/* The two columns of the lede row each carry their own backing, so the lattice
   still shows in the channel between them rather than under one wide slab. */
@media (min-width: 60rem) {
  .hero__body {
    background: none;
  }
}

/* ── Controls whose content is more than one text run ───────────────────── */

/* `.button` is `inline-flex` with a `gap`, which is right for a label beside a
   mark and wrong for two words that have to wrap together: as flex items each
   child becomes its own column, and at 320px with 200% text they come out one
   letter per line. This project hit that four times, on the contact location,
   the 404 privacy action and the career disclosure. This is the rule that ends
   it, and any control carrying more than one run wears it.
 
   Normal text flow, padding where the `gap` was, and the 44px floor bought with
   `padding-block` rather than a `min-height` a flex box would have absorbed. */
.button--flow {
  display: inline-block;
  align-items: initial;
  gap: initial;
  padding-block: calc((44px - 1.5em) / 2);
  /* Pixel-bounded, like the other narrow-width gutters here: the shared 1.5rem
     is 48px a side at a 32px root, which is most of a 320px screen. */
  padding-inline: clamp(12px, 4vw, 24px);
  line-height: 1.5;
  text-align: start;
  text-wrap: pretty;
}

.button--flow > span {
  display: inline;
}

/* The separator is decorative and already `aria-hidden`; this keeps it off both
   words now that the flex gap is gone. */
.button--flow > span[aria-hidden="true"] {
  padding-inline: 0.4em;
}

/* The mark is decoration and the label carries the whole meaning, so at the
   width where every pixel is a letter the mark stops competing for them. */
@media (max-width: 544px) {
  .button--flow > .mark {
    display: none;
  }
}

/* The count continues the label as a sentence continues, so it needs a space
   in front of it and must not be pushed onto a column of its own. */
.button--flow > .button__count {
  padding-inline-start: 0.4em;
}

/* ── Headings wrap as words, not as letters ─────────────────────────────── */

/* `.plate` carries `overflow-wrap: anywhere` so a long token cannot widen the
   column it sits in, and it inherits into everything, headings included. A
   heading broken mid-word is unreadable at a glance: at 320px with 200% text
   this page was rendering CUARENT / A, ThoughtW / orks and a lone S left over
   from Certificaciones.
 
   This is every visual heading class the page renders, as one list of class
   selectors at equal specificity, declared after `.plate` so both specificity
   and source order settle it. A new heading class does not ship without
   joining this list. `hyphens: manual` means a break happens only where the
   copy asks for one, and `text-wrap: balance` keeps a last line from holding a
   single word or letter. */
h1,
h2,
.hero__name,
.plate__title,
.bounce__name,
.station__org,
.work__title,
.instrument__title,
.note__title,
.legend__title,
.career__heading,
.clock__title,
.privacy__title {
  overflow-wrap: break-word;
  hyphens: manual;
  text-wrap: balance;
}

/* And `anywhere` is re-declared exactly where it is still needed rather than
   left to reach these by inheritance: an address, a URL and a definition may
   all carry a token longer than the column. */
.contact__email,
.glossary__definition,
.work__host,
.link-mono[href^="http"] {
  overflow-wrap: anywhere;
}

/* Narrow screens: the display steps come down by their floor, never by their
   ceiling, so a visitor asking for 200% text still gets roughly twice the
   size. Capping the maximum would have cancelled their setting, which is the
   one thing this must not do. */
@media (max-width: 544px) {
  :root {
    --step-3: clamp(1.15rem, 1.05rem + 0.5vw, 2.75rem);
    --step-4: clamp(1.4rem, 1.2rem + 1vw, 3.8rem);
    --step-5: clamp(1.75rem, 1.4rem + 1.75vw, 6rem);
  }

  /* Utility navigation was taking most of the opening viewport, which pushed
     the primary action past 2,000px. */
  .chrome {
    gap: var(--space-3xs) var(--space-2xs);
  }

  .plate__head {
    gap: var(--space-2xs);
    padding-block-end: var(--space-2xs);
    margin-block-end: var(--space-m);
  }

  .plate {
    padding-block: var(--space-l);
  }
}

/* Every new route is a target, including the short ones. "Top" is three
   characters and was 27px wide. */
.footer__top,
.plate__jump > a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
}

.plate__jump {
  margin-block-start: var(--space-m);
}

/* Narrow screens, in px because these reduce sizes and spacing: a rem
   breakpoint moves with the root font size, so doubling the text would drop a
   desktop viewport into this bucket and make the page smaller. */
@media (max-width: 767px) {
  /* A 2.25rem indent is 72px once the root doubles, which is a quarter of a
     320px screen given to an indent. */
  /* Below this width the decoration is removed outright rather than hidden:
     `visibility` or zero opacity would keep reserving the gutter that enlarged
     text needs. The copy then runs full width, and no indentation is restored
     to compensate. `.station__now` and `.station__mark` are text, not
     decoration, and do not move. */
  .station {
    --rail-clearance: 0rem;
    padding-inline-start: 0;
  }

  .station::before,
  .station::after {
    content: none;
  }

  /* CUARENTA was breaking into CUARENT / A. The floor comes down; the ceiling
     is untouched, so an enlarged setting still enlarges. */
  /* Above the body step's own floor, so a project title never renders smaller
     than the prose under it, and small enough that CUARENTA does not break. */
  .work__title {
    font-size: clamp(1.2rem, 1.05rem + 0.6vw, 1.55rem);
  }
}

/* C4. Above the desktop breakpoint the name is a pure rem length.

   `clamp(3.2rem, 2.4rem + 4vw, 6rem)` looks fluid and is not: at 1440px the
   `4vw` term contributes 57.6px, which is fixed in pixels and does not grow
   when the root does. At a 16px root that is 60 percent of the size, so
   doubling the text moved 96px to 134.4px, a 1.4x rise where the visitor asked
   for 2x. A viewport-anchored term inside a size that must answer text
   enlargement is the defect. Below the breakpoint the fluid clamp stays: a
   small viewport needs viewport response more than it needs the guarantee. */
@media (min-width: 960px) {
  /* Every step, not only the name: each one carries its own `vw` term and each
     one was attenuated the same way. At 1440px all of them already sit at
     their ceiling with a 16px root, so these values change nothing at 100 per
     cent and make the response exact at 200. */
  :root {
    --step--2: 0.74rem;
    --step--1: 0.92rem;
    --step-0: 1.1875rem;
    --step-1: 1.55rem;
    --step-2: 2.05rem;
    --step-3: 2.75rem;
    --step-4: 3.8rem;
    --step-5: 6rem;
  }

  .hero__name {
    font-size: var(--step-5);
  }
}


/* Generated by build/build.mjs from content/profile.json. */
:root {
  --span: 30;
}
