/* Paperus platform preview.
   Sections 7 to 11 of the 21 September brief govern everything here.

   TYPEFACES ARE STAND-INS. The brief specifies Neue Machina, General Sans and
   Gambarino. Neue Machina is sold by Pangram Pangram and its free download is
   licensed for personal use only, so it cannot ship on a commercial site
   without a purchased Web licence. General Sans and Gambarino are free on
   Fontshare, but a published preview can only load Google Fonts. Section 7
   asks for the dependency to be reported and an explicit temporary fallback
   used rather than a silent substitution, so:

     Neue Machina  ->  Space Grotesk   (squared grotesk, closest available)
     General Sans  ->  Figtree         (geometric humanist, closest available)
     Gambarino     ->  Instrument Serif italic

   Swapping in the real files is three @font-face rules and one token change.

   DARK IS THE DEFAULT. Both themes are complete and the switch is in the
   header, but the product renders are cut out against black, the exploded
   artwork was supplied on a dark ground, and electric blue does its best work
   there. The light theme is a full second design, not an afterthought.

   NO DECORATION. Section 10 bans dots, particles, orbs, glowing circles and
   scattered light, in either theme and by any technique. There is not one in
   this file. The column grid that used to carry the page is gone too, at the
   owner's direction. What carries it instead: type at a scale that commits,
   one editorial serif word per screen, a rule that draws itself under every
   section head, full-bleed artwork, a pinned sequence, and motion that only
   ever moves something into place. */

/* ---------- Registered properties ----------
   A custom property is a string until it is registered. Registering gives it a
   type, an initial value, and the one thing that matters here: the ability to
   be interpolated. --progress can then be transitioned directly instead of a
   transform standing in for it, and --condense can be driven by a scroll
   timeline, which is not possible with an unregistered property. */

@property --progress {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@property --condense {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* The stagger index of a word, a card or a part. It is written into the markup
   by the generator and read only by the stylesheet, so the timing of every
   staggered thing on the site lives in one place and turns off in one place. */
@property --i {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* Pointer position inside the element it is over, normalised to -1..1 on each
   axis. Registered so it can be transitioned back to centre on the way out
   rather than snapping. */
@property --px { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --py { syntax: "<number>"; inherits: true; initial-value: 0; }

/* ---------- Cross-document view transitions ----------
   Both documents opt in, so moving from the homepage to a campaign page is one
   continuous animation rather than a white flash and a reload. The microphone
   on the card and the microphone in the campaign hero carry the same
   view-transition-name, so the browser morphs one into the other across the
   navigation. This is a static multi-page site; nothing here is a framework. */

@view-transition { navigation: auto; }

/* The names below are for moving between pages. During a theme change every
   one of them would be lifted out of the root snapshot into a group of its own
   and cross-faded in place, which puts it on top of the beam and outside the
   clip that is supposed to be uncovering it. The script sets this attribute for
   the length of the change. */
[data-theme-turning] [data-vt] { view-transition-name: none; }

[data-vt="shot-xlr"] { view-transition-name: shot-xlr; }
[data-vt="shot-usb"] { view-transition-name: shot-usb; }
[data-vt="wordmark"] { view-transition-name: paperus-wordmark; }

/* The morph carries its own timing; the default cross-fade underneath it only
   muddies the edges. */
::view-transition-group(shot-xlr),
::view-transition-group(shot-usb) { animation-duration: 520ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); }
::view-transition-old(shot-xlr), ::view-transition-new(shot-xlr),
::view-transition-old(shot-usb), ::view-transition-new(shot-usb) { animation: none; mix-blend-mode: normal; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none !important;
  }
}

:root {
  /* Raw palette, section 8, exactly as specified. */
  --black: #000000;
  --white: #ffffff;
  --electric-blue: #0066ff;
  --violet: #b000ff;
  --rust-copper: #b7410e;
  --himalayan-blue-poppy: #4f6edb;

  /* Lifted accents. Measured, not guessed: on the #0A0A0A surface the four
     specified accents carry 4.10, 4.04, 3.56 and 4.32 to one as text, so every
     one of them fails AA at normal size on the dark ground. These clear 4.98
     and above. Used ONLY where an accent is a letterform or a hairline; a
     filled accent block keeps the exact brand value, because white on it
     already measures 4.58 to 5.56 and the brand colour should not drift. */
  --electric-blue-lift: #2e7dff;
  --violet-lift: #be3cff;
  --rust-copper-lift: #d2601f;
  --poppy-lift: #6b87e8;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4.5rem;
  --space-9: 7rem;
  --space-10: 10rem;

  /* 12px at the floor, not 11. Every label, chip, part number and form
     label on the site is set at this step, and at 11px on a phone they were
     the smallest thing on the page by a clear margin. */
  --step--2: clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem);
  --step--1: clamp(0.8125rem, 0.79rem + 0.11vw, 0.875rem);
  --step-0: clamp(0.9375rem, 0.9rem + 0.2vw, 1.0625rem);
  --step-1: clamp(1.0625rem, 1rem + 0.3vw, 1.25rem);
  --step-2: clamp(1.25rem, 1.05rem + 0.9vw, 1.875rem);
  --step-3: clamp(1.75rem, 1.2rem + 2.6vw, 3.5rem);
  --step-4: clamp(2.25rem, 1.3rem + 4.4vw, 5rem);
  --step-5: clamp(2.75rem, 0.8rem + 7.4vw, 7rem);

  --page: 78rem;
  --wide: 100rem;
  --gutter: clamp(1.25rem, 0.5rem + 3.2vw, 4rem);
  --header-h: 4.5rem;
  --rule: var(--accent-text);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --spring: cubic-bezier(0.16, 1, 0.3, 1);
  --dur: 280ms;

  --display: "Space Grotesk", "Neue Machina", ui-sans-serif, system-ui, sans-serif;
  --sans: "Figtree", "General Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --editorial: "Instrument Serif", "Gambarino", Georgia, serif;
}

/* ---------- Themes ----------
   Both are written out in full. Dark is the default, so it lives on :root and
   light is the override, which is the opposite of the usual arrangement and
   the reason the page never flashes white on load. */

:root, [data-theme="dark"] {
  color-scheme: dark;
  --bg: #000000;
  --surface: #0a0a0b;
  --raised: #121216;
  --ink: #ffffff;
  --ink-2: #a3a3a3;
  --ink-3: #6f6f78;
  --line: #222228;
  --line-strong: #3a3a44;
  --accent: var(--electric-blue);
  --accent-text: var(--electric-blue-lift);
  --secondary-text: var(--violet-lift);
  --warm-text: var(--rust-copper-lift);
  --soft-text: var(--poppy-lift);
  --on-accent: #ffffff;
  --ghost: rgba(255, 255, 255, 0.055);
}

[data-theme="light"] {
  color-scheme: light;
  --bg: #ffffff;
  --surface: #ffffff;
  --raised: #f3f4f6;
  --ink: #000000;
  --ink-2: #6b7280;
  --ink-3: #9aa0ab;
  --line: #e5e5e5;
  --line-strong: #c8cbd2;
  --accent: var(--electric-blue);
  --accent-text: var(--electric-blue);
  --secondary-text: var(--violet);
  --warm-text: var(--rust-copper);
  --soft-text: var(--himalayan-blue-poppy);
  --on-accent: #ffffff;
  --ghost: rgba(0, 0, 0, 0.06);
}

/* An inverted panel, used once or twice a page. It redeclares tokens rather
   than overriding rules, so everything nested keeps working unchanged. */
.invert {
  --bg: #ffffff; --surface: #ffffff; --raised: #f3f4f6;
  --ink: #000000; --ink-2: #6b7280; --ink-3: #9aa0ab;
  --line: #e5e5e5; --line-strong: #c8cbd2;
  --accent-text: var(--electric-blue);
  --secondary-text: var(--violet);
  --warm-text: var(--rust-copper);
  --soft-text: var(--himalayan-blue-poppy);
  --ghost: rgba(0, 0, 0, 0.06);
  background: var(--bg); color: var(--ink);
}
[data-theme="light"] .invert {
  --bg: #000000; --surface: #0a0a0b; --raised: #121216;
  --ink: #ffffff; --ink-2: #a3a3a3; --ink-3: #6f6f78;
  --line: #222228; --line-strong: #3a3a44;
  --accent-text: var(--electric-blue-lift);
  --secondary-text: var(--violet-lift);
  --warm-text: var(--rust-copper-lift);
  --soft-text: var(--poppy-lift);
  --ghost: rgba(255, 255, 255, 0.055);
}

/* ---------- View transition ----------
   The theme change is revealed by a band of light opening out from the switch. The two
   snapshots must not cross-fade against each other or the wipe turns to mud,
   so both default animations are cancelled and the new one is simply stacked
   on top and clipped by the script. */

::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 2; }

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The gutter is reserved so opening a long page does not shift the layout by
     a scrollbar width, and the scrollbar itself is coloured to the theme
     rather than left as the browser's own light chrome on a black page. */
  scrollbar-gutter: stable;
  scrollbar-color: var(--line-strong) var(--bg);
  background: var(--bg);
}
[id] { scroll-margin-block-start: calc(var(--header-h) + var(--space-5)); }

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

