/* 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;
  /* One step lifted again, for accent text that sits on a photograph rather
     than on a flat dark surface. #2e7dff is sound over #000, but over the hero
     frame the eyebrow measured 4.4:1 and dipped to 3.2:1 on the bright patches.
     This holds 6.9:1, and 5.1:1 at the worst pixel, without leaving the blue. */
  --electric-blue-over-photo: #6ea8ff;
  --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: #75757e;
  --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: #686f7d;
  --ink-3: #6f7685;
  --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: #686f7d; --ink-3: #6f7685;
  --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: #75757e;
  --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 header is sticky, so an anchor that lands flush with the viewport top
     lands underneath it. Nothing accounted for that: clicking "Enquire" put the
     top of the form -- its heading and its first field -- behind the header or
     above it entirely, which is the one link on the page that has to arrive
     cleanly. This reserves the header's height plus a margin, so every anchor
     on the site lands below it. The phone header is two rows and needs more. */
  scroll-padding-block-start: 8.5rem;
  /* 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 ---------- */

/* The wordmark is drawn, not set. It was previously Space Grotesk at 700 with
   0.24em tracking, which is close but not the mark: the real one is a monoline
   geometric with a crossbar-less A, and no font substitution produces that A.
   The letterforms are traced from the supplied artwork -- cap height 43px,
   stroke 4.5px, so 10.5 units against a 100-unit cap -- which also means there
   is no font file to license, load or wait for. The type properties below are
   kept because the SVG is sized in em and inherits currentColor from them. */
.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;
  display: inline-flex; align-items: center;
}
.wordmark__mark {
  display: block;
  /* Matched to the cap height of the type it replaces, so the header keeps its
     vertical rhythm; the width follows from the viewBox. */
  block-size: 0.7em;
  inline-size: auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 10.5;
  stroke-linecap: butt;
  stroke-linejoin: round;
  overflow: visible;
}

.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%);
  }
  /* Narrow takes a tall slice out of a wide frame, so which slice it takes is
     the whole composition. 58% landed on the sunlit middle: the brightest part
     of the picture, under the copy, needing the most cover to stay readable and
     showing the least once it had it. 26% is the textured wall and the spring
     of the first arch -- darker, and the part of the photograph that still
     reads as architecture at 375px. */
  .hero__shot { object-position: 26% 26%; }
  [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 change, so a reader
   without it never sees a page of de-emphasised text.

   This used to fade the inactive parts to opacity 0.35. Measured, that put
   their body text at 1.58:1 in the light theme and 1.82:1 in the dark one,
   against a 4.5:1 requirement, so eight of the nine parts were unreadable
   until they reached the middle of the viewport. On the one section whose
   whole purpose is explaining the product, that is the wrong trade.

   Opacity cannot be rescued by choosing a better value either: --ink-2 starts
   at 4.83:1 on white, so any fade at all takes body text below the line. The
   emphasis therefore moves to colour, which has room. The heading of the
   active part goes to full ink while the others sit at --ink-2, and its
   number takes the campaign accent. The sequence still singles one part out.
   It no longer hides the other eight. */
[data-sequenced="true"] .parts h3 {
  color: var(--ink-2);
  transition: color var(--dur) var(--ease);
}
[data-sequenced="true"] .parts li[data-active="true"] h3 { color: var(--ink); }
[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)); }
}
/* Grouped specifications. Each group is its own table with a visible caption,
   so the grouping reaches a screen reader and a crawler through the structure
   rather than existing only in the spacing between blocks. Nothing is behind a
   disclosure control: every figure stays in the markup and on the page. */
.spec-groups { display: grid; gap: clamp(var(--space-7), 4vw, var(--space-8)); }
.specs--group caption {
  text-align: start;
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-2);
  padding-block-end: var(--space-4);
}

.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); }

/* The comparison is three columns, so the 45% row-label width the two-column
   table uses would leave the two products 27% each. It also carries a head,
   which the specifications table does not. */
.specs--compare th { inline-size: auto; padding-inline-end: var(--space-3); }
.specs--compare thead th {
  font-family: var(--display); font-weight: 700; color: var(--ink);
  font-size: var(--step--1); letter-spacing: 0.02em; text-transform: uppercase;
  padding-block-end: var(--space-2); border-block-end-color: var(--ink-3);
}
.specs--compare tbody th { inline-size: 36%; }
.specs--compare td { inline-size: 32%; font-size: var(--step-0); }
/* The product whose page this is stays the emphasis; the other reads as the
   reference being compared against, not as a rival offer. */
.specs--compare td:first-of-type { color: var(--ink); }
.specs--compare td:last-child { color: var(--ink-2); font-weight: 500; }
.specs--compare tbody tr:first-child th,
.specs--compare tbody tr:first-child td { border-block-start: 0; }
.compare__go { margin-block-start: var(--space-5); }

@media (max-width: 34rem) {
  /* Three columns do not fit a phone at any type size that stays legible:
     measured at 390 and 430, "172 x 109 x 62 mm" broke across two lines, and a
     wrapped dimension reads as two measurements. So below this width the table
     stops being a table and each row becomes a block, the label above and each
     product's value named. Every ARIA role is declared in the markup because
     changing display here removes the implicit ones. */
  .specs--compare, .specs--compare tbody,
  .specs--compare tr, .specs--compare th, .specs--compare td { display: block; }
  .specs--compare thead { display: none; }
  .specs--compare tbody tr {
    padding-block: var(--space-4); border-block-end: 1px solid var(--line);
  }
  .specs--compare tbody tr:first-child { border-block-start: 1px solid var(--line); }
  .specs--compare th, .specs--compare td { border: 0; padding: 0; inline-size: auto; }
  .specs--compare tbody th {
    font-size: var(--step--1); margin-block-end: var(--space-2);
  }
  .specs--compare td {
    display: flex; justify-content: space-between; gap: var(--space-4);
    align-items: baseline; font-family: var(--sans); font-weight: 600;
    font-size: var(--step--1); letter-spacing: 0; padding-block: 2px;
  }
  .specs--compare td::before {
    content: attr(data-product); color: var(--ink-2); font-weight: 400;
    font-size: var(--step--2); white-space: nowrap;
  }
}

/* ---------- Glass ----------
   One surface on the site, not a house style. The enquiry panel is the point
   where reading the product turns into talking to someone, and it is the only
   place worth lifting off the page; putting every section in a card would cost
   the flat editorial ground that carries everything else.

   Built from --ghost and --line-strong rather than hardcoded white, because
   both already flip with the theme AND again inside .invert, which is what
   this panel sits in. That is four combinations, and a literal
   rgba(255,255,255,.055) would be wrong in two of them.

   Section 10 still holds. This is a translucent plane with a border and a
   shadow: no glow, no orbs, no scattered light, nothing radial. The backdrop
   filter is a real blur of whatever sits behind the panel, and where that is
   flat it simply reads as a lifted surface, which is the intent. */

.glass {
  background: var(--ghost);
  border: 1px solid var(--line-strong);
  border-radius: clamp(20px, 2vw, 28px);
  padding: clamp(var(--space-6), 4vw, var(--space-8));
  backdrop-filter: blur(24px) saturate(135%);
  -webkit-backdrop-filter: blur(24px) saturate(135%);
  box-shadow: inset 0 1px 0 var(--ghost), 0 20px 60px rgb(0 0 0 / 0.28);
}
/* Without backdrop-filter the panel would be a barely tinted rectangle, so it
   falls back to the opaque raised surface instead of nearly disappearing. */
@supports not (backdrop-filter: blur(1px)) {
  .glass { background: var(--raised); }
}

/* The panel's tint is paid for by whatever sits on it. On a light ground
   --ghost composites white down to #f0f0f0, which took the form labels from
   4.83:1 to 4.24:1, measured. There is no tint that rescues this: --ink-2
   starts at 4.83:1 on white, so even a 3% tint only reaches 4.51 and a 3%
   panel is invisible. The label takes full ink on glass instead, which is
   where a form label belongs anyway. */
.glass .field label { color: var(--ink); }

/* ---------- 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); }
.field-grid[hidden] { display: none; }
.field-grid [data-recipient-retry][hidden] { display: none; }
.field-grid [data-enquiry-recipient] { overflow-wrap: anywhere; min-inline-size: 0; }
.field-grid button:disabled { cursor: not-allowed; opacity: 0.6; }
@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-help { margin: 0; font-size: var(--step--1); line-height: 1.5; color: var(--ink); }
.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);
}

/* ---------- Enlarging the exploded view ----------
   The anatomy comment above concedes the problem in its own first line: nine
   labels baked into a picture are unreadable at any size a phone can show. The
   sequence answers that by naming the parts beside the picture, which is the
   right answer for the names. It is not an answer for the picture, where the
   callout rules, the cutaway and the colour of the capsule are the reason the
   artwork exists and are legible at roughly 1400 pixels and nowhere smaller.

   So the figure opens. Without script it is a link straight to the webp, which
   is a working way to read a diagram and has been since 1993. With script the
   same link opens it in a dialog instead, at the size the artwork was drawn
   for, with the theme-correct ground still under it. */

.anatomy__zoom {
  display: block; position: relative; cursor: zoom-in;
  color: inherit; text-decoration: none;
}
/* The control sits in the figure's own meta bar rather than on the picture.
   Every corner of both diagrams is already carrying print -- the RØDE mark,
   the strapline, the callouts, and bottom right the PAPERUS · RØDE credit --
   so a pill anywhere on the artwork covers something that was put there on
   purpose, and covering a co-brand credit is not a thing to do to a client's
   artwork. The bar underneath is chrome, it is empty in the middle, and a
   control is more obviously a control when it sits among the other chrome. */
