/* =========================================================
   Components. Mobile-first; widescreen rules live in
   responsive.css.
   ========================================================= */

/* ---------- Primitives ---------- */

/* The gutter is a floor, not a fixed value: with viewport-fit=cover the page
   runs under the notch in landscape, and the safe-area insets take over
   whenever they are the larger of the two. They are 0 everywhere else, so this
   reads as plain --gutter on every other screen. */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}

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

/* Clipped away rather than parked off-screen. Hiding it by position — absolute
   above the document edge, or fixed with a negative transform — leaves a real
   box somewhere that elastic overscroll can drag into view, which is what made
   it flash in when the page was pulled down past the top. Collapsed to a
   clipped 1px it has nowhere to be revealed from, in any browser. This is the
   same technique as .visually-hidden above, undone on focus. */
.skip-link {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  background: var(--fg);
  color: var(--bg);
  font-size: var(--step-00);
  font-weight: 600;
  text-decoration: none;
}

.skip-link:focus-visible {
  width: auto;
  height: auto;
  padding: 0.75rem 1.25rem;
  overflow: visible;
  clip-path: none;
  outline-color: var(--bg);
  outline-offset: -4px;
}

.eyebrow,
.stage__index,
.card__index,
.detail__index,
.tier__index,
.card__more,
.site-footer__heading,
.site-footer__base,
.page-toc,
.cta__email,
.hero__tagline {
  font-family: var(--font-mono);
  font-size: var(--step-00);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
}

.eyebrow {
  margin-bottom: 1rem;
}

/* A rule trailing off the label. With no hue to separate a label from body
   text, the rule is what marks it as a tab on the section rather than a line of
   copy. Only .eyebrow takes it — the index numbers above share the type styling
   but sit inside cards, where a trailing rule would read as a divider. */
.eyebrow::after {
  content: "";
  display: inline-block;
  width: 2.2rem;
  height: 1px;
  margin-left: 0.85rem;
  background: currentColor;
  opacity: 0.45;
  vertical-align: middle;
  transform: translateY(-1px);
}

/* ---------- Text ---------- */

/* Retracting underline. The rule is a background image rather than a
   text-decoration so its width can be animated: it is full width at rest and
   pulls away to the right on hover, which is the opposite gesture to the
   navigation links and keeps a body link from reading as one. */
.body-link,
.prose a,
.contact-address__link,
.cta__email {
  padding-bottom: 1px;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
  text-decoration: none;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  transition:
    color 0.28s var(--ease),
    opacity 0.28s var(--ease),
    background-size 0.32s var(--ease);
}

.body-link:hover,
.prose a:hover,
.contact-address__link:hover,
.cta__email:hover {
  color: var(--fg);
  background-size: 0% 1px;
  background-position: 100% 100%;
}

/* Marker pass. The one italic serif on a site that is otherwise sans and mono,
   over a wash a single step off the surface. Deliberately unlike both a link
   and an underline, which matters more here than it would in colour: with no
   hue, emphasis and interactivity have to be told apart by texture. Named
   .marker, not the kit's .mark — .mark is already the skull glyph. */
.marker {
  padding: 0.06em 0.3em;
  margin: 0 -0.06em;
  background-color: var(--accent-tint);
  color: var(--fg);
  font-family: var(--font-serif);
  font-size: 1.08em;
  font-style: italic;
  font-weight: 500;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* Aligned to the left edge of the text column rather than centred in the
   container. Centred on the container it drifts right of a 64ch prose block and
   reads as a mistake; the size, the serif and the hairline are already doing
   the work of breaking the page up. */
.pullquote {
  max-width: 44rem;
  margin-block: clamp(2.5rem, 6vw, 4rem);
  text-align: center;
}

.pullquote::before {
  content: "\201C";
  display: block;
  margin-bottom: 0.6rem;
  color: var(--fg);
  font-family: var(--font-serif);
  font-size: 5.5rem;
  line-height: 1;
  opacity: 0.28;
}

.pullquote p {
  font-family: var(--font-serif);
  font-size: clamp(1.5rem, 3.2vw, 2.3rem);
  font-style: italic;
  font-weight: 500;
  line-height: 1.34;
  text-wrap: balance;
}

/* Softer variant: a hairline in place of the quote glyph. */
.pullquote--soft::before {
  content: "";
  width: 32px;
  height: 1px;
  margin: 0 auto 1.8rem;
  background: var(--fg);
  opacity: 0.5;
}

/* ---------- Divider ---------- */

/* The mark between two rules that fade out at both ends. The double fade is
   what stops it reading as a plain <hr>. */
.divider {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  color: var(--faint);
}

.divider__rule {
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--line) 22%, var(--line) 78%, transparent);
}

.divider .mark {
  flex: none;
  opacity: 0.75;
}

/* ---------- Brand ---------- */

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--fg);
  text-decoration: none;
}

.brand__word {
  font-size: 1rem;
  font-weight: 300;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}

.brand__word b {
  font-weight: 700;
}

.mark {
  flex: none;
}

/* ---------- Header ---------- */

/* Sticky at every width. On a phone the navigation was previously only
   reachable by scrolling back to the top of a long page; the bar travels with
   the page instead. The background stays fully opaque here — the settled,
   translucent state in motion.css is a widescreen-only affectation, because on
   a phone the body text passing under a half-transparent bar reads as a
   rendering fault rather than as depth. */
