/* ---------------------------------------------------------------------------
   INVADE International — site styles

   Palette is taken from the company logo: the charcoal of the wordmark and the
   amber of the chevron. Amber is rationed to active states and rules only; it
   fails WCAG AA against white at body sizes, so it never carries body copy.
   --------------------------------------------------------------------------- */

@font-face {
  font-family: "Archivo";
  src: url("../fonts/archivo.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-stretch: 62% 125%;
  font-display: swap;
}
@font-face {
  font-family: "Public Sans";
  src: url("../fonts/publicsans.woff2") format("woff2-variations");
  font-weight: 400 600;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/plexmono.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

:root {
  --ink: #23262B;
  --slate: #57585A;
  --panel: #F2F3F1;
  --surface: #FFFFFF;
  --amber: #FBB03F;
  --signal: #1F6F5C;

  /* Fixed in both themes. The footer and the video frame are dark blocks
     whatever the page around them is doing; amber controls always carry dark
     text; partner logos always sit on white, because several of the files
     have white baked in. */
  --deep: #23262B;
  --on-deep: #F2F3F1;
  --on-amber: #23262B;
  --tile: #FFFFFF;

  --rule: color-mix(in srgb, var(--slate) 22%, transparent);
  --muted: color-mix(in srgb, var(--slate) 30%, transparent);

  --font-display: "Archivo", system-ui, sans-serif;
  --font-body: "Public Sans", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;

  --step--1: clamp(0.83rem, 0.8rem + 0.15vw, 0.9rem);
  --step-0: clamp(1rem, 0.96rem + 0.2vw, 1.12rem);
  --step-1: clamp(1.3rem, 1.2rem + 0.5vw, 1.6rem);
  --step-2: clamp(1.8rem, 1.5rem + 1.4vw, 2.6rem);
  --step-3: clamp(2.2rem, 1.6rem + 2.9vw, 4.2rem);

  --measure: 62ch;
  --gutter: clamp(1.25rem, 4vw, 3rem);

  color-scheme: light;
}

/* Dark theme. Follows the browser setting unless the reader has picked a theme
   with the footer switch, which theme.js records as data-theme on the root. The
   two blocks carry the same values, one for each route in. Amber stays as it
   is: it was only ever rationed on white for contrast, and reads well on
   charcoal. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #E6E7E4;
    --slate: #A3A5A6;
    --panel: #16181B;
    --surface: #1F2226;
    --signal: #3A9C81;
    --deep: #0D0E10;
    --on-deep: #E6E7E4;
    color-scheme: dark;
  }
}
:root[data-theme="dark"] {
  --ink: #E6E7E4;
  --slate: #A3A5A6;
  --panel: #16181B;
  --surface: #1F2226;
  --signal: #3A9C81;
  --deep: #0D0E10;
  --on-deep: #E6E7E4;
  color-scheme: dark;
}

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

/* Reserve the scrollbar gutter on every page. Without it, a short page (404,
   Contact) has no scrollbar and a long one does, so the whole layout jumps
   sideways by the scrollbar width on each navigation. overflow-y is the
   fallback for browsers without scrollbar-gutter. */
html {
  overflow-y: scroll;
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  background: var(--panel);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-stretch: 118%;
  font-weight: 700;
  text-wrap: balance;
  margin: 0 0 0.5em;
}

/* Tracking and leading belong to the size, not to the heading level. One pair
   across a range that runs from 1.3rem to 4.2rem is necessarily wrong at one
   end: large type reads too loose unless it is tightened, and 1.05 leading on a
   two-line subhead is cramped. Each step carries its own pair, and headings set
   at a step other than their own take that step's pair with it. */
h1 { font-size: var(--step-3); letter-spacing: -0.028em; line-height: 1.02; }
h2 { font-size: var(--step-2); letter-spacing: -0.018em; line-height: 1.1; }
h3 {
  font-size: var(--step-1);
  font-stretch: 105%;
  letter-spacing: -0.004em;
  line-height: 1.2;
}

p,
li {
  max-width: var(--measure);
}

p { text-wrap: pretty; }

a {
  color: var(--ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
a:hover { text-decoration-color: var(--amber); }

:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
}
/* The footer background is --ink, so the default ring is drawn in the colour it
   sits on and disappears entirely — including on the Privacy Policy link. */
.site-footer :focus-visible {
  outline-color: var(--on-deep);
}

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: var(--gutter);
  top: 0.5rem;
  z-index: 10;
  background: var(--ink);
  color: var(--surface);
  padding: 0.6rem 1rem;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--slate);
  max-width: none;
}

.wrap {
  width: min(76rem, 100% - var(--gutter) * 2);
  margin-inline: auto;
}

img {
  max-width: 100%;
  height: auto;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* --- Site chrome ---------------------------------------------------------- */

.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}
.site-header__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.75rem 0;
}