.anatomy__cue {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding: 0.5em 0.9em; border-radius: 999px;
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase; text-decoration: none;
  color: var(--ink); background: var(--ghost);
  border: 1px solid var(--line-strong);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.anatomy__cue svg { inline-size: 1.05em; block-size: 1.05em; }
.anatomy__cue:hover { border-color: var(--ink); background: var(--raised); }
.anatomy__cue:focus-visible, .anatomy__zoom:focus-visible {
  outline: 2px solid var(--accent-text); outline-offset: 3px;
}
/* The counter keeps the end of the bar; the control takes the middle. */
.anatomy__meta { gap: var(--space-4); }
.anatomy__meta .anatomy__count { margin-inline-start: auto; }
@media (max-width: 29.99rem) {
  /* Three things will not sit on one line on a small phone, and the one that
     matters least is the word that names the picture you are looking at. */
  .anatomy__meta > .label { display: none; }
}

/* The dialog is the artwork and nothing else: no chrome over the picture but
   the one control that closes it, because every pixel spent on a frame is a
   pixel taken off the thing the visitor opened this to read. */
.lightbox {
  border: 0; padding: 0; margin: 0;
  inline-size: 100%; max-inline-size: 100%;
  block-size: 100%; max-block-size: 100%;
  background: var(--bg); color: var(--ink);
  overflow: hidden;
}
.lightbox::backdrop { background: rgb(0 0 0 / 0.86); }
.lightbox__frame {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  /* The bar is absolutely positioned over this, so the artwork is inset far
     enough to clear it rather than being printed underneath it. */
  padding: 3.6rem 1rem 1rem;
  overflow: auto; overscroll-behavior: contain;
  /* A pinch on a touch screen is the browser's own zoom on this element, which
     is better than any two-finger handler written here. */
  touch-action: pinch-zoom pan-x pan-y;
}
/* Percentages are no use for the fit. The grid row is auto-sized, so both
   100% and max-block-size: 100% have nothing definite to resolve against and
   a 1086 pixel diagram simply overflows an 800 pixel frame -- which is what
   it did. The viewport, on the other hand, is definite, and the dialog is
   exactly the viewport, so the ceiling is written in viewport units and the
   intrinsic ratio does the rest. dvh so a phone's collapsing address bar
   does not leave the bottom of the diagram under the chrome. */
.lightbox__frame img {
  display: block;
  inline-size: auto; block-size: auto;
  max-inline-size: calc(100vw - 2rem);
  max-inline-size: calc(100dvw - 2rem);
  max-block-size: calc(100vh - 4.6rem);
  max-block-size: calc(100dvh - 4.6rem);
  cursor: zoom-in; transition: none;
}
/* Zoomed, the image leaves the fit and the frame scrolls over it. Width alone
   is set so the aspect ratio carries the height. */
.lightbox[data-zoomed] .lightbox__frame { place-items: start; }
.lightbox[data-zoomed] .lightbox__frame img {
  inline-size: var(--zoom-w, 1600px); block-size: auto;
  max-inline-size: none; max-block-size: none;
  cursor: grab;
}
.lightbox[data-zoomed] .lightbox__frame:active img { cursor: grabbing; }

.lightbox__bar {
  position: absolute; inset-block-start: 0; inset-inline: 0; z-index: 2;
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-5);
  padding: var(--space-4) clamp(var(--space-4), 3vw, var(--space-6));
  font-size: var(--step--1); color: var(--ink-2);
  background: linear-gradient(to bottom, var(--bg) 55%, transparent);
}
.lightbox__hint { font-family: var(--sans); letter-spacing: 0.02em; }
.lightbox__close {
  flex: none; display: inline-flex; align-items: center; gap: 0.55em;
  font: inherit; font-weight: 600; color: var(--ink);
  padding: 0.5em 1em; border-radius: 999px; cursor: pointer;
  background: var(--raised); border: 1px solid var(--line-strong);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.lightbox__close:hover { border-color: var(--ink); }
.lightbox__close:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 3px; }
.lightbox__close svg { inline-size: 1em; block-size: 1em; }

@media (max-width: 47.99rem) {
  .lightbox__hint { display: none; }
}

/* ---------- The way back to the form ----------
   On a phone the enquiry form is the better part of four thousand pixels below
   the hero button that points at it, and the header that carried the other
   route out has already stepped aside on the way down. This is one line and a
   control, on narrow screens only, and it takes itself off the screen again as
   soon as the form it points at is in view: a button that scrolls you to
   something you are already looking at is just clutter with a shadow. */

.dock {
  position: fixed; z-index: 60;
  inset-block-end: 0; inset-inline: 0;
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  padding-block-end: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  background: var(--bg); border-block-start: 1px solid var(--line);
  translate: 0 110%;
  transition: translate 420ms var(--spring);
}
@supports (backdrop-filter: blur(1px)) {
  .dock { background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(18px) saturate(140%); }
}
.dock[data-shown] { translate: 0 0; }
.dock__name {
  font-family: var(--display); font-weight: 700; font-size: var(--step-0);
  letter-spacing: -0.02em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dock__go { flex: none; padding-block: 0.7em; }
/* Wide screens keep the header, which is one row there and never leaves. */
@media (min-width: 48rem) { .dock { display: none; } }
/* Desktop collapses the header to a single row, so the anchor reserve follows. */
@media (min-width: 48rem) { html { scroll-padding-block-start: 5.5rem; } }
@media (prefers-reduced-motion: reduce) { .dock { transition: none; } }

/* ---------- Corrections ----------

   The consent line. .field label sets the uppercase, tracked label style for
   every label in the grid, and the consent line is a label, so the one piece
   of legal text on the page was being rendered in tracked capitals. .consent
   already asked for sentence case but lost on specificity: .field label is
   two selectors and .consent is one. Written as .field .consent it wins on
   equal terms, and the sentence reads as a sentence. */
.field .consent {
  text-transform: none; letter-spacing: 0;
  font-weight: 400; font-size: var(--step--1);
  color: var(--ink-2);
}
.glass .field .consent { color: var(--ink); }
.field .consent input:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 3px; }

/* The sticky specifications intro was a grid item with the default stretch, so
   it was as tall as the whole table beside it and its closing rule landed at
   the bottom of that column with a screen of nothing above it. Sticky only
   ever needed the height of its own content. */
@media (min-width: 58rem) {
  .spec-intro { align-self: start; }
}

/* The inverted panel swapped every colour token but not the one thing the
   browser reads when it paints a control it owns. A checkbox, a select's
   dropdown, a textarea's grab handle and a scrollbar are drawn by the
   platform from color-scheme, so inside .invert on the dark theme they were
   still being drawn dark: the consent box came out as a charcoal square on a
   near-white panel, which reads as filled, or disabled, or both. The panel
   flips its colours, so it has to flip this too. */
.invert { color-scheme: light; }
[data-theme="light"] .invert { color-scheme: dark; }

/* And the box itself was the wrong shape. .field input is written for the text
   inputs -- a 2.9rem minimum height and vertical padding, so the underline sits
   where a thumb can reach it -- and a checkbox is an input, so the consent box
   was being stretched to twenty by forty-six and sitting well below the line it
   belongs to. The type selector takes it back out of that rule. */
.field input[type="checkbox"] {
  min-block-size: 0; padding: 0;
  inline-size: 1.15rem; block-size: 1.15rem;
}
.field .consent { align-items: flex-start; }
.field .consent input { margin-block-start: 0.15em; }
.field .consent span { line-height: 1.45; }

/* ---------- Hero on glass ----------
   The hero photograph above is a macro of a folded sheet with two thirds of
   the frame in shadow, and everything around it is built to keep the headline
   inside that shadow: a horizontal mask on wide screens, an ellipse in the
   light theme, a band on a phone. All three exist because the text had no
   surface of its own and had to borrow one from the picture.

   A full-bleed frame has no shadow to lend. So the text gets a surface
   instead, and the mask that was standing in for one is no longer needed.

   This is the site's second glass panel and the enquiry note above says glass
   is one surface and not a house style. The reason it holds there still holds
   here: the objection is to putting every section in a card and losing the
   flat editorial ground. The hero is the one place that is not flat ground --
   it is a photograph -- and a backdrop-filter over a photograph is doing real
   work, where the same filter on a flat panel is only a tint. Two surfaces,
   both earned, and nothing between them. */

.hero--glass .hero__shot-wrap {
  /* No mask: the frame is the whole hero, and the panel carries the text. A
     single wash keeps the picture from competing at full strength. */
  -webkit-mask-image: none; mask-image: none;
}
.hero--glass .hero__shot-wrap::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgb(0 0 0 / 0.72) 0 34%, rgb(0 0 0 / 0.28) 72%, transparent);
}

/* The hero holds its own ground in both themes. The frame is dark whichever
   theme is on, so dark ink over it is unreadable and the light theme was
   previously masking most of the picture away to avoid that. Redeclaring the
   tokens here is what .invert already does elsewhere, and it lets the light
   theme keep the whole photograph. */
.hero--glass {
  --bg: #000000; --surface: #0a0a0b; --raised: #121216;
  --ink: #ffffff; --ink-2: #a3a3a3; --ink-3: #75757e;
  --line: #222228; --line-strong: #3a3a44;
  --accent-text: var(--electric-blue-over-photo);
  /* .label--accent reads --rule, and --rule was substituted against :root's
     --accent-text before it ever got here, so it has to be redeclared or the
     eyebrow keeps the flat #0066ff it cannot hold over a photograph. */
  --rule: var(--electric-blue-over-photo);
  --secondary-text: var(--violet-lift);
  --warm-text: var(--rust-copper-lift);
  --soft-text: var(--poppy-lift);
  --ghost: rgba(255, 255, 255, 0.055);
  color-scheme: dark;
  color: var(--ink);
}
[data-theme="light"] .hero--glass .hero__shot-wrap {
  -webkit-mask-image: none; mask-image: none;
}

/* ---------- The light theme's hero reads warm ----------
   The frame here is a sunlit photograph whose left third is a wall in shadow:
   warm brown, and already dark enough to carry white type at nearly 8:1. The
   hero was covering that with a neutral black wash at 0.82, which did two
   things wrong -- it threw away the tone the picture already had, and it turned
   a warm photograph cold.

   Measured against the reference: it uses essentially no wash at all (the ratio
   between the bare photograph and the finished frame is 1.00 across the width),
   and buys its legibility from warm, bright ink instead of from cover. So the
   ink warms and brightens, the wash lightens to roughly a third of what it was,
   and what wash remains is brown rather than black, so it deepens the shadow
   the photograph already has instead of greying it out. */

[data-theme="light"] .hero--glass {
  --ink: #f8f4ec;          /* warm white, off the reference headline */
  --ink-2: #e7d6c4;        /* warm cream; #a3a3a3 was grey on a brown frame */
  --ink-3: #c9b6a2;
  --line: rgb(248 244 236 / 0.22);
  /* The eyebrow goes warm here rather than blue. Blue on a sunlit brown frame
     is a fight the blue loses -- it measured 2.0:1 -- and the reference sets
     this line in the same warm tan as the rest of the type. */
  --rule: #dcc9b4;
}

/* Brown, not black, and light: it deepens the wall the photograph already has
   rather than covering it. Gone entirely by 84%, so the lit half keeps the
   sky and the arches at full strength. */
[data-theme="light"] .hero--glass .hero__shot-wrap::after {
  background:
    linear-gradient(90deg, rgb(38 24 14 / 0.72) 0 44%, rgb(38 24 14 / 0.50) 70%, rgb(38 24 14 / 0.14) 90%, transparent),
    linear-gradient(180deg, rgb(38 24 14 / 0.16) 0 16%, transparent 46%, rgb(38 24 14 / 0.24));
}
@media (max-width: 52rem) {
  [data-theme="light"] .hero--glass .hero__shot-wrap::after {
    background: linear-gradient(180deg, rgb(38 24 14 / 0.50) 0, rgb(38 24 14 / 0.48) 22%, rgb(38 24 14 / 0.56) 72%, rgb(38 24 14 / 0.62));
  }
}

/* The panel used to be a glass card: a tinted rectangle with a border, a drop
   shadow and backdrop-filter: blur(26px). All of that existed to make text
   readable over a frame that was itself blurred to a colour field. The frame is
   sharp now, and a translucent rectangle over a sharp photograph reads as a
   smear across it rather than as a surface -- it blurs the one thing the hero
   is there to show. So the card is gone and the type sits directly on the
   photograph, with the wash below doing the work the card was doing. What is
   left here is a measure and a rhythm, not a surface. */