/* The inset is padding on the bar rather than an offset on it, so wherever the
   viewport does start at the physical top of the screen — a standalone window,
   a notch in landscape — the bar's own background fills the strip and its
   contents still clear the clock. Zero on anything without an inset. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  padding-top: env(safe-area-inset-top);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

/* iOS Safari in portrait is the case the inset above does not cover: the
   viewport already begins below the status bar, so the inset reports 0, and the
   page is drawn behind the translucent status strip regardless. The bar was
   pinned correctly at the top of the viewport and body text was still visibly
   travelling through the band above it. This extends the bar's own surface past
   its top edge to fill that band. Off screen in every other case — including
   the top of the page, where it sits above the document — so it costs nothing
   except when there is something to cover. */
.site-header::before {
  content: "";
  position: absolute;
  right: 0;
  bottom: 100%;
  left: 0;
  height: 6rem;
  background: var(--bg);
}

/* One row on a phone: the lockup, and a button for everything else. Five
   uppercase labels wrapped to two rows below the brand, which cost a third of
   the screen before a word of the page had been read. */
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
}

.site-header .brand {
  gap: 0.625rem;
  padding-block: 1.25rem;
}

/* ---------- Menu (phone) ---------- */

.menu {
  flex: none;
}

.menu__button {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  /* The whole button is a tap target, not just the text inside it. */
  min-height: 2.75rem;
  padding-inline: 0.875rem;
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--step-000);
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
  transition:
    color 0.28s var(--ease),
    border-color 0.28s var(--ease);
}

/* Safari still draws the disclosure triangle without this. */
.menu__button::-webkit-details-marker {
  display: none;
}

.menu__button:hover,
.menu[open] .menu__button {
  color: var(--fg);
  border-color: var(--fg);
}

/* Two bars that cross into an X when the panel is open: the button says what
   it will do next without needing a second label. */
.menu__bars {
  position: relative;
  width: 1rem;
  height: 0.625rem;
}

.menu__bars::before,
.menu__bars::after {
  content: "";
  position: absolute;
  right: 0;
  left: 0;
  /* A whole pixel, not a hairline: on Android the device pixel ratios are often
     fractional (1.75, 2.625), and a 1.5px box landing on a fractional offset
     can snap away to nothing when it is painted. */
  height: 2px;
  background: currentColor;
  transition: transform 0.32s var(--ease);
}

.menu__bars::before {
  top: 0;
}

.menu__bars::after {
  bottom: 0;
}

/* Both bars travel to the same line through the middle of the box, so the X
   crosses at its centre: 4px each, from 1px and 9px to 5px. */
.menu[open] .menu__bars::before {
  transform: translateY(0.25rem) rotate(45deg);
}

.menu[open] .menu__bars::after {
  transform: translateY(-0.25rem) rotate(-45deg);
}

/* On the narrowest phones the wordmark and a labelled button do not both fit on
   the row. The bars carry the button on their own there; the word stays in the
   accessibility tree, so the control is still called Menu.

   The cut is at 22em rather than nearer the point the row actually breaks
   (around 21.5em) so the common 360px Android viewport keeps the word. It was
   at 23.5em, which caught those phones and left them with a button that iPhones
   showed labelled. */
@media (max-width: 22em) {
  .menu__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .menu__button {
    padding-inline: 0.75rem;
  }
}

/* Dropped over the page rather than pushed into it, so opening the menu never
   moves what is being read underneath. */
.menu__panel {
  position: absolute;
  top: 100%;
  right: 0;
  left: 0;
  max-height: calc(100dvh - 100%);
  overflow-y: auto;
  padding-left: max(var(--gutter), env(safe-area-inset-left));
  padding-right: max(var(--gutter), env(safe-area-inset-right));
  /* The panel can reach the bottom of the screen, so the last link clears the
     home indicator rather than sitting under it. */
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  box-shadow: 0 18px 40px -30px rgb(0 0 0 / 0.8);
  animation: menu-drop 0.26s var(--ease) both;
}

/* The panel is out of flow, so it can arrive from under the bar without
   anything below it moving. */
@keyframes menu-drop {
  from {
    opacity: 0;
    transform: translateY(-0.5rem);
  }
}

.nav__list {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav__list > li + li {
  border-top: 1px solid var(--line-soft);
}

.nav__link {
  position: relative;
  display: block;
  padding-block: 0.9375rem;
  font-size: var(--step-00);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.28s var(--ease);
}

.nav__link:hover {
  color: var(--fg);
}

/* A bar in the gutter marks the current page, rather than the underline the
   widescreen row uses: stacked rows are full width, and a full-width underline
   there would read as another divider. */
.nav__link.is-current {
  color: var(--fg);
}

.nav__link.is-current::before {
  content: "";
  position: absolute;
  top: 0.9375rem;
  bottom: 0.9375rem;
  left: calc(var(--gutter) * -0.5);
  width: 2px;
  background: currentColor;
}

/* ---------- Buttons ---------- */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3rem;
  padding-inline: 1.5rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: 0.9375rem;
  font-weight: 600;
  /* Pushed out from 0.01em. Tracking is the only tone control left once hue is
     gone, so the buttons get more of it than they would in a colour palette —
     but not the 0.2em the kit uses, because these labels are sentences
     ("Request a security assessment") rather than two-word calls to action. */
  letter-spacing: 0.06em;
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  box-shadow: var(--shadow-sm);
  transition:
    background-color 0.15s ease,
    color 0.15s ease,
    border-color 0.15s ease,
    transform 0.3s var(--ease),
    box-shadow 0.3s var(--ease);
}