/* Sticky only where there is vertical room to spare. On a 320px screen a
   three-line sticky header would eat a seventh of the viewport. */
@media (min-width: 52rem) {
  .site-header {
    position: sticky;
    top: 0;
    z-index: 5;
  }
  /* Anything scrolled to by fragment — the skip link's #main-content above all —
     otherwise lands underneath the sticky header. */
  html {
    scroll-padding-top: 5rem;
  }
  .site-header__inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
  }
}
.site-header__logo img {
  display: block;
  height: 34px;
  width: auto;
}

/* Navigation wraps rather than collapsing behind a disclosure. */
.site-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
  align-items: center;
}
.site-nav li { max-width: none; }
.site-nav a {
  text-decoration: none;
  font-weight: 600;
  display: inline-block;
  padding: 0.15rem 0;
  transition: box-shadow 0.15s ease;
}
.site-nav a[aria-current="page"] {
  box-shadow: inset 0 -0.35em 0 var(--amber);
}
/* Hover states are gated on a device that actually has a pointer. On touch a
   tap leaves :hover latched until something else is tapped, so an unguarded
   rule paints a nav link as though it were the current page. */
@media (hover: hover) {
  .site-nav a:hover { box-shadow: inset 0 -0.35em 0 var(--amber); }
}
/* The portal link is a destination, not a page in this site's navigation, so
   it is boxed and set smaller to read as a control rather than a nav item.
   Written at .site-nav a specificity so the shared nav rules do not win. */
.site-nav a.site-nav__portal {
  font-family: var(--font-mono);
  font-size: calc(var(--step--1) * 0.9);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 400;
  border: 1px solid var(--ink);
  padding: 0.35rem 0.75rem;
  margin-left: 0.25rem;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    transform 0.1s ease-out;
}
.site-nav a.site-nav__portal:focus-visible,
.site-nav a.site-nav__portal:active {
  box-shadow: none;
  color: var(--on-amber);
  background: var(--amber);
  border-color: var(--amber);
}
@media (hover: hover) {
  .site-nav a.site-nav__portal:hover {
    box-shadow: none;
    color: var(--on-amber);
    background: var(--amber);
    border-color: var(--amber);
  }
}

/* --- Mobile navigation ----------------------------------------------------
   Below the row breakpoint the nav collapses behind a disclosure. Wrapping it
   onto two lines, as it did before, left 22px tap targets packed together —
   present, but not usable with a thumb.

   Every collapsed rule is scoped to .site-header--collapsible, which nav.js
   adds. Without JS none of this applies and the nav wraps as it always did,
   so the menu can never end up hidden behind a button that cannot open it.
   -------------------------------------------------------------------------- */

.nav-toggle {
  display: none;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: calc(var(--step--1) * 0.9);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--ink);
  padding: 0.5rem 0.85rem;
  min-height: 44px; /* the control itself has to be thumb-sized too */
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    transform 0.1s ease-out;
}
.nav-toggle:focus-visible,
.nav-toggle:active {
  color: var(--on-amber);
  background: var(--amber);
  border-color: var(--amber);
}
@media (hover: hover) {
  .nav-toggle:hover {
    color: var(--on-amber);
    background: var(--amber);
    border-color: var(--amber);
  }
}

/* Three bars drawn from one element: the span is the middle rule, its two
   pseudo-elements the outer pair. */
.nav-toggle__bars {
  position: relative;
  display: block;
  width: 1.15rem;
  height: 2px;
  background: currentColor;
}
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  transition: transform 0.18s ease;
}
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after { top: 6px; }