.hero__panel {
  /* Wide enough that the headline keeps the three-line setting it was drawn
     for. At 42rem it broke to four and "Your / product." lost its pairing. */
  max-inline-size: 54rem;
  /* No padding at all. The card needed its own inset because it was a box; a
     column of type is not, and .section already sets the block rhythm above and
     below. Keeping both put 173px of dead space in the hero and pushed the
     section index off the first screen. */
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
/* One step down inside the panel: step-5 was sized against the full width of
   the page and a panel is narrower than the page by definition. */
.hero--glass .hero__title { font-size: var(--step-4); }
/* The measure stays on the paragraph. The panel sets the outer bound, but a
   line of body copy at 54rem is past the point it is comfortable to read. */
.hero--glass .lede.measure { max-inline-size: 34rem; }
.hero--glass .hero__index { margin-block-start: var(--space-7); }

@media (max-width: 52rem) {
  /* The frame is the whole hero here too, so the band treatment goes with the
     mask it belonged to. */
  .hero--glass .hero__shot-wrap { inset-block-start: 0; block-size: 100%; }
  .hero--glass .hero__shot-wrap::after {
    background: linear-gradient(180deg, rgb(0 0 0 / 0.58), rgb(0 0 0 / 0.72));
  }
  .hero__panel { max-inline-size: none; }
}

/* ---------- The hero reel ----------
   The hero is the product turning, at the size it deserves. The section pins
   for the length of its own height and the page's vertical scroll walks the
   views sideways; the reading follows underneath rather than sitting beside
   the picture and halving it.

   The finish is a FILTER, not a position. Choosing black shows the five black
   views and nothing else. That is what someone picking a colour is asking
   for, and it also means each view gets a fifth of the walk rather than a
   tenth, so the product turns at a pace you can follow.

   No scroll handler and no scroll jacking: the walk is one keyframe on a view
   timeline, mapped by the browser on the compositor. Wheel, trackpad, finger,
   Page Down and a scrollbar drag all drive it, because each is just scrolling.

   Three audiences never get the pinned version and all three get the filmstrip
   below: no scroll timelines, a screen too small to spend the height on, and
   anyone who asked for less motion. There all ten frames stay visible, because
   with no walk there is nothing for a filter to help with. */

/* ---------- base layer: a filmstrip, everywhere ---------- */

.hero-reel { padding-block: var(--space-7) var(--space-5); }
.reel__head { margin-block-end: var(--space-6); }
.reel__head h1 { font-size: var(--step-4); margin-block-start: var(--space-4); }

.reel__viewport { overflow: clip; min-inline-size: 0; }
.reel__track {
  display: grid; grid-auto-flow: column; grid-auto-columns: max-content;
  gap: var(--space-5);
  /* The strip starts where the text starts and runs off the far side, which
     is how a strip says it continues. Without the padding the first frame is
     flush to the window edge and reads as a mistake. */
  padding-inline: var(--gutter);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scrollbar-width: none;
}
.reel__track::-webkit-scrollbar { display: none; }
.reel__track:focus-visible { outline: 2px solid var(--accent-text); outline-offset: -4px; }

.frame { margin: 0; scroll-snap-align: center; inline-size: min(78vw, 30rem); }
.frame__stage {
  display: block; position: relative; cursor: zoom-in;
  aspect-ratio: 1 / 1; overflow: clip; border: 1px solid var(--line);
}
/* The renders have a transparent ground, so the frame supplies one: black
   bodies on the light ground, white bodies on the dark one. Any other pairing
   loses one of the two finishes into its own background. */
.frame[data-ground="light"] .frame__stage { background: #f3f4f6; }
.frame[data-ground="dark"] .frame__stage { background: #121216; }
.frame__stage img {
  inline-size: 100%; block-size: 100%; object-fit: contain;
  padding: clamp(var(--space-5), 4vw, var(--space-6));
}
.frame__stage:focus-visible { outline: 2px solid var(--accent-text); outline-offset: -2px; }

.frame__cap {
  padding-block-start: var(--space-4);
  display: flex; align-items: baseline; gap: var(--space-3) var(--space-4); flex-wrap: wrap;
}
.frame__num {
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.22em; color: var(--ink-3); font-variant-numeric: tabular-nums;
}
.frame__name {
  font-family: var(--display); font-size: var(--step-0); font-weight: 700;
  letter-spacing: -0.02em; color: var(--ink);
}
.frame__note { flex-basis: 100%; font-size: var(--step--1); color: var(--ink-2); }

.reel__foot { margin-block-start: var(--space-5); }
.reel__rail { display: none; }

/* The finish control is the finish itself: a disc of the actual colour, and a
   ring that closes around it when it is the one being shown. The ring is a
   shadow rather than a border so the disc does not change size when it becomes
   current and nudge its neighbour along. */
.swatches { display: flex; align-items: center; gap: var(--space-3); }
.swatch {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: 0.45rem 0.9rem 0.45rem 0.5rem;
  border-radius: 999px; text-decoration: none;
  border: 1px solid transparent; color: var(--ink-2);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease),
              background var(--dur) var(--ease);
}
.swatch:hover { color: var(--ink); border-color: var(--line); }
.swatch[data-active="true"] { color: var(--ink); border-color: var(--line-strong); background: var(--ghost); }
.swatch:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
.swatch__chip {
  inline-size: 1.35rem; block-size: 1.35rem; border-radius: 50%; flex: none;
  transition: box-shadow 320ms var(--spring);
}
.swatch__chip[data-chip="black"] { background: #131316; box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.18); }
.swatch__chip[data-chip="white"] { background: #f4f4f6; box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.18); }
.swatch[data-active="true"] .swatch__chip {
  box-shadow: inset 0 0 0 1px rgb(127 127 127 / 0.35), 0 0 0 2px var(--bg), 0 0 0 4px var(--accent-text);
}
.swatch__name {
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase;
}

/* Filtering. The attribute is set by the script, so a reader without it keeps
   all ten frames and simply scrolls the strip. */
.hero-reel[data-showing="black"] .frame[data-finish="white"],
.hero-reel[data-showing="white"] .frame[data-finish="black"] { display: none; }

/* ---------- the reading, underneath ---------- */

.hero-detail { display: grid; gap: clamp(var(--space-6), 4vw, var(--space-8)); }
@media (min-width: 58rem) {
  .hero-detail { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr); align-items: start; }
}
.hero-detail__lead { display: grid; gap: var(--space-6); }
.hero-detail__body { display: grid; gap: var(--space-4); }
.hero-detail__aside { color: var(--ink-2); font-size: var(--step--1); }

/* ---------- pinned layer ---------- */

@supports (animation-timeline: view()) {
  @media (min-width: 56rem) and (prefers-reduced-motion: no-preference) {

    .hero-reel {
      /* --frames is ten in the markup and the script drops it to five once it
         has filtered to one finish, so a reader without the script still gets
         a section tall enough to walk all ten. */
      block-size: calc(var(--frames) * 62vh);
      padding-block: 0; position: relative;
      view-timeline-name: --reel;
      view-timeline-axis: block;
    }

    .hero-reel .reel__pin {
      position: sticky; inset-block-start: 0;
      block-size: 100vh; block-size: 100dvh;
      overflow: clip;
      display: grid; grid-template-rows: auto 1fr auto;
      /* The track is max-content -- five frames -- and overflow:clip hides
         that but does not stop it deciding how wide the grid's column wants to
         be. Left alone the column becomes the width of the whole track and
         everything centred in the pin ends up off the side of the screen. */
      grid-template-columns: minmax(0, 1fr);
    }

    .hero-reel .reel__head {
      /* margin-inline stays auto: .shell centres itself with it, and setting
         margin: 0 here flattened that and pushed the heading off the left. */
      grid-row: 1; margin-block: 0; margin-inline: auto;
      /* The header is sticky and sits over the pin, so the heading starts
         below it rather than behind it. */
      padding-block: calc(var(--header-h) + clamp(var(--space-4), 2.5vh, var(--space-6))) 0;
      pointer-events: none;
    }
    .reel__head h1 { font-size: var(--step-3); margin-block-start: var(--space-3); }

    .hero-reel .reel__viewport {
      grid-row: 2; min-block-size: 0; block-size: 100%;
      display: grid; align-items: center;
      /* The frames want a definite size, and the one they want is this box's.
         Percentages cannot give it: the track is max-content, so a percentage
         width inside it resolves against nothing and the frames collapse; and
         the frame's own height is content-driven, so a percentage height on
         the picture resolves against nothing either and the picture renders at
         its intrinsic 780 pixels, pushing the caption off the bottom of the
         screen. Both of those happened. container-type: size makes this box's
         width AND height addressable, which is what the two need. */
      container-type: size;
    }

    .hero-reel .reel__track {
      display: flex; gap: 0; padding-inline: 0;
      overflow: visible; scroll-snap-type: none;
      inline-size: max-content; block-size: 100%;
      align-items: center;
      animation: reel-walk linear both;
      animation-timeline: --reel;
      /* contain 0% is the moment the section's top meets the top of the
         scrollport and contain 100% the moment its bottom meets the bottom:
         exactly the span the pin is stuck for. */
      animation-range: contain 0% contain 100%;
      will-change: translate;
    }
    /* One frame short of the track's own width, which leaves the last view
       filling the screen rather than walking off it. */
    @keyframes reel-walk { to { translate: calc(-100% + 100cqi) 0; } }

    .hero-reel .frame {
      inline-size: 100cqi; block-size: 100cqb; flex: none;
      scroll-snap-align: none;
      /* minmax(0, 1fr) rather than 1fr: a bare 1fr is floored at min-content,
         so the picture's row could grow past the frame instead of bounding the
         picture, which is how the caption ended up below the fold. */
      display: grid; grid-template-rows: minmax(0, 1fr) auto;
    }
    /* The ground moves off the frame and onto the page: a full-screen hero is
       the page's own ground with the product standing on it, not a panel
       floating in the middle of it. */
    .hero-reel .frame__stage {
      aspect-ratio: auto; border: 0; background: none !important;
      display: grid; place-items: center;
      min-block-size: 0; block-size: 100%;
    }
    .frame__stage img {
      inline-size: auto; block-size: auto;
      /* As large as the screen will carry it. This is the whole point of the
         section; anything smaller is the old hero with extra steps. */
      max-inline-size: min(64vw, 780px);
      max-block-size: 100%;
      padding: 0;
      filter: drop-shadow(0 32px 64px rgb(0 0 0 / 0.5));
    }
    [data-theme="light"] .hero-reel .frame__stage img { filter: drop-shadow(0 32px 64px rgb(0 0 0 / 0.18)); }
    .hero-reel .frame__cap {
      justify-content: center; text-align: center;
      padding-block: var(--space-5) 0;
    }
    .hero-reel .frame__note { flex-basis: auto; }

    .hero-reel .reel__foot {
      grid-row: 3; margin-block: 0; margin-inline: auto;
      display: grid; gap: var(--space-4);
      padding-block: 0 clamp(var(--space-5), 4vh, var(--space-7));
    }
    /* One line that fills as the walk goes, which is all the indication of
       length the section needs. */
    .hero-reel .reel__rail { display: block; block-size: 2px; background: var(--line); overflow: clip; }
    .reel__rail span {
      display: block; block-size: 100%; background: var(--rule);
      transform-origin: 0 50%; scale: 0 1;
      animation: reel-rail linear both;
      animation-timeline: --reel;
      animation-range: contain 0% contain 100%;
    }
    @keyframes reel-rail { to { scale: 1 1; } }
  }
}

/* The hero's third mark. The panel takes the left of the frame and the
   photograph had the right to itself, which on a wide screen is a great deal
   of nothing between the two. The sweep starts in that empty half, comes back
   down and ends pointing at the one control the hero offers.

   Positioned against the hero rather than the panel, and above the photograph
   but under the panel, so a wide window gets the mark and the panel is never
   crossed by it. Poppy rather than the accent blue, because the headline is
   already carrying the blue and two marks in one colour would read as one
   shape interrupted. It draws itself from the rule the hero marks already
   use. */
.sketch--sweep {
  position: absolute; z-index: 0;
  opacity: 0.92;
  inline-size: clamp(8rem, 15vw, 15rem);
  /* Sat over the brightest run of particles at 7%, where a two-pixel stroke
     had nothing to be seen against. This puts it in the quieter band between
     the panel and that streak. */
  inset-inline-end: 13%; inset-block-start: 22%;
  color: var(--soft-text);
}
/* Below this the panel is the full width of the frame and there is no empty
   half for it to live in. */
@media (max-width: 68rem) { .sketch--sweep { display: none; } }

/* ---------- The hero frame, blurred ----------
   The particle field is no longer a picture to look at; it is the colour the
   glass is lit by. Blurred to the point where nothing in it resolves, it stops
   competing with the headline and starts doing what the panel's own backdrop
   filter only does behind the panel: giving the whole hero a ground with depth
   in it rather than a photograph with text on top.

   A blur samples from outside the element, so the frame is grown past its box
   to keep the soft edge off the screen.

   The two ambient animations come off here, and that is the point rather than
   an omission. breathe scales the frame on a 54 second clock and park drifts
   it on the scroll; either one moving under a 34 pixel blur makes the browser
   re-run the blur every frame, which is the most expensive thing on the page
   in exchange for motion nobody can see -- the field has no edges left to
   read movement against. Static, the blurred layer is rasterised once and
   then it is just a bitmap. */

/* The frame is shown sharp. It used to carry blur(34px) with scale(1.14), the
   scale being there only to push the blur's soft edges off screen; with no blur
   there is nothing to hide, so the scale goes back to 1 and the photograph is
   framed as shot rather than cropped 14% in. The wash below is what keeps the
   panel copy readable now that the picture has detail of its own. */
.hero--glass .hero__shot-wrap {
  filter: saturate(112%);
  scale: 1;
}
.hero--glass .hero__shot {
  animation: none;
  scale: 1;
  translate: 0;
}

/* Two frames, one per theme, because the light and the dark page want different
   photographs rather than the same one lit differently. display:none is what
   does the switching: a lazy image inside a none branch is never fetched, so
   the visitor downloads one frame, not both, and the theme toggle costs a
   request only at the moment someone actually crosses over. */
.hero__shot-wrap [data-ground="light"] { display: none; }
[data-theme="light"] .hero__shot-wrap [data-ground="dark"] { display: none; }
[data-theme="light"] .hero__shot-wrap [data-ground="light"] { display: block; }

/* A wash of its own for the dark frame. The expectation was that a dark
   photograph would need much less cover than the sunlit one, and that is only
   half true: the frame is mostly black, but the oculus is the brightest thing
   on either hero and it falls across the longer lines of the lede. Measured
   over the real line boxes rather than the element box -- which flatters it,
   because the short lines end before the light -- 0.60 left the lede at 2.5:1.
   This is the lightest that clears AA there: 4.85:1 at the worst pixel, 7.7:1
   on average. It does stay open on the right where the light theme's does not,
   so the oculus and the far wall keep their detail. */
[data-theme="dark"] .hero--glass .hero__shot-wrap::after {
  background:
    linear-gradient(90deg, rgb(0 0 0 / 0.78) 0 38%, rgb(0 0 0 / 0.55) 62%, rgb(0 0 0 / 0.14) 84%, transparent),
    linear-gradient(180deg, rgb(0 0 0 / 0.22) 0 18%, transparent 44%, rgb(0 0 0 / 0.30));
}
@media (max-width: 52rem) {
  [data-theme="dark"] .hero--glass .hero__shot-wrap::after {
    background: linear-gradient(180deg, rgb(0 0 0 / 0.58) 0, rgb(0 0 0 / 0.56) 22%, rgb(0 0 0 / 0.64) 72%, rgb(0 0 0 / 0.70));
  }
}
/* The wash is sized for a sharp frame, which is what this is: with the blur
   gone the picture keeps its own highlights, and the lede sits over them in
   #a3a3a3. Measured over the lede's own box, 0.5/0.18 gave 2.4:1 and 0.72/0.26
   still dipped to 3.2:1 on the bright patches; this holds 4.55:1 at the single
   worst pixel and 6.0:1 on average. It covers to 56%, which is where the panel
   ends, and is gone by 82%, so the open half keeps the photograph. */
.hero--glass .hero__shot-wrap::after {
  /* Two axes now the card is gone and this is the only thing carrying the type.
     The horizontal pass grounds the column the copy sits in; the vertical one
     weights the foot, where the lede and the button sit and where the paving in
     the photograph is brightest. Layered rather than one steeper gradient, so
     the falloff stays a shape in the picture instead of a visible band. */
  background:
    linear-gradient(90deg, rgb(0 0 0 / 0.84) 0 30%, rgb(0 0 0 / 0.58) 58%, rgb(0 0 0 / 0.14) 80%, transparent),
    linear-gradient(180deg, rgb(0 0 0 / 0.30) 0 18%, transparent 42%, rgb(0 0 0 / 0.38));
}

/* The wash above runs horizontally, which is right for the wide layout where
   the panel sits in the left third and the open half is meant to keep its
   colour. Narrow, the panel is full width, so body copy sits under the
   transparent end of that gradient and its contrast becomes a property of
   whichever photograph happens to be loaded -- a dark frame read fine and a
   bright one did not. Vertical and uniform here, so the copy holds whatever
   the picture is doing behind it. */
@media (max-width: 52rem) {
  .hero--glass .hero__shot-wrap::after {
    background: linear-gradient(180deg, rgb(0 0 0 / 0.80) 0, rgb(0 0 0 / 0.78) 22%, rgb(0 0 0 / 0.84) 72%, rgb(0 0 0 / 0.88));
  }
}

/* ---------- Second pass on the motion ----------
   Five additions, and every one of them is in the vocabulary the site already
   speaks: things lift into place, hairlines draw, nothing glows and nothing
   floats. Section 10 still holds.

   All of it is scroll-driven CSS on view timelines, so the browser does the
   work on the compositor and there is not a line of script behind any of it.
   All of it sits inside a reduced-motion guard, and all of it is additive: the
   page with none of it is the page as it was, which is what makes it safe to
   add this much at once. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* (The About points already stagger, further up this file, from the same
       --i. A second rule here was overriding that one's timing with identical
       intent, so it is not repeated.) */

    /* 2. The specification rows. A table of figures transcribed from a
          datasheet is the one place on the site where the reading is the
          product, so each row arrives on its own as it comes up the screen --
          no index needed, because rows are stacked and the viewport reaches
          them in order anyway. */
    .specs tbody tr {
      animation: lift-sm linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 44%;
    }

    /* 3. The chips under a campaign title. Four short facts, so they come in
          across rather than down. */
    .chips li {
      animation: lift-sm linear both;
      animation-timeline: view();
      animation-range: entry calc(6% + (var(--n, 0) * 5%)) entry calc(38% + (var(--n, 0) * 5%));
    }

    /* 4. The compare table's current column. The head cell already says which
          product you are on with aria-current; this draws a rule under it as
          the table arrives, so the column you are reading down is marked by
          something other than the word at the top of it. */
    .specs--compare thead th[aria-current]::after {
      content: ""; position: absolute;
      inset-inline: 0; inset-block-end: -1px;
      block-size: 2px; background: var(--accent-text);
      transform-origin: 0 50%; scale: 0 1;
      animation: rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 12% entry 52%;
    }

    /* 5. The footer wordmark, which had been the one piece of the page that
          simply appeared. */
    .footer__mark { animation: lift linear both; animation-timeline: view(); animation-range: entry 0% entry 46%; }

    /* 6. The seam above an inverted band. The band arrives as a hard edge
          between two grounds, and this draws an accent hairline along that
          edge as it comes up, so the change of ground is announced rather
          than merely happening. */
    .invert { position: relative; }
    .invert::before {
      content: ""; position: absolute;
      inset-inline: 0; inset-block-start: 0;
      block-size: 2px; background: var(--accent-text);
      transform-origin: 0 50%; scale: 0 1;
      z-index: 2; pointer-events: none;
      animation: rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 40%;
    }
  }
}
@keyframes lift { from { translate: 0 20px; } to { translate: 0 0; } }
@keyframes lift-sm { from { translate: 0 10px; } to { translate: 0 0; } }

/* The compare head cell needs somewhere to hang its rule. */
.specs--compare thead th { position: relative; }

@media (prefers-reduced-motion: reduce) {
  .specs tbody tr, .chips li, .footer__mark { translate: 0; animation: none; }
  .invert::before, .specs--compare thead th[aria-current]::after { scale: 1 1; animation: none; }
}

/* ---------- The marks, second pass ----------
   The site had three hand marks and all three were on the home page: the
   underline under "obsession", the loop around "whole page", the arrow beside
   "Currently in focus". The campaign pages, which are the pages anybody
   actually arrives on from an advertisement, had none. These give them two,
   and both are aimed at something rather than decorating a corner. */

/* 1. The ring round the connector plate.
      The last of the five views is the back of the microphone, and the whole
      argument of the page is what is on that back: the compare table says it
      in a row of text and this says it by circling it.

      It is a ring rather than an arrow because the thing being marked is a
      cluster with an area, not a point.

      THE FOUR REAR PHOTOGRAPHS ARE NOT FRAMED ALIKE, which I had assumed and
      had to be shown otherwise: measured, the microphone sits at a different
      size and height in each, and one ring that circled the ports on the USB
      landed on the serial number on the white XLR. So each ring is drawn
      at its own centre and radius, read off its own render, rather than one
      ring being pushed around by a transform -- translate and scale about a
      shifted origin compound, and the placement stopped being predictable.
      They share one wobble, so four separately drawn loops still read as one
      hand. */
.frame__shot { position: relative; display: grid; min-block-size: 0; }
.sketch--ports {
  position: absolute; inset: 0; z-index: 3;
  inline-size: 100%; block-size: 100%;
  color: var(--accent-text);
  pointer-events: none;
}
/* Drawn by default, so a browser with no scroll timelines and a reader who
   asked for less motion both simply get the mark. */
.sketch--ports path { stroke-dashoffset: 0; }

/* 2. The campaign title marks the word that separates the two products. The
      home page underlines "obsession" because that is the promise; a campaign
      page underlines USB or XLR because that is the difference. Same hand,
      same rule, pointed at the thing that matters on each page. */
.hero-reel h1 .marked { display: inline-block; }
.hero-reel h1 .sketch--underline { color: var(--accent-text); }

@supports (animation-timeline: view()) {
  @media (min-width: 56rem) and (prefers-reduced-motion: no-preference) {
    /* In the pinned reel the frame never scrolls into view -- the track is
       translated under a fixed viewport -- so a view timeline on the mark
       itself would never fire. It rides the reel's own timeline instead, drawn
       across the stretch of the walk where the fifth view is arriving. */
    .sketch--ports path {
      stroke-dashoffset: 1;
      animation: draw linear both;
      animation-timeline: --reel;
      animation-range: contain 76% contain 90%;
    }
    .sketch--ports .sketch__second { animation-range: contain 82% contain 94%; }
  }
}

/* 3. The loop and the arrow were single strokes where the underline had two.
      A mark drawn once reads as a shape; drawn twice, slightly out of
      register, it reads as a hand. These give the other two the same second
      pass the underline always had. */
.sketch--ring .sketch__second,
.sketch--arrow .sketch__second { opacity: 0.7; }

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

/* ---------- Advertisements ----------
   A published advertisement at /adv/<name>/view. It borrows the campaign
   pages' components wholesale -- the chips, the points, the specifications
   table, the photograph strip -- because an advertisement that looked like a
   different site would tell a visitor they had left Paperus, which is the one
   thing it must not say. */

.adv-head h1 { font-size: var(--step-4); margin-block: var(--space-5) var(--space-4); }
.adv-where { color: var(--ink-2); font-size: var(--step-0); }
.adv-price {
  font-family: var(--display); font-weight: 700; font-size: var(--step-3);
  letter-spacing: -0.03em; color: var(--ink);
  margin-block-start: var(--space-4);
}
.adv-price small {
  font-family: var(--sans); font-size: var(--step--1); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2);
  margin-inline-start: var(--space-3);
}
/* Said before the visitor taps, not after: opening WhatsApp hands their number
   to a third party, and that is worth one plain sentence. */