/* The sweep. isolation:isolate on .btn plus z-index:-1 here is what lets this
   sit above the button's own background without a wrapper element — it cannot
   escape behind the button, because the isolation pins the bottom of the
   stacking context to .btn itself. currentColor means the sheen is light on a
   dark button and dark on a light one, so one rule covers both variants and
   both schemes. */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(135deg, transparent 0%, currentColor 140%);
  opacity: 0;
  transition: opacity 0.35s var(--ease);
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lift);
}

.btn:hover::before {
  opacity: 0.16;
}

.btn:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
}

.btn--primary {
  background: var(--invert-bg);
  border-color: var(--invert-bg);
  color: var(--invert-fg);
}

.btn--primary:hover {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--fg);
}

.btn--secondary {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--fg);
}

.btn--secondary:hover {
  background: var(--invert-bg);
  border-color: var(--invert-bg);
  color: var(--invert-fg);
}

/* ---------- Lattice surfaces ----------
   The four backgrounds that paint the isometric lattice. The gradients are
   declared on them rather than on :root because a custom property resolves its
   var() references on the element that declares it: at :root the line colour
   would be baked in before it inherited, and the dark slab further down could
   not re-tint it. Declared here, each surface resolves the geometry against its
   own --grid-line, and the geometry is still written once.

   The first stop sits at -0.5px so the strongest line is centred on the tile
   edge rather than tucked inside it; the neighbouring tile paints the other
   half of it, and the period stays 50%. */
.hero::before,
.section--inset::before,
.page-head::before,
.cta::before {
  --grid-line-mid: color-mix(in srgb, var(--grid-line) 60%, transparent);
  --grid-line-fine: color-mix(in srgb, var(--grid-line) 28%, transparent);

  --grid-lines:
    repeating-linear-gradient(
      to top left,
      var(--grid-line) -0.5px 0.5px,
      transparent 0.5px calc(12.5% - 0.5px),
      var(--grid-line-fine) calc(12.5% - 0.5px) calc(12.5% + 0.5px),
      transparent calc(12.5% + 0.5px) calc(25% - 0.5px),
      var(--grid-line-mid) calc(25% - 0.5px) calc(25% + 0.5px),
      transparent calc(25% + 0.5px) calc(37.5% - 0.5px),
      var(--grid-line-fine) calc(37.5% - 0.5px) calc(37.5% + 0.5px),
      transparent calc(37.5% + 0.5px) calc(50% - 0.5px)
    ),
    repeating-linear-gradient(
      to top right,
      var(--grid-line) -0.5px 0.5px,
      transparent 0.5px calc(12.5% - 0.5px),
      var(--grid-line-fine) calc(12.5% - 0.5px) calc(12.5% + 0.5px),
      transparent calc(12.5% + 0.5px) calc(25% - 0.5px),
      var(--grid-line-mid) calc(25% - 0.5px) calc(25% + 0.5px),
      transparent calc(25% + 0.5px) calc(37.5% - 0.5px),
      var(--grid-line-fine) calc(37.5% - 0.5px) calc(37.5% + 0.5px),
      transparent calc(37.5% + 0.5px) calc(50% - 0.5px)
    ),
    repeating-linear-gradient(
      to bottom,
      var(--grid-line) -0.5px 0.5px,
      transparent 0.5px calc(12.5% - 0.5px),
      var(--grid-line-fine) calc(12.5% - 0.5px) calc(12.5% + 0.5px),
      transparent calc(12.5% + 0.5px) calc(25% - 0.5px),
      var(--grid-line-mid) calc(25% - 0.5px) calc(25% + 0.5px),
      transparent calc(25% + 0.5px) calc(37.5% - 0.5px),
      var(--grid-line-fine) calc(37.5% - 0.5px) calc(37.5% + 0.5px),
      transparent calc(37.5% + 0.5px) calc(50% - 0.5px)
    );
}

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

/* The kit's hero is a full-height dark panel behind a photograph. There is no
   photography here and the page is already dark, so what carries over is the
   lighting rather than the panel: two pools of light and a vignette over the
   band the hero already occupies. isolation:isolate keeps both layers behind
   the copy without either one needing a wrapper element. */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: clamp(3.5rem, 11vw, 7rem) clamp(3rem, 8vw, 5.5rem);
  border-bottom: 1px solid var(--line);
  border-image: var(--rule-fade) 1;
}

/* Lattice over the light pools. It rides the same drift animation the veil
   does, so the geometry parallaxes against the copy as the hero leaves — which
   is what stops it reading as a flat backdrop. The vignette in ::after darkens
   the edges, so this one needs no mask of its own. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background-image: var(--grid-lines), var(--veil);
  background-size: var(--grid-tiles), auto, auto;
}

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--grain-mid), var(--vignette);
  background-size: 220px 220px, cover;
  mix-blend-mode: var(--grain-blend);
  opacity: 0.5;
}

/* The mark, set into the hero surface rather than laid over it.
   z-index -1 puts it in the same band as .hero::after, and a pseudo-element
   counts as its parent's last child — so the grain and the vignette paint over
   the top of this, which is what keeps it reading as part of the surface
   instead of a sticker on it.

   Sized and placed off the right edge the same way the closing slab's is, held
   further back because the hero is a lighter surface and the display type runs
   across it. Mobile puts it in the corner above the copy; the widescreen
   framing is in responsive.css. */