/* Open: the outer bars cross and the middle one gets out of the way. */
.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before {
  transform: translateY(6px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after {
  transform: translateY(-6px) rotate(-45deg);
}

@media (max-width: 51.999rem) {
  .site-header--collapsible .nav-toggle { display: inline-flex; }

  /* Logo and toggle share one row; the panel drops full width beneath them. */
  .site-header--collapsible .site-header__inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
  }
  .site-header--collapsible .site-nav { flex-basis: 100%; }
  .site-header--collapsible .site-nav ul { display: none; }

  .site-header--collapsible .site-nav[data-open="true"] ul {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-bottom: 0.5rem;
  }
  .site-header--collapsible .site-nav[data-open="true"] li {
    border-top: 1px solid var(--rule);
  }
  .site-header--collapsible .site-nav[data-open="true"] a {
    display: block;
    padding: 0.85rem 0.15rem 0.85rem 0.85rem; /* ~53px tall, past the 44px minimum */
  }
  /* Stacked, the shared underline renders as a heavy bar across the full row and
     reads as a divider. A left marker is the vertical-list equivalent. */
  .site-header--collapsible .site-nav[data-open="true"] a[aria-current="page"] {
    box-shadow: inset 0.28rem 0 0 var(--amber);
  }
  @media (hover: hover) {
    .site-header--collapsible .site-nav[data-open="true"] a:hover {
      box-shadow: inset 0.28rem 0 0 var(--amber);
    }
  }
  /* The portal keeps its box, but as a full-width control closing the list. */
  .site-header--collapsible .site-nav[data-open="true"] li:last-child {
    border-top: none;
  }
  .site-header--collapsible .site-nav[data-open="true"] a.site-nav__portal {
    margin: 0.9rem 0 0.25rem;
    padding: 0.75rem;
    text-align: center;
  }
}

.site-footer {
  background: var(--deep);
  color: var(--on-deep);
  margin-top: clamp(3rem, 8vw, 6rem);
  padding: clamp(2.5rem, 6vw, 4rem) 0 2rem;
}
.site-footer a { color: var(--on-deep); }
.site-footer a:hover { text-decoration-color: var(--amber); }
.site-footer .eyebrow { color: var(--amber); }
.site-footer__inner {
  display: grid;
  gap: 2.5rem;
}
/* The mark heads the footer as a full-width row, so the ink block opens with the
   brand rather than with an address. The light knockout is a second file rather
   than a filter on the first: only the charcoal half may be lightened, and the
   amber has to stay amber. */
.site-footer__mark {
  grid-column: 1 / -1;
  margin: 0;
}
.site-footer__mark img {
  display: block;
  height: 40px;
  width: auto;
}
@media (min-width: 52rem) {
  .site-footer__inner { grid-template-columns: 1fr 1.2fr 1fr; }
}
.site-footer__address {
  font-style: normal;
  line-height: 1.8;
}
/* Statutory disclosure. A UK company must show its registered number and place
   of registration on its website. It sits under the office address, in the same
   muted tone as the build credit, because it is a legal requirement rather than
   something a reader came for. */
.site-footer__reg {
  margin: 0.9rem 0 0;
  font-size: var(--step--1);
  line-height: 1.7;
  color: color-mix(in srgb, var(--on-deep) 68%, var(--slate));
}

.site-footer__contact {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}
.site-footer__contact li { max-width: none; }
.site-footer__contact span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: color-mix(in srgb, var(--on-deep) 60%, var(--slate));
}
.site-footer__certs {
  font-family: var(--font-display);
  font-stretch: 118%;
  font-weight: 700;
  font-size: var(--step-1);
  line-height: 1.2;
}
/* Second eyebrow in the same column needs the separation the first gets from
   the column top. */
.site-footer__certs + .eyebrow { margin-top: 1.5rem; }

/* Social profiles. Set as words rather than glyph buttons: two links do not
   justify an icon set, and "LinkedIn" is its own accessible name. */
.site-footer__social {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
}
.site-footer__social li { max-width: none; }
.site-footer__meta {
  font-size: var(--step--1);
  margin-top: 2rem;
  line-height: 1.9;
}

/* Build credit, sat under the three columns as a full-width bottom rule. The
   footer's own --rule is mixed from slate and all but vanishes on ink, so this
   one is mixed from the panel colour instead. */
.site-footer__credit {
  grid-column: 1 / -1;
  max-width: none;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  padding-top: 1.35rem;
  border-top: 1px solid color-mix(in srgb, var(--on-deep) 20%, transparent);
  font-size: var(--step--1);
  color: color-mix(in srgb, var(--on-deep) 68%, var(--slate));
}
/* Light and dark switch, added to the credit row by theme.js. Square and
   outlined like the header's portal button, icon only because the sun and moon
   are understood without a word; the label carries the name. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--on-deep);
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--on-deep) 30%, transparent);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease,
    transform 0.1s ease-out;
}
.theme-toggle:focus-visible,
.theme-toggle:active {
  color: var(--amber);
  border-color: var(--amber);
}
@media (hover: hover) {
  .theme-toggle:hover {
    color: var(--amber);
    border-color: var(--amber);
  }
}
.theme-toggle:active { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) {
  .theme-toggle:active { transform: none; }
}

/* Amber rather than the conventional red: it is the only accent this palette
   spends, and the footer already uses it on the eyebrows. */
.heart {
  color: var(--amber);
  font-style: normal;
}

/* --- The call-channel band ------------------------------------------------
   The signature element. Every tick is one telephony channel moving through
   the states a predictive dialler drives: idle, ringing, connected, interview.
   Amber marks the moment of connection, which is the only place on the page
   amber is spent.
   -------------------------------------------------------------------------- */