h1, h2, h3 {
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 0.94;
  text-wrap: balance;
  /* Trims the half-leading above the caps and below the baseline, so a heading
     sits on the grid by its letterforms instead of by its line box. It is the
     difference between type that is placed and type that is dropped in.
     Ignored where unsupported, which costs nothing. */
  text-box: trim-both cap alphabetic;
}
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-1); letter-spacing: -0.02em; line-height: 1.25; }

p { margin: 0; text-wrap: pretty; }
img { max-width: 100%; display: block; }
a { color: var(--accent-text); text-underline-offset: 0.2em; }

:where(a, button, input, select, textarea):focus-visible {
  outline: 2px solid var(--accent-text); outline-offset: 3px;
}
::selection { background: var(--accent); color: var(--on-accent); }

.shell { width: min(100% - (var(--gutter) * 2), var(--page)); margin-inline: auto; }
.shell--wide { width: min(100% - (var(--gutter) * 2), var(--wide)); }
.measure { max-inline-size: 42ch; }

.visually-hidden {
  position: absolute; inline-size: 1px; block-size: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.skip-link {
  position: absolute; inset-inline-start: var(--space-4); inset-block-start: -5rem;
  z-index: 120; padding: var(--space-3) var(--space-5);
  background: var(--accent); color: var(--on-accent); font-weight: 600;
  transition: inset-block-start var(--dur) var(--ease);
}
.skip-link:focus { inset-block-start: var(--space-4); }

/* ---------- Micro type ---------- */

.label {
  display: inline-flex; align-items: center; gap: var(--space-3);
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--ink-2); line-height: 1;
}
.label--accent { color: var(--rule); }
.label::before {
  content: ""; inline-size: 1.5rem; block-size: 1px; flex: none;
  background: currentColor;
}
.label--bare::before { content: none; }

.editorial { font-family: var(--editorial); font-style: italic; font-weight: 400; letter-spacing: -0.005em; }
.lede { font-size: var(--step-1); color: var(--ink-2); line-height: 1.55; }

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

.wordmark {
  font-family: var(--display); font-weight: 700; font-size: var(--step-1);
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--ink); text-decoration: none; line-height: 1;
}

.header {
  position: sticky; inset-block-start: 0; z-index: 100;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(180%) blur(16px);
  border-block-end: 1px solid var(--line);
}
@supports not (backdrop-filter: blur(1px)) { .header { background: var(--bg); } }

/* The header tightens as the page moves under it, driven by the scroll
   position rather than by a listener. Desktop only: the phone header is two
   rows and has nothing to give. */
@media (min-width: 48rem) and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .header__inner {
      animation: condense linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 200px;
    }
    .header {
      animation: sharpen linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 200px;
    }
  }
}
@keyframes condense { to { block-size: 3.4rem; } }
@keyframes sharpen { to { background: color-mix(in srgb, var(--bg) 96%, transparent); } }

.header__inner { block-size: var(--header-h); display: flex; align-items: center; gap: var(--space-5); }

/* Where you are, written beside the wordmark once you have left the hero. It
   takes the name from the section's own label, so it cannot drift out of step
   with the page, and it is hidden from assistive technology because it repeats
   a heading that is already there. Below this width the header is two rows and
   has nothing to spare. */
.header__where {
  display: none;
  font-size: var(--step--2); font-weight: 700; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--ink-3);
  white-space: nowrap; user-select: none;
  padding-inline-start: var(--space-5);
  border-inline-start: 1px solid var(--line);
  opacity: 0; transition: opacity var(--dur) var(--ease);
}
@media (min-width: 64rem) { .header__where { display: block; } }
.header__where[data-filled="true"] { opacity: 1; }
.header__nav {
  margin-inline-start: auto;
  display: flex; align-items: center; gap: clamp(var(--space-4), 2vw, var(--space-7));
  font-size: var(--step--1);
}
.header__nav a {
  position: relative; color: var(--ink-2); text-decoration: none; font-weight: 500;
  white-space: nowrap; padding-block: var(--space-2);
  transition: color var(--dur) var(--ease);
}
.header__nav a::after {
  content: ""; position: absolute; inset-inline: 0; inset-block-end: 0;
  block-size: 1px; background: var(--accent-text);
  scale: 0 1; transform-origin: 0 50%;
  transition: scale var(--dur) var(--spring);
}
.header__nav a:hover { color: var(--ink); }
.header__nav a:hover::after, .header__nav a[aria-current="page"]::after { scale: 1 1; }
.header__nav a[aria-current="page"] { color: var(--ink); }

/* ---------- Theme switch ----------
   A track with a travelling thumb, not an icon that swaps. Both marks sit in
   the track so the two states are legible at once, and the thumb slides
   between them, which gives the press somewhere to go. The wipe it triggers
   is in the script. */