.hero__mark {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--fg);
  opacity: 0.04;
  -webkit-mask-image: var(--skull-glyph);
  mask-image: var(--skull-glyph);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: right -3rem top 5%;
  mask-position: right -3rem top 5%;
  -webkit-mask-size: auto 30%;
  mask-size: auto 30%;
}

.hero__title {
  font-size: var(--step-display);
  font-weight: 200;
  line-height: 0.9;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}

.hero__title b {
  font-weight: 800;
}

.hero__tagline {
  margin-top: clamp(1.75rem, 5vw, 2.5rem);
  padding-top: 1.125rem;
  border-top: 1px solid var(--line);
}

.hero__statement {
  margin-top: 0.875rem;
  font-size: var(--step-4);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: var(--fg);
}

/* No span rules here: the statement is a single clause, so there is no sentence
   boundary to break on and nothing to set at a second weight. */

.hero__body {
  margin-top: 1.25rem;
  max-width: 48ch;
  font-size: var(--step-1);
  color: var(--muted);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: clamp(2rem, 5vw, 2.75rem);
}

/* Stacked on small screens, both buttons take the full width so they do not
   end up as two mismatched blocks. */
.hero__actions .btn {
  flex: 1 1 100%;
}

/* ---------- Facts row ---------- */

/* Three short claims under the hero, on the hairline rules the rest of the site
   uses for lists. Stacked on a phone and a single row from 48em, so the first
   thing under the headline is three lines a reader takes in without deciding
   to.
   No rule along the top: the row butts straight against the hero, and the
   hero's own closing rule is the one above it. */
.facts {
  display: grid;
  gap: 1px;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--line);
  border-bottom: 1px solid var(--line);
}

.facts li {
  position: relative;
  padding: 0.875rem 0 0.875rem 1.75rem;
  background: var(--bg);
  font-size: 0.9375rem;
  color: var(--muted);
}

/* Centred on the first line rather than on the box: the third claim wraps to
   two lines at most widths, and a marker centred on the cell drifts down it. */
.facts li::before {
  content: "";
  position: absolute;
  top: calc(0.875rem + 0.72em);
  left: 0;
  width: 0.75rem;
  height: 1px;
  background: var(--line-strong);
}

/* ---------- Disclosure ---------- */

/* Optional detail, folded away. The summary reads as a control rather than as a
   heading: mono, tracked, and carrying a sign that flips when it opens, so the
   page shows at a glance that there is more here without spending a section on
   saying so. */
.disclosure {
  margin-top: 1.5rem;
  border-top: 1px solid var(--line);
}

.disclosure__summary {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* The row is the tap target, not the words in it. */
  min-height: 2.75rem;
  padding-block: 0.5rem;
  font-family: var(--font-mono);
  font-size: var(--step-00);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
  cursor: pointer;
  list-style: none;
  transition: color 0.28s var(--ease);
}

.disclosure__summary::-webkit-details-marker {
  display: none;
}

.disclosure__summary:hover,
.disclosure[open] .disclosure__summary {
  color: var(--fg);
}

/* A plus that loses its upright when the panel opens. Two gradients rather than
   a glyph, so it takes the row's colour and needs no font to be in. */
.disclosure__summary::after {
  content: "";
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
  background:
    linear-gradient(currentColor 0 0) center / 100% 1px no-repeat,
    linear-gradient(currentColor 0 0) center / 1px 100% no-repeat;
  transition: transform 0.32s var(--ease);
}

.disclosure[open] .disclosure__summary::after {
  background:
    linear-gradient(currentColor 0 0) center / 100% 1px no-repeat;
  transform: rotate(180deg);
}

/* The list inside opens against the summary otherwise: the detail lists draw
   their own top rule, and two rules a line apart read as a mistake. */
.disclosure__body {
  padding-bottom: 0.5rem;
}

.disclosure__body > :first-child {
  border-top: 0;
}

/* ---------- Ask the assistant ---------- */

/* The third step of the disclosure ladder: essential copy, optional detail,
   then this. Styled as a line of copy with a control at the end of it rather
   than as a button, because it is an offer to answer a question, not a call to
   action competing with the one in the slab below. */
.ask {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 0.75rem;
  margin-top: 2rem;
  font-size: 0.9375rem;
  color: var(--faint);
}

.ask[hidden] {
  display: none;
}

.ask__button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.375rem 0;
  border: 0;
  background: none;
  color: var(--fg);
  font: inherit;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
  /* The same retracting rule the body links take, so an offer to open the
     assistant reads as a link rather than as a second button style. */
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-origin: content-box;
  background-size: 100% 1px;
  transition: background-size 0.32s var(--ease);
}

.ask__button:hover {
  background-size: 0% 1px;
  background-position: 100% 100%;
}

.ask__button:hover .arrow {
  transform: translateX(3px);
}

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

.section {
  position: relative;
  isolation: isolate;
  padding-block: var(--section-space);
  /* The rule fades out at both ends rather than running the full width.
     border-image draws it, so no pseudo-element is spent on it — both of
     .section's are needed elsewhere. */
  border-bottom: 1px solid var(--line);
  border-image: var(--rule-fade) 1;
}

.section--inset {
  background: var(--bg-inset);
}

/* For a run of sections whose whole content is a heading, a line and a folded
   list. At the full section height five of them in sequence are mostly empty
   band, and the page scrolls like a long one while saying very little. */
.section--compact {
  padding-block: clamp(2.25rem, 5vw, 3.25rem);
}

/* The lead is the last thing in a compact section apart from the fold, so it
   owes no gap below it. The head's own gap is handled by the :has rule further
   down, which every section with a lead already takes. */