.band {
  margin: clamp(2rem, 5vw, 3.5rem) 0 0;
}
/* The ticks are built by channel-band.js. With no JS the track stays empty, and
   an empty bordered box with a legend under it looks like a fault rather than
   an omission — so the figure folds away entirely. */
.band:has(.band__track:empty) { display: none; }
.band__track {
  display: flex;
  gap: 3px;
  align-items: stretch; /* every tick is full height; scaleY draws the level */
  height: 84px;
  padding: 0.75rem;
  background: var(--surface);
  border: 1px solid var(--rule);
}
/* The tick is laid out at full height once and scaled from its base, rather
   than animating its own height. Height on 48 flex children is a full layout
   pass of the track on every frame, for as long as the page is open; scaleY is
   a compositor transform and costs nothing to lay out. The frames look the
   same. */
.channel {
  flex: 1 1 auto;
  min-width: 2px;
  transform: scaleY(0.34);
  transform-origin: bottom;
  background: var(--muted);
  animation: channel-cycle 7s linear infinite;
  animation-delay: calc(var(--i) * -0.146s);
}
/* Held while the band is off screen — channel-band.js sets the attribute. */
.band__track[data-paused] .channel {
  animation-play-state: paused;
}

/* Interview holds for roughly two-thirds of every cycle. Dialling, ringing and
   the moment of connection are seconds; the interview that follows runs for
   minutes, and a dialler is judged on how much of the hour it fills. */
@keyframes channel-cycle {
  0%,
  15% { transform: scaleY(0.34); background: var(--muted); }
  16%,
  23% { transform: scaleY(0.55); background: var(--slate); }
  24%,
  29% { transform: scaleY(0.76); background: var(--amber); }
  30%,
  99% { transform: scaleY(1); background: var(--signal); }
  100% { transform: scaleY(0.34); background: var(--muted); }
}

.band__legend {
  margin-top: 0.85rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.75rem;
  align-items: baseline;
}
.band__legend ul {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--slate);
}
.band__legend li {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  max-width: none;
}
.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  display: inline-block;
  flex: none;
}
.dot--dial { background: var(--muted); }
.dot--ring { background: var(--slate); }
.dot--conn { background: var(--amber); }
.dot--intv { background: var(--signal); }

/* Reduced motion: hold a static pattern rather than animating. The band still
   reads as a call centre, it just stops moving. */
@media (prefers-reduced-motion: reduce) {
  .channel { animation: none; transform: scaleY(1); background: var(--signal); }
  /* Interview is the resting state here too, so the still frame carries the
     same proportions as the animated one. */
  .channel:nth-child(7n + 1) { transform: scaleY(0.34); background: var(--muted); }
  .channel:nth-child(7n + 3) { transform: scaleY(0.55); background: var(--slate); }
  .channel:nth-child(7n + 6) { transform: scaleY(0.76); background: var(--amber); }
}

/* --- Page layouts ---------------------------------------------------------
   Section padding is set on classed selectors rather than bare elements, so a
   later rule cannot silently cancel it.
   -------------------------------------------------------------------------- */

.hero {
  background: var(--panel);
  padding: clamp(2.25rem, 5vw, 4rem) 0 clamp(2rem, 5vw, 3.5rem);
}

/* Headline and call-centre video sit side by side, with the band spanning
   underneath both. The type steps down from the full display scale to make
   room: at --step-3 in a half-width column the headline sets to six or seven
   ragged lines. */
.hero__inner {
  display: grid;
  gap: 2.5rem;
}
@media (min-width: 58rem) {
  .hero__inner {
    grid-template-columns: 1.1fr 1fr;
    gap: 2.5rem 3rem;
    align-items: end;
  }
  .hero__body { grid-column: 1; }
  .hero__media { grid-column: 2; }
  .hero .band { grid-column: 1 / -1; }
}
/* The grid gap already separates the band; its own top margin would double it. */
.hero .band { margin-top: 0; }
/* Stepped down from --step-3, so its tracking eases back off the tightening h1
   takes at full display size. */
.hero h1 {
  font-size: clamp(1.9rem, 1.15rem + 2.4vw, 3.1rem);
  letter-spacing: -0.022em;
  line-height: 1.04;
  max-width: 20ch;
}
/* A lede sits a step above body copy, so body's 1.6 leading is loose on it. */
.hero__lede {
  font-size: var(--step-1);
  line-height: 1.42;
  letter-spacing: -0.005em;
  max-width: 44ch;
  color: var(--slate);
  margin-bottom: 1.25rem;
}
.hero__cta { margin-bottom: 0; }