.switch {
  position: relative; flex: none;
  inline-size: 4rem; block-size: 2rem; padding: 0; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: 2rem;
  background: var(--raised);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.switch:hover { border-color: var(--ink-3); }
.switch__track {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: space-between; padding-inline: 0.44rem;
  color: var(--ink-3); pointer-events: none;
}
.switch__thumb {
  position: absolute; inset-block-start: 3px; inset-inline-start: 3px;
  inline-size: 1.5rem; block-size: 1.5rem;
  display: grid; place-items: center; border-radius: 50%;
  background: var(--ink); color: var(--bg);
  transition: translate 460ms var(--spring), background var(--dur) var(--ease);
}
.switch[aria-checked="true"] .switch__thumb { translate: 2rem 0; }
.switch svg { inline-size: 0.8125rem; block-size: 0.8125rem; }
.switch__thumb .icon-moon { display: none; }
.switch[aria-checked="true"] .switch__thumb .icon-moon { display: block; }
.switch[aria-checked="true"] .switch__thumb .icon-sun { display: none; }

@media (max-width: 47.99rem) {
  :root { --header-h: 6.75rem; }
  .header__inner { block-size: auto; flex-wrap: wrap; padding-block: var(--space-3); gap: var(--space-3); }
  .header__nav {
    order: 3; inline-size: 100%; margin-inline-start: 0;
    justify-content: flex-start; gap: var(--space-5);
    padding-block-start: var(--space-3); border-block-start: 1px solid var(--line);
  }
  .switch { margin-inline-start: auto; }
}

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

.section { position: relative; padding-block: clamp(var(--space-8), 9vw, var(--space-10)); }
.section--tight { padding-block: clamp(var(--space-7), 6vw, var(--space-9)); }
.section > .shell { position: relative; z-index: 1; }
.section + .section:not(.invert):not(.section--seam) { border-block-start: 1px solid var(--line); }

.section__head {
  position: relative;
  display: grid; gap: var(--space-4);
  justify-items: start;
  margin-block-end: clamp(var(--space-7), 5vw, var(--space-9));
}
.section__head h2 { max-inline-size: 15ch; }
.section__head .lede { max-inline-size: 42ch; margin-block-start: var(--space-2); }

/* ---------- Hand-drawn marks ----------
   Each path is declared with pathLength="1", so one dash pattern draws any
   curve regardless of its real length and the same rule works for an
   underline, a loop and an arrow. The stroke is drawn from a scroll timeline
   where there is one and on load in the hero, which has nothing to scroll
   into. A mark that has not drawn yet is invisible rather than missing: the
   word underneath it is the content. */

.marked { position: relative; display: inline-block; }
.sketch {
  position: absolute; z-index: -1;
  color: var(--rule);
  overflow: visible;
  pointer-events: none;
}
.sketch path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.sketch--underline {
  inset-inline: -0.06em -0.02em;
  inset-block-end: -0.24em;
  block-size: 0.26em;
}
.sketch--ring {
  inset: -0.32em -0.5em -0.36em -0.42em;
  inline-size: auto; block-size: auto;
  color: var(--warm-text);
}
.sketch--arrow {
  position: absolute; z-index: 0;
  inline-size: clamp(4rem, 7vw, 7rem);
  inset-inline-end: 3%; inset-block-start: 0.25rem;
  rotate: 8deg;
  color: var(--secondary-text);
}
@media (max-width: 52rem) { .sketch--arrow { display: none; } }

@media (prefers-reduced-motion: no-preference) {
  /* The hero mark draws itself once the line it belongs to has arrived. */
  .hero .sketch path { animation: draw 720ms var(--ease) 820ms both; }
  .hero .sketch .sketch__second { animation-delay: 1080ms; }

  @supports (animation-timeline: view()) {
    .section:not(.hero) .sketch path {
      animation: draw linear both;
      animation-timeline: view();
      animation-range: entry 18% entry 58%;
    }
    .section:not(.hero) .sketch .sketch__second {
      animation-range: entry 34% entry 76%;
    }
  }
}
@keyframes draw { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
  .sketch path { stroke-dashoffset: 0; animation: none; }
}

/* ---------- About ---------- */

.about { display: grid; gap: clamp(var(--space-6), 4vw, var(--space-8)); }
@media (min-width: 62rem) {
  .about {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
    gap: clamp(var(--space-7), 6vw, var(--space-9));
    align-items: end;
  }
}

.about__lead { display: grid; gap: var(--space-5); justify-items: start; }
.about__lead h2 { font-size: var(--step-4); max-inline-size: 12ch; }
.about__lead h2 em {
  font-family: var(--editorial); font-style: italic; font-weight: 400;
  letter-spacing: -0.02em;
}
.about__body { display: grid; gap: var(--space-5); }
.about__body p { color: var(--ink-2); font-size: var(--step-1); line-height: 1.6; }
.about__body p:first-child { color: var(--ink); }

.about__points {
  list-style: none; margin: clamp(var(--space-8), 6vw, var(--space-9)) 0 0; padding: 0;
  display: grid; gap: 1px; background: var(--line);
  border-block: 1px solid var(--line);
}
@media (min-width: 52rem) { .about__points { grid-template-columns: repeat(3, 1fr); } }
.about__points li {
  background: var(--bg);
  padding: clamp(var(--space-5), 3vw, var(--space-7));
  display: grid; gap: var(--space-3); align-content: start;
}
.about__points h3 { font-size: var(--step-1); }
.about__points p { color: var(--ink-2); font-size: var(--step--1); line-height: 1.6; }

.section__head--arrow { padding-inline-end: clamp(4rem, 8vw, 8rem); }

/* ---------- Home hero ---------- */

.hero {
  position: relative; overflow: clip; isolation: isolate;
  padding-block: clamp(var(--space-8), 10vw, 9rem) clamp(var(--space-7), 6vw, var(--space-9));
}
/* Restrained lighting, not a glow: one linear wash falling from the header. */
.hero::before {
  content: ""; position: absolute; inset-block-start: 0; inset-inline: 0;
  block-size: 76%; z-index: -2;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 22%, transparent), transparent 88%);
}
[data-theme="light"] .hero::before {
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 9%, transparent), transparent 88%);
}
/* The photograph is its own light source, so the wash behind it is pulled back
   to the job it still does: lifting the left side, where the frame is black. */
.hero--shot::before { opacity: 0.5; }

/* ---------- Hero photograph ----------
   A macro of a folded sheet, lit hard from the upper right. Two thirds of the
   frame is black, which is the point: the headline sits in it rather than on
   top of a scrim.

   The frame is mirrored in the prep step, not here, so object-position and the
   mask below both describe the orientation that is actually on screen.

   Aperture and picture are separate elements. The mask belongs to the wrapper
   and never moves; the photograph inside it drifts on the page's scroll
   timeline and breathes on a clock. Two animations on one element is fine as
   long as they drive different properties, which is why one owns translate and
   the other owns scale. */