.adv-consent {
  margin-block-start: var(--space-5);
  font-size: var(--step--1); color: var(--ink-2); max-inline-size: 42ch;
}

/* The advertiser's own creative. It is visibly a panel with an edge, because a
   visitor is entitled to see where Paperus stops and an advertisement starts.
   An advertisement that is indistinguishable from the page around it is the
   thing every advertising standard is written against. */
.adv-frame { border: 1px solid var(--line-strong); background: var(--surface); }
.adv-frame iframe {
  display: block; inline-size: 100%; border: 0;
  /* Height comes from the declared value in the brochure. The frame is not
     allowed to size itself: a frame that can tell the page how tall to be can
     push everything else off the screen. */
  max-block-size: 90vh;
}
.adv-frame__note {
  padding: var(--space-4) clamp(var(--space-4), 2vw, var(--space-5));
  border-block-start: 1px solid var(--line);
  font-size: var(--step--2); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3);
}

/* The permit, stated where the enquiry happens rather than buried in the
   footer, because that is the moment it matters. */
.adv-permit {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-4);
  margin-block-start: var(--space-5);
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-2);
}
.adv-permit small { font-weight: 400; letter-spacing: 0.06em; color: var(--ink-3); }

/* The photograph strip reuses the reel's track without the pinning: an
   advertisement is not the place to take the screen hostage. */