/* The one photographic element on the site. Contained and captioned rather
   than run as a background behind the headline: that would force light-on-dark
   type, need a scrim, and compete with the band, which is the signature. */
.hero__media {
  margin: 0;
}
/* The pause control is positioned against this, not against the figure — the
   figure now also holds the caption, and anchoring to it would drop the button
   on top of the text. */
.hero__frame {
  position: relative;
}
.hero__video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  /* Charcoal fills the box before the poster decodes. Clipped to the padding
     box so it stops at the frame: the rule below is semi-transparent, and left
     to paint over charcoal it composites to an almost-black hairline. */
  background: var(--deep);
  background-clip: padding-box;
  border: 1px solid var(--rule);
}

/* Same control language as the header's portal button. */
.hero__video-toggle {
  position: absolute;
  right: 0.7rem;
  bottom: 0.7rem;
  font-family: var(--font-mono);
  font-size: calc(var(--step--1) * 0.85);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--ink);
  padding: 0.3rem 0.7rem;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    transform 0.1s ease-out;
}
.hero__video-toggle:focus-visible,
.hero__video-toggle:active {
  color: var(--on-amber);
  background: var(--amber);
  border-color: var(--amber);
}
@media (hover: hover) {
  .hero__video-toggle:hover {
    color: var(--on-amber);
    background: var(--amber);
    border-color: var(--amber);
  }
}

/* Press feedback lands on pointer-down rather than waiting for the click. On
   touch there is no hover to stand in for it, so without this a tap reads as
   though nothing happened until the panel or the video state catches up.
   Written after the three controls' own rules so source order settles it. */
.nav-toggle:active,
.site-nav a.site-nav__portal:active,
.hero__video-toggle:active {
  transform: scale(0.97);
}
/* The scale goes; the amber that lands with it stays, so a control still
   confirms the touch without anything moving. A media query carries no
   specificity of its own, so this has to sit after the rule it cancels. */
@media (prefers-reduced-motion: reduce) {
  .nav-toggle:active,
  .site-nav a.site-nav__portal:active,
  .hero__video-toggle:active {
    transform: none;
  }
}

.hero__cta a,
.about__cta a,
.cta-link a,
.article__back a {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-decoration: none;
  border-bottom: 2px solid var(--amber);
  padding-bottom: 0.2rem;
  transition: border-color 0.18s ease;
}
.hero__cta a:hover,
.about__cta a:hover,
.cta-link a:hover,
.article__back a:hover {
  border-bottom-color: var(--ink);
}
.cta-link { margin-top: 1.75rem; }
.hero__cta a + a { margin-left: 1.5rem; }

.pillars {
  display: grid;
  gap: 2.5rem;
  padding-block: clamp(2.25rem, 4vw, 3.25rem) clamp(2rem, 4vw, 2.75rem);
}
@media (min-width: 52rem) {
  .pillars { grid-template-columns: repeat(3, 1fr); gap: 3rem; }
}
/* Set at --step-1, so it takes that step's tracking and leading rather than the
   pair h2 carries at its own, much larger, size. */
.pillar h2 {
  font-size: var(--step-1);
  font-stretch: 105%;
  letter-spacing: -0.004em;
  line-height: 1.2;
  margin: 0.85rem 0 0.6rem;
}
.pillar { border-top: 2px solid var(--amber); padding-top: 1rem; }
/* A second row of pillars below the about section closes it off the same way
   the about section's own rule closes off the first row. */
.about + .pillars { border-top: 1px solid var(--rule); }
.pillar p { color: var(--slate); }
/* The columns are uneven by nature, so the trailing margin on the shortest one
   is invisible while the tallest one pushes it into the gap below. */
.pillar p:last-child { margin-bottom: 0; }

.about {
  /* Asymmetric: the rule above should sit close to the pillars it closes off,
     while the section still needs room beneath it before the next one. */
  padding-block: clamp(2rem, 4vw, 2.75rem) clamp(2.5rem, 6vw, 4rem);
  border-top: 1px solid var(--rule);
  display: grid;
  gap: 2rem;
}
@media (min-width: 52rem) {
  .about { grid-template-columns: 1.4fr 1fr; gap: 4rem; align-items: start; }
}
.about h2 { max-width: 20ch; }
.about__cta { margin-top: 1.75rem; }
.about__aside { align-self: center; }
/* Proof points beside the copy. Each claim leads on the thing that can be
   checked — the standard, the year, the cover — with the qualifier after it in
   body weight, so the column scans as evidence rather than as a slogan. */