.hero__shot-wrap {
  position: absolute; inset: 0; z-index: -3;
  display: block; overflow: clip;
  /* Opaque only past the point the headline can reach; the lit plane starts at
     half the frame, so nothing of the fold is lost to this. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0 30%, #000 58%);
          mask-image: linear-gradient(90deg, transparent 0 30%, #000 58%);
  pointer-events: none; user-select: none;
}
/* Both dimensions are declared. The width and height attributes on the element
   are presentational hints, and overriding only one of them leaves the other
   at the attribute's value. */
.hero__shot {
  display: block;
  inline-size: 100%; block-size: 100%;
  object-fit: cover; object-position: 54% 22%;
}
@media (prefers-reduced-motion: no-preference) {
  .hero__shot {
    animation-name: breathe;
    animation-duration: 54s;
    animation-timing-function: ease-in-out;
    animation-iteration-count: infinite;
    animation-direction: alternate;
  }
  @supports (animation-timeline: scroll()) {
    .hero__shot {
      animation-name: breathe, park;
      animation-duration: 54s, auto;
      animation-timing-function: ease-in-out, linear;
      animation-iteration-count: infinite, 1;
      animation-direction: alternate, normal;
      animation-fill-mode: none, both;
      animation-timeline: auto, scroll(root block);
      animation-range: normal, 0 100vh;
    }
  }
  /* Paused while the hero is off screen, from the observer in the script. An
     ambient animation nobody can see is battery and nothing else. */
  .hero__shot[data-idle="true"] { animation-play-state: paused; }
}
/* Scaled about the right edge rather than the centre. Growing from the middle
   walks the lit plane leftward into the headline over the cycle; growing from
   the right keeps that edge where it was put. */
.hero__shot { transform-origin: 100% 34%; }
@keyframes breathe { from { scale: 1; } to { scale: 1.05; } }
@keyframes park { to { translate: 0 7%; } }

/* Fades the foot of the photograph into whichever ground is underneath, so the
   frame has no bottom edge in either theme and the ghost wordmark stays clean.
   It sits above the photograph and below the wordmark. */
.hero--shot::after {
  content: ""; position: absolute; inset: 0; z-index: -2;
  background: linear-gradient(180deg, transparent 54%, var(--bg) 97%);
  pointer-events: none;
}
/* The light theme cannot carry the frame behind text at all: the ink is dark
   and so is the photograph. A vertical fade is also wrong here, because the
   black half of the frame turns to grey on the way out and reads as fog. The
   mask is an ellipse around the lit plane instead, so the fold arrives as a
   form with no edge anywhere rather than a panel with a soft side. */
[data-theme="light"] .hero__shot-wrap {
  -webkit-mask-image: radial-gradient(ellipse 58% 84% at 84% 46%, #000 30%, transparent 74%);
          mask-image: radial-gradient(ellipse 58% 84% at 84% 46%, #000 30%, transparent 74%);
}

/* Narrow: cover would crop to a 30% slice straight through the crease and put
   the lit plane behind every line of the headline. The frame moves under the
   text instead and becomes a band. */
@media (max-width: 52rem) {
  .hero__shot-wrap {
    inset-block-start: auto; block-size: 38%;
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 52%);
            mask-image: linear-gradient(180deg, transparent 0, #000 52%);
  }
  .hero__shot { object-position: 58% 22%; }
  [data-theme="light"] .hero__shot-wrap {
    -webkit-mask-image: radial-gradient(ellipse 92% 128% at 60% 80%, #000 22%, transparent 76%);
            mask-image: radial-gradient(ellipse 92% 128% at 60% 80%, #000 22%, transparent 76%);
  }
  .hero--shot::after { background: linear-gradient(180deg, transparent 72%, var(--bg) 99%); }
}

.hero__title {
  font-size: var(--step-5); line-height: 0.9; letter-spacing: -0.05em;
  margin-block: var(--space-6) var(--space-7);
}
.hero__title > span, .hero__title > .marked { display: block; }
/* The third line is the marked one, so the mark has to sit on a block. */
.hero__title > .marked { inline-size: fit-content; }
/* One editorial word per screen. Section 7 calls Gambarino a rare accent, and
   a single word set against six of display grotesk is what rare looks like. */
.hero__title em {
  font-family: var(--editorial); font-style: italic; font-weight: 400;
  letter-spacing: -0.02em; color: var(--accent-text); padding-inline-end: 0.05em;
}

.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: center; }
@media (max-width: 30rem) {
  .hero__actions { flex-direction: column; align-items: stretch; }
  .hero__actions .btn { justify-content: center; }
}

/* A hairline index of what is below. Navigation, so it adds nothing section 3
   excludes from the homepage. */
.hero__index {
  position: relative;
  margin-block-start: clamp(var(--space-7), 5vw, var(--space-8));
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: var(--space-4) clamp(var(--space-6), 5vw, var(--space-9));
}
.hero__index a {
  display: flex; align-items: baseline; gap: var(--space-3);
  text-decoration: none; color: var(--ink-2);
  font-size: var(--step--1); font-weight: 500;
  transition: color var(--dur) var(--ease);
}
.hero__index a:hover { color: var(--ink); }
.hero__index a::before {
  content: ""; inline-size: 1.25rem; block-size: 1px; flex: none;
  background: var(--rule); align-self: center;
  transition: inline-size var(--dur) var(--spring);
}
.hero__index a:hover::before { inline-size: 2rem; }

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

.btn {
  position: relative; overflow: hidden;
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--space-4) var(--space-7); min-block-size: 3.1rem;
  font-family: var(--sans); font-size: var(--step-0); font-weight: 600;
  border: 1px solid var(--accent); border-radius: 2rem;
  background: var(--accent); color: var(--on-accent);
  text-decoration: none; cursor: pointer;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease),
              translate 520ms var(--spring);
}
/* The fill wipes in from the left rather than cross-fading, which is one clear
   movement instead of a colour change nobody reads as a state. */
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: 0; background: var(--ink);
  scale: 0 1; transform-origin: 0 50%;
  transition: scale var(--dur) var(--spring);
}
.btn span { position: relative; z-index: 1; }
.btn:hover::before { scale: 1 1; }
.btn:hover { border-color: var(--ink); color: var(--bg); }

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

.arrow-link {
  display: inline-flex; align-items: center; gap: var(--space-3);
  font-weight: 600; text-decoration: none; color: var(--ink); font-size: var(--step-0);
}
.arrow-link::after { content: "\2192"; color: var(--rule); transition: translate var(--dur) var(--spring); }
.arrow-link:hover::after { translate: 5px 0; }

/* ---------- Philosophy ---------- */

.philosophy { display: grid; gap: var(--space-7); align-items: start; }
@media (min-width: 58rem) {
  .philosophy {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    gap: clamp(var(--space-7), 6vw, var(--space-9));
  }
  .philosophy .label { grid-column: 1 / -1; }
}
.philosophy h2 { font-size: var(--step-4); }
.philosophy h2 em {
  font-family: var(--editorial); font-style: italic; font-weight: 400;
  letter-spacing: -0.02em; color: var(--rule);
}
.philosophy p { font-size: clamp(1.0625rem, 0.95rem + 0.5vw, 1.4375rem); line-height: 1.45; }

/* ---------- Campaign cards ---------- */

.cards { display: grid; gap: var(--space-6); }
@media (min-width: 52rem) { .cards { grid-template-columns: repeat(2, 1fr); gap: var(--space-7); } }

.card {
  position: relative; display: block;
  border: 1px solid var(--line); border-radius: 2px;
  background: var(--surface); text-decoration: none; color: inherit;
  overflow: hidden;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
/* Section 5 makes the accent a per-campaign setting. The card reads it from
   the campaign record, so the two previews differ by exactly one token. */
.card[data-accent="primary"] { --rule: var(--accent-text); }
.card[data-accent="secondary"] { --rule: var(--secondary-text); }
.card[data-accent="warm"] { --rule: var(--warm-text); }
.card[data-accent="soft"] { --rule: var(--soft-text); }
.card::before {
  content: ""; position: absolute; inset-block-start: 0; inset-inline: 0;
  block-size: 2px; background: var(--rule); z-index: 3;
  scale: 0 1; transform-origin: 0 50%;
  transition: scale 520ms var(--spring);
}
.card:hover, .card:focus-visible { border-color: var(--line-strong); background: var(--raised); }
.card:hover::before, .card:focus-visible::before { scale: 1 1; }

.card__stage {
  position: relative; container-type: inline-size;
  aspect-ratio: 4 / 3; min-block-size: 0;
  padding: clamp(var(--space-6), 4vw, var(--space-8)) clamp(var(--space-5), 3vw, var(--space-7))
           clamp(var(--space-7), 5vw, var(--space-9));
  overflow: hidden;
}
/* min-block-size on a flex or grid item defaults to auto, which lets the item
   grow past a declared aspect-ratio to fit its content. Without the reset the
   stage took the render's intrinsic height, the two cards came out 873 and 945
   tall against a declared ratio, and their bodies did not line up. */
.card__stage img {
  position: relative; z-index: 2;
  inline-size: 100%; block-size: 100%; object-fit: contain;
  transition: scale 700ms var(--spring), translate 700ms var(--spring);
}
.card:hover .card__stage img { scale: 1.05; translate: 0 -1.5%; }

/* The product name, very large and very quiet, behind its own render. */
.card__ghost {
  position: absolute; inset-block-end: -0.12em; inset-inline-start: 50%;
  translate: -50% 0; z-index: 1;
  font-family: var(--display); font-weight: 700; font-size: 34cqw; line-height: 0.72;
  letter-spacing: 0.01em; color: var(--ghost);
  white-space: nowrap; pointer-events: none; user-select: none;
}
.card__body {
  position: relative; z-index: 2;
  padding: clamp(var(--space-5), 3vw, var(--space-7));
  border-block-start: 1px solid var(--line);
  display: grid; gap: var(--space-3);
}
.card__brand { font-size: var(--step--2); font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-2); }
.card__name { font-family: var(--display); font-weight: 700; font-size: var(--step-3); letter-spacing: -0.04em; line-height: 1; }
.card__go { margin-block-start: var(--space-4); }

/* ---------- Campaign hero ---------- */

.campaign-hero { display: grid; gap: clamp(var(--space-7), 5vw, var(--space-9)); align-items: center; }
@media (min-width: 60rem) { .campaign-hero { grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr); } }
.campaign-hero h1 { font-size: var(--step-4); margin-block: var(--space-5) var(--space-6); }
.campaign-hero__stage {
  position: relative; container-type: inline-size; min-block-size: 0;
  padding: clamp(var(--space-5), 3.5vw, var(--space-7));
  background: var(--raised); border: 1px solid var(--line);
  aspect-ratio: 1 / 1; overflow: hidden;
}
.campaign-hero__stage img { position: relative; z-index: 2; inline-size: 100%; block-size: 100%; object-fit: contain; }
.campaign-hero__ghost {
  position: absolute; inset-block-end: -0.16em; inset-inline-start: 50%;
  translate: -50% 0; z-index: 1;
  font-family: var(--display); font-weight: 700; font-size: 15cqw; line-height: 0.74;
  letter-spacing: 0.01em; color: var(--ghost);
  white-space: nowrap; pointer-events: none; user-select: none;
}