.viewer-strip .reel__track { padding-inline: 0; }
.viewer-strip .frame { inline-size: min(78vw, 30rem); }

/* The example address on the advertise section. Set in the display face at a
   size that lets somebody read their own name into it, which is the entire
   point of showing it rather than describing it. */
.advertise__addr {
  margin-block-start: var(--space-6);
  font-family: var(--display); font-size: var(--step-1); font-weight: 500;
  letter-spacing: -0.02em; color: var(--ink-2);
  overflow-wrap: anywhere;
}
.advertise__addr b { color: var(--ink); font-weight: 700; }

/* The address in full, beside the button that would otherwise be the only way
   through. A mailto link depends on the visitor having a mail handler
   registered; where they have not, the click does nothing and says nothing.
   Printed, the address is selectable, copyable and readable aloud, and the
   button becomes a convenience rather than a requirement. */
.advertise__mail {
  margin-block-start: var(--space-5);
  font-size: var(--step--1); color: var(--ink-2);
}
.advertise__mail a { color: var(--ink); text-underline-offset: 0.2em; }

/* .about bottom-aligns its two columns, which suits the About section where a
   three line heading and three paragraphs finish at about the same depth. The
   advertise section has a two line heading against a longer body, so the same
   rule left a screen of nothing above the label. Top-aligned, the heading
   starts where the reading starts. */
@media (min-width: 62rem) {
  #advertise .about { align-items: start; }
}

/* The price answer. Somebody searching for a price needs to see the word on
   the page, and what they get instead of a figure has to be a real offer
   rather than an absence. */
.price-note {
  margin-block-start: var(--space-4);
  font-size: var(--step-0); color: var(--ink-2); max-inline-size: 46ch;
}
.price-note b { color: var(--ink); font-weight: 700; }

/* The starting price. A searcher scanning a page of results is looking for a
   number; giving them one and then saying what it is for converts better than
   silence, and it is the only thing that earns a price rich result. */
.price-from { display: grid; gap: var(--space-3); margin-block-start: var(--space-5); }
.price-from__amount {
  font-family: var(--display); font-size: var(--step-2); font-weight: 700;
  letter-spacing: -0.03em; color: var(--ink);
}
.price-from__note { font-size: var(--step--1); color: var(--ink-2); max-inline-size: 48ch; }

/* The viewer's own controls. Opening one view and closing it to see the next
   made the dialog a dead end; these are the way through it. */
.lightbox__nav { display: flex; align-items: center; gap: var(--space-3); }
.lightbox__step {
  display: inline-grid; place-items: center;
  inline-size: 2.25rem; block-size: 2.25rem;
  border-radius: 50%; cursor: pointer;
  color: var(--ink); background: var(--raised);
  border: 1px solid var(--line-strong);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.lightbox__step:hover { border-color: var(--ink); }
.lightbox__step:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 3px; }
.lightbox__step svg { inline-size: 1.1em; block-size: 1.1em; }
.lightbox__count {
  font-family: var(--sans); font-size: var(--step--2); font-weight: 700;
  letter-spacing: 0.18em; color: var(--ink-2);
  font-variant-numeric: tabular-nums; min-inline-size: 5ch; text-align: center;
}
/* The hint is the first thing to go when the bar runs out of room: the two
   arrows say the same thing and take a tenth of the width. */
@media (max-width: 40rem) { .lightbox__hint { display: none; } }

/* ---------- The mark on the diagram ----------
   The anatomy sequence named the parts beside the picture and the picture
   never answered. Eight rows of type stepped past an artwork that sat still,
   so the one question the section exists to settle -- which of those objects,
   in the picture, is the acoustic chamber -- was left to print the artwork
   sets at a size no phone can show. The names were legible and the thing they
   named was not.

   So the mark moves. One hand-drawn loop, the same hand as the marks on the
   photographs, travels across the exploded view and closes around whichever
   part is being read. Nothing is covered that was not already the subject: a
   loop around the capsule sits on the capsule, and the callouts, the RODE
   mark and the co-brand credit in the corner are all still there under nothing.

   Its centre and its size for every part were measured off the renders and
   written into the markup rather than computed, for the reason the rings on
   the photographs were: a component's place in the frame is a fact about that
   render, and the two grounds are not the same crop. The light USB diagram is
   3:2 and the dark one 4:3. The XLR pair share a frame and share one set.

   The loop is stretched to each part's own proportions instead of being scaled
   to fit, so the wide housing gets a wide loop and the stand adapter a small
   round one, and the stroke is held at a single width through vector-effect,
   or the stretch would thin the line along whichever axis was pulled. */

.anatomy__mark {
  position: absolute;
  inset-inline-start: calc(var(--mx, 50) * 1%);
  inset-block-start: calc(var(--my, 50) * 1%);
  inline-size: calc(var(--mw, 20) * 1%);
  block-size: calc(var(--mh, 20) * 1%);
  translate: -50% -50%;
  z-index: 2;
  opacity: 0;
  pointer-events: none;
}
.sketch--part {
  position: absolute; inset: 0; z-index: 2;
  inline-size: 100%; block-size: 100%;
  color: var(--accent-text);
}
/* Drawn by default, so a browser that cannot animate it and a reader who asked
   for less motion both simply get the mark. */
.sketch--part path { stroke-dashoffset: 0; }

/* The loop appears the first time a part reaches the middle of the screen,
   which is the first moment anybody is looking at it. */
.anatomy__mark[data-mark-live] { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .anatomy__mark {
    transition:
      inset-inline-start 640ms var(--spring),
      inset-block-start 640ms var(--spring),
      inline-size 640ms var(--spring),
      block-size 640ms var(--spring),
      opacity 300ms var(--ease);
  }
  .anatomy__mark[data-mark-live] .sketch--part path {
    stroke-dashoffset: 1;
    animation: draw 860ms var(--ease) both;
  }
  .anatomy__mark[data-mark-live] .sketch--part .sketch__second {
    animation-duration: 660ms; animation-delay: 240ms;
  }
}

/* The bar beside the list. One element measured onto the active row rather
   than a border on every row: nine borders fade past each other, one bar
   travels, and travelling is what says these rows are a sequence. The rows
   make room for it only once the script is sequencing them, so a reader
   without it never gets an indent with nothing in it. */
.parts { position: relative; }
[data-sequenced="true"] .parts li { padding-inline-start: var(--space-5); }
[data-sequenced="true"] .parts::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: calc(var(--cursor-y, 0) * 1px);
  block-size: calc(var(--cursor-h, 0) * 1px);
  inline-size: 2px;
  background: var(--rule);
  opacity: 0;
}
[data-sequenced="true"] .parts[data-cursor-ready]::before { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  [data-sequenced="true"] .parts::before {
    transition:
      inset-block-start 560ms var(--spring),
      block-size 560ms var(--spring),
      opacity 300ms var(--ease);
  }
}

/* The counter lifts its digit on the change. An inline box cannot be
   translated, so the digit is given a box of its own. */
.anatomy__count b { display: inline-block; }

/* The picture pins on a phone too, which it did not before. The reason it did
   not was a wrong guess at the arithmetic: the figure is 330 pixels tall on a
   390-wide screen, a third of an 844-tall one rather than the two thirds that
   ruled it out, so pinning it still leaves the list most of the screen to run
   in. It is worth pinning there more than anywhere, because the loop is the
   only thing on a phone that says which object a row is naming -- the print
   inside the artwork is unreadable at that size and always was, which is the
   admission the section was built on.

   Short screens keep the old arrangement. Under about 700 pixels of viewport
   the pinned picture leaves room for a row and a half, and a list you have to
   read through a letterbox is worse than a picture that scrolls away. */
@media (max-width: 61.99rem) and (min-height: 44rem) {
  /* The grid has to go for this. A sticky box can only travel inside its own
     containing block, and in a single-column grid that block is the row the
     figure sits in -- its own height, so it sticks for nothing. Laid out as
     blocks instead, the containing block is the whole anatomy, list included,
     which is the distance it needs to travel. Side by side the grid already
     gives it that distance, because the column stretches to the list.

     The gap goes with the grid, so the list takes it back as padding. */
  .anatomy { display: block; }
  .anatomy__pin {
    position: sticky;
    /* Pinned at nought with the header's worth of room taken as padding,
       rather than pinned below the header. The header retreats on the way
       down, and a figure held the header's height from the top then has a
       band of nothing above it with the list showing through -- a line of
       somebody else's description floating over the picture. Opaque padding
       is the band, and the header paints over it when it comes back. */
    inset-block-start: 0;
    padding-block-start: calc(var(--header-h) + var(--space-4));
    background: var(--bg);
    /* Sticky makes the stacking context; the list is later in the source and
       would otherwise paint over the picture it is scrolling under. */
    z-index: 1;
  }
  .parts {
    padding-block-start: var(--space-7);
    /* The last part has to be able to reach the line that decides, and at the
       end of the document there is nothing left to scroll. */
    padding-block-end: 30vh;
  }
}