.section--compact .section__lead {
  margin-bottom: 0;
}

/* Drafting grid on the alternating bands. Masked to a soft oval so it dissolves
   before it reaches the edges — a grid that tiles flat to the boundary reads as
   a background image, one that fades reads as paper. The cards sitting on top
   are opaque, so it only ever shows in the margins around them. */
.section--inset::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--grid-lines);
  background-size: var(--grid-tiles);
  background-position: center;
  -webkit-mask-image: radial-gradient(120% 90% at 50% 50%, #000 20%, transparent 76%);
  mask-image: radial-gradient(120% 90% at 50% 50%, #000 20%, transparent 76%);
}

.section__head {
  margin-bottom: clamp(2rem, 5vw, 3.25rem);
}

/* The head is full width so that content passing under it while pinned is
   covered edge to edge; the heading text keeps the measure instead. */
.section__head h2 {
  max-width: 46rem;
  font-size: var(--step-3);
  letter-spacing: -0.03em;
}

/* When a lead follows, the head hands the section's bottom spacing over to it
   rather than both of them contributing a gap. */
.section__head:has(+ .section__lead) {
  margin-bottom: 1rem;
}

/* Standfirst. Mobile-first: on a phone the size bump is dropped and the lead is
   carried by contrast alone, because a seven-line paragraph at --step-2 on a
   390px screen is a wall rather than an entry point. The larger setting comes
   back at 40em in responsive.css, where there is width to absorb it. */
.section__lead {
  margin-block: 0 clamp(2rem, 5vw, 3.25rem);
  max-width: 56ch;
  font-size: var(--step-1);
  line-height: 1.45;
  color: var(--muted);
}

/* Flex rather than static flow, so a section that offers two routes onward
   spaces them the way the hero does. With one button it renders as it always
   did. */
.section__action {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 2.5rem;
}

/* A second heading inside one band, where a section carries two related blocks:
   the engagement sequence and what the report it produces contains. Set between
   the section heading and a card title so it reads as a division of the section
   rather than as another cell in the grid above it. */
.section__subhead {
  margin-top: clamp(2.75rem, 6vw, 4rem);
  margin-bottom: clamp(1.25rem, 3vw, 1.75rem);
  font-size: var(--step-2);
  letter-spacing: -0.02em;
}

.prose {
  max-width: 64ch;
}

.prose p {
  font-size: var(--step-1);
  color: var(--muted);
}

/* The opening paragraph of a block carries its argument, so it is set at full
   contrast on a shorter measure — the entry point a skimmer needs before
   deciding whether to read the rest. It takes the size bump at 40em, for the
   same reason the section lead does. */
.prose > p:first-child {
  max-width: 54ch;
  line-height: 1.45;
  color: var(--fg);
}

.prose p + p {
  margin-top: 1.125rem;
}

/* .prose carries no margin of its own, which is right where it is the only
   thing in a section body and wrong where it follows one of the bordered
   blocks — the first line was sitting directly against the last cell's edge.
   The step matches .section__action, so everything that trails a block in a
   section body is set off from it by the same distance. */
:is(.card-grid, .people, .stages, .detail-list) + .prose {
  margin-top: 2.5rem;
}

/* ---------- Hairline grids ----------
   A 1px gap over a rule-coloured background draws the dividers,
   so no cell needs its own border. */

.stages,
.card-grid,
.people {
  position: relative;
  display: grid;
  gap: 1px;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--line);
  border: 1px solid var(--line);
}

/* Registration marks at the four corners, the way a drawing is cropped. Eight
   gradients — a vertical and a horizontal stroke per corner — each pinned by
   background-position, which is cheaper than eight elements and keeps the
   grid's children free. Absolute, so it is not picked up as a grid item. */
.stages::after,
.card-grid::after,
.people::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: 2;
  pointer-events: none;
  opacity: 0.5;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--fg) 0 0), linear-gradient(var(--fg) 0 0),
    linear-gradient(var(--fg) 0 0), linear-gradient(var(--fg) 0 0),
    linear-gradient(var(--fg) 0 0), linear-gradient(var(--fg) 0 0),
    linear-gradient(var(--fg) 0 0), linear-gradient(var(--fg) 0 0);
  background-size:
    1px 14px, 14px 1px,
    1px 14px, 14px 1px,
    1px 14px, 14px 1px,
    1px 14px, 14px 1px;
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}

.stage,
.card,
.person {
  position: relative;
  padding: clamp(1.5rem, 4vw, 2.25rem);
  background: var(--bg);
  box-shadow: var(--shadow-rim);
}

/* Double border: an inset second rule about half a rem in. The cheapest
   letterpress trick there is, and the one place in the kit that does not need a
   colour to work. All three cell types take it, because they are the same cell
   in the same hairline grid and only one of them carrying it would read as a
   mistake. It never takes a pointer event, so the full-card hit area that
   .card__link::after draws over the top of it is unaffected. */
.stage::before,
.card::before,
.person::before {
  content: "";
  position: absolute;
  inset: 0.55rem;
  border: 1px solid var(--line-soft);
  pointer-events: none;
}

.section--inset .stage,
.section--inset .card,
.section--inset .person {
  background: var(--bg-inset);
}

.stage__title {
  margin-top: 0.75rem;
  font-size: var(--step-2);
}

.stage__lead {
  margin-top: 0.75rem;
  font-weight: 500;
}