.quote {
  font-size: clamp(1.375rem, 1rem + 1.7vw, 2.25rem); line-height: 1.2;
  border-inline-start: 1px solid var(--warm-text);
  padding-inline-start: clamp(var(--space-5), 2vw, var(--space-6));
  margin-block: var(--space-7); max-inline-size: 26ch;
}

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-block-start: var(--space-6); padding: 0; }
.chips li {
  list-style: none; font-size: var(--step--2); font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-2);
  border: 1px solid var(--line); border-radius: 2rem; padding: var(--space-2) var(--space-4);
}

/* ---------- Anatomy sequence ----------
   The diagram pins while the parts step past it. This is the centrepiece and
   the reason the artwork was worth commissioning: nine labels baked into a
   picture are unreadable at any size a phone can show, and this walks through
   them one at a time while the picture stays put. Without JavaScript every
   part is simply legible at once, which is what the CSS ships in. */

.anatomy { display: grid; gap: var(--space-7); }
@media (min-width: 62rem) {
  .anatomy {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    gap: clamp(var(--space-7), 5vw, var(--space-9));
  }
}

.anatomy__figure { margin: 0; position: relative; border: 1px solid var(--line); background: var(--raised); }
.anatomy__figure img { inline-size: 100%; block-size: auto; }
/* The diagram holds while the parts run past it. Declared after the base rule,
   because at equal specificity source order decides and position: relative
   above would otherwise win. */