/* ---------- The reel, turned ----------
   Measured, the pinned reel was spending a great deal of the page on very
   little. At 1440 by 900 the section ran 2790 pixels -- a third of the whole
   document's scroll -- to show five photographs one at a time, and the
   photograph itself came out 662 pixels wide in a 1440-wide frame. The
   pictures are square and the screen is not, so 778 pixels of every view was
   empty ground, and the reader was asked to scroll three screens through it.

   The answer is not to shrink the section. It is to put the rest of the set in
   the ground that was empty. The five views are one microphone turning --
   front turned, front, front reversed, three-quarter, and the outputs from
   behind -- so the view before and the view after belong on screen with the
   one being read. Three at a time, the middle one full size and the two beside
   it small and dim, and the walk stops being a slide of separate pictures and
   becomes the object turning.

   The neighbours are pulled in by a translate rather than by narrowing the
   frames. The frames stay a screen wide, so the track's geometry and the walk
   that drives it are untouched, and the middle picture -- which is limited by
   the height of the screen and not its width -- is not made one pixel smaller
   by the company. The neighbours appear in ground that was doing no work.

   And because the next view is now visible before you arrive at it, the walk
   can be shorter: 52vh a view rather than 62, which takes about a screen and a
   half of scrolling off the page.

   All of it is gated on [data-showing], which the script sets when it filters
   to one finish. Without the script the old full-width walk is exactly what
   still runs. This is an enhancement on a working section, not a replacement
   that leaves a hole when it does not load. */

/* A view is full size as it passes the middle and small, dim and drawn in
   toward the centre at either end of its own slice of the walk, so the one
   arriving and the one leaving cross over exactly halfway between them.

   The two sides of the curve are deliberately not mirror images. A symmetric
   one puts the arriving and the departing view at exactly the same size and
   the same weight at the moment they change places, and two identical
   microphones either side of an empty middle reads as a stall rather than as
   movement -- the more so here, where views two and three are both front-on.
   The departing view therefore gives way faster than the arriving one takes
   over, so at the handover the one coming is plainly ahead and the eye has
   somewhere to go.

   54% of a screen-wide frame is the distance that brings a neighbour's picture
   about a quarter of the way onto the screen at 1440 by 900 -- close enough to
   be read as the next view, far enough to leave the middle one alone. The
   translate is not scaled: individual transform properties apply translate
   before scale, so this is 54% of the frame at its full width either way. */
@keyframes view-focus {
  from { scale: 0.52; opacity: 0.22; translate: -54% 0; }
  40%  { scale: 0.94; opacity: 0.92; translate: -6% 0; }
  50%  { scale: 1;    opacity: 1;    translate: 0 0; }
  58%  { scale: 0.88; opacity: 0.70; translate: 10% 0; }
  to   { scale: 0.52; opacity: 0.22; translate: 54% 0; }
}
/* The caption belongs to one view at a time. Two more of them, small and half
   off the edges of the screen, is clutter and not information. */
@keyframes cap-focus {
  from, 34% { opacity: 0; }
  50%       { opacity: 1; }
  66%, to   { opacity: 0; }
}

/* The two ends of the set are a special case, and leaving them out of it put a
   small, dim, off-centre product at the top of the page. The first view's
   slice of the walk starts a quarter of a step BEFORE the section pins, and
   the walk itself does not begin until it pins -- so through that quarter the
   first view was already backing out of a turn that had not started. It is
   the view being shown before anything moves, and the last view is still the
   one being shown after everything has. Both hold their ground on the outer
   half of their own slice. */
@keyframes view-focus-out {
  from, 50% { scale: 1;    opacity: 1;    translate: 0 0; }
  58%       { scale: 0.88; opacity: 0.70; translate: 10% 0; }
  to        { scale: 0.52; opacity: 0.22; translate: 54% 0; }
}
@keyframes view-focus-in {
  from      { scale: 0.52; opacity: 0.22; translate: -54% 0; }
  40%       { scale: 0.94; opacity: 0.92; translate: -6% 0; }
  50%, to   { scale: 1;    opacity: 1;    translate: 0 0; }
}
@keyframes cap-focus-out { from, 50% { opacity: 1; } 66%, to { opacity: 0; } }
@keyframes cap-focus-in  { from, 34% { opacity: 0; } 50%, to { opacity: 1; } }

@supports (animation-timeline: view()) {
  @media (min-width: 56rem) and (prefers-reduced-motion: no-preference) {
    .hero-reel[data-showing] { block-size: calc(var(--frames) * 52vh); }

    /* The neighbours have to be able to leave their own frame, which is a
       screen wide and clipped by default. */
    .hero-reel[data-showing] .frame { overflow: visible; }
    .hero-reel[data-showing] .frame__stage { overflow: visible; }

    /* --focus-start and --focus-end are written per frame by the script,
       because where a view sits in the walk depends on how many views the
       chosen finish has, and that count is the script's own answer -- filtering
       them is what it is for. The fallbacks are the whole walk, which is what
       a single view would want. */
    .hero-reel[data-showing] .frame__stage {
      animation: view-focus linear both;
      animation-timeline: --reel;
      animation-range: contain var(--focus-start, 0%) contain var(--focus-end, 100%);
    }
    .hero-reel[data-showing] .frame__cap {
      animation: cap-focus linear both;
      animation-timeline: --reel;
      animation-range: contain var(--focus-start, 0%) contain var(--focus-end, 100%);
    }
    .hero-reel[data-showing] .frame[data-end="first"] .frame__stage { animation-name: view-focus-out; }
    .hero-reel[data-showing] .frame[data-end="first"] .frame__cap { animation-name: cap-focus-out; }
    .hero-reel[data-showing] .frame[data-end="last"] .frame__stage { animation-name: view-focus-in; }
    .hero-reel[data-showing] .frame[data-end="last"] .frame__cap { animation-name: cap-focus-in; }
  }
}



/* ---------- PodMic USB: the stage ----------
   The reel above is a strip of pictures. This is the same five pictures shown
   as one object being looked at. Everything here is scoped to
   .hero-reel--stage, which only the USB campaign carries; the XLR page keeps
   the strip exactly as it was.

   Three things change. The card goes: no border, no square of fill, no shadow
   under a panel, because the product is standing in the page rather than
   sitting in a box. The walk goes: the track no longer travels a screen per
   view -- the five views are stacked in one place and each is moved a little
   way and a long way BACK, so the motion reads as walking around one
   microphone instead of five pictures flying past. And the ends of the set
   stay legible: the black body gets a light pooled behind it and a rim off its
   top edge, which is what separates a black object from a black ground without
   turning a glow on. */

/* ===== the picture, with nothing around it ===== */

.hero-reel--stage .frame { inline-size: min(88vw, 32rem); }
.hero-reel--stage .frame__stage {
  border: 0; overflow: visible; background: none !important;
  aspect-ratio: auto;
  display: grid; place-items: center;
}
.hero-reel--stage .frame__shot {
  position: relative; isolation: isolate;
  inline-size: 100%; aspect-ratio: 1 / 1;
  display: grid;
}
.hero-reel--stage .frame__shot img {
  grid-area: 1 / 1;
  inline-size: 100%; block-size: 100%;
  object-fit: contain; padding: 0;
}

/* The ground is a pool of light with no edge, not a panel: feathered out well
   before it reaches the frame, so there is nothing for the eye to read as a
   container. Which way it leans is the product's own tone -- a light pool
   under the black body, a deeper one under the white. */
.hero-reel--stage .frame__shot::before {
  content: ""; grid-area: 1 / 1; z-index: -1;
  /* The extents are written wide rather than the box being scaled up: a
     scaled grid item is still scrollable overflow, and the pool is a good
     quarter wider than the picture it sits under. */
  background:
    radial-gradient(ellipse 69% 56% at 50% 58%, var(--pool) 0%, transparent 70%),
    radial-gradient(ellipse 44% 26% at 50% 88%, var(--pool-foot) 0%, transparent 75%);
}
.hero-reel--stage .frame[data-ground="light"] { --pool: rgb(255 255 255 / 0.085); --pool-foot: rgb(255 255 255 / 0.05); }
.hero-reel--stage .frame[data-ground="dark"]  { --pool: rgb(255 255 255 / 0.032); --pool-foot: rgb(0 0 0 / 0.35); }
[data-theme="light"] .hero-reel--stage .frame[data-ground="light"] { --pool: rgb(0 0 0 / 0.05);  --pool-foot: rgb(0 0 0 / 0.07); }
[data-theme="light"] .hero-reel--stage .frame[data-ground="dark"]  { --pool: rgb(0 0 0 / 0.085); --pool-foot: rgb(0 0 0 / 0.1); }

/* Rim, not glow. A one-pixel offset of the silhouette in the light's own
   direction draws the top edge of every part -- grille, yoke, stand -- and
   stops there. A blur radius here is what makes a halo, so there is none. */
.hero-reel--stage .frame[data-ground="light"] .frame__shot img {
  filter:
    drop-shadow(0 -1.2px 0 rgb(255 255 255 / 0.22))
    drop-shadow(1.4px 0 0 rgb(255 255 255 / 0.07))
    drop-shadow(0 24px 44px rgb(0 0 0 / 0.55));
}
.hero-reel--stage .frame[data-ground="dark"] .frame__shot img {
  filter: drop-shadow(0 24px 44px rgb(0 0 0 / 0.6));
}
[data-theme="light"] .hero-reel--stage .frame[data-ground="light"] .frame__shot img {
  filter: drop-shadow(0 18px 36px rgb(0 0 0 / 0.2));
}
[data-theme="light"] .hero-reel--stage .frame[data-ground="dark"] .frame__shot img {
  filter:
    drop-shadow(0 -1.1px 0 rgb(0 0 0 / 0.1))
    drop-shadow(0 18px 36px rgb(0 0 0 / 0.16));
}

/* ===== the hand ===== */

/* Marks in the margin the product does not reach, never over it: a brace down
   one side, a curved arrow at the thing worth looking at, a scale of ticks, an
   underline that misses. Drawn by default, so a reader with no scroll
   timelines and a reader who asked for less motion both simply get them. */
/* The count below names the view, so the number on the caption says it twice. */
.hero-reel--stage .frame__num { display: none; }

.hero-reel--stage .sketch--note {
  position: absolute; inset: 0; z-index: 3;
  inline-size: 100%; block-size: 100%;
  color: var(--ink-2); opacity: 0.55;
  pointer-events: none;
}
.hero-reel--stage .sketch--note path { stroke-dashoffset: 0; }
.hero-reel--stage .sketch--note .sketch__cue { stroke: var(--accent-text); }
.hero-reel--stage .sketch--note .sketch__second { opacity: 0.72; }

/* ===== the count ===== */

.hero-reel--stage:not([data-showing]) .reel__count { display: none; }
.hero-reel--stage .reel__ghost { display: none; }