.trust {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 1rem 1.25rem;
  background: var(--surface);
  border-left: 4px solid var(--amber);
  max-width: 40ch;
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--slate);
}
.trust li {
  max-width: none;
  padding-block: 0.55rem;
  border-top: 1px solid var(--rule);
}
.trust li:first-child {
  padding-top: 0;
  border-top: 0;
}
.trust li:last-child { padding-bottom: 0; }
.trust strong {
  display: block;
  font-family: var(--font-display);
  font-stretch: 118%;
  font-weight: 700;
  font-size: var(--step-0);
  color: var(--ink);
}

/* Interior pages sit on the same rail as `.wrap`, so the content edge lines up
   with the header, footer, home page, news list and articles — nothing shifts
   sideways when you move between pages. Body copy is kept off the full width by
   `--measure` on the copy itself, not by narrowing the container. */
.page {
  padding-block: clamp(2.5rem, 6vw, 4rem);
}
.page h2 {
  font-size: var(--step-2);
  margin-top: 2.5em;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}
.page h2:first-of-type { margin-top: 1.5em; }
/* Error pages are otherwise bare text. Setting the mark above the heading tells
   someone who has landed somewhere wrong that they are at least still on
   INVADE. Sized to stay inside the source file's own resolution. */
.page__mark {
  margin: 0 0 1.5rem;
}
.page__mark img {
  display: block;
  height: 64px;
  width: auto;
}
.lede {
  font-size: var(--step-1);
  line-height: 1.42;
  letter-spacing: -0.005em;
  color: var(--slate);
  max-width: 54ch;
}

.page--legal p { font-size: var(--step-0); }
.page--legal { --measure: 72ch; }

.page--contact h2 {
  font-size: var(--step-1);
  font-stretch: 105%;
  letter-spacing: -0.004em;
  line-height: 1.2;
  border-top: none;
  padding-top: 0;
  margin-top: 2.25rem;
}
.contact-address {
  font-style: normal;
  line-height: 1.8;
  max-width: var(--measure);
}
.contact-list {
  list-style: none;
  margin: 0;
  padding-left: 0;
  display: grid;
  gap: 1rem;
}
.contact-list li { max-width: none; }
.contact-list span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--slate);
}

/* Partner logos. Real partners, so this row is content rather than decoration.
   Each sits on its own white tile: the source files carry different baked-in
   backgrounds (SPSS is knocked out of dark, Forsta out of white), so without a
   tile the row reads as a set of mismatched boxes. */
.logos {
  list-style: none;
  margin: 2.5rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 1rem;
}
.logos li {
  max-width: none;
  background: var(--tile);
  border: 1px solid var(--rule);
  padding: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.logos img {
  display: block;
  width: 100%;
  height: 44px;
  object-fit: contain;
}

/* --- Interior page components --------------------------------------------
   These sit after `.page h2` on purpose: several of them cancel that rule's
   top rule and margin, and they match its specificity, so source order is
   what decides.
   -------------------------------------------------------------------------- */

/* Deployment spec. A dialler is bought on what it will run on, so the
   options are set as a spec sheet rather than buried in a sentence. */
.spec {
  margin: 2rem 0 1.5rem;
  max-width: 52rem;
  border-top: 1px solid var(--rule);
}
.spec > div {
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--rule);
  display: grid;
  gap: 0.15rem 1.5rem;
}
@media (min-width: 40rem) {
  .spec > div { grid-template-columns: 13rem 1fr; align-items: baseline; }
}
.spec dt {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--slate);
}
.spec dd { margin: 0; }

/* Installation steps. Numbered because the deployment genuinely runs in this
   order — the numerals are the sequence, not decoration. */
.steps {
  list-style: none;
  counter-reset: step;
  margin: 2rem 0 2rem;
  padding: 0;
  max-width: 52rem;
  border-top: 1px solid var(--rule);
}
.steps li {
  counter-increment: step;
  max-width: none;
  border-bottom: 1px solid var(--rule);
  padding: 1.5rem 0;
  display: grid;
  grid-template-columns: 2.75rem 1fr;
}
.steps li::before {
  content: counter(step, decimal-leading-zero);
  grid-column: 1;
  grid-row: 1 / -1;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.9;
  color: var(--slate);
}
.steps li > * { grid-column: 2; margin: 0; }
.steps li h3 { margin-bottom: 0.35rem; }
.steps li p { color: var(--slate); }

/* Product screenshots. The portal UI is a dark theme, so each shot is padded to
   a common 4:3 on its own background — a ragged row of native aspect ratios
   reads as three unrelated images rather than one interface. */