@media (min-width: 62rem) {
  .anatomy__figure {
    position: sticky;
    inset-block-start: calc(var(--header-h) + var(--space-6));
  }
}
.anatomy__meta {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-block-start: 1px solid var(--line);
  font-size: var(--step--1); color: var(--ink-2);
}
.anatomy__count { font-family: var(--display); font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.anatomy__count b { color: var(--rule); }

/* The rail fills as the sequence advances. One dimension, one colour. */
.anatomy__rail { block-size: 2px; background: var(--line); overflow: hidden; }
.anatomy__rail span {
  display: block; block-size: 100%; background: var(--rule);
  scale: var(--progress) 1; transform-origin: 0 50%;
}
/* --progress is registered as a number, so it interpolates. The rail follows
   the value rather than the value snapping and a transform chasing it. */
.anatomy__rail { transition: --progress 420ms var(--spring); }

.parts { list-style: none; margin: 0; padding: 0; }
@media (min-width: 62rem) { .parts { padding-block-end: 22vh; } }
.parts li {
  display: grid; gap: var(--space-3);
  grid-template-columns: auto minmax(0, 1fr);
  padding-block: clamp(var(--space-6), 4vw, var(--space-7));
  border-block-end: 1px solid var(--line);
}
.parts li:first-child { border-block-start: 1px solid var(--line); }
.parts .part__num {
  font-family: var(--display); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.1em; color: var(--ink-3);
  padding-block-start: 0.55em; padding-inline-end: var(--space-4);
  transition: color var(--dur) var(--ease);
}
.parts h3 {
  grid-column: 2;
  font-family: var(--display); font-size: var(--step-2); font-weight: 700;
  letter-spacing: -0.03em; line-height: 1.1; color: var(--ink);
}
.parts p { grid-column: 2; margin: 0; color: var(--ink-2); font-size: var(--step-0); line-height: 1.55; }

/* Only once the script has taken over does anything dim, so a reader without
   it never sees a page of greyed-out text. */
[data-sequenced="true"] .parts li { opacity: 0.35; transition: opacity 420ms var(--ease); }
[data-sequenced="true"] .parts li[data-active="true"] { opacity: 1; }
[data-sequenced="true"] .parts li[data-active="true"] .part__num { color: var(--rule); }

/* ---------- Finishes ---------- */

.finishes { display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); }
@media (min-width: 48rem) { .finishes { grid-template-columns: repeat(2, 1fr); } }
.finishes figure { margin: 0; background: var(--bg); }
.finish-stage { position: relative; aspect-ratio: 4 / 3; min-block-size: 0; padding: clamp(var(--space-6), 5vw, var(--space-9)); }
/* Fixed grounds, not theme tokens. A black microphone on the dark theme's own
   surface disappears, and the white one does the same on the light theme. */
.finish-stage[data-ground="light"] { background: #f1f2f4; }
.finish-stage[data-ground="dark"] { background: #0c0c0e; }
.finish-shot { inline-size: 100%; block-size: 100%; object-fit: contain; }
.finishes figcaption {
  padding: var(--space-5) clamp(var(--space-5), 3vw, var(--space-7));
  display: flex; align-items: baseline; gap: var(--space-4);
  font-family: var(--display); font-size: var(--step-2); font-weight: 700;
  letter-spacing: -0.03em; color: var(--ink);
}
.finishes figcaption small {
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-3);
}

/* ---------- Specifications ---------- */

.spec-grid { display: grid; gap: clamp(var(--space-7), 5vw, var(--space-9)); }
@media (min-width: 58rem) { .spec-grid { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr); } }
.spec-intro { margin-block-end: 0; }
@media (min-width: 58rem) {
  .spec-intro { position: sticky; inset-block-start: calc(var(--header-h) + var(--space-6)); }
}
.specs { inline-size: 100%; border-collapse: collapse; }
.specs th, .specs td { text-align: start; padding: var(--space-4) 0; border-block-end: 1px solid var(--line); vertical-align: baseline; }
.specs th { font-weight: 500; color: var(--ink-2); font-size: var(--step--1); inline-size: 45%; padding-inline-end: var(--space-5); }
.specs td { font-family: var(--display); font-weight: 700; font-size: var(--step-1); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.specs tr:first-child th, .specs tr:first-child td { border-block-start: 1px solid var(--line); }

/* ---------- Enquiry ---------- */

.enquiry { display: grid; gap: clamp(var(--space-7), 5vw, var(--space-9)); }
@media (min-width: 58rem) { .enquiry { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr); } }
.field-grid { display: grid; gap: var(--space-5); }
@media (min-width: 44rem) { .field-grid { grid-template-columns: repeat(2, 1fr); } }
.field { display: grid; gap: var(--space-2); }
.field--wide { grid-column: 1 / -1; }
.field label { font-size: var(--step--2); font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-2); }
.field input, .field textarea, .field select {
  font: inherit; font-size: var(--step-0); color: var(--ink); background: transparent;
  border: 0; border-block-end: 1px solid var(--line-strong); border-radius: 0;
  padding: var(--space-3) 0; min-block-size: 2.9rem;
  transition: border-color var(--dur) var(--ease);
}
/* The underline thickening is the resting affordance; it is not a focus
   indicator on its own, because a one pixel colour change is not something a
   keyboard user can find. The outline is restored for :focus-visible, so a
   pointer click still gets the quiet version and a Tab gets a real ring. */
.field input:focus, .field textarea:focus, .field select:focus {
  outline: none;
  border-block-end-color: var(--accent-text);
  box-shadow: 0 1px 0 0 var(--accent-text);
}
.field input:focus-visible, .field textarea:focus-visible, .field select:focus-visible {
  outline: 2px solid var(--accent-text); outline-offset: 3px;
}
.field textarea {
  min-block-size: 6rem; resize: vertical;
  /* Grows with what is typed instead of holding a fixed box and scrolling
     inside itself. */
  field-sizing: content;
  max-block-size: 18rem;
}
/* display is a discrete property, so a message going from none to block would
   normally appear instantly. allow-discrete lets it take part in the
   transition, and @starting-style gives it somewhere to come from. */
.form-status:empty { display: none; }
.form-status {
  margin-block-start: var(--space-4); font-size: var(--step--1); color: var(--warm-text);
  opacity: 1; translate: 0;
  transition: opacity 320ms var(--ease), translate 320ms var(--spring),
              display 320ms allow-discrete;
}
@starting-style { .form-status { opacity: 0; translate: 0 6px; } }

/* Shown by the script only where the platform can actually share. */
.share { display: none; }
.share[data-ready="true"] { display: inline-flex; }
.notice {
  margin-block-start: var(--space-6); padding-inline-start: var(--space-5);
  border-inline-start: 1px solid var(--warm-text);
  font-size: var(--step--1); color: var(--ink-2); line-height: 1.6;
}

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

.footer { position: relative; border-block-start: 1px solid var(--line); padding-block: var(--space-9) var(--space-6); overflow: clip; }
.footer__top { display: flex; flex-wrap: wrap; gap: var(--space-6) var(--space-8); align-items: baseline; font-size: var(--step--1); }
.footer__mark { margin-inline-end: auto; }
.footer nav { display: flex; flex-wrap: wrap; gap: var(--space-6); }
.footer a { color: var(--ink-2); text-decoration: none; transition: color var(--dur) var(--ease); }
.footer a:hover { color: var(--ink); }
.footer__legal {
  margin-block-start: var(--space-7); padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-6);
  font-size: var(--step--1); color: var(--ink-2);
}

/* ---------- Theme-paired artwork ----------
   The exploded views were supplied as light-ground and dark-ground versions of
   the same microphone, so the diagram changes with the theme rather than being
   a light picture stranded on a black page. Both ship in the markup and CSS
   chooses; swapping the src would blank the frame for the length of a request,
   which during the wipe is the worst possible moment. */

.theme-shot { position: relative; display: block; }
.theme-shot img { inline-size: 100%; block-size: auto; }
.theme-shot [data-ground="light"] { display: none; }
.theme-shot [data-ground="dark"] { display: block; }
[data-theme="light"] .theme-shot [data-ground="light"] { display: block; }
[data-theme="light"] .theme-shot [data-ground="dark"] { display: none; }