.reel__count {
  display: flex; align-items: flex-end; gap: var(--space-5);
  padding-block-start: 1.5rem;
}
.reel__step { display: flex; gap: var(--space-2); }
.reel__arrow {
  inline-size: 2.4rem; block-size: 2.4rem; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 999px;
  background: none; color: var(--ink-2); cursor: pointer;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease),
              background var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.reel__arrow svg { inline-size: 1.05rem; block-size: 1.05rem; }
.reel__arrow:hover:not(:disabled) { color: var(--ink); border-color: var(--line-strong); background: var(--ghost); }
.reel__arrow:disabled { opacity: 0.3; cursor: default; }
.reel__arrow:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }

/* Numbers, not dots. The one you are on is the size of a heading and the other
   four are a hairline of a caption, so the set reads as a position rather than
   as a row of controls. The growth is a scale about the baseline, which costs
   no layout and therefore moves nothing beside it. */
.reel__index {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: baseline;
  gap: clamp(var(--space-4), 1.6vw, var(--space-6));
}
.reel__index li { line-height: 1; }
.reel__index button {
  display: block; margin: 0; padding: 0; border: 0; background: none;
  cursor: pointer; color: var(--ink-3); opacity: 0.5;
  font-family: var(--display); font-weight: 700; font-size: var(--step--1);
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1;
  transition: color 420ms var(--spring), opacity 420ms var(--spring);
}
.reel__index button > span {
  display: block; transform-origin: 50% 100%;
  transition: scale 520ms var(--spring);
}
.reel__index button:hover { color: var(--ink-2); opacity: 0.85; }
.reel__index button:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 4px; }
.reel__index li[aria-current="true"] button { color: var(--ink); opacity: 1; cursor: default; }
.reel__index li[aria-current="true"] button > span { scale: 2.2; }

@media (max-width: 40rem) {
  .reel__count { gap: var(--space-4); }
  .reel__index { gap: var(--space-4); }
  .reel__index li[aria-current="true"] button > span { scale: 1.9; }
}
@media (prefers-reduced-motion: reduce) {
  .reel__index button,
  .reel__index button > span { transition: none; }
}

/* ===== the pinned stage ===== */

@supports (animation-timeline: view()) {
  @media (min-width: 56rem) and (prefers-reduced-motion: no-preference) {

    /* Five views over five and a bit screens. The old walk spent a screen
       getting each view across the window; this one spends it turning, so the
       same height buys a slower change. */
    .hero-reel--stage[data-showing] { block-size: calc(var(--frames) * 60vh); }

    /* With no script there is no filter, no stacking and no stage: the section
       is the original walk of all ten views, and a frame is a screen wide
       again. The base layer widened the frame for a narrow screen, and that
       must not reach up into a layer it was never meant for. */
    .hero-reel--stage .frame { inline-size: 100cqi; }

    /* Title, count and rail come off the stack and sit over the scene, which
       hands the whole height of the pin to the product. The title is top left
       and the product is centred, so the two never meet. */
    .hero-reel--stage[data-showing] .reel__pin { display: block; }
    .hero-reel--stage[data-showing] .reel__head,
    .hero-reel--stage[data-showing] .reel__foot {
      position: absolute; inset-inline: 0; z-index: 4;
    }
    .hero-reel--stage[data-showing] .reel__head { inset-block-start: 0; }
    .hero-reel--stage[data-showing] .reel__foot {
      inset-block-end: 0;
      grid-template-columns: auto minmax(0, 1fr);
      align-items: end;
    }
    .hero-reel--stage[data-showing] .reel__foot > .swatches { grid-column: 1; }
    .hero-reel--stage[data-showing] .reel__count { grid-column: 2; justify-self: end; padding-block-start: 0; }
    .hero-reel--stage[data-showing] .reel__rail { grid-column: 1 / -1; }
    .hero-reel--stage[data-showing] .reel__viewport { position: absolute; inset: 0; z-index: 1; }

    /* The word the page is about, behind the thing the page is about. Low
       enough to be a tone rather than a word you read, and it drifts against
       the walk so the composition is not the same twice. */
    .hero-reel--stage[data-showing] .reel__ghost {
      display: grid; place-items: center;
      position: absolute; inset: 0; z-index: 0; margin: 0;
      font-family: var(--display); font-weight: 700;
      font-size: min(29vw, 46vh); line-height: 0.76; letter-spacing: -0.055em;
      color: var(--ink); opacity: 0.05;
      pointer-events: none; user-select: none;
    }
    [data-theme="light"] .hero-reel--stage[data-showing] .reel__ghost { opacity: 0.042; }
    .hero-reel--stage[data-showing] .reel__ghost span {
      display: block;
      animation: reel-ghost-drift linear both;
      animation-timeline: --reel;
      animation-range: contain 0% contain 100%;
    }
    /* Named for the reel. Keyframe names are global however deeply they are
       nested, and a plain "ghost-drift" already belongs to the home page's
       card. */
    @keyframes reel-ghost-drift {
      from { translate: 4.5% -1.2%; }
      to   { translate: -4.5% 1.2%; }
    }

    /* The five stack in one cell and the track stops travelling. Everything
       that used to be sideways distance is depth now. */
    .hero-reel--stage[data-showing] .reel__track {
      display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
      inline-size: 100%; block-size: 100%;
      /* clip, not visible: an element with visible overflow reports the
         overflowing children in scrollWidth, and the script reads exactly that
         to tell the pinned mode from the strip. clip is the one overflow value
         that does not flatten a 3D child. */
      overflow: clip;
      animation: none; will-change: auto;
    }
    .hero-reel--stage[data-showing] .frame {
      grid-area: 1 / 1;
      inline-size: 100%; block-size: 100%;
      display: block; position: relative;
      overflow: visible; pointer-events: none;
      /* Each frame carries its own vanishing point. They are all the same size
         in the same place, so one perspective per frame is the same picture as
         one shared 3D space, and it needs no preserve-3d chain to survive a
         container or a clip. */
      perspective: 1500px; perspective-origin: 50% 46%;
    }
    .hero-reel--stage[data-showing] .frame[data-live] { pointer-events: auto; z-index: 3; }

    .hero-reel--stage[data-showing] .frame__stage {
      position: absolute; inset: 0;
      padding-block: var(--header-h) clamp(6.5rem, 15vh, 9.5rem);
      padding-inline: var(--gutter);
      animation: stage-depth linear both;
      animation-timeline: --reel;
      animation-range: contain var(--wide-start, 0%) contain var(--wide-end, 100%);
    }
    /* The strip names the two ends of the set and hands them their own
       keyframes, at a specificity above the rule just above. The stage has no
       use for the special case -- its slice is wide enough that the first
       view's station already falls in the middle of it -- so both are put
       back on the one animation. */
    .hero-reel--stage[data-showing] .frame[data-end="first"] .frame__stage,
    .hero-reel--stage[data-showing] .frame[data-end="last"] .frame__stage { animation-name: stage-depth; }

    /* As tall as the space allows. This is the point of the section: the
       active view is the page, not an illustration on it. */
    .hero-reel--stage[data-showing] .frame__shot {
      block-size: min(100%, 54vw, 900px);
      inline-size: auto;
      translate: var(--comp-x, 0%) var(--comp-y, 0%);
      scale: var(--comp-s, 1);
    }
    /* No two angles are framed alike. A few per cent of size and offset is
       enough that the set reads as a shoot rather than a turntable. */
    .hero-reel--stage[data-showing] .frame[data-index="1"] { --comp-x: 0%;     --comp-y: 0%;     --comp-s: 1; }
    .hero-reel--stage[data-showing] .frame[data-index="2"] { --comp-x: 1.8%;   --comp-y: -1.6%;  --comp-s: 1.06; }
    .hero-reel--stage[data-showing] .frame[data-index="3"] { --comp-x: -2.4%;  --comp-y: 1%;     --comp-s: 0.95; }
    .hero-reel--stage[data-showing] .frame[data-index="4"] { --comp-x: 2.6%;   --comp-y: 1.4%;   --comp-s: 1.02; }
    .hero-reel--stage[data-showing] .frame[data-index="5"] { --comp-x: -1.4%;  --comp-y: -0.8%;  --comp-s: 1.05; }

    .hero-reel--stage[data-showing] .frame__cap {
      position: absolute; inset-inline: 0;
      inset-block-end: clamp(5rem, 11.5vh, 7rem);
      z-index: 4; padding-block: 0;
      justify-content: center; text-align: center;
      pointer-events: none;
    }

    /* The turn. Eleven per cent of sideways travel against seven hundred and
       sixty pixels of depth -- the view arrives out of the distance and is
       inspected, rather than flown in from the side. It holds dead centre
       across the middle of its slice, which is the beat that makes the whole
       thing feel deliberate instead of continuous. The angle runs one way
       through the set, +12 to -12, so five stills read as one rotation. */
    .hero-reel--stage[data-showing] .frame__stage,
    .hero-reel--stage[data-showing] .frame__cap { backface-visibility: hidden; }
  }
}

/* Four stations wide: the far end of the slice is two views away, the quarter
   marks are the view before and the view after standing in the distance, and
   the middle is the one being looked at. Eleven per cent of sideways travel
   against nine hundred of depth -- the view arrives out of the distance and is
   inspected, rather than flown in from the side. It holds dead centre across
   the middle of its slice, which is the beat that makes the whole thing read
   as deliberate rather than continuous. The angle runs one way through the
   set, +15 to -15, so five stills read as one rotation. */
@keyframes stage-depth {
  from     { opacity: 0;    transform: translate3d(21%, 2.4%, -940px) rotateY(16deg) rotateZ(0.7deg); }
  25%      { opacity: 0.32; transform: translate3d(15%, 1.6%, -560px) rotateY(11deg) rotateZ(0.45deg); }
  39%      { opacity: 0.64; transform: translate3d(8.4%, 0.7%, -270px) rotateY(5.6deg) rotateZ(0.2deg); }
  47%, 53% { opacity: 1;    transform: translate3d(0, 0, 0) rotateY(0deg) rotateZ(0deg); }
  61%      { opacity: 0.64; transform: translate3d(-8.4%, 0.7%, -270px) rotateY(-5.6deg) rotateZ(-0.2deg); }
  75%      { opacity: 0.32; transform: translate3d(-15%, 1.6%, -560px) rotateY(-11deg) rotateZ(-0.45deg); }
  to       { opacity: 0;    transform: translate3d(-21%, 2.4%, -940px) rotateY(-16deg) rotateZ(-0.7deg); }
}

/* The marks draw themselves as their view comes to rest and are gone before
   the next one lands. Their window is a short slice of its own, written out by
   the script rather than worked out here: a slice that starts before the
   section pins is a negative percentage, and calc() cannot subtract one of
   those from another without the two signs running together. */
@supports (animation-timeline: view()) {
  @media (min-width: 56rem) and (prefers-reduced-motion: no-preference) {
    .hero-reel--stage[data-showing] .sketch--note path {
      stroke-dashoffset: 1;
      animation: draw linear both;
      animation-timeline: --reel;
      animation-range: contain var(--note-a, 0%) contain var(--note-b, 100%);
    }
    .hero-reel--stage[data-showing] .sketch--note .sketch__second {
      animation-range: contain var(--note-c, 0%) contain var(--note-d, 100%);
    }
    /* Off the picture entirely except when its own view is the one being
       looked at, so the scene is never crossed by two hands at once. */
    .hero-reel--stage[data-showing] .sketch--note {
      opacity: 0;
      animation: note-hold linear both;
      animation-timeline: --reel;
      animation-range: contain var(--wide-start, 0%) contain var(--wide-end, 100%);
    }
  }
}
@keyframes note-hold { from, 36% { opacity: 0; } 47%, 53% { opacity: 0.5; } 64%, to { opacity: 0; } }




