/* ==========================================================================
   Base — reset, typography, layout primitives, shared utilities.
   Reads only from tokens.css. No literal colors / sizes below.
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--sp-4));
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* `hidden` computes the other axis to `auto`, which makes the body a scroll
   container — and a sticky element positions against its nearest scroll
   container, not the viewport. That is the classic way a pinned hero, and a
   sticky header, die on iOS Safari. `clip` is the one overflow value that can
   sit opposite `visible` without forcing a scroller.

   This used to be written as `overflow-x: hidden` followed by `overflow-x:
   clip`, so engines that don't know `clip` — Safari before 16 — kept the
   `hidden` that breaks them, which is the one case the fallback existed for.
   Behind @supports they get neither.

   Safe to drop there, measured rather than assumed: at 320, 375 and 1440 on
   every page, through 24 scroll positions of the pinned scene and with the
   mobile menu open, nothing overflows horizontally. If something ever does, it
   will scroll sideways on old Safari instead of silently unsticking the
   header — the louder of the two failures. */
@supports (overflow-x: clip) {
  body {
    overflow-x: clip;
  }
}

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

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

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

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

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

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

::selection {
  background: var(--color-dark);
  color: var(--color-light);
}

/* --- Typography roles ---------------------------------------------------- */

.display {
  font-size: var(--fs-display);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  text-wrap: balance;
}

.h1 {
  font-size: var(--fs-h1);
  font-weight: var(--fw-medium);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  text-wrap: balance;
}

.h2 {
  font-size: var(--fs-h2);
  font-weight: var(--fw-medium);
  line-height: 1.2;
  letter-spacing: var(--ls-tight);
  text-wrap: balance;
}

.h3 {
  font-size: var(--fs-h3);
  font-weight: var(--fw-medium);
  line-height: 1.3;
  letter-spacing: var(--ls-normal);
}

.lead {
  font-size: var(--fs-lead);
  line-height: 1.45;
}

/* The label role: mono, 14px, sentence case. Uppercase was removed
   deliberately — the markup is already written in sentence case, so nothing
   below needed rewording. */
.mono {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-label);
  color: var(--color-muted);
  line-height: 1.4;
}

/* Kept for now, but .mono no longer uppercases or tracks wide, so this
   modifier is close to a no-op — worth deleting once the type settles. */
.mono--plain {
  text-transform: none;
  letter-spacing: 0.01em;
}

.caption {
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  line-height: 1.5;
  color: var(--color-muted);
  letter-spacing: 0.01em;
}

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

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

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

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

/* --- Section grounds ------------------------------------------------------ */
/* Each home-page section names its own ground, so the colour change is what
   marks the break rather than a rule. */

.ground-warm {
  background: var(--ground-warm);
}

.ground {
  background: var(--ground);
}

.ground-white {
  background: var(--ground-white);
}

/* A dark band. Carries its own ink, so anything dropped into it is legible
   without each component knowing which ground it is standing on. */
.ground-ink {
  background: var(--ground-ink);
  color: var(--ink-on-dark);
  /* Rebind the muted role rather than restyling .mono / .caption / .muted one
     by one. --color-mid is #999, which sat at 6.9:1 on the near-black this
     band used to be and collapses to 2.3:1 on a mid-tone ground — the section
     labels would have gone quietly unreadable. Anything dropped in here now
     inherits a muted that belongs to this ground. */
  --color-muted: var(--ink-on-dark-muted);
}

/* The band has its own link colours. At rest a link is the band's ink —
   the blue --link-ink would all but vanish on the violet (1.8) — and on hover
   and focus it turns --on-band-accent, the pale pink (5.42). The focus ring
   swaps the same way: the global ring is the blue. */
.ground-ink :is(.link, .link-arrow, .contact-link, .back-link, .cs-next__link) {
  color: inherit;
}

.ground-ink .link:hover,
.ground-ink .link:focus-visible,
.ground-ink .link-arrow:hover,
.ground-ink .link-arrow:focus-visible {
  color: var(--on-band-accent);
  opacity: 1;
}

.ground-ink :focus-visible {
  outline-color: var(--focus-ring-on-dark);
}

/* --- Parallax (progressive enhancement) ---------------------------------- */
/* assets/js/parallax.js drifts these as they cross the viewport. Nothing is
   positioned by the transform, so with JS off the layout is identical. */

.js [data-parallax] {
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  [data-parallax] {
    transform: none !important;
  }
}

.stack > * + * {
  margin-top: var(--sp-5);
}

.stack-sm > * + * {
  margin-top: var(--sp-3);
}

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

.measure {
  max-width: var(--container-text);
}

/* --- The rail grid ------------------------------------------------------- */
/* Two ways to sit on the site's vertical line, both inside a .container:
     .rail         two cells — a label in the narrow left rail, content on
                   the line. Give it exactly two children.
     .rail-offset  content on the line with nothing in the rail.
   Below 62rem both collapse, so everything aligns to the logomark. */

.rail {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-4);
}