/* ---------- Motion ----------
   Section 9 asks for purposeful, restrained motion. Content is visible by
   default and the scroll animations only exist where the browser supports a
   view timeline, so nothing can be left hidden by a script that did not run. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 2% entry 32%;
    }
    /* The artwork settles into place as it arrives. One property, one
       direction, finished before the figure is halfway up the viewport. */
    .anatomy__figure img {
      animation: settle linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
    }
  }
}
@keyframes rise { from { opacity: 0; translate: 0 18px; } to { opacity: 1; translate: 0; } }
@keyframes settle { from { scale: 1.05; } to { scale: 1; } }

/* The hero is above the fold, so it has nothing to scroll into. It plays once
   on load instead, one line after another. */
@media (prefers-reduced-motion: no-preference) {
  .hero [data-enter] { animation: enter 880ms var(--spring) both; }
  .hero [data-enter="1"] { animation-delay: 40ms; }
  .hero [data-enter="2"] { animation-delay: 130ms; }
  .hero [data-enter="3"] { animation-delay: 220ms; }
  .hero [data-enter="4"] { animation-delay: 340ms; }
  .hero [data-enter="5"] { animation-delay: 430ms; }
  .hero [data-enter="6"] { animation-delay: 540ms; }
}
@keyframes enter { from { opacity: 0; translate: 0 0.5em; } to { opacity: 1; translate: 0; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal, .hero [data-enter] { animation: none; opacity: 1; translate: 0; }
  .anatomy__figure img { animation: none; scale: 1; }
  .card:hover .card__stage img { scale: 1; translate: 0; }
  .arrow-link:hover::after { translate: 0; }
  .switch__thumb { transition: none; }
}

/* ---------- Words ----------
   Every heading on the site is set word by word, each word in a clip box of
   its own, so a heading arrives as a line of type being set rather than a
   block fading in. The clip box carries padding and an equal negative margin,
   because a box clipped to the line would cut the descenders and the italic
   overhang off at rest, which is worse than any animation is good. */

.rise {
  /* inline-flex rather than inline-block: an inline-block sizes itself to a
     line box and the word then sits on that box's baseline, leaving most of a
     line hanging below the clip edge. A flex container is exactly as tall as
     its item, and its baseline is the item's baseline, so words still sit on
     the same line as the text around them. */
  display: inline-flex;
  /* The word rises from below, so only the bottom edge needs to hide anything.
     clip-path does that and nothing else: it takes no space and moves no
     neighbour, where padding with a matching negative margin pulls the next
     word in by twice its value and changes where the headline wraps. The three
     open sides leave the descenders and the italic overhang alone, and the
     small negative bottom keeps a descender at rest while still being far
     above a word parked a whole line lower. */
  clip-path: inset(-0.6em -0.6em -0.16em -0.6em);
}
.rise > span { display: block; }

@keyframes word-rise { from { translate: 0 118%; } to { translate: 0 0; } }

@media (prefers-reduced-motion: no-preference) {
  /* Above the fold, so there is nothing to scroll into: these play on load. */
  .hero__title .rise > span,
  .campaign-hero h1 .rise > span {
    animation: word-rise 900ms var(--spring) both;
    animation-delay: calc(90ms + var(--i) * 54ms);
  }

  @supports (animation-timeline: view()) {
    /* Everything else is set by the scroll. The range is offset per word, so
       the line assembles left to right as the section comes up rather than all
       at once at one threshold. */
    h2 .rise > span {
      animation: word-rise linear both;
      animation-timeline: view();
      animation-range: entry calc(4% + var(--i) * 2.5%) entry calc(42% + var(--i) * 2.5%);
    }
  }
}

/* ---------- Scroll rule ----------
   How far down the document you are, as a line across the foot of the header.
   It is the scroll position itself rather than a number derived from it, so it
   cannot drift out of step with the page. */

.header__progress {
  position: absolute; inset-inline: 0; inset-block-end: -1px;
  block-size: 2px; background: var(--accent-text);
  transform-origin: 0 50%; scale: 0 1;
  pointer-events: none;
}
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .header__progress {
      animation: progress linear both;
      animation-timeline: scroll(root block);
    }
  }
}
@keyframes progress { to { scale: 1 1; } }

/* ---------- Cards ----------
   Two things the pointer does here, and one the scroll does.

   The scroll brings each card up with its own offset, so the pair arrives as a
   sequence rather than a row. The pointer tilts the card a couple of degrees
   and pulls the render the other way inside it, which is what makes a flat
   panel read as having depth. And a label follows the cursor, because the card
   is a link and the cursor should say so. None of it exists on a touch screen,
   where there is no pointer to answer. */

.card { transition: transform 420ms var(--ease), border-color var(--dur) var(--ease), background var(--dur) var(--ease); }

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .card[data-tilt] {
    transform: perspective(1100px)
               rotateY(calc(var(--px) * 2.6deg))
               rotateX(calc(var(--py) * -2.1deg));
  }
  /* Slow to return to centre, quick to follow. */
  .card[data-tilt]:hover { transition-duration: 110ms; }
  .card[data-tilt]:hover .card__stage img {
    translate: calc(var(--px) * -10px) calc(-1.5% + var(--py) * -8px);
  }
}

.card__cursor {
  position: absolute; z-index: 4; inset-block-start: 0; inset-inline-start: 0;
  translate: calc(var(--cx, 0) * 1px - 50%) calc(var(--cy, 0) * 1px - 50%);
  padding: 0.5rem 0.85rem;
  background: var(--rule); color: var(--on-accent);
  font-size: var(--step--2); font-weight: 700; letter-spacing: 0.16em;
  text-transform: uppercase; line-height: 1; white-space: nowrap;
  opacity: 0; scale: 0.7;
  transition: opacity 160ms var(--ease), scale 300ms var(--spring);
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .card:hover .card__cursor, .card:focus-visible .card__cursor { opacity: 1; scale: 1; }
}
@media (prefers-reduced-motion: reduce) { .card__cursor { display: none; } }

/* ---------- Ambient ----------
   Three things move without being asked, all of them slowly enough that they
   read as the page being alive rather than as animation. Each one is paused by
   the observer in the script while its section is off screen. */

@media (prefers-reduced-motion: no-preference) {
  /* The accent rule beside a label extends and retracts. */
  .label--accent::before {
    transform-origin: 0 50%;
    animation: rule-breathe 6.5s ease-in-out infinite alternate;
  }
  /* The product name behind its own render drifts across the card. */
  .card__ghost { animation: ghost-drift 26s ease-in-out infinite alternate; }

  /* Scoped to the three ambient animations by name rather than to every
     descendant, because a blanket pause would also freeze the scroll-driven
     ones, and a scroll animation that resumes from where it was paused is out
     of step with the page. */
  [data-idle="true"] .label--accent::before,
  [data-idle="true"] .card__ghost { animation-play-state: paused; }
  [data-idle="true"] .hero__shot { animation-play-state: paused, running; }
}
@keyframes rule-breathe { from { scale: 1 1; } to { scale: 1.9 1; } }
@keyframes ghost-drift { from { translate: -52% 0; } to { translate: -47% 0; } }