.stage__body {
  margin-top: 0.5rem;
  font-size: 0.9375rem;
  color: var(--muted);
}

/* ---------- Service cards ---------- */

.card {
  display: flex;
  flex-direction: column;
}

.card__title {
  margin-top: 0.75rem;
  font-size: var(--step-2);
}

.card__link {
  text-decoration: none;
}

/* The whole card is the hit area; the link keeps the accessible name. */
.card__link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.card:hover {
  background: var(--bg-inset);
}

.section--inset .card:hover {
  background: var(--bg);
}

.card__summary {
  margin-top: 0.75rem;
  color: var(--muted);
  font-size: 0.9375rem;
}

.card__more {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.75rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.card:hover .card__more {
  color: var(--fg);
}

.arrow {
  transition: transform 0.15s ease;
}

.card:hover .arrow {
  transform: translateX(3px);
}

/* ---------- Founders ---------- */

.person__name {
  font-size: var(--step-2);
}

.person__role {
  margin-top: 0.5rem;
  color: var(--muted);
}

/* The part a reader is actually weighing. Set at body size rather than as a
   caption, because with no case studies yet this is the evidence. */
.person__background {
  margin-top: 1rem;
  font-size: 0.9375rem;
  color: var(--muted);
}

.person__site {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--step-00);
  letter-spacing: 0.08em;
}

.person__site a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding-block: 0.25rem;
  color: var(--muted);
  text-decoration-color: var(--line-strong);
}

.person__site a:hover {
  color: var(--fg);
}

.person__site a:hover .arrow {
  transform: translateX(3px);
}

/* ---------- Detail lists ---------- */

.detail-list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line);
}

.detail {
  padding-block: 1.5rem;
  border-bottom: 1px solid var(--line);
}

.detail__index {
  margin-bottom: 0.5rem;
}

.detail__title {
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* A service title in a spec list can link to its own section. The underline is
   toned down to the rule colour so the list still reads as a document. */
.detail__title a {
  text-decoration-color: var(--line-strong);
}

.detail__title a:hover {
  text-decoration-color: currentColor;
}

.detail__body {
  margin-top: 0.375rem;
  max-width: 68ch;
  color: var(--muted);
}

/* ---------- Assessment tiers ---------- */

/* The ladder on /pricing. Four cells side by side would read as packages set up
   to be compared against each other, which is the one thing this page is not:
   they are four points on a single scale of how complex the application is. So
   they run down the page on the detail list's specification grid instead — what
   it is called and who it is for on the left, what it covers and what it costs
   on the right. */
.tiers {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line);
}

.tier {
  padding-block: clamp(2.25rem, 5vw, 3rem);
  border-bottom: 1px solid var(--line);
}

.tier__name {
  margin-top: 0.5rem;
  font-size: var(--step-2);
  letter-spacing: -0.02em;
}

.tier__detail {
  margin-top: 1rem;
}

/* The line a reader is scanning for: whether this one is them. Full contrast,
   so it is found before the paragraph under it is read. */
.tier__for {
  font-weight: 500;
  color: var(--fg);
}

.tier__lead {
  margin-top: 0.625rem;
  color: var(--muted);
}

.tier__scope {
  margin: 1.25rem 0 0;
  padding: 0;
  list-style: none;
}

.tier__scope li {
  position: relative;
  margin-top: 0.5rem;
  padding-left: 1.75rem;
  font-size: 0.9375rem;
  color: var(--muted);
}

/* The same hairline marker the enquiry list uses, so "what this covers" reads
   the same wherever on the site it appears. */
.tier__scope li::before {
  content: "";
  position: absolute;
  top: 0.7em;
  left: 0;
  width: 0.75rem;
  height: 1px;
  background: var(--line-strong);
}

/* What this tier does not stretch to. It is set at the note size rather than
   hidden further down the page, because a limit found after the price is a
   limit the reader thinks was being kept from them. */
.tier__note {
  margin-top: 1.25rem;
  font-size: 0.9375rem;
  color: var(--faint);
}

/* Effort, price and action on one line: a number is not worth much without the
   thing that acts on it. */
/* Three items wrapping freely put the quote beside the days in one card and on
   its own line in the next, because where the row broke depended on how long
   the words happened to be. The effort and the quote are one unit now, and the
   button always starts the line below it on a phone, so every card in the
   ladder has the same shape whether or not it has a day count to show. */
.tier__action {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.125rem;
  margin-top: 1.75rem;
}

.tier__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}

/* Days carry the prominence a price would, because days are the number this
   page can actually stand behind: a reader with a rate in their head can do the
   multiplication themselves, and a reader comparing quotes has the one unit
   both quotes share. */
.tier__effort {
  font-size: var(--step-1);
  font-weight: 500;
  color: var(--fg);
}

.tier__quote {
  font-size: 0.9375rem;
  color: var(--faint);
}

/* A closing aside under a section's main content — secondary to it, and carrying
   the gap off whatever grid or list it follows, since the grids have no bottom
   margin of their own. */
.section__note {
  margin-top: clamp(2.25rem, 5vw, 3rem);
  max-width: 64ch;
  font-size: 0.9375rem;
  color: var(--faint);
}

/* A note that introduces the grid instead of following it. It reads as the last
   line of the lead, so it sits close under it and owes only the gap above the
   grid, which is smaller than the one a trailing note leaves behind it. */
.section__note--intro {
  margin-top: 0;
  margin-bottom: clamp(1.5rem, 3.5vw, 2rem);
}