.shots {
  margin: 2rem 0 0;
  display: grid;
  gap: 1.5rem;
}
@media (min-width: 46rem) {
  .shots { grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
}
.shots figure { margin: 0; }
/* <picture> is inline by default, which would leave a text-baseline gap under
   each shot that the bare <img> rule never had to deal with. */
.shots picture,
.logos picture { display: block; }
.shots img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
}
.shots figcaption {
  margin-top: 0.6rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--slate);
}

/* Literal values — an address, a setting — set in the utility face. */
.lit {
  font-family: var(--font-mono);
  font-size: 0.92em;
  word-break: break-word;
}

/* Leadership bios. Same hairline-separated row as .spec and .steps, so the
   page reads as one structure rather than three different list treatments. */
.people {
  list-style: none;
  margin: 2rem 0 0;
  padding: 0;
  max-width: 52rem;
  border-top: 1px solid var(--rule);
}
.person {
  max-width: none;
  border-bottom: 1px solid var(--rule);
  padding: 2rem 0;
  display: grid;
  gap: 1.25rem;
  justify-items: start;
}
@media (min-width: 40rem) {
  .person {
    grid-template-columns: 9.5rem 1fr;
    gap: 2rem;
    align-items: start;
  }
}
.person__photo {
  display: block;
  width: 9.5rem;
  height: 9.5rem;
  object-fit: cover;
  border: 1px solid var(--rule);
}
.person h3 { margin: 0 0 0.2rem; }
.person__role {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--slate);
  margin: 0 0 0.9rem;
}
.person__text p { margin: 0 0 0.75rem; }
.person__text p:last-child { margin-bottom: 0; }

/* Two-up block, reusing .pillar for the amber rule and type. */
.duo {
  display: grid;
  gap: 2rem;
  margin-top: 2rem;
}
@media (min-width: 46rem) {
  .duo { grid-template-columns: 1fr 1fr; gap: 2.5rem; }
}
.duo .pillar h3 { margin: 0 0 0.5rem; }
.duo .pillar p { margin: 0; }

/* Closing call to action. White against the page's panel grey, so it lifts
   without competing with the ink footer directly below it. */
.cta-panel {
  margin-top: clamp(3rem, 7vw, 4.5rem);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--amber);
  padding: clamp(1.75rem, 4vw, 2.75rem);
}
/* The eyebrow and the mark share the panel's top line, so the brand signs off
   the closing call to action. A flex row rather than an absolutely positioned
   corner device: the heading runs to 22ch and would collide with it on a narrow
   screen. The bottom margin stands in for the eyebrow's own, which is dropped
   here so the two items align on their centres. */
.cta-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.85rem;
}
.cta-panel__head .eyebrow { margin: 0; }
.cta-panel__mark {
  display: block;
  height: 28px;
  width: auto;
  flex: none;
}

/* Beats `.page h2:first-of-type`, which matches here because the panel is the
   heading's parent. */
.page .cta-panel h2 {
  border-top: none;
  padding-top: 0;
  margin: 0.4rem 0 0.75rem;
  max-width: 22ch;
}
.cta-panel > p { color: var(--slate); }
.cta-panel__actions {
  list-style: none;
  margin: 1.75rem 0 0;
  padding: 1.5rem 0 0;
  border-top: 1px solid var(--rule);
  display: grid;
  gap: 1.25rem;
}
@media (min-width: 46rem) {
  .cta-panel__actions { grid-template-columns: repeat(3, 1fr); gap: 2rem; }
}
.cta-panel__actions li { max-width: none; }
.cta-panel__actions span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--slate);
  margin-bottom: 0.15rem;
}
.cta-panel__actions a {
  font-weight: 600;
  text-decoration: none;
  box-shadow: inset 0 -0.12em 0 var(--amber);
  transition: box-shadow 0.15s ease;
}
.cta-panel__actions a:hover { box-shadow: inset 0 -0.35em 0 var(--amber); }

/* News */
.news { padding-block: clamp(2.5rem, 6vw, 4rem); }
.news-list { list-style: none; margin: 2.5rem 0 0; padding: 0; }
.news-item {
  border-top: 1px solid var(--rule);
  padding: 1.75rem 0;
  max-width: none;
}
.news-item:last-child { border-bottom: 1px solid var(--rule); }
.news-item__meta { margin: 0 0 0.4rem; }
.news-item h2 {
  font-size: var(--step-1);
  font-stretch: 105%;
  letter-spacing: -0.004em;
  line-height: 1.2;
  margin: 0;
  max-width: 40ch;
}
.news-item a { text-decoration: none; transition: box-shadow 0.15s ease; }
.news-item a:hover { box-shadow: inset 0 -0.3em 0 var(--amber); }