/* ---------- Pointer-aware underlines ----------
   The rule under a link grows from the side the pointer came in on and
   retracts toward the side it left by, which is the difference between a link
   that responds and a link that plays a canned animation. The side is the only
   thing the script supplies; the rest is the transition that was already here. */

.header__nav a::after,
.arrow-link::before { transform-origin: calc(var(--origin, 0) * 100%) 50%; }

/* The link is a grid item wherever it appears, so without this the rule below
   it stretches the full column instead of sitting under the words. */
.arrow-link { position: relative; justify-self: start; inline-size: fit-content; }
.arrow-link::before {
  content: ""; position: absolute; inset-inline: 0; inset-block-end: -0.2em;
  block-size: 1px; background: currentColor;
  scale: 0 1; transition: scale var(--dur) var(--spring);
}
.arrow-link:hover::before, a:hover .arrow-link::before { scale: 1 1; }

/* ---------- Cross-document title morph ----------
   The card and the campaign page carry the same words at different sizes, so
   they are given one name and the browser moves one into the other. */

[data-vt="title-xlr"] { view-transition-name: title-xlr; }
[data-vt="title-usb"] { view-transition-name: title-usb; }
::view-transition-group(title-xlr),
::view-transition-group(title-usb) {
  animation-duration: 520ms;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  .rise > span { animation: none; translate: 0; }
  .header__progress { animation: none; scale: 1 1; }
  .hero__shot { animation: none; scale: 1; translate: 0; }
  .label--accent::before, .card__ghost { animation: none; }
  .card[data-tilt] { transform: none; }
}

/* ---------- Theme beam ----------
   The light that uncovers the new theme. It is a plain element with a slanted
   gradient on it, given a transition name so it travels inside the view
   transition rather than being stranded underneath it, and tilted to the same
   angle as the clip it rides. It exists for the length of one theme change and
   is removed afterwards; it is never part of the page. */

.flare {
  position: fixed;
  inset-block: -30vh -30vh;
  inset-inline-start: 0;
  inline-size: 56vw;
  view-transition-name: theme-beam;
  pointer-events: none;
  contain: layout paint;
}
/* Above the root pair, and with the browser's own cross-fade turned off: the
   beam is moved, not dissolved. */
::view-transition-group(theme-beam) { z-index: 3; animation-duration: 820ms; animation-timing-function: cubic-bezier(0.5, 0.02, 0.28, 1); }
::view-transition-old(theme-beam),
::view-transition-new(theme-beam) { animation: none; mix-blend-mode: plus-lighter; }

@media (prefers-reduced-motion: reduce) { .flare { display: none; } }


/* ---------- Section rule ----------
   With the column grid gone the page needs its structure to come from the
   content, so every section head closes with a rule the width of the measure.
   It draws itself as the section arrives, and where there is no view timeline
   it is simply already drawn: the base state is the finished one and only the
   keyframe reaches back to nothing. */

.section__head::after {
  content: ""; display: block;
  inline-size: 100%; block-size: 1px;
  background: var(--line-strong);
  margin-block-start: clamp(var(--space-5), 2vw, var(--space-6));
  transform-origin: 0 50%;
}
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .section__head::after {
      animation: rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 56%;
    }
  }
}
@keyframes rule-draw { from { scale: 0 1; } to { scale: 1 1; } }

/* ---------- Staggered lists ----------
   The three About points arrive one after another rather than as one block,
   from the same --i the headings use. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .about__points li {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry calc(4% + var(--i) * 9%) entry calc(40% + var(--i) * 9%);
    }
  }
}

/* The way back, at the end of the legal line. */
.footer__up { margin-inline-start: auto; font-size: var(--step--1); }
@media (max-width: 40rem) { .footer__up { margin-inline-start: 0; } }

@media (prefers-reduced-motion: reduce) {
  .section__head::after { animation: none; scale: 1 1; }
  .about__points li { animation: none; opacity: 1; translate: 0; }
}

/* ---------- Target size ----------
   WCAG 2.2 asks for 24 by 24 CSS pixels on anything that is not a link inside
   a sentence. Measured, the header links, the wordmark, the hero index and the
   footer links were all 17 to 22 pixels tall: the text was the whole target.
   These give each one a box to be hit, without moving anything, because the
   extra height is taken as padding inside a flex line that was already taller. */

.wordmark,
.header__nav a,
.hero__index a,
.footer__top a,
.footer nav a {
  display: inline-flex; align-items: center;
  min-block-size: 1.5rem;
}
/* The index links put their rule and their text on a baseline, so they keep
   that alignment and take the height as padding instead. */
.hero__index a { align-items: baseline; padding-block: 0.16rem; }

/* The phone header leaves on the way down. The transition is on translate
   alone, so it is a compositor move and nothing reflows; the sticky position
   and everything inside it, including the scroll rule, travel with it. */
@media (max-width: 47.99rem) {
  .header { transition: translate 300ms var(--ease); will-change: translate; }
  .header[data-away] { translate: 0 -101%; }
}
@media (prefers-reduced-motion: reduce) {
  .header { transition: none; }
  .header[data-away] { translate: 0; }
}

/* ---------- Enquiry states ----------
   The status line is the only thing on the page that ever claims a send
   succeeded, and it is written by the script after the endpoint has answered
   with a row id. These are the three states it can be in. */

.form-status[data-state="sending"] { color: var(--ink-2); }
.form-status[data-state="sent"] { color: var(--accent-text); font-weight: 600; }
.form-status[data-state="error"] { color: var(--warm-text); font-weight: 600; }

.field-grid[data-busy] { cursor: progress; }
.field-grid[data-busy] .btn { opacity: 0.55; pointer-events: none; }

/* The consent line is a control, so the whole line is the target rather than
   the twelve pixel box at the start of it. */
.consent {
  display: flex; align-items: flex-start; gap: var(--space-3);
  font-size: var(--step--1); letter-spacing: 0; text-transform: none;
  font-weight: 400; color: var(--ink-2); cursor: pointer;
  min-block-size: 1.5rem;
}
.consent input {
  inline-size: 1.25rem; block-size: 1.25rem; flex: none;
  margin-block-start: 0.1rem; accent-color: var(--accent);
}
.consent span { padding-block-start: 0.05rem; }

/* A field only a machine fills in. Taken out of the layout and off the tab
   order rather than hidden with display:none, because some form fillers skip
   what is display:none and this one is meant to be filled by them. */
.field--trap {
  position: absolute; inline-size: 1px; block-size: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  pointer-events: none;
}

/* An invalid field only says so once it has been left, not while it is being
   typed into for the first time. */
.field input:user-invalid, .field textarea:user-invalid {
  border-block-end-color: var(--warm-text);
}