/* Without this the lead's bottom gap and the note's own would stack into two
   full gaps between one paragraph and the grid it introduces. */
.section__lead:has(+ .section__note--intro) {
  margin-bottom: 0.75rem;
}

/* ---------- Page heads ---------- */

/* The inner pages' opening band gets the hero's treatment at lower strength, so
   /about and /contact open with the same language the home page does instead of
   dropping straight into flat copy. */
.page-head {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: clamp(3rem, 8vw, 5rem) clamp(2.5rem, 6vw, 3.75rem);
  border-bottom: 1px solid var(--line);
  border-image: var(--rule-fade) 1;
}

.page-head::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--grid-lines), var(--veil);
  background-size: var(--grid-tiles), auto, auto;
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent 92%);
  mask-image: linear-gradient(to bottom, #000, transparent 92%);
}

.page-head h1 {
  max-width: 22ch;
  font-size: var(--step-4);
  letter-spacing: -0.035em;
}

.page-head--center {
  text-align: center;
}

/* Only the 404 page is centred, so this is its watermark alone. It is the one
   page with nothing to sell and the most room for the mark, and a large skull
   behind "that page does not exist" does the job the copy is already doing. */
.page-head--center::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--fg);
  opacity: 0.045;
  -webkit-mask-image: var(--skull-glyph);
  mask-image: var(--skull-glyph);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: auto 150%;
  mask-size: auto 150%;
}

.page-head--center h1 {
  max-width: none;
}

.page-head__lead {
  margin-top: 1.25rem;
  max-width: 60ch;
  font-size: var(--step-1);
  color: var(--muted);
}

.page-head--center .page-head__lead {
  margin-inline: auto;
}

/* Date line under a page's lead, on documents where when it last changed is
   part of what the reader came to check. */
.page-head__meta {
  margin-top: 1.5rem;
  font-family: var(--font-mono);
  font-size: var(--step-00);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
}

.page-toc ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 2rem 0 0;
  padding: 0;
  list-style: none;
}

.page-head--center .page-toc ul {
  justify-content: center;
}

.page-toc a {
  display: inline-block;
  /* Keeps the tap target at least 24px tall without changing the visual rhythm. */
  padding-block: 0.3125rem;
  color: var(--faint);
  text-decoration-color: var(--line-strong);
}

.page-toc a:hover {
  color: var(--fg);
}

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

.contact-address {
  margin-top: 2rem;
}

.contact-address__link {
  display: inline-block;
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

.enquiry {
  max-width: 64ch;
}

/* The action leads the block rather than closing it: the next step is the point
   of the page, and a reader should not have to get past a list of things to
   mention before they find the way to send the email. The list below it is what
   to put in the message, not a hurdle in front of it. */
.enquiry__prompt {
  margin-top: 2.5rem;
  font-weight: 600;
}

.enquiry__points {
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line);
}

.enquiry__points li {
  position: relative;
  padding: 0.75rem 0 0.75rem 1.75rem;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
}

.enquiry__points li::before {
  content: "";
  position: absolute;
  top: 1.4em;
  left: 0;
  width: 0.75rem;
  height: 1px;
  background: var(--line-strong);
}

.enquiry__note {
  margin-top: 1.5rem;
  font-size: 0.9375rem;
  color: var(--faint);
}

.enquiry__action {
  margin-top: 0;
}

/* Full width on a phone, the same as the hero's pair: the one action on the
   page should not read as an afterthought floating at its own width. */
.enquiry__action .btn {
  display: flex;
}

/* ---------- Contact CTA ---------- */

.cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: clamp(3.5rem, 9vw, 5.5rem);
  background: var(--cta-bg);
  color: var(--cta-fg);
  /* Fading rules top and bottom, so the slab is cut out of the page rather
     than butted against it. */
  border-block: 1px solid transparent;
  border-image: var(--rule-fade-on-dark) 1;
}

/* The drafting lattice, matching the hero and the alternating bands. This
   replaces the soft radial pooling that used to sit here: light pools read as a
   lit room, and the page reads as a technical document. The grain keeps it from
   banding. The slab is dark in both schemes, so the line colour is re-pointed
   rather than inherited. */
.cta::before {
  --grid-line: var(--grid-line-on-dark);

  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: var(--grid-lines), var(--grain-mid);
  background-size: var(--grid-tiles), 220px 220px;
  mix-blend-mode: screen;
  opacity: 0.65;
}

/* The mark, oversized and running off the right edge. Drawn as a mask so it
   takes the slab's own foreground colour rather than shipping a second asset,
   and kept low enough that it reads as a watermark under the copy instead of
   an illustration behind it. Hidden from the accessibility tree by being a
   background: it is the logo repeated, so it carries no new information. */
.cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--cta-fg);
  opacity: 0.05;
  -webkit-mask-image: var(--skull-glyph);
  mask-image: var(--skull-glyph);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  /* Sized so the whole skull sits inside the slab rather than bleeding out of
     it. The mascot is much rounder than the glyph this replaced, and scaled past
     the slab it stops reading as a skull at all: the two eyes become a pair of
     enormous circles behind the copy and nothing identifies them. Its viewBox
     carries about a tenth of a margin on each side, so these percentages are of
     the box, not of the drawing.

     Held back further on a phone. There the button is full width and the copy
     runs the whole slab, so the mark has to sit in the corner above them;
     at 40em the copy takes the left half and it can come up to nearly the
     slab's height. */
  -webkit-mask-position: right -1.5rem top 50%;
  mask-position: right -1.5rem top 50%;
  -webkit-mask-size: auto 70%;
  mask-size: auto 70%;
}