/* ---------- The parts, drawn as signals ----------
   Every part of this microphone does something to the sound, so the row that
   names one is drawn with what it does. The grille clips the peaks it stops.
   The foam absorbs a plosive across the width of the row. The capsule carries
   a voice. The USB output turns a smooth trace into steps halfway along,
   which is the conversion it performs. Eight parts, eight signals, none of
   them the same shape.

   The trace is only ever under the part being read. The other seven rows stay
   the plain text they always were, which is the same trade the colour
   emphasis above makes: single one row out, do not dim the rest.

   The heading's own letters sit on the trace rather than beside it, each one
   taking its height from the signal under it. That is the point of the
   treatment; a waveform behind a block of static type is wallpaper.

   The canvas and the letter boxes are both built by the script, so a reader
   without it gets this list exactly as it was. */

[data-sequenced="true"] .parts li { position: relative; }
[data-sequenced="true"] .parts li > *:not(.part__wave) { position: relative; z-index: 1; }

.part__wave {
  /* Sized entirely by CSS. A canvas is a replaced element: left to size
     itself it takes the pixel width of its bitmap, which is the CSS width
     times the device pixel ratio, and on a narrow screen that made every row
     wider than the page. The trace is kept off the number column by where it
     is drawn instead, which is the script's business, not the layout's. */
  position: absolute; inset: 0; z-index: 0;
  inline-size: 100%; block-size: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 560ms var(--ease);
  /* The trace runs out of the row rather than stopping against its edge. */
  mask-image: linear-gradient(90deg, transparent, #000 2%, #000 92%, transparent);
}
[data-sequenced="true"] .parts li[data-active="true"] .part__wave { opacity: 1; }

/* A letter riding the trace. The custom property is the only thing the frame
   loop writes to the DOM, and it is a transform, so it never reaches layout. */
.parts h3 [data-glyph] {
  display: inline-block;
  translate: 0 var(--glyph-y, 0px);
  white-space: pre;
}
/* The drawn letters are a second copy of the heading, held out of the
   accessibility tree. Selection has to skip them too, or copying a part's name
   yields it twice. */
.parts h3 [aria-hidden="true"] { user-select: none; }

@media (prefers-reduced-motion: reduce) {
  .parts h3 [data-glyph] { translate: none; }
  .part__wave { transition: none; }
}


/* Enquiry success confirmation: hidden until the endpoint confirms a saved row. */
.enquiry-success { display:none; width:100%; padding:24px; box-sizing:border-box; font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Helvetica Neue",Helvetica,Arial,sans-serif; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
.enquiry-success.is-visible { display:block; }
.success-card { position:relative; overflow:hidden; width:100%; max-width:570px; margin:0 auto; padding:44px 30px 30px; box-sizing:border-box; text-align:center; border-radius:32px; border:1px solid rgba(0,0,0,.07); background:radial-gradient(circle at 50% 6%,rgba(0,102,255,.09),transparent 38%),#fff; box-shadow:0 35px 90px rgba(0,0,0,.09),0 8px 24px rgba(0,0,0,.035); color:#050505; }
.success-art { position:relative; width:240px; height:220px; margin:0 auto 17px; }
.sketch-ring { position:absolute; width:240px; height:240px; top:-12px; left:0; overflow:visible; }
.ring-line { fill:none; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:700; stroke-dashoffset:700; }
.ring-line-1 { stroke:rgba(0,102,255,.22); stroke-width:2; animation:sketchDraw 1.45s cubic-bezier(.65,0,.35,1) forwards; }
.ring-line-2 { stroke:rgba(0,0,0,.09); stroke-width:1.4; animation:sketchDraw 1.2s ease .2s forwards; }
.envelope-wrap { position:absolute; top:52%; left:50%; width:150px; height:120px; transform:translate(-50%,-50%); animation:envelopeFloat 4s ease-in-out infinite; }
.envelope { width:140px; height:110px; overflow:visible; }
.draw { fill:rgba(255,255,255,.78); stroke:#111; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:400; stroke-dashoffset:400; }
.envelope-body { animation:sketchDraw 1s ease .15s forwards; }
.envelope-fold { animation:sketchDraw .8s ease .65s forwards; }
.envelope-bottom-left { animation:sketchDraw .6s ease .9s forwards; }
.envelope-bottom-right { animation:sketchDraw .6s ease 1s forwards; }
.check-badge { position:absolute; right:3px; bottom:0; width:46px; height:46px; opacity:0; animation:checkPop .55s cubic-bezier(.2,1.7,.3,1) 1.2s forwards; }
.check-badge svg { width:100%; height:100%; }
.check-badge circle { fill:#0066ff; stroke:#fff; stroke-width:2; }
.check-path { fill:none; stroke:#fff; stroke-width:3; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:30; stroke-dashoffset:30; animation:drawCheck .5s ease 1.55s forwards; }
.motion-lines { position:absolute; width:72px; height:50px; left:-13px; top:84px; overflow:visible; }
.motion-line { fill:none; stroke:rgba(0,102,255,.38); stroke-width:2; stroke-linecap:round; stroke-dasharray:100; stroke-dashoffset:100; }
.motion-1 { animation:sketchDraw .5s ease .4s forwards; }
.motion-2 { animation:sketchDraw .5s ease .55s forwards; }
.motion-3 { animation:sketchDraw .5s ease .7s forwards; }
.sketch-spark { position:absolute; fill:none; stroke:#0066ff; stroke-width:1.5; stroke-linejoin:round; opacity:0; }
.spark-one { width:25px; height:25px; top:28px; right:20px; animation:sparkIn .65s ease 1.1s forwards,sparkFloat 3s ease-in-out 1.75s infinite; }
.spark-two { width:18px; height:18px; bottom:28px; left:30px; animation:sparkIn .65s ease 1.3s forwards,sparkFloat2 3.4s ease-in-out 1.95s infinite; }
.sketch-accent { position:absolute; right:-8px; bottom:54px; width:50px; fill:none; stroke:rgba(0,0,0,.27); stroke-width:1.5; stroke-linecap:round; stroke-dasharray:100; stroke-dashoffset:100; animation:sketchDraw .8s ease 1s forwards; }
.success-card h2 { margin:0 0 9px; color:#050505; font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Helvetica Neue",Helvetica,Arial,sans-serif; font-size:34px; line-height:1.05; font-weight:600; letter-spacing:-.045em; }
.success-card p { margin:0; color:rgba(0,0,0,.52); font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Helvetica Neue",Helvetica,Arial,sans-serif; font-size:15px; line-height:1.6; font-weight:400; letter-spacing:-.01em; }
.success-card button { appearance:none; -webkit-appearance:none; width:100%; height:52px; margin-top:28px; padding:0 20px; border:0; border-radius:15px; background:#050505; color:#fff; font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Helvetica Neue",Helvetica,Arial,sans-serif; font-size:14px; line-height:1; font-weight:600; letter-spacing:-.01em; cursor:pointer; transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease; }
.success-card button:hover { transform:translateY(-2px); box-shadow:0 12px 28px rgba(0,0,0,.16); }
.success-card button:active { transform:translateY(0); opacity:.85; }
@keyframes sketchDraw { to { stroke-dashoffset:0; } }
@keyframes drawCheck { to { stroke-dashoffset:0; } }
@keyframes checkPop { 0% { opacity:0; transform:scale(.35) rotate(-12deg); } 70% { opacity:1; transform:scale(1.12) rotate(3deg); } 100% { opacity:1; transform:scale(1) rotate(0); } }
@keyframes envelopeFloat { 0%,100% { transform:translate(-50%,-50%); } 50% { transform:translate(-50%,calc(-50% - 5px)); } }
@keyframes sparkIn { from { opacity:0; transform:scale(.25) rotate(-25deg); } to { opacity:.9; transform:scale(1) rotate(0); } }
@keyframes sparkFloat { 0%,100% { transform:translateY(0) rotate(0); } 50% { transform:translateY(-6px) rotate(7deg); } }
@keyframes sparkFloat2 { 0%,100% { transform:translateY(0) rotate(0); } 50% { transform:translateY(5px) rotate(-8deg); } }
@media (max-width:600px) { .enquiry-success { padding:16px; } .success-card { padding:35px 20px 22px; border-radius:25px; } .success-art { transform:scale(.9); margin-top:-10px; margin-bottom:0; } .success-card h2 { font-size:30px; } }
@media (prefers-reduced-motion:reduce) { .enquiry-success *, .enquiry-success *::before, .enquiry-success *::after { animation-duration:.01ms !important; animation-iteration-count:1 !important; scroll-behavior:auto !important; } }

/* ---------- consent ----------
   A strip at the foot of the page rather than a modal over it: nothing here
   is urgent enough to take the page away from someone, and a banner that traps
   the reader is the pattern regulators single out. It does not block content,
   it does not return if refused, and the two answers carry the same weight --
   same size, same shape, side by side. Refusing is not a second-class button,
   which is the whole point: a choice that is harder to decline than to accept
   is not freely given, and a consent that is not freely given is not consent.

   Named .consent-bar, not .consent: the enquiry form's own permission checkbox
   already owns .consent, and a second rule under that name further down the
   file won the cascade and tore the checkbox out of the panel to the foot of
   the viewport. */
.consent-bar {
  position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 200;
  background: var(--surface);
  border-block-start: 1px solid var(--line);
  padding: var(--space-5) 0;
  translate: 0 110%;
  transition: translate 320ms var(--spring);
}
.consent-bar[data-shown] { translate: 0 0; }
.consent-bar__inner {
  inline-size: min(100% - 2 * var(--gutter), 72rem);
  margin-inline: auto;
  display: flex; flex-wrap: wrap; gap: var(--space-5);
  align-items: center; justify-content: space-between;
}
.consent-bar__copy { flex: 1 1 28rem; }
.consent-bar__title {
  font-family: var(--display); font-size: var(--step-0); font-weight: 700;
  letter-spacing: -0.01em; margin: 0 0 0.35em;
}
.consent-bar__copy p { margin: 0; color: var(--ink-2); font-size: var(--step--1); line-height: 1.6; max-inline-size: 60ch; }
.consent-bar__more { color: var(--accent-text); text-underline-offset: 0.25em; }
.consent-bar__actions { display: flex; gap: var(--space-4); flex: 0 0 auto; flex-wrap: wrap; }
.consent-bar__actions .btn { flex: 0 0 auto; }
/* The quiet variant is a different weight, not a smaller target: it reads as
   the secondary option without being harder to hit than the other one. */
.btn--quiet {
  background: transparent; color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
.btn--quiet:hover { background: var(--ghost); }
@media (max-width: 40rem) {
  .consent-bar__actions { inline-size: 100%; }
  .consent-bar__actions .btn { flex: 1 1 auto; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) { .consent-bar { transition: none; } }