@media (min-width: 62rem) {
  .rail {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    /* No gap: --rail *is* the distance to the line, so it stays one number */
    gap: 0;
    /* Top, not baseline. Baseline hung the label off the first line of whatever
       it labelled, so a label beside a 44px heading sat level with that
       heading's baseline and a label beside a list sat level with the list's
       first line of text — two different heights for the same component. The
       case-study rows (.cs-row) never did this, which is why the two halves of
       the site disagreed. One value now, and the two `:has()` exceptions that
       used to patch the list cases are gone with it. */
    align-items: start;
  }

  /* Keep rail labels off the line when they run long enough to wrap */
  .rail > .mono:first-child {
    padding-right: var(--sp-6);
  }

  .rail-offset {
    margin-left: var(--rail);
  }
}

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

/* Skip link */
.skip-link {
  position: absolute;
  top: var(--sp-2);
  left: var(--sp-2);
  z-index: 100;
  padding: var(--sp-2) var(--sp-4);
  background: var(--color-dark);
  color: var(--color-light);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

/* --- Links --------------------------------------------------------------- */

/* Text link with an underline that wipes in on hover */
.link {
  position: relative;
  display: inline-block;
  color: var(--link-ink);   /* the nav sets its own ink, site.css */
  transition: color var(--dur-fast) var(--ease);
}

.link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.12em;
  height: 1px;
  background: currentColor;
  transform: scaleX(var(--u, 0));
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.link:hover,
.link:focus-visible {
  color: var(--link-hover);
}

.link:hover::after,
.link:focus-visible::after {
  --u: 1;
}

/* Same, but underlined at rest and wiping out on hover */
.link--on {
  --u: 1;
}

.link--on:hover::after,
.link--on:focus-visible::after {
  transform: scaleX(0);
  transform-origin: right;
}

/* The arrow, wherever one appears beside a label. It is the site's one arrow
   geometry — the same path .btn__icon and .cs-next__icon draw — inlined rather
   than loaded from assets/ because an <img> cannot take currentColor, and this
   has to follow its link into the accent on hover.

   Sized in `em`, not px: 1em square on a 24-unit viewBox puts the visible
   stroke at 14/24 of the type size and its weight at 2/24 of it, so the arrow
   keeps the same proportions against 14px mono as it would against anything
   else. It replaced a text glyph, whose box measured 8.57px beside 14px type;
   this draws at 8.17px, which is why the swap needs no spacing changes. */
.arrow {
  width: 1em;
  height: 1em;
  flex: none;
  transition: transform var(--dur) var(--ease);
}

/* Arrow link: label + arrow that nudges */
.link-arrow {
  display: inline-flex;
  /* Centre, not baseline: an SVG has no baseline of its own, so a baseline row
     would hang it off its bottom edge and sit it high against the label. */
  align-items: center;
  gap: var(--sp-2);
  transition: color var(--dur-fast) var(--ease);
}

.link-arrow:hover,
.link-arrow:focus-visible {
  color: var(--link-hover);
}

.link-arrow:hover .arrow,
.link-arrow:focus-visible .arrow {
  transform: translateX(0.28em);
}

/* --- Figures ------------------------------------------------------------- */

.figure {
  margin: 0;
}

.figure__plate {
  background: var(--color-plate);
  box-shadow: var(--shadow-soft);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.figure__plate img {
  width: 100%;
}

.figure figcaption {
  margin-top: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-caption);
  line-height: 1.5;
  color: var(--color-muted);
  letter-spacing: 0.01em;
}

/* --- No reveal on scroll ------------------------------------------------- */
/* Blocks used to start invisible and fade in on a 620ms timer once they were
   6% in view. It lagged a fast scroll, left whole sections faint, and below a
   row of cards it made the next row look like the end of the page. Content
   is visible from the first frame now; the .reveal class stays in the markup
   and does nothing. The one scroll effect left is scroll-driven and never
   hides anything — the About fill in motion.css. */

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

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

}

/* Moved out of hero.css. These two keyframes are the hero's entrance, but the
   mobile menu panel in site.css reuses them verbatim for its open and close —
   and hero.css is loaded ONLY by index.html, while site.css is loaded by every
   page. So off the home page `animation: hero-shape-in ...` named a keyframe
   that did not exist, the animation never ran, and the panel kept the
   opacity: 0 of its closed state: the hamburger became an X and nothing else
   happened. Defined here, in the file every page loads. */
@keyframes hero-shape-in {
  0% {
    opacity: 0;
    transform: translate(var(--hero-in-shape-off), var(--hero-in-shape-off))
      scale(var(--hero-in-shape-from));
    clip-path: circle(0% at 0% 0%);
  }
  8% {
    opacity: 1;
  }
  100% {
    opacity: 1;
    transform: none;
    clip-path: circle(100% at 50% 50%);
  }
}

@keyframes hero-line-in {
  from {
    opacity: 0;
    transform: translateY(var(--hero-in-lift));
  }
  to {
    opacity: 1;
    transform: none;
  }
}