/* Registration brackets framing the copy, the same corner language the hairline
   grids use. Absolute, so the grid this sits on does not pick it up as a
   child. */
.cta__inner {
  position: relative;
}

.cta__inner::before {
  content: "";
  position: absolute;
  inset: calc(var(--gutter) * -0.5) 0;
  pointer-events: none;
  opacity: 0.35;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--cta-fg) 0 0), linear-gradient(var(--cta-fg) 0 0),
    linear-gradient(var(--cta-fg) 0 0), linear-gradient(var(--cta-fg) 0 0),
    linear-gradient(var(--cta-fg) 0 0), linear-gradient(var(--cta-fg) 0 0),
    linear-gradient(var(--cta-fg) 0 0), linear-gradient(var(--cta-fg) 0 0);
  background-size:
    1px 18px, 18px 1px,
    1px 18px, 18px 1px,
    1px 18px, 18px 1px,
    1px 18px, 18px 1px;
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}

.cta__inner {
  display: grid;
  gap: 2rem;
}

.cta h2 {
  font-size: var(--step-3);
  letter-spacing: -0.03em;
}

.cta__text p {
  margin-top: 1rem;
  max-width: 52ch;
  opacity: 0.78;
}

/* The address now carries its own padding for the tap target, so the stack gap
   is smaller than it looks: the two add up to about the 1.25rem this used to
   set on its own. At 40em the actions become a row and take the wider gap. */
.cta__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

/* Full width on a phone, the same as the hero's pair and for the same reason: a
   single button at its own width, floating in the middle of a slab this wide,
   reads as an afterthought rather than as the point of the section. It takes
   its own width back at 40em, where the actions become a row. */
.cta__actions .btn {
  align-self: stretch;
}

/* The address takes its quiet from the slab's own foreground rather than from
   --faint, which is a page token: against the slab it was thin, and the shared
   hover further up resolves to --fg, which in the light scheme is very nearly
   the slab's own background — the address disappeared under the pointer. */
.cta__email {
  color: color-mix(in srgb, var(--cta-fg) 66%, transparent);
}

.cta__email:hover {
  color: var(--cta-fg);
}

/* The button inverts the panel it sits on rather than the page, so it stays
   legible whichever surface the CTA resolves to. */
.cta .btn--primary {
  background: var(--cta-fg);
  border-color: var(--cta-fg);
  color: var(--cta-bg);
}

.cta :focus-visible {
  outline-color: var(--cta-fg);
}

.cta .btn--primary:hover {
  background: transparent;
  border-color: currentColor;
  color: var(--cta-fg);
}

/* The tap target comes from padding rather than from a centred flex box with a
   min-height. The retracting underline further up is a background pinned to the
   bottom of the positioning area, so on a 40px box centring the text left the
   rule floating a third of an inch below the address with nothing between them:
   on a phone, where the address is the last thing in the slab, it read as a
   stray hairline rather than as a link. Anchoring the background to the content
   box puts it back under the text and keeps the 44px target. */
.cta__email {
  display: inline-block;
  padding-block: 0.8rem;
  background-origin: content-box;
  color: inherit;
  opacity: 0.78;
  overflow-wrap: anywhere;
}

/* Holds the slab's own foreground rather than taking the page's --fg from the
   shared body-link hover — on the light scheme those are opposites, and --fg
   would put near-black text on a near-black slab. */
.cta__email:hover {
  color: inherit;
  opacity: 1;
}

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

/* The page now runs to the bottom edge as well, so the last row of the footer
   clears the home indicator. */
.site-footer {
  padding-block: clamp(3rem, 7vw, 4.5rem) calc(1.75rem + env(safe-area-inset-bottom));
}

.site-footer__inner {
  display: grid;
  gap: 2.5rem;
}

.brand--footer {
  display: flex;
}

.site-footer__tagline {
  margin-top: 1rem;
  font-weight: 500;
}

.site-footer__statement {
  margin-top: 0.25rem;
  color: var(--faint);
}

/* The one place on every page that states which company this brand belongs to,
   so it sits a step above the statement's weight rather than below it. */
.site-footer__parent {
  margin-top: 0.75rem;
  font-size: var(--step-00);
  color: var(--muted);
}

.site-footer__nav {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2rem 1.5rem;
}

/* Two narrow columns cannot hold the email address on one line, so it takes
   the full row until there is room for three columns. */
.site-footer__group--contact {
  grid-column: 1 / -1;
}

.site-footer__heading {
  margin-bottom: 0.875rem;
  font-weight: 500;
}

.site-footer__nav ul {
  display: grid;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer__nav a {
  display: inline-block;
  padding-block: 0.1875rem;
  font-size: 0.875rem;
  color: var(--muted);
  text-decoration: none;
  /* The email address is one long unbreakable token — let it wrap rather than
     push the footer grid wider than the viewport. */
  overflow-wrap: anywhere;
}

.site-footer__nav a:hover {
  color: var(--fg);
  text-decoration: underline;
}

/* The divider replaces the rule this row used to draw for itself, so the two do
   not stack into a double line. */
.site-footer .divider {
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
}

.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  padding-top: 1.5rem;
}

/* The separator is a span rather than a border or a ::before, so the row can
   wrap between the links on a narrow screen without leaving a rule hanging. */
.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.site-footer__base a {
  color: inherit;
}

.site-footer__base a:hover {
  color: var(--fg);
}