/* Articles */
.article { padding-block: clamp(2.5rem, 6vw, 4rem); }
.article h1 { max-width: 24ch; }
.article__meta { margin-bottom: 1.25rem; }
.article__meta a { color: var(--slate); }
.article__image {
  display: block;
  margin: 2rem 0;
}
.article__image img {
  display: block;
  width: 100%;
  max-width: 44rem;
  height: auto;
  border: 1px solid var(--rule);
}
.article__back {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

/* --- Home page ------------------------------------------------------------
   The page runs as a story: who we are, who trusts us, why research calling is
   different, how it works, what it fits, who answers the phone, then the ask.
   -------------------------------------------------------------------------- */

/* Large logo at the top of the hero, handed off to the sticky header on scroll
   by hero-logo.js. Hidden until that script adds .logo-morph, so without JS,
   or below the breakpoint where the header stops being sticky, the header
   keeps its own logo and the page never shows two. Raised above the header so
   it slides over the bar on its way into the slot rather than under it. */
.hero__logo { display: none; }
.logo-morph .hero__logo {
  display: block;
  position: relative;
  z-index: 6;
  width: fit-content;
  margin: 0 0 1.75rem;
  transform-origin: 0 0;
  will-change: transform;
}
.hero__logo img {
  display: block;
  height: clamp(6rem, 3.5rem + 4.5vw, 8.5rem);
  width: auto;
}
.logo-docked .hero__logo { visibility: hidden; }
.logo-morph:not(.logo-docked) .site-header__logo { opacity: 0; }
.logo-morph:not(.logo-docked) .site-header__logo:focus-visible { opacity: 1; }

/* Clients first. The strip sits straight under the hero so the names that
   carry the most weight are read before any of the argument. */
.proof-strip {
  background: var(--surface);
  border-block: 1px solid var(--rule);
}
.proof-strip__list {
  list-style: none;
  margin-block: 0;
  padding: 1.4rem 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem 2rem;
}
.proof-strip__list li {
  max-width: none;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.proof-strip__list span {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.4;
  color: var(--slate);
}
.proof-strip__list strong {
  font-family: var(--font-display);
  font-stretch: 118%;
  font-weight: 700;
  font-size: var(--step-1);
  line-height: 1.2;
}
.proof-strip__clients { grid-column: 1 / -1; }
.proof-strip__clients strong { font-size: clamp(1.5rem, 1.1rem + 1.2vw, 2.1rem); letter-spacing: -0.018em; line-height: 1.1; }
@media (min-width: 52rem) {
  .proof-strip__list {
    grid-template-columns: 2.2fr 1fr 1fr 1fr;
    align-items: end;
  }
  .proof-strip__clients { grid-column: auto; }
  .proof-strip__list li + li {
    border-left: 1px solid var(--rule);
    padding-left: 1.5rem;
  }
}

.section {
  padding-block: clamp(3rem, 6vw, 4.5rem);
  border-top: 1px solid var(--rule);
}
.proof-strip + .section { border-top: 0; }

/* Every home section shares one shape: eyebrow and heading on the left,
   content on the right, one link at the foot. The first line of each column
   starts on the same top edge. */
.split {
  display: grid;
  gap: 1.25rem 4rem;
}
@media (min-width: 52rem) {
  .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr); }
}
.split__head .eyebrow { margin-top: 0; }
.split__head h2 { margin: 0; max-width: 16ch; }
.split__body > :first-child { margin-top: 0; }
.split__body > :last-child { margin-bottom: 0; }
.split__body .cta-link { margin-top: 1.75rem; }

/* Hairline rows, the same treatment as .spec on the interior pages. */
.items {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}
.items li {
  max-width: none;
  padding: 1.1rem 0;
  border-bottom: 1px solid var(--rule);
  display: grid;
  gap: 0.25rem 1.5rem;
}
@media (min-width: 40rem) {
  .items li { grid-template-columns: 11rem 1fr; align-items: baseline; }
}
.items h3 {
  margin: 0;
  font-size: var(--step-0);
  font-stretch: 105%;
  letter-spacing: 0;
  line-height: 1.35;
}
.items p { margin: 0; color: var(--slate); }

/* --- Theme image swaps ----------------------------------------------------
   The charcoal logo and mark disappear on a dark page, so each comes as a
   pair: .on-light is the original, .on-dark the light knockout. Kept last so
   source order beats the display rules on the containers above. The dark copy
   is lazy, and a hidden lazy image is never fetched, so light-mode readers
   pay nothing for it.
   -------------------------------------------------------------------------- */

img.on-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) img.on-light { display: none; }
  :root:not([data-theme="light"]) img.on-dark { display: block; }
}
:root[data-theme="dark"] img.on-light { display: none; }
:root[data-theme="dark"] img.on-dark { display: block; }
