/* ═══════════════════════════════════════════════════════════
   SEMVAC | SANITATION — engineered clean
   Light, Scandinavian, precise. Everything themed from tokens.
   ═══════════════════════════════════════════════════════════ */

/* THE FONTS, SERVED FROM THE SITE ITSELF. They came from fonts.googleapis.com,
   which hands every visitor's IP address to Google on every page — for a site
   read in Germany a GDPR problem in its own right (LG München I, 3 O 17493/20).
   The same variable files Google serves (both families are SIL Open Font
   License), latin and latin-ext, each a weight RANGE, so one file covers
   400-900. The latin Schibsted Grotesk is preloaded in every page's head. */
@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url("../fonts/schibsted-grotesk-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url("../fonts/schibsted-grotesk-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("../fonts/jetbrains-mono-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("../fonts/jetbrains-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* ── surfaces ── */
  --paper:      #dcdfdd;   /* page base — a soft green-grey, not white */
  --paper-2:    #d0d4d1;   /* recessed band */
  --card:       #eef0ee;   /* raised surface, still lighter than the page */
  --ink-block:  #0e1512;   /* dark band, near-black w/ green undertone */

  /* ── ink ── */
  --ink:        #0e1512;
  --ink-2:      #47544d;   /* secondary copy */
  --ink-3:      #56625b;   /* meta — 4.75:1 on --paper: small text, so the 4.5:1 of WCAG AA (it was #6d7a73, 3.34:1) */
  --ink-rev:    #f4f7f4;   /* text on dark */
  --ink-rev-2:  #9fada5;

  /* ── brand ── */
  --green:      #009836;   /* brand green, as declared on semvac.com */
  --green-deep: #006e24;   /* 4.8:1 on --paper, so it is legal at small sizes */
  --green-lift: #1cbf4a;   /* for dark surfaces */
  --green-wash: #e3efe6;   /* tint fill */

  /* ── lines ── */
  --rule:       #d2dad4;
  --rule-soft:  #e2e8e3;
  --rule-rev:   #2a3630;

  /* ── type ── */
  --f-display: "Schibsted Grotesk", "Helvetica Neue", Arial, sans-serif;
  --f-body:    "Schibsted Grotesk", "Helvetica Neue", Arial, sans-serif;
  --f-mono:    "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* ── metrics ── */
  /* --pad and --head-h grow past 1440 and are unchanged at 1440 and below, at any
     height. --pad keeps today's 4.5vw below 1440 and never drops under today's
     value on any page: the inner clamp is 5rem up to 1440, so the old ceiling
     still holds between 1778 and ~2097, then the ramp takes it to 90.72 (x1.40)
     at 2560. --head-h is read by JS only through head.offsetHeight: a clamp()
     here makes parseInt(getPropertyValue) NaN, so nothing may parse it. The
     landing page's --maxw lives on body.home at the end of this file. */
  --pad:   clamp(1.25rem, 4.5vw, clamp(5rem, calc(64.8px + (100vw - 1440px) * 0.023143), 90.72px));
  --maxw:  1320px;
  --head-h: clamp(74px, calc(74px + (100vw - 1440px) * 0.026786), 104px);
  --intro-tile: clamp(96px, 11.5vw, 190px);

  /* ── photography ── */
  /* every photo on the page runs through this. set to `none` for full colour. */
  --photo-filter: grayscale(1) contrast(1.04);   /* the intro strip */
  --hero-filter:  grayscale(0) contrast(1.04);   /* the hero keeps its colour */
  /* What we do: the photographs of the work, in colour at Andreas's request —
     they were on --photo-filter with the intro's tiles and read as archive. The
     contrast is the same as the other two, so only the colour changes. */
  --proc-filter:  contrast(1.04);
  --hero-focus: 50% 50%;   /* shared by the hero and the intro tile it grows from; the film's frame is centred */

  /* ── motion ── */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* The header trigger's four, from the handoff, kept at its exact values.
     --ease-panel is NOT --ease: (.22,.61,.36,1) against (.22,1,.36,1). Two
     curves that differ in one number and read differently is exactly the pair
     to name apart rather than to reuse by eye. */
  --ease-fill:   cubic-bezier(0.34, 0, 0.14, 1);   /* colour + hover blocks */
  --ease-stroke: cubic-bezier(0.5, 0, 0.2, 1);     /* line draw */
  --ease-snake:  cubic-bezier(0.45, 0, 0.25, 1);   /* the travelling dash */
  --ease-panel:  cubic-bezier(0.22, 0.61, 0.36, 1);/* language dropdown */
}

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

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--f-body);
  font-size: 1rem;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
body.is-locked { overflow: hidden; height: 100vh; }

::selection { background: var(--green); color: #fff; }

img, svg, picture { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
sub { font-size: 0.62em; }

h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 800;
  line-height: 1.02;
  letter-spacing: -0.028em;
  text-wrap: balance;
}
/* German and Danish write one word where English writes three —
   Frischwassertanksystem, spildevandstanksystem — and at display sizes one such
   word can be wider than its column. MEASURED on the generated pages: 16–30px
   past the box at 1440, and the German tanks hero pushed off a 390px screen.
   The pages carry lang="de"/"da", so the browser can break those words at a
   real syllable, and only words of 12 letters or more, 5 on either side of the
   break. English headings are not touched. */
:lang(de) :is(h1, h2, h3, h4),
:lang(da) :is(h1, h2, h3, h4) {
  hyphens: auto;
  hyphenate-limit-chars: 12 5 5;
}

/* Reserve the scrollbar's column permanently. Opening the menu sets
   body{overflow:hidden}, and on any machine whose scrollbar TAKES layout width
   — Windows, or a Mac set to "always show scroll bars" — that hands 11px back to
   the viewport and every fixed, right-aligned thing in the header jumps sideways
   by 11px at the exact moment you click it. Reserving the gutter means the lock
   changes no width at all. Headless Chrome uses overlay scrollbars and cannot
   reproduce the jump, which is why this is reasoned from the mechanism rather
   than measured here. */
:root { scrollbar-gutter: stable; }
::-webkit-scrollbar { width: 11px; }
::-webkit-scrollbar-track { background: var(--paper-2); }
::-webkit-scrollbar-thumb { background: #a6b2aa; border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: var(--green); }

:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }
/* Scoped to the two header controls and nothing else. They are the only places
   where focus is moved PROGRAMMATICALLY on a pointer interaction — the trigger
   has to be focused explicitly on open because Safari does not focus a button
   on click — and Safari then reads that as keyboard focus and paints a ring
   nobody asked for. Chrome does not: measured, outline-style stays `none` after
   a click, which is why this could not be reproduced here and was reasoned from
   the mechanism and a screenshot.
   The ring is NOT removed. It is shown to the people it exists for: any keydown
   flips the flag back, so tabbing after using the mouse shows it on the very
   next stop. Everywhere else the engine's own judgement is left alone. */
:root[data-input="pointer"] .trigger:focus-visible,
:root[data-input="pointer"] .lang__button:focus-visible { outline: none; }

/* Visually gone, still in the accessibility tree. text-transform and
   letter-spacing are reset because Chrome and Safari fold CSS casing into the
   computed accessible name, and some readers spell all-caps out letter by
   letter — .lang__item, .lang__button and .menu__lang all uppercase and
   letterspace their text.
   position:absolute keeps it out of flow, so no measured box moves. */
.u-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  text-transform: none;
  letter-spacing: normal;
}

/* ─────────────── shared components ─────────────── */

.eyebrow {
  font-family: var(--f-mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--green-deep);
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
}
.eyebrow::before {
  content: "";
  width: 1.6em; height: 2px;
  background: var(--green);
  flex: none;
}
.eyebrow--rev { color: var(--green-lift); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  font-family: var(--f-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  padding: 1.02em 1.7em;
  border: 1px solid transparent;
  border-radius: 2px;
  transition: background 0.3s var(--ease), color 0.3s var(--ease),
              border-color 0.3s var(--ease), transform 0.3s var(--ease);
}
.btn__arrow { width: 1em; height: 1em; transition: transform 0.3s var(--ease); }
.btn:hover .btn__arrow { transform: translateX(4px); }

/* white on pure brand green is only 3.8:1, so solid buttons use the deeper tint */
.btn--solid { background: var(--green-deep); color: #fff; }
.btn--solid:hover { background: #006620; transform: translateY(-2px); }

.btn--line { border-color: var(--rule); color: var(--ink); }
.btn--line:hover { border-color: var(--green); color: var(--green-deep); background: var(--card); }

.btn--rev { border-color: var(--rule-rev); color: var(--ink-rev); }
.btn--rev:hover { border-color: var(--green-lift); color: var(--green-lift); }

.wrap { max-width: var(--maxw); margin-inline: auto; }

/* scroll reveal */
.reveal { opacity: 0; transform: translateY(26px); }
.reveal.is-shown { opacity: 1; transform: none; }

/* ─────────────── intro overlay ─────────────── */

/* 600, over the header's 500. The header was lifted above .menu so its square
   can be the menu's close control; the intro has to stay above the header, or
   it would hand over from in FRONT of the cabin instead of from behind it. */
.intro {
  position: fixed; inset: 0;
  z-index: 600;
  background: var(--paper);
  display: grid;
  place-items: center;
  overflow: hidden;
}
/* fixed to the viewport, so the edge fade sits at the screen edges rather than
   at the ends of the strip that moves inside it */
.intro__word {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 0;
  margin: 0;
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: -0.04em;
  font-size: clamp(6rem, 22vw, 20rem);
  line-height: 1;
  /* .rig__word's chassis, verbatim: the wash is a background layer so the
     pointer's spot can be a second layer clipped to the same letterforms.
     Parked far off until a pointer has been in the intro. */
  --wash: rgba(14, 21, 18, 0.06);
  color: transparent;
  -webkit-text-fill-color: transparent;
  background-image:
    radial-gradient(circle 200px at var(--wx, -9999px) var(--wy, -9999px),
      color-mix(in oklab, var(--ink), transparent 82%) 0%,
      rgba(0, 0, 0, 0) 72%),
    linear-gradient(var(--wash), var(--wash));
  -webkit-background-clip: text;
  background-clip: text;
  white-space: nowrap;
  /* a background, not text: pointer-events alone only stops a selection STARTING
     on the word — a drag from anywhere else, or select-all, still lit it up */
  -webkit-user-select: none; user-select: none;
  pointer-events: none;
  opacity: 0;   /* main.js brings it in with the rail */
}
.intro__window {
  position: relative; z-index: 1;   /* over the word */
  width: 100%;
  overflow: hidden;
  opacity: 0;
  /* the speed blur lives here: its filter region is one screen wide instead of
     the whole multi-screen strip, which is far cheaper to blur. The grade
     cannot live here too — it would drain the green out of the locomotive. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 17%, #000 83%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0%, #000 17%, #000 83%, transparent 100%);
}
.intro__window.is-rushing { filter: url(#speedBlur); }
.intro__rail {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1.1vw, 18px);
  width: max-content;
  will-change: transform;
}
.intro__tile {
  width: var(--intro-tile);
  aspect-ratio: 1;
  flex: none;
  overflow: hidden;
  border-radius: 2px;
  opacity: 0;
  background: var(--paper-2);
}
/* picture needs an explicit box or the img falls back to auto height and
   object-fit/object-position never take effect */
.intro__tile picture { display: block; width: 100%; height: 100%; }
.intro__tile img { width: 100%; height: 100%; object-fit: cover; filter: var(--photo-filter); }
.intro__tile--key { position: relative; z-index: 2; }
/* the key tile grows into the hero, so it must frame the photo identically */
.intro__tile--key img { object-position: var(--hero-focus); }

/* the track: a green rail with sleepers running past underneath the strip */
.intro__track {
  position: absolute; z-index: 1;   /* over the word */
  left: 0; right: 0;
  top: calc(50% + var(--intro-tile) / 2 + 10px);
  height: 26px;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 17%, #000 83%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0%, #000 17%, #000 83%, transparent 100%);
}
/* Two rails and a sleeper course, drawn as if seen from just above and to the
   side: the far rail sits high and thin, the near rail low and solid, and the
   sleepers run under both and stick out past them. */
.intro__line {
  position: absolute;
  left: 0; right: 0;
  transform: scaleX(0);
  transform-origin: left center;
}
.intro__line--far {
  top: 4px;
  height: 1.5px;
  background: #8c9a92;
}
.intro__line--near {
  top: 15px;
  height: 3px;
  background: var(--green);
  border-radius: 1px;
}
/* the logo's train, running in front of the strip. Its bottom edge sits on the
   rail, and it keeps a constant horizontal blur because it is fast throughout. */
.intro__loco {
  height: var(--intro-tile);
  width: auto;
  flex: 0 0 auto;
  align-self: center;
  pointer-events: none;
}

/* the pattern is scrolled with background-position rather than by moving the
   element — a repeating gradient is endless, an element is not */
/* leaned a few degrees off vertical, which is what sells the viewing angle */
.intro__sleepers {
  position: absolute;
  inset: 0 0 auto 0;
  height: 23px;
  opacity: 0;
  background-image: repeating-linear-gradient(97deg,
    #9daaa1 0 5px, transparent 5px 34px);
}

/* ── THE GATE'S METER ──
   The third instance of an object this site already has: .rig__bar and
   .creed__bar are both min(120px, 22vw) of 2px rule, centred, and the creed's
   note says it is matched to the rig's "so the meter hands over at the same
   screen pixel". Meeting it here should be recognition, not introduction.
   Under the rails and not at the screen's foot, because Andreas asked for it
   under the train: tile, 10px, 23px of sleepers, 10px, meter — the 10px is the
   track's own gap from the carriages, used twice, not a new number.
   --green-deep, the light-ground meter colour (.steps__meter), and not --green:
   the near rail twenty pixels above is already --green at 3px, and two greens
   that close compete. */
.intro__meter {
  position: absolute; z-index: 1;
  top: calc(50% + var(--intro-tile) / 2 + 10px + 23px + 10px);
  /* centred without a transform — the transform is main.js's, which lays the
     meter in with scaleX alongside the rails */
  left: 0; right: 0; margin-inline: auto;
  width: min(120px, 22vw); height: 2px;
  background: var(--rule);
  overflow: hidden;
  transform: scaleX(0);
  pointer-events: none;
}
/* scaleX and not width: this fill moves while the main thread is decoding the
   model, and a transform stays on the compositor where a width is a layout */
.intro__meter span {
  display: block; width: 100%; height: 100%;
  background: var(--green-deep);
  transform: scaleX(var(--p, 0));
  transform-origin: left center;
}
/* THE PULSE, only on the part not yet filled, and only while the segment in
   flight has nothing to report. railDrop turned on its side — the same 2s, the
   same curve and the same rest as the hero's scroll cue — because it says the
   same thing: working, no number. The fill never creeps to cover for it. */
.intro__meter i {
  position: absolute; top: 0; bottom: 0; right: 0;
  left: calc(var(--p, 0) * 100%);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}
.intro__meter.is-blind i { opacity: 1; }
.intro__meter.is-blind i::after {
  content: "";
  position: absolute; top: 0; bottom: 0; left: 0;
  width: 34%;
  background: color-mix(in oklab, var(--green-deep), transparent 55%);
  animation: introPulse 2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
@keyframes introPulse {
  0%        { transform: translateX(-110%); }
  55%, 100% { transform: translateX(300%); }
}

/* ─────────────── header ─────────────── */

/* No bar any more — just the mark and the two controls sitting over the page.

   z-index 500, ABOVE .menu's 400. There is one menu button on this page and it
   is here: the square draws itself into an X when open, so the overlay is drawn
   underneath it rather than carrying a close control of its own. #menuClose is
   gone, and focusables() in main.js spans .head and #menu for the same reason. */
.head {
  position: fixed; top: 0; left: 0; right: 0;
  z-index: 500;
  padding-inline: var(--pad);
  opacity: 0;
  transform: translateY(-14px);
  pointer-events: none;   /* re-enabled once the intro hands over */
}
.head__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  height: var(--head-h);
  /* NOT capped at --maxw. The cap centres the row, and MEASURED the trigger then
     sits 65px from the right edge at 1440 but 300px at 1920 and 620px at 2560 —
     it drifts inward as the screen grows and stops reading as a header control.
     The header is furniture pinned to the viewport, not part of the text column,
     so it takes the page's own margin at every width instead. */
}

.brand { display: flex; align-items: center; gap: clamp(0.72rem, calc(0.72rem + (100vw - 1440px) * 0.004), 16px); color: var(--ink); }
/* The brand green is the LOGO's own #0b933e, not --green (#009836). They are close
   but not the same, and the mark is a supplied asset — matching it to the site's
   accent would be silently redrawing the client's logo.
   The fill is pinned INSIDE the file rather than driven from here: an <img> renders
   its SVG in a separate document, so currentColor there resolves against the SVG's
   own initial colour — black — not the header's. Measured: the mark came out black.
   Recolouring from CSS would mean inlining the markup, and the mark is green on
   both grounds anyway, so there is nothing to recolour. */
.brand__mark { height: clamp(26px, calc(26px + (100vw - 1440px) * 0.0092857), 36.4px); width: auto; }
/* Set to sit where the old wordmark sat: 17px of cap height. Schibsted Grotesk's
   caps are 0.72em, so 23.5px of font-size lands on 16.9px of cap. Tracking is
   slightly negative because the wordmark was drawn tight and the default at this
   size reads loose beside the mark. */
.brand__name {
  font-family: var(--f-display);
  font-size: clamp(23.5px, calc(23.5px + (100vw - 1440px) * 0.0083929), 32.9px);
  font-weight: 700;
  letter-spacing: -0.005em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
}

/* over a dark section: the name is text and simply inherits, so there is no
   second asset to keep in step. The controls below take their colours from the
   same class through the token block. */
.head.on-dark .brand, .head.on-hero .brand { color: var(--ink-rev); }
/* The handoff's ROLES, on this site's tokens. Its palette is a different one
   (#F2EFE9 paper, #ECEEEC surface, #0AA64F green) and the values were not
   adopted — what carries over is which colour does which job:

     handoff --ink            the square on light, the lines on dark   -> --ink
     handoff --paper          the square on dark,  the lines on light  -> --ink-rev
     handoff --surface-light  page surface + dropdown panel            -> --paper
     handoff dark surface     page surface + dropdown panel, dark      -> --ink-block
     handoff --accent         hover fill and border, focus ring        -> --green

   All of it hangs off .head.on-dark, the class the two ScrollTriggers already
   derive. The prototype set these from JS instead and shipped with
   data-surface="light" already in the markup, so its own `if (already this
   surface) return` guard fired on the first measure and the stroke variable was
   never written: the two lines were literally `stroke: none` until you hovered.
   There is no JS state here to forget to write. */
.head {
  --sq:         var(--ink);        /* the block */
  --sq-line:    var(--ink-rev);    /* the lines on it — always the inverse */
  --hdr-ink:    var(--ink);
  --hdr-panel:  var(--paper);      /* the PAGE colour, not the square's */
  /* 55% ink, not the handoff's 34%. The two sides cannot share an alpha: the
     panel sits on the page's OWN colour on both surfaces, so this border is the
     only thing that says "popup" rather than "page" — it is load-bearing and
     has to clear the 3:1 floor for a boundary. Composited over --paper and
     measured: 34% is rgb(150,154,151) = 2.12:1, 45% (the dark side's value) is
     2.83:1, 55% is 3.71:1. Solid --ink-2 would give 5.92:1 and was rejected —
     at 2px it reads as heavy as the square's own border and stops the panel
     sitting under it. Dark needs no lift: --ink-rev at 45% on --ink-block
     measures 4.30:1. */
  --hdr-rule:   color-mix(in oklab, var(--ink), transparent 45%);
  --hdr-muted:  var(--ink-2);
}
.head.on-dark {
  --sq:         var(--ink-rev);
  --sq-line:    var(--ink);
  --hdr-ink:    var(--ink-rev);
  --hdr-panel:  var(--ink-block);
  --hdr-rule:   color-mix(in oklab, var(--ink-rev), transparent 55%);
  --hdr-muted:  var(--ink-rev-2);
}
/* OVER THE HERO'S FILM the square is the brand green with light lines on it —
   the look it has only under the pointer everywhere else — and hovering it there
   turns it WHITE: the fill that rises is --ink-rev and the lines go to ink.
   THE NAME AND THE LANGUAGE CONTROL FOLLOW, and white is not a guess: Semvac's
   own knockout logo (img/logo-full-knockout.png) is the green mark with a light
   wordmark — sampled, the name is (224,224,224) where the standard logo's is
   black — so over a photograph the brand's answer is already a white name. The
   language control takes the whole dark token set, not just its text colour, so
   a dropdown opened over the film is the dark panel with light options rather
   than light options on a paper panel. js/main.js sets .on-hero from the same
   function as .on-dark and never both, so the two cannot disagree about the header. */
.head.on-hero {
  --sq:         var(--green);
  --sq-line:    var(--ink-rev);
  --hdr-ink:    var(--ink-rev);
  --hdr-panel:  var(--ink-block);
  --hdr-rule:   color-mix(in oklab, var(--ink-rev), transparent 55%);
  --hdr-muted:  var(--ink-rev-2);
}
.head.on-hero .trigger__fill { background: var(--ink-rev); }
/* ON A PHONE, A BAR after all. The mark and the square float over the page on
   a wide screen, where the column of text passes under the header's empty
   middle; on a phone the text runs the full width and passes under the name
   and the square themselves, and the two read as one. So below 761px (and on a
   phone turned on its side) the header gets a ground, and it follows the same
   classes as everything else in it: the page colour over light sections, the
   dark band over dark ones and over the open menu, whose ground it then is.
   Over the film it is clear only at the very top — the film's first frame,
   whole, under a white name — and turns to the dark band as soon as the page
   moves, because the hero's own title passes under it on the way up. A hairline
   under it, a shade off its own colour, so it reads as a bar on a page the same
   colour as itself. */
.head {
  --hdr-bar:      var(--paper);
  --hdr-bar-line: color-mix(in oklab, var(--ink), transparent 88%);
}
.head.on-dark {
  --hdr-bar:      var(--ink-block);
  --hdr-bar-line: color-mix(in oklab, var(--ink-rev), transparent 90%);
}
.head.on-hero         { --hdr-bar: var(--ink-block); --hdr-bar-line: transparent; }
.head.on-hero.at-top  { --hdr-bar: transparent; }
@media (max-width: 760px), (max-height: 500px) and (orientation: landscape) and (pointer: coarse) {
  .head { background-color: var(--hdr-bar); box-shadow: 0 1px 0 var(--hdr-bar-line); }
  .head.bar-live { transition: background-color 0.3s ease, box-shadow 0.3s ease; }
  .head.is-settling { transition: none; }
}
/* The first state of the page is set, not animated: a square that wiped green
   while the header was fading in on load would be a colour change nobody asked
   for. js/main.js holds this for two frames around the first sync. */
.head.is-settling .trigger,
.head.is-settling .trigger__layer,
.head.is-settling .trigger__icon,
.head.is-settling .brand__ink,
.head.is-settling .lang__button,
.head.is-settling .lang__caret,
.head.is-settling .lang__rule,
.head.is-settling .lang__panel { transition: none !important; }

/* THE NAME CHANGES COLOUR THE WAY THE SQUARE DOES — a fill, not a fade. It used
   to cross-fade its colour over .3s while the menu square beside it slid a new
   block up through itself over .55s, so the two halves of the header changed in
   two different ways at the same moment; Andreas asked for the square's
   principle on the logo too. The mark is the client's green on both grounds and
   does not change; the name does.
   The square's mechanism, on text: two FIXED copies of the name in one grid cell,
   ink and reversed, whose ROLES swap. The incoming copy is revealed from below —
   clip-path from a top inset past the box to one above it — over the square's own
   .55s on --ease-fill, so name and square fill as one gesture. The outgoing copy
   is given a 0s transition delayed to .6s, so it only disappears once it is fully
   covered: no gap, no flash, and the resting colour is right even if nothing ever
   animates. The insets overhang the box so the clip never shaves a glyph. Every
   copy but the first is aria-hidden: the link's accessible name stays "Semvac",
   once. */
.brand__name { display: grid; }
.brand__ink { grid-area: 1 / 1; color: var(--ink); }
.brand__ink--rev { color: var(--ink-rev); }
.head:not(.on-dark):not(.on-hero) .brand__ink--ink,
.head.on-dark                     .brand__ink--rev,
.head.on-hero                     .brand__ink--rev {
  clip-path: inset(-20% -4% -20% -4%);
  z-index: 2;
  transition: clip-path 0.55s var(--ease-fill);
}
.head:not(.on-dark):not(.on-hero) .brand__ink--rev,
.head.on-dark                     .brand__ink--ink,
.head.on-hero                     .brand__ink--ink {
  clip-path: inset(120% -4% -20% -4%);
  z-index: 1;
  transition: clip-path 0s linear 0.6s;
}

/* AND UNDER THE POINTER IT FILLS GREEN, as the square beside it does: a third
   copy of the name, green, rises through it from below over the square's own
   .55s on --ease-fill, and sinks back the same way when the pointer leaves —
   the square's .trigger__fill, on text. It sits above both role copies, so a
   scroll that swaps the header's colours under a hovered name changes nothing
   you can see until the pointer goes.
   The square's green on paper. On dark, --green-lift, the green the tokens keep
   for dark surfaces: --green went dull over the dark PHOTOGRAPHS the header also
   counts as dark (the cabins hero, seen in a screenshot), where the square, a
   solid block, still reads and a line of text does not. OVER THE HERO'S FILM the
   hero title's own lighter green (.hero__title em): --green there MEASURED
   1.4–2.0:1 against the veiled film, next to invisible, and #45de78 3.1–4.4:1.
   (The name is the wordmark, which contrast rules exempt; these are for being
   seen, not for passing.) */
.brand__ink--hot {
  color: var(--green);
  z-index: 3;
  clip-path: inset(120% -4% -20% -4%);
  transition: clip-path 0.55s var(--ease-fill);
}
.head.on-dark .brand__ink--hot { color: var(--green-lift); }
.head.on-hero .brand__ink--hot { color: #45de78; }
@media (hover: hover) {
  .brand:hover .brand__ink--hot { clip-path: inset(-20% -4% -20% -4%); }
}

.head__ctl { display: flex; align-items: center; gap: clamp(18px, calc(18px + (100vw - 1440px) * 0.0064286), 25.2px); }

/* ── the square ──
   48 x 48 is a floor, not a size that shrinks on small screens. It is under the
   74px band by 26px, so head.offsetHeight is still 74 at 1440 and below and the
   .cost trigger that reads it (`start: top ${head.offsetHeight}px`) does not
   move. Past 1440 the square and the band grow together (67.2 in 104 at 2560),
   and every trigger that reads the band reads it live. The 2px border does not
   scale; the radius is shape, not stroke, so it does. */
.trigger {
  position: relative;
  isolation: isolate;
  flex: none;
  width: clamp(48px, calc(48px + (100vw - 1440px) * 0.017143), 67.2px);
  height: clamp(48px, calc(48px + (100vw - 1440px) * 0.017143), 67.2px);
  border: 2px solid var(--sq);
  border-radius: clamp(4px, calc(4px + (100vw - 1440px) * 0.0014286), 5.6px);
  overflow: hidden;
  /* never seen — the layers below are opaque and cover the box — but it means a
     square with no children is still the right colour rather than a hole */
  background: var(--sq);
  transition: border-color 0.3s ease 0.22s;
}

/* Two FIXED colours whose ROLES swap. Recolouring one layer and re-running the
   animation flips the box first and moves afterwards, which reads as a fault.
   The incoming block slides up over .55s; the outgoing one is given a 0s
   transition DELAYED past that, so it only snaps back down once it is
   completely covered — no gap, no flash, and the resting colour is correct even
   if the animation never runs at all. */
/* THREE since the landing page's hero: the square is GREEN while the header sits
   over the hero's film, and hands back to the dark and light roles as it leaves.
   Every layer rests below and out of sight, with the delayed snap, and exactly
   one role at a time lifts it into place — so a third colour is one more
   selector on each rule, not a second mechanism. */
.trigger__layer {
  position: absolute; inset: auto 0 0 0; height: 100%;
  transform: translateY(101%);
  z-index: 1;
  transition: transform 0s linear 0.6s;
}
.trigger__layer--dark  { background: var(--ink); }
.trigger__layer--light { background: var(--ink-rev); }
.trigger__layer--green { background: var(--green); }
.head:not(.on-dark):not(.on-hero) .trigger__layer--dark,
.head.on-dark:not(.on-hero)       .trigger__layer--light,
.head.on-hero                     .trigger__layer--green {
  transform: translateY(0);
  z-index: 2;
  transition: transform 0.55s var(--ease-fill);
}

/* The same motion again, in green. Hover only: never on open, never on scroll. */
.trigger__fill {
  position: absolute; inset: auto 0 0 0; height: 100%;
  z-index: 3;
  background: var(--green);
  transform: translateY(101%);
  transition: transform 0.55s var(--ease-fill);
}

/* inset:0 inside a 2px border resolves to the padding box — 44px, which is what
   the viewBox is drawn in, so the icon is 1:1 with its own units. The stroke
   cross-fades .3s after a .2s delay, so the colour changes mid-travel, on the
   moving front, rather than at either end. */
.trigger__icon {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  z-index: 4;
  fill: none;
  stroke: var(--sq-line);
  /* 2px rendered at every width: the icon box grows x1.436 past 1440, so the
     stroke in user units falls as it grows (2 x 44 / 63.2 at 2560). Not
     vector-effect:non-scaling-stroke, which would break the dash geometry. */
  stroke-width: clamp(1.3924px, calc(2px - (100vw - 1440px) * 0.0005425), 2px);
  transition: stroke 0.3s ease 0.2s;
}

/* Guarded, unlike the rest of the header's hovers, because this one is 780ms of
   animation and a stuck :hover after a tap on iOS would leave the snake frozen
   mid-route under a green fill. */
@media (hover: hover) {
  .trigger:hover { border-color: var(--green); }
  .trigger:hover .trigger__fill { transform: translateY(0); }
  .trigger:hover .trigger__icon { stroke: var(--ink-rev); }
  /* over the hero the hover is the other way round: white, with ink lines.
     THE LINES GO TO INK AT ONCE, not on the square's delayed cross-fade. A click
     that closes the menu leaves the pointer on the square, so the hover's white
     fill is already up the moment the hero state returns — and with the usual
     .2s delay the lines stayed white on white: MEASURED, the X was invisible or
     faint for about 300ms after every click-close over the hero. Ink reads on
     both grounds the lines can be over there — 4.89:1 on the green, 17.15:1 on
     the white — so there is no moment to wait for. */
  .head.on-hero .trigger:hover { border-color: var(--ink-rev); }
  .head.on-hero .trigger:hover .trigger__icon { stroke: var(--ink); transition-delay: 0s; transition-duration: 0.12s; }
  /* Four states, and the selectors make them mutually exclusive rather than
     leaving it to source order: hovering, un-hovering, opening, closing. Each
     hover rule therefore has to stand aside for BOTH travels — is-closing is
     transient, set for the length of the close and then dropped.
     is-hot is what keeps any of this off the first paint: a fill-mode:both
     animation matching on load would paint its 0% key over the resting dash. */
  .trigger.is-hot:not([aria-expanded="true"]):not(.is-closing):hover .route--long  { animation: snakeLong 0.78s var(--ease-snake) both; }
  .trigger.is-hot:not([aria-expanded="true"]):not(.is-closing):hover .route--short { animation: snakeShort 0.78s var(--ease-snake) 0.05s both; }
  /* Leaving hover plays the travel in REVERSE, which the handoff asks for and the
     reference does not do. It needs its own keyframes, not a transition: the
     forward animation carries fill-mode:both and so holds the offset, meaning the
     base value never left 0 and a transition has nothing to fire on — the same
     mechanism that was making the cross snap. Mirrored in POSITION as well as
     value: the forward 45% key lands at 55% here, so the dash stretches at the
     same point of the ROUTE rather than of the clock. The easing is NOT mirrored
     — reversing cubic-bezier(.45,0,.25,1) gives a fifth curve, and the handoff
     declares four. The 0.05s stays on the SHORT line in both directions: the
     stagger is which line leads, not an artefact of the clock. */
  .trigger.is-hot:not([aria-expanded="true"]):not(.is-closing):not(:hover) .route--long  { animation: snakeLongBack 0.78s var(--ease-snake) both; }
  .trigger.is-hot:not([aria-expanded="true"]):not(.is-closing):not(:hover) .route--short { animation: snakeShortBack 0.78s var(--ease-snake) 0.05s both; }
}

/* Each line is a moving dash on its own route: out along one edge, round the
   corner, back in on the other side. The head extends while the tail is eaten,
   so the two lines travel and swap places instead of teleporting.
   The GAPS carry a .9858 the handoff does not have, and every offset with them.
   The handoff calls the routes 60 and 44 units; they are 60.9858 and 44.9858.
   Each corner is a symmetric quadratic with 4-unit legs, and such a curve is
   6.4929 long, not the 6 a quarter-circle of radius 4 would be — analytically
   16√2∫₀^½√(u²+¼)du = 6.4929010, and getTotalLength() agrees to 2e-5. So
   `22 38` is a 60-unit pattern on a 60.9858-unit path: it wraps, and a 0.99-unit
   stub of the tail stays lit past the end. At rest each route's stub hides under
   the OTHER line — which is why this survived review — but the lines land a unit
   short of their route ends, and after the hover swap that is visible as the two
   lines no longer sitting flush at x=11.

   pathLength="60" was the first fix and it does NOT work here. Measured in
   Chrome 131, rasterised at 32x: it shrinks the stub from 0.97 to 0.34 units
   but never closes it, and it stretches the lines 1.6%. The reason is that
   Chrome scales the dash pattern by a COARSELY flattened length (60.642, solved
   from a sweep of pathLength 60→61.2) while the dasher walks the finely
   flattened one (60.9858). Its own two length computations disagree by a third
   of a unit. Confirmed against a control: on `M2 6 H42 V27`, exactly 61 units
   and no curves, pathLength="60" closes perfectly — the defect needs the
   quadratics. So the fix cannot be a declared length; it has to be the real one.

   Widening the gap is engine-independent in the way pathLength is not: it asks
   only that the dasher walk the true arc length, which every engine does to
   within a rounding error, instead of asking one engine to agree with itself.
   Cost: ten numbers that are now geometry-derived, so re-deriving the corner
   radius means re-deriving these. The excess is 2×(6.4929−6) = 0.9858 for both
   routes, since both have exactly two corners.
   Measured after the change, rasterised at 32x: rest long x 11→33, rest short
   x 11→25, hover-end long x 11→33, hover-end short x 11→25, and no stub on
   any row in any state. Every one of those was off by 0.2–0.8px before. */
/* Each route now RUNS ON past the bars and onto one arm of the cross, so the
   same travelling dash that swaps the lines on hover carries them into the X.
   The handoff ate the routes from behind and drew a separate <path class="x">
   over the top — two motions, and the lines visibly vanish between them.
   MEASURED with getTotalLength(): long 91.8268, short 83.4417, and the final
   straight of each is the arm, 25.4558 (hypot 18,18). Parking the window at the
   very end therefore lands the long line on (13,31)->(31,13) and the short on
   (13,13)->(31,31) — the exact two arms the old .x path drew.
   The arms are equal but the bars are 22 and 14, so the two lines EQUALISE as
   they become the cross. That is unavoidable if it is the same two strokes, and
   it is the reason the short line's dash grows further than the long one's. */
.route { transition: stroke-dasharray 0.5s ease, stroke-dashoffset 0.6s var(--ease-stroke); }
.route--long  { stroke-dasharray: 22 69.8268; stroke-dashoffset: 0; }
.route--short { stroke-dasharray: 14 69.4417; stroke-dashoffset: 0; }
/* The sums are the invariant, not the individual gaps: dasharray is transitioned
   and animated, so if any keyframe sums to less than the path length the stub
   reappears for that part of the travel. Every pair here sums to the route. */
@keyframes snakeLong {
  0%   { stroke-dasharray: 22 69.8268; stroke-dashoffset: 0; }
  45%  { stroke-dasharray: 31 60.8268; stroke-dashoffset: -17; }
  100% { stroke-dasharray: 22 69.8268; stroke-dashoffset: -38.9858; }
}
@keyframes snakeShort {
  0%   { stroke-dasharray: 14 69.4417; stroke-dashoffset: 0; }
  45%  { stroke-dasharray: 21 62.4417; stroke-dashoffset: -13; }
  100% { stroke-dasharray: 14 69.4417; stroke-dashoffset: -30.9858; }
}
@keyframes snakeLongBack {
  0%   { stroke-dasharray: 22 69.8268; stroke-dashoffset: -38.9858; }
  55%  { stroke-dasharray: 31 60.8268; stroke-dashoffset: -17; }
  100% { stroke-dasharray: 22 69.8268; stroke-dashoffset: 0; }
}
@keyframes snakeShortBack {
  0%   { stroke-dasharray: 14 69.4417; stroke-dashoffset: -30.9858; }
  55%  { stroke-dasharray: 21 62.4417; stroke-dashoffset: -13; }
  100% { stroke-dasharray: 14 69.4417; stroke-dashoffset: 0; }
}

/* Open: the window simply keeps travelling until it is parked on the arm. The
   snake easing and duration, not the stroke ones, because this IS the snake —
   it is the same gesture continued, not a different animation that follows it.
   The long line travels 66.371 units from rest and the short 57.986, so the
   0.05s stagger stays on the SHORT line exactly as on hover: it is which line
   leads, not a correction for distance. */
/* An ANIMATION, not a transition — and this is the whole reason the cross used to
   snap into place instead of travelling.

   MEASURED: with the pointer resting on the square, stroke-dashoffset was already
   at its open value -66.4 on the first sample 40ms after the click. The hover
   snake runs with fill-mode:both, so it HOLDS the offset at -38.9858. Opening
   removes that animation and changes the base value in the same frame, and a
   transition has nothing to fire on: it never observes the animation's held
   value, only the base, which went straight to the target. Clicking from script
   without hovering hid this completely, because then no animation was holding
   anything and the transition ran normally. An animation has no such problem —
   it declares both ends itself.

   The start differs by whether hover left the lines swapped, so it is passed in
   as --from rather than written out as four more keyframe blocks: keyframes
   resolve custom properties at computed-value time, so each rule below simply
   states where its own journey begins. */
.trigger[aria-expanded="true"] .route--long,
.trigger[aria-expanded="true"] .route--short {
  animation: toCross 0.78s var(--ease-snake) forwards;
}
.trigger[aria-expanded="true"] .route--short { animation-delay: 0.05s; }
.trigger[aria-expanded="true"] .route--long  { --to: -66.371;  --to-dash: 25.4558 66.371; }
.trigger[aria-expanded="true"] .route--short { --to: -57.9858; --to-dash: 25.4558 57.9858; }
/* where the cross is travelled FROM: the resting bars, or the swapped ones if the
   hover snake has already run */
.trigger[aria-expanded="true"] .route--long  { --from: 0;         --from-dash: 22 69.8268; }
.trigger[aria-expanded="true"] .route--short { --from: 0;         --from-dash: 14 69.4417; }
.trigger.is-hot:hover[aria-expanded="true"] .route--long  { --from: -38.9858; --from-dash: 22 69.8268; }
.trigger.is-hot:hover[aria-expanded="true"] .route--short { --from: -30.9858; --from-dash: 14 69.4417; }

/* Closing is the same road backwards, and needs its own name so re-entering a
   state always changes animation-name and restarts by construction. */
.trigger.is-closing:not([aria-expanded="true"]) .route--long,
.trigger.is-closing:not([aria-expanded="true"]) .route--short {
  animation: fromCross 0.78s var(--ease-snake) forwards;
}
.trigger.is-closing:not([aria-expanded="true"]) .route--short { animation-delay: 0.05s; }
.trigger.is-closing:not([aria-expanded="true"]) .route--long  { --from: -66.371;  --from-dash: 25.4558 66.371;  --to: 0; --to-dash: 22 69.8268; }
.trigger.is-closing:not([aria-expanded="true"]) .route--short { --from: -57.9858; --from-dash: 25.4558 57.9858; --to: 0; --to-dash: 14 69.4417; }
/* closing while the pointer is still on the square lands on the swapped bars,
   because hover is still true and that is where hover holds them */
.trigger.is-closing.is-hot:hover:not([aria-expanded="true"]) .route--long  { --to: -38.9858; }
.trigger.is-closing.is-hot:hover:not([aria-expanded="true"]) .route--short { --to: -30.9858; }

@keyframes toCross {
  from { stroke-dasharray: var(--from-dash); stroke-dashoffset: var(--from); }
  to   { stroke-dasharray: var(--to-dash);   stroke-dashoffset: var(--to); }
}
@keyframes fromCross {
  from { stroke-dasharray: var(--from-dash); stroke-dashoffset: var(--from); }
  to   { stroke-dasharray: var(--to-dash);   stroke-dashoffset: var(--to); }
}

/* ── the language dropdown ──
   Replaces the flag badge, which was an indicator and not a control. Codes, not
   endonyms: the same reasoning as .menu__lang — the geometry is authored for two
   characters and "Francais" set in mono at 12px reads as a part number.
   line-height 1.2 rather than the page's 1.62: the handoff's `bottom: -6px` rule
   and `top: 28px` panel are both measured off this box, and the inherited 1.62
   would add 5px of half-leading to it and drop both by 2.5px.
   z-index 1 so the open panel paints over the square's bottom-left corner, which
   it overlaps by 8 x 3px and would otherwise be nicked by. */
.lang {
  position: relative;
  z-index: 1;
  font-family: var(--f-mono);
  font-size: clamp(12px, calc(12px + (100vw - 1440px) * 0.0026786), 15px);
  line-height: 1.2;
  letter-spacing: 0.14em;
}
.lang__button {
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(8px, calc(8px + (100vw - 1440px) * 0.0028571), 11.2px);
  /* MEASURED: without these the code rendered ITALIC at normal tracking. The
     codes are <i> elements, so the UA slants them — and since only the upright
     JetBrains faces are loaded the browser synthesises a fake oblique. A button
     also does not inherit the font on its own, which is where the 0.14em went. */
  font: inherit;
  font-style: normal;
  letter-spacing: inherit;
  color: var(--hdr-ink);
  text-transform: uppercase;
  transition: color 0.45s ease;
}
/* The visible button is the text plus the caret and nothing else — measured at
   30.4 x 14.4 — so on its own it is a 14px-tall tap target sitting 18px from a
   square the handoff insists never shrinks below 48. That is under the 24x24
   floor of WCAG 2.2 SC 2.5.8, and it is the one control here you have to hit
   before you can hit anything else.
   Grown with an overlay rather than padding: `bottom: -6px` on the rule and
   `top: 28px` on the panel are both measured off this box, so padding would
   push the underline down and the panel with it. This changes no authored
   dimension — 44.4 x 46.4 of hit area, entirely invisible.
   -8px, not more: the gap to the square is 18px, so the two hit areas still
   clear each other by 10px and neither steals the other's edge. */
.lang__button::after { content: ''; position: absolute; inset: -15px -8px; }
.lang__caret {
  width: 0; height: 0;
  /* integer ceilings: Chrome floors border widths to device pixels */
  border-left: clamp(4px, calc(4px + (100vw - 1440px) * 0.001786), 6px) solid transparent;
  border-right: clamp(4px, calc(4px + (100vw - 1440px) * 0.001786), 6px) solid transparent;
  border-top: clamp(5px, calc(5px + (100vw - 1440px) * 0.001786), 7px) solid var(--hdr-ink);
  transition: transform 0.26s var(--ease-panel), border-color 0.45s ease;
}
.lang__rule {
  position: absolute; left: 0; bottom: clamp(-8.4px, calc(-6px - (100vw - 1440px) * 0.0021429), -6px);
  width: 0; height: 1px;
  background: var(--hdr-ink);
  transition: width 0.28s var(--ease-panel), background-color 0.45s ease;
}
/* Open is styled off the button's own aria-expanded, not off a data-open on the
   wrapper. The wrapper flag was a second write of one fact — every place that
   set it also set aria-expanded, so what the eye saw and what a reader
   announced were two values kept in step by hand, which is the same shape as
   the .on-dark two-writer bug. Same reasoning as .menu__lang[aria-current]
   below: an attribute cannot disagree with itself.
   Plain combinators, no :has() — the caret and the rule are the button's
   children and the panel is its next sibling, so the state is already reachable
   from where it lives.
   The sibling rules land at (0,3,0) / (0,4,0) against the base (0,1,0), and the
   explicit hover-and-open caret rule below stays because open must beat hover:
   without it the two are (0,3,0) each and source order would hand it to hover. */
.lang__button[aria-expanded="true"] .lang__caret { transform: rotate(180deg); }
.lang__button[aria-expanded="true"] .lang__rule { width: 100%; }
@media (hover: hover) {
  .lang:hover .lang__caret { transform: translateY(2px); }
  .lang:hover .lang__button[aria-expanded="true"] .lang__caret { transform: rotate(180deg); }
  .lang:hover .lang__rule { width: 100%; }
}

/* The panel takes the PAGE colour, not the square's. They are two different
   values and conflating them puts ink on ink. */
.lang__panel {
  /* left moves with .lang__item's side padding, so options stay 2px right of the
     code; top is rebuilt from its parts (button + rule offset + clear) */
  position: absolute; left: clamp(-19.6px, calc(-14px - (100vw - 1440px) * 0.005), -14px); top: clamp(28px, calc(28px + (100vw - 1440px) * 0.0080714), 37.04px);
  width: clamp(78px, calc(78px + (100vw - 1440px) * 0.027857), 109.2px);
  display: flex;
  flex-direction: column;
  padding: clamp(6px, calc(6px + (100vw - 1440px) * 0.0021429), 8.4px) 0;
  border: 2px solid var(--hdr-rule);
  border-radius: clamp(4px, calc(4px + (100vw - 1440px) * 0.0014286), 5.6px);
  background: var(--hdr-panel);
  transform-origin: top center;
  transform: translateY(-5px) scaleY(0.92);
  opacity: 0;
  /* visibility, not display: the panel has to transition, and visibility is also
     what takes the four buttons out of the tab order while it is shut. It does
     NOT null their offsetParent, which is why the focus trap in main.js tests
     computed visibility as well. */
  visibility: hidden;
  transition: opacity 0.22s ease, transform 0.28s var(--ease-panel), visibility 0.22s,
              background-color 0.45s ease, border-color 0.45s ease;
}
.lang__button[aria-expanded="true"] ~ .lang__panel { opacity: 1; visibility: visible; transform: translateY(0) scaleY(1); }
/* Links, not buttons, since the head and the menu name each language's page for
   crawlers as well as people: display block for the padding a button had. */
.lang__item {
  display: block;
  padding: clamp(6px, calc(6px + (100vw - 1440px) * 0.0021429), 8.4px) clamp(14px, calc(14px + (100vw - 1440px) * 0.005), 19.6px);
  text-align: left;
  text-transform: uppercase;
  color: var(--hdr-muted);
  opacity: 0;
  transform: translateY(-7px);
  transition: opacity 0.24s ease, transform 0.28s var(--ease-panel), color 0.2s ease;
}
/* --ink-2 / --ink-rev-2, not the handoff's 40% ink. These are selectable
   options, so they are text on a control: 5.9:1 and 7.9:1 on their panels
   against the handoff value's 1.9:1, which is a look, not a legible one. */
.lang__item[aria-current="true"] { color: var(--hdr-ink); }
.lang__item:hover { color: var(--hdr-ink); }
.lang__button[aria-expanded="true"] ~ .lang__panel .lang__item { opacity: 1; transform: translateY(0); }
.lang__button[aria-expanded="true"] ~ .lang__panel .lang__item:nth-child(1) { transition-delay: 0.05s; }
.lang__button[aria-expanded="true"] ~ .lang__panel .lang__item:nth-child(2) { transition-delay: 0.1s; }
.lang__button[aria-expanded="true"] ~ .lang__panel .lang__item:nth-child(3) { transition-delay: 0.15s; }
/* four languages, not the handoff's three */
.lang__button[aria-expanded="true"] ~ .lang__panel .lang__item:nth-child(4) { transition-delay: 0.2s; }

/* The visible code and the spoken name are picked by the attribute, never by JS
   reaching into the DOM, so both are right at first paint and stay right when
   main.js never runs — gsap.registerPlugin throws outright if the CDN is
   missing. English is the fallback rather than a fourth case, so an unknown
   data-lang shows a code instead of an empty button. */
/* font-style on the BUTTON does not reach these: the UA rule `i { font-style:
   italic }` matches the <i> itself and beats an inherited value. It has to be
   reset on the element. MEASURED: italic survived the parent reset. */
.lang__code { font-style: normal; }
.lang__code,
.lang__button [data-lang-name] { display: none; }
:root[data-lang="da"] .lang__code[data-lang-code="da"],
:root[data-lang="de"] .lang__code[data-lang-code="de"],
:root[data-lang="fr"] .lang__code[data-lang-code="fr"],
:root[data-lang="da"] .lang__button [data-lang-name="da"],
:root[data-lang="de"] .lang__button [data-lang-name="de"],
:root[data-lang="fr"] .lang__button [data-lang-name="fr"] { display: block; }
:root:not([data-lang="da"]):not([data-lang="de"]):not([data-lang="fr"]) .lang__code[data-lang-code="en"],
:root:not([data-lang="da"]):not([data-lang="de"]):not([data-lang="fr"]) .lang__button [data-lang-name="en"] { display: block; }

/* The handoff's ring, at the handoff's offset. --green-deep measures 2.9:1 on
   --ink-block, under the 3:1 floor for a focus indicator — and .head is on-dark
   over .creed, over .cost, over the footer, and for as long as the menu is open,
   which is when the square is the only way out. --green-lift is 7.7:1.
   .foot is in this list for its OWN links and not for the header's: the footer's
   four anchors and its tel/mailto pair sit on --ink-block themselves, so a
   --green-deep ring on them is the same 2.9:1 failure one landmark lower. */
.head.on-dark :focus-visible,
.menu :focus-visible,
.foot :focus-visible { outline-color: var(--green-lift); }
/* Over the hero neither green is a ring: the square itself is --green, and the
   film behind the header is any colour at all. White is 3.51:1 against the
   green square, over the 3:1 floor for a focus indicator, and is the colour the
   header's text already takes there. */
.head.on-hero :focus-visible { outline-color: var(--ink-rev); }

/* ─────────────── fullscreen menu ─────────────── */

/* The top bar is gone with #menuClose: the header floats above this overlay and
   its square is the close control. That also fixes the bar's own two faults —
   it was the only block in here without max-width:var(--maxw), so past 1320px
   its brand sat wider than the header's; and being in the scroll flow it took
   the close button off the top of a short screen with it. */
.menu {
  position: fixed; inset: 0;
  z-index: 400;
  background: var(--ink-block);
  color: var(--ink-rev);
  display: flex;
  flex-direction: column;
  /* the header's own band, reserved on the FLEX CONTAINER so it is not scrollable
     content — the nav must not pass under a transparent fixed header */
  padding: var(--head-h) var(--pad) clamp(1.5rem, 4vh, clamp(3rem, calc(3rem + (100vw - 1440px) * 0.0021429), 50.4px));
  overflow: hidden;
  opacity: 0;
}
.menu[hidden] { display: none; }

/* the scroll lives here, below the reserved band, not on .menu */
/* THE WORDMARK BEHIND THE MENU, the footer's, on the footer's ground. A wash of
   the light ink clipped to nothing else — no pointer spot here, because the menu
   is as often opened by a thumb as by a mouse and the six links are the thing to
   read. .menu__body below is positioned for it: both boxes then paint in the same
   step and source order decides, so the word stays behind the links. */
.menu__word {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--f-display); font-weight: 800; letter-spacing: -0.04em;
  /* 4.0995 is the word's width in ems at this weight and tracking, so this sets
     it to 0.88 of the room between the page's gutters. */
  font-size: calc((100vw - var(--pad) * 2) * 0.88 / 4.0995);
  line-height: 1;
  color: color-mix(in oklab, var(--ink-rev), transparent 94%);
  white-space: nowrap;
  -webkit-user-select: none; user-select: none;
  pointer-events: none;
}
.menu__body {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* a swipe that reaches the menu's end stops there instead of moving the page
     behind it (main.js also takes it out of Lenis: data-lenis-prevent) */
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
}

.menu__nav {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(1.5rem, 5vh, 4rem);
  /* Its own cap, not var(--maxw): the overlay is the same on every page and its
     wide layout needs >= 1403px at 2560, whatever a page does with the token. */
  max-width: clamp(1320px, calc(1320px + (100vw - 1440px) * 0.58929), 1980px);
  width: 100%;
  margin-inline: auto;
  /* ONE source for the number column. The second level is indented to the WORD
     above it, and that offset is the number's own width plus the gap — both of
     which change with the viewport, so they are declared once here and read by
     the link and the sub-list alike rather than guessed at twice. */
  --num: calc(clamp(0.6rem, 0.9vw, 0.78rem) + clamp(0px, (100vw - 1440px) * 0.0027857, 3.12px));
  --num-gap: clamp(1rem, min(2.5vw, calc(36px + (100vw - 1440px) * 0.012857)), 50.4px);
  --indent: calc(2.4 * var(--num) + var(--num-gap));
}
.menu__list {
  display: flex;
  flex-direction: column;
  gap: clamp(0.1rem, 0.6vh, 0.5rem);
}
.menu__link {
  display: flex;
  align-items: baseline;
  gap: var(--num-gap);
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.06;
  font-size: calc(clamp(2rem, 6vw, 4.4rem) + clamp(0px, (100vw - 1440px) * 0.034571, 38.72px));
  color: var(--ink-rev);
  transition: color 0.3s var(--ease), opacity 0.3s var(--ease);
}
.menu__link i {
  font-family: var(--f-mono);
  font-style: normal;
  font-size: var(--num);
  letter-spacing: 0.2em;
  color: var(--green-lift);
  flex: none;
  width: 2.4em;
  /* baseline-aligning a 12px number to a 90px word drops it to the floor */
  align-self: center;
  transform: translateY(-0.15em);
}
/* THE WORD FILLS RATHER THAN GETTING A RULE UNDER IT. Two layers clipped to
   the same letterforms: white underneath, green on top, and what moves is the
   green one's SIZE — from nothing at the bottom-left corner to the full box.
   Because both dimensions grow at once from that corner, the colour rises
   through the word and crosses it at the same time, which is the ask.

   Anchored bottom-left and grown, not slid: a gradient slid across would paint
   the far edge of the word before the near one at any position where the two
   sizes disagree, and the point of the corner origin is that they never do. */
.menu__link span {
  position: relative;
  color: transparent;
  -webkit-text-fill-color: transparent;   /* Safari wants this as well as color */
  background-image: linear-gradient(var(--green-lift), var(--green-lift)),
                    linear-gradient(var(--ink-rev), var(--ink-rev));
  background-repeat: no-repeat;
  background-size: 0% 0%, 100% 100%;
  background-position: left bottom, left top;
  -webkit-background-clip: text;
          background-clip: text;
  /* Asymmetric, and the two halves live on the two rules: an element in its
     resting state transitions with the resting rule's timing, so this is the
     way BACK — quick, because moving along the list otherwise leaves a green
     block shrinking into the corner of the word you just left for half a
     second, which SEEN is a stray mark rather than a retreat. */
  transition: background-size 0.24s var(--ease), transform 0.35s var(--ease);
}
/* Navigation, so it gets the guard the decorative wordmarks do not: without
   background-clip the fill would paint nothing and a transparent word is an
   invisible menu. */
/* THE DOCK'S TRICK, ON A LIST OF WORDS. The word under the pointer grows, and
   only its two immediate neighbours give way — the far ones are not touched,
   so the list reads as being leaned into at one place rather than as six things
   all resizing at once.

   The WORD scales, not the row: the number stays where it is, as an index
   should, and because the transform lives on a child of the link the link's own
   box never changes. That matters more than it looks — scale the hit area
   itself and the neighbour shrinking away from the cursor opens a gap where
   nothing is hovered, the effect resets, and the row flickers between two
   states as the mouse holds still.
   Anchored left so the word grows into the space it has, and :is(:hover,
   :focus-within) so the keyboard leans on the list the same way. The previous
   sibling needs :has() — there is no other way to look backwards in CSS. */
@media (hover: hover) and (pointer: fine) {
  .menu__link span { transform-origin: left center; }
  .menu__item:is(:hover, :focus-within) > .menu__link span { transform: scale(1.07); }
  /* A neighbour gives way only if it is not itself the active one. Written as
     an exclusion rather than left to the cascade: with the mouse on one item
     and the keyboard on the one above it, the neighbour rules outrank the grow
     rule on specificity and BOTH collapsed to 0.94 — measured, the focused word
     shrank away from the reader using it. Mutually exclusive, order stops
     mattering, and two inputs in two places each show where they are. */
  .menu__item:is(:hover, :focus-within) + .menu__item:not(:hover):not(:focus-within) > .menu__link span,
  .menu__item:not(:hover):not(:focus-within):has(+ .menu__item:is(:hover, :focus-within)) > .menu__link span { transform: scale(0.94); }
}

@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .menu__link span {
    color: var(--ink-rev);
    -webkit-text-fill-color: currentColor;
    background-image: none;
  }
  .menu__link:hover span, .menu__item.is-open > .menu__link span { color: var(--green-lift); }
}
/* The word that is pointed at fills green; the other five are left ALONE. They
   were dimmed to a quarter here and it was dropped: with the fill already
   saying which word is live, standing the rest down said it a second time and
   made a six-item menu look like a five-item mistake. */
.menu__link:hover span,
.menu__item.is-open > .menu__link span {
  background-size: 100% 100%, 100% 100%;
  /* the way IN: --ease-fill is declared for exactly this, and it starts slowly
     enough that the corner is seen filling rather than the word switching.
     transform is repeated because a transition declaration REPLACES the one it
     overrides — leaving it out here dropped the scale below to an instant snap
     on the way in and animated it only on the way out. */
  transition: background-size 0.5s var(--ease-fill), transform 0.35s var(--ease);
}


/* THE SECOND LEVEL, FOLDED UNDER ITS OWN ITEM. This is the small-screen and
   touch shape and it is also the fallback: a grid whose single row goes from
   0fr to 1fr, which is the only way to animate to a height nobody has measured.
   visibility carries the real work — it is what keeps the collapsed links out
   of the focus trap, because focusables() in main.js filters on exactly that
   and a link with height 0 is otherwise still tabbable — and it is switched
   with a delay on the way out so the fold is seen before it goes. */
.menu__sub {
  display: grid;
  grid-template-rows: 0fr;
  padding-left: var(--indent);
  opacity: 0;
  visibility: hidden;
  transition: grid-template-rows 0.38s var(--ease), opacity 0.26s var(--ease),
              visibility 0s linear 0.38s;
}
.menu__sub > ul { overflow: hidden; min-height: 0; }
.menu__item.is-open > .menu__sub {
  grid-template-rows: 1fr;
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}
.menu__sub li { list-style: none; }
/* Read as part of the menu, not as a footnote under it. --ink-rev-2 at 1.1rem
   was the same grey and nearly the same size as the address block at the foot
   of the overlay, three hundred pixels below — so the column that answers the
   word you are pointing at was set no louder than the postcode. White, half
   again as large, and with air enough that six of them still read as a list. */
.menu__sub a {
  display: inline-block;
  padding-block: clamp(0.46rem, calc(0.46rem + (100vw - 1440px) * 0.0026286), 10.304px);
  font-size: calc(clamp(1.1rem, 1.6vw, 1.4rem) + clamp(0px, (100vw - 1440px) * 0.0066964, 7.5px));
  letter-spacing: -0.006em;
  color: var(--ink-rev);
  transition: color 0.25s var(--ease);
}
/* A head for the column, and the same one the eyebrows on this site wear: 34px
   of green hairline. It gives the list somewhere to start from without
   repeating the word that is already standing beside it in green. */
.menu__sub > ul::before {
  content: "";
  display: block;
  width: clamp(34px, calc(34px + (100vw - 1440px) * 0.015179), 51px); height: 1px;
  background: var(--green-lift);
  /* air above as well as below: folded, the hairline sits directly under the
     word it belongs to, and hugging it would read as an underline on that word
     rather than as the head of the list beneath it */
  margin: clamp(0.5rem, 1.2vh, clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.00064286), 15.12px)) 0 clamp(0.7rem, 1.6vh, clamp(1.1rem, calc(1.1rem + (100vw - 1440px) * 0.0022857), 20.16px));
}
/* The entries arrive one after another. With the other words no longer dimmed,
   MOVEMENT is what carries the eye across to the column — and it costs nothing
   at rest, because the delays live on the open rule and a closing panel drops
   them all at once rather than unwinding in reverse. */
.menu__sub li {
  opacity: 0;
  transform: translateY(7px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.menu__item.is-open .menu__sub li { opacity: 1; transform: none; }
.menu__item.is-open .menu__sub li:nth-child(1) { transition-delay: 0.04s; }
.menu__item.is-open .menu__sub li:nth-child(2) { transition-delay: 0.10s; }
.menu__item.is-open .menu__sub li:nth-child(3) { transition-delay: 0.16s; }
.menu__item.is-open .menu__sub li:nth-child(4) { transition-delay: 0.22s; }
.menu__item.is-open .menu__sub li:nth-child(5) { transition-delay: 0.28s; }
.menu__item.is-open .menu__sub li:nth-child(6) { transition-delay: 0.34s; }
.menu__sub a:hover { color: var(--green-lift); }

/* BESIDE, NOT UNDER — but only where both conditions hold: a pointer that can
   hover, and room for two columns. Opening on hover in a folded layout would
   shove every item below it down as the mouse crossed the list, so the two go
   together or not at all. The panel is taken out of flow against the list's own
   box, so which item is open never changes where the six sit. */
@media (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  .menu__link { font-size: calc(clamp(2rem, 3.9vw, 3.5rem) + clamp(0px, (100vw - 1440px) * 0.0275, 30.8px)); }
  .menu__list { position: relative; width: max-content; max-width: 100%; }
  .menu__sub {
    position: absolute;
    left: calc(100% + clamp(2.5rem, min(6vw, calc(86.4px + (100vw - 1440px) * 0.030857)), 120.96px));
    top: 50%;
    transform: translateY(-50%);
    /* grows with its own link type (x1.344), so the 1440 line breaks hold */
    width: max(15rem, min(19vw, calc(273.6px + (100vw - 1440px) * 0.084055), 367.74px));
    padding-left: 0;
    grid-template-rows: 1fr;   /* no height to animate: it fades in place */
    transition: opacity 0.26s var(--ease), visibility 0s linear 0.26s;
  }
  .menu__sub a { font-size: clamp(1.25rem, min(1.55vw, calc(22.32px + (100vw - 1440px) * 0.0068571)), 30px); }
}

.menu__foot {
  flex: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(2rem, calc(2rem + (100vw - 1440px) * 0.011429), 44.8px);
  max-width: clamp(1320px, calc(1320px + (100vw - 1440px) * 0.58929), 1980px);   /* the nav's cap; see .menu__nav */
  width: 100%;
  margin-inline: auto;
  padding-top: clamp(1.2rem, 3vh, clamp(2rem, calc(2rem + (100vw - 1440px) * 0.0051786), 37.8px));
  border-top: 1px solid var(--rule-rev);
}
.menu__col { display: flex; flex-direction: column; gap: 0.4rem; }
/* h3 as well as h4. The footer reuses this column verbatim — same ground, same
   0.9rem links, same --ink-rev-2, same hover — but sits under a contentinfo <h2>
   of its own, so its kickers are one level down. :is() takes the specificity of
   its most specific argument, so this stays (0,1,1) and the menu is untouched.
   One rule for a column on dark, rather than a second recipe drifting from it. */
.menu__col :is(h3, h4, .menu__kicker) {
  font-family: var(--f-mono); font-size: 0.62rem; font-weight: 500; line-height: 1.02;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--green-lift); margin-bottom: 0.5rem;
}
.menu__col a, .menu__col span { font-size: 0.9rem; color: var(--ink-rev-2); }
.menu__col a:hover { color: var(--ink-rev); }
.menu__col--lang { align-items: flex-start; }
/* Under .menu__col, whose `a` rule would otherwise set these links' size and
   colour now that they are links. */
.menu__col .menu__lang {
  font-family: var(--f-mono); font-size: clamp(0.72rem, calc(0.72rem + (100vw - 1440px) * 0.0025714), 14.4px); letter-spacing: 0.14em;
  color: var(--ink-rev-2); padding: clamp(0.15rem, calc(0.15rem + (100vw - 1440px) * 0.00085714), 3.36px) 0;
}
/* Styled off aria-current rather than a parallel .is-on class: one attribute
   cannot disagree with itself, so what the eye sees and what a screen reader
   announces are the same fact instead of two writes kept in step by hand. */
.menu__col .menu__lang[aria-current="true"] { color: var(--ink-rev); }
.menu__col .menu__lang:hover { color: var(--green-lift); }

/* ─────────────── hero ─────────────── */

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  text-align: center;
  overflow: hidden;
  padding: calc(var(--head-h) + clamp(2rem, calc(2rem + (100vw - 1440px) * 0.011429), 2.8rem)) var(--pad) clamp(3rem, 11vh, clamp(7rem, calc(7rem + (100vw - 1440px) * 0.023751), 138.6px));
}

/* full-bleed — the frame the intro grows into */
.hero__media { position: absolute; inset: 0; z-index: 0; }
.hero__media picture { display: block; width: 100%; height: 100%; }
.hero__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--hero-focus);
  filter: var(--hero-filter);
}
.hero__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--hero-focus);
  filter: var(--hero-filter);
  opacity: 0;
  transition: opacity 0.8s var(--ease);
}
.hero__video.is-live { opacity: 1; }
/* White type cannot sit on a white cabin: measured, it is 1.6:1. This is the
   least veil that gets it legible — held to the foot of the frame, so the top
   two thirds of the photograph keep their colour and detail untouched. */
.hero__veil {
  position: absolute; inset: 0;
  /* Two layers now. The foot as before, for the type. And a shallow band at
     the head for the header: the film's workshop frames run to a white
     ceiling under the mark, where the photograph was dark — measured on the
     loop's brightest frame the brand was barely there. 0.42 at the edge to
     nothing by 22%, so the frame keeps its light. */
  background:
    /* STRONGER AT THE VERY TOP since the header's name and EN turn white over
       the film. Swept across the whole loop (44s then; the 10s cut now is a part of it) at 1440x900 and 390x844 with the
       old band, white text fell under the WCAG line on the ceiling frames — the
       name under 3:1 on 24% of frames, EN under 4.5:1 on about 40% — even though
       it beat the old ink on median (5.17:1 against 3.32:1). With this band: no
       frame under the line, worst frame medians 5.05 (name) and 4.99-5.06 (EN).
       It still reaches nothing by 22%, so the frame below the header keeps its
       light. */
    linear-gradient(to bottom,
      rgba(10, 14, 12, 0.66) 0%,
      rgba(10, 14, 12, 0.58) 7%,
      rgba(10, 14, 12, 0.18) 14%,
      rgba(10, 14, 12, 0.00) 22%),
    linear-gradient(to top,
      rgba(10, 14, 12, 0.62) 0%,
      rgba(10, 14, 12, 0.58) 26%,
      rgba(10, 14, 12, 0.34) 48%,
      rgba(10, 14, 12, 0.08) 70%,
      rgba(10, 14, 12, 0.00) 100%);
  opacity: 0;
}

.hero__inner {
  position: relative; z-index: 2;
  width: 100%;
  max-width: var(--maxw);
}

.hero__eyebrow { opacity: 0; color: #fff; }
.hero__eyebrow::before { background: #45de78; }
.hero__title {
  /* min() keeps 6.6vw at and below 1440; past it the title runs linearly to
     148px (x1.557) and stops. Each line fits only because body.home's --maxw
     grows with it. */
  font-size: clamp(2.5rem, min(6.6vw, calc(95.04px + (100vw - 1440px) * 0.047286)), 9.25rem);
  max-width: 15ch;
  margin: clamp(1.4rem, calc(1.4rem + (100vw - 1440px) * 0.008), 1.96rem) auto clamp(2.1rem, calc(2.1rem + (100vw - 1440px) * 0.012), 2.94rem);
  color: #fff;
}
/* The German title's two lines are 1.3x the English ones — MEASURED at 1440,
   1111 and 1190px against 866 and 851 — so in a 15ch box each broke in two and
   the title stood four lines high at every width. 20ch holds both lines from
   1024 to 2560 (1217px of box at 1440); the type size is left alone. */
:lang(de) .hero__title { max-width: 20ch; }
.hero__title .ln {
  display: block;
  overflow: hidden;
  /* room for descenders and the comma inside the clipping box */
  padding: 0.09em 0.06em 0.18em;
  margin: -0.09em -0.06em -0.18em;
}
.hero__title .ln > span { display: block; transform: translateY(104%); }
.hero__title em { font-style: normal; color: #45de78; }   /* 4.2:1 on the veiled ground */
/* .btn stays as it is for every other button; only the hero's grows. Padding in
   em so it follows the label, with its own ramp so it grows x1.40, not x1.25. */
.hero__cta {
  opacity: 0;
  font-size: clamp(0.72rem, calc(0.72rem + (100vw - 1440px) * 0.0025715), 0.9rem);
  padding: clamp(1.02em, calc(1.02em + (100vw - 1440px) * 0.0015738), 1.1424em) clamp(1.7em, calc(1.7em + (100vw - 1440px) * 0.0026229), 1.904em);
}

/* A hairline with a green pulse running down it — the same track language as
   the intro, and it links to the section it is pointing at. */
.hero__scroll, .creed__scroll {
  position: relative; z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.65rem, calc(0.65rem + (100vw - 1440px) * 0.0037143), 0.91rem);
  margin-top: clamp(1.8rem, 4.5vh, clamp(3rem, calc(3rem + (100vw - 1440px) * 0.0077679), 56.7px));
  font-family: var(--f-mono);
  font-size: clamp(0.6rem, calc(0.6rem + (100vw - 1440px) * 0.0021429), 0.75rem);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.74);
  opacity: 0;
  transition: color 0.3s var(--ease);
}
.hero__scroll:hover, .creed__scroll:hover { color: #fff; }
.hero__scroll i, .creed__scroll i {
  position: relative;
  display: block;
  width: 1px;
  height: clamp(36px, calc(36px + (100vw - 1440px) * 0.012858), 50.4px);
  background: rgba(255, 255, 255, 0.26);
  overflow: hidden;
}
.hero__scroll i::after, .creed__scroll i::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 45%;
  background: var(--green-lift);
  animation: railDrop 2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
}
@keyframes railDrop {
  0%        { transform: translateY(-110%); }
  55%, 100% { transform: translateY(230%); }
}

/* ─────────────── creed ─────────────── */

/* Tall enough to give the reveal something to scroll through; the pin is a
   plain sticky box, which avoids ScrollTrigger's pin-spacer entirely. */
.creed {
  position: relative;
  height: 320svh;
  background: var(--ink-block);
  color: var(--ink-rev);
}
.creed__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  padding: calc(var(--head-h) + clamp(2rem, calc(2rem + (100vw - 1440px) * 0.01143), 2.8rem)) var(--pad) clamp(3rem, 8vh, clamp(5rem, calc(5rem + (100vw - 1440px) * 0.018572), 100.8px));
}
.creed__stage {
  position: relative; width: 100%;
  /* the text measure: grows by exactly the line's ratio (17.308em of line type) */
  max-width: min(92vw, clamp(1080px, calc(1080px + (100vw - 1440px) * 0.5304), 1674px));
}
.creed__line {
  grid-area: 1 / 1;
  position: absolute;
  inset: 0;
  margin: auto;
  height: max-content;
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: -0.03em;
  text-align: center;
  font-size: clamp(2.05rem, 4.5vw, clamp(3.9rem, calc(3.9rem + (100vw - 1440px) * 0.030643), 6.045rem));
  line-height: 1.09;
  text-wrap: balance;
  opacity: 0;
  will-change: transform, opacity;
}
.creed__line:first-child { position: relative; }
.creed__w {
  color: #37453e;                /* unread */
  transition: color 0.22s linear;
}
.creed__w.on { color: var(--ink-rev); }

.creed__bar {
  position: absolute;
  left: 50%; transform: translateX(-50%);
  /* matched to .rig__bar so the meter hands over at the same screen pixel. One
     expression for every copy of the meter (intro, creed, rig, cost): 54 / 120
     at 1440x900, 75.6 / 180 at 2560x1440. */
  bottom: clamp(2rem, 6vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.0175), 75.6px));
  width: min(clamp(120px, calc(120px + (100vw - 1440px) * 0.05358), 180px), 22vw);
  height: 2px;
  background: var(--rule-rev);
}
.creed__bar span { display: block; height: 100%; width: 0; background: var(--green-lift); }
/* The cue shares the hero's rules above. Absolute in the pin, just over the
   bar; the hero fades its own in with the rest of its bits, this one is simply
   on — the section it sits in is entered on a lit sentence, never blank. */
.creed__scroll {
  position: absolute;
  left: 50%; transform: translateX(-50%);
  bottom: calc(clamp(2rem, 6vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.0175), 75.6px)) + clamp(1.25rem, calc(1.25rem + (100vw - 1440px) * 0.00715), 1.75rem));
  margin-top: 0;
  opacity: 1;
}

/* ─────────────── product rig ─────────────── */

/* 640svh leaves ~540svh of actual scroll travel across three segments — roughly
   1.8 screens per beat, so each part is held long enough to read its paragraph. */
/* 700svh: the first 9% of the scrub is the lights coming up, leaving the four
   shots the same ~540svh they had before that slice was taken. */
/* 1480svh = the pin's own 100 plus 13.80 svh units of scrub: 0.54 light-up ·
   5.46 four shots · 1.20 dock · 1.80 bench · 1.80 text · 1.20 fly · 1.80 drift */
.rig {
  /* 990svh, a third off 1480. MEASURED before: the whole page was 26 screens and
     this section alone 14.8 of them, 133 wheel notches. Nothing else changes —
     measureSpans() in product.js derives every beat from BUDGET scaled by
     rest/NEED, so every beat trims by the same ratio and the choreography keeps
     its shape at two thirds the length. That scaling was designed for exactly
     this and is why the height is the only number touched. */
  position: relative; height: 900svh; background: var(--paper);
  --t: 1; --dock: 0; --fly: 0; --open: 0; --say: 0;
  /* --asm is a BELL over the assembly, not a ramp: it has to come back to zero,
     because everything it drives is a change of mood for the duration of the
     build and has to leave the finished cabin framed exactly as it was. */
  --asm: 0; --build: 0;
  /* The finished cabin, as a NUMBER rather than a class, because the sheet has to
     ramp rather than switch. Three candidates were measured and two rejected.
     --asm is a bell and is back to 0 the moment the room closes, so it cannot hold
     anything on the bench. .is-cabin latches and releases at different points, so
     on the way back up it would hold full strength while the room is visibly coming
     apart. --build looks like the clean signal and is not: the fit-out falls through
     to the last stage group and each part is nudged further on top of it, so the
     final fitting only lands at build 0.985 — a gate on build 0.90 would open with
     parts still travelling. --fly is 1.000 by then and stays there for the whole
     bench, so this opens just AFTER the last fitting is home and is fully open on
     the bench's first frame. It closes symmetrically going back up: a pure function
     of scroll, no latch. */
  --cabin: clamp(0, calc((var(--fly) - 0.96) * 25), 1);
}
/* A real ruler for one svh. The budget used to divide by innerHeight, which on a
   phone differs from svh by up to 1.16x as the URL bar moves — enough to swing a
   whole beat's length. Zero width and fixed, so it cannot lengthen what it measures. */
#rigSvh {
  position: fixed; top: 0; left: 0; width: 0;
  height: 100vh; height: 100svh;
  visibility: hidden; pointer-events: none;
}

/* ── the join with .creed ──
   .rig enters wearing the same near-black the statement section is painted in,
   so there is no edge to see where they meet; --t then runs 0 → 1 and lifts the
   ground, the copy and the chrome together with the studio lights in WebGL.
   Mixed in oklab rather than sRGB: interpolated in sRGB the ramp passes through
   a dead neutral, in oklab it holds the green undertone the whole way. */
.rig.is-armed {
  /* The join's own ramp, then a second, much smaller one over the assembly: the
     ground cools and deepens while the room is being put together, and comes
     back to paper as it closes. MEASURED: mixing to --paper-2 alone moved the
     field 1.5% in oklab lightness, which is not a beat, it is a rounding error. */
  background: color-mix(in oklab,
    color-mix(in oklab, var(--ink-block), var(--paper) calc(var(--t) * 100%)),
    color-mix(in oklab, var(--paper-2), var(--ink-block) 12%) calc(var(--asm) * 100%));
}
.rig.is-armed .rig__pin::before,
.rig.is-armed .rig__hotspots,
.rig.is-armed .rig__eyebrow { opacity: var(--t); }
/* the corner blocks clear out as the unit docks — the lower-left one sits exactly
   where the box lands, and the panel takes over the job of carrying the copy */
.rig.is-armed .rig__copy {
  opacity: calc(var(--t) * clamp(0, calc(1 - var(--dock) / 0.45), 1));
}
.rig.is-armed .rig__bar {
  background: color-mix(in oklab, var(--rule-rev), var(--rule) calc(var(--t) * 100%));
}
.rig.is-armed .rig__bar span {
  background: color-mix(in oklab, var(--green-lift), var(--green) calc(var(--t) * 100%));
}
.rig.is-armed .rig__word {
  --wash: color-mix(in oklab, rgba(244, 247, 244, 0.05), rgba(14, 21, 18, 0.06) calc(var(--t) * 100%));
}
.rig__pin {
  position: sticky; top: 0;
  height: 100svh;
  overflow: hidden;
}
/* soft studio ground so the object is not floating on a flat field */
.rig__pin::before {
  content: "";
  position: absolute; inset: 0;
  /* The pool of light tightens and walks to the middle as the room closes, so
     the finished cabin ends up standing in it. Back to the tuned values at
     --asm 0, which is where every other beat sits. */
  background:
    radial-gradient(calc(62% - var(--asm) * 15%) calc(55% - var(--asm) * 11%)
                    at calc(66% - var(--asm) * 15%) 46%,
                    #f0f2f0 0%, rgba(240,242,240,0) 70%),
    radial-gradient(45% 40% at 30% 78%, #cfd4d1 0%, rgba(207,212,209,0) 72%);
  pointer-events: none;
}

/* The drawing sheet. Faint for the whole section — the rig is a technical
   drawing of an object and the ground should say so — and stronger while the
   room is actually being put together. It rides --t, so it comes up out of the
   dark with everything else instead of being printed across the join. */
.rig__grid {
  position: absolute; inset: 0;
  /* MEASURED: --rule on paper is a ten-level difference, and at the resting
     opacity that came out at 1.6 levels — a grid nobody can see. Ink at 8% is
     sixteen levels before the opacity, which survives the ramp. */
  /* In the finished cabin, and only there, the sheet is gone over one notch harder:
     ink 8% -> 12%. MEASURED at the bench's own 0.5 opacity, against paper: 8.2 ->
     12.4 levels at the centre of the field, and 4.1 -> 6.1 in the bands either side
     of the room — which is the only paper the room leaves, since the cabin overfills
     the frame. The line renders #d0d3d1: one channel off --paper-2, so the strongest
     this sheet ever gets is one step down the PAPER ramp, never a step toward ink.
     The assembly bell still flashes past it at 16.5 and stays the loudest moment.
     The WEIGHT, not the opacity: opacity is a group operation and would drag
     ::after's pointer pool up with it — 40% on the loudest pixel in the frame to buy
     50% on the quietest. ::after redeclares its own --line, so lifting this one
     leaves the pool nearly where it was.
     Not a major/minor lattice either: majors anchored to the pin's top-left put a
     heavy crosshair at 672,448 against an optical centre of 720,468 — 48px and 20px
     out, which reads as a misalignment rather than a division. Measured, not feared.
     var(--cabin, 0) and not var(--cabin): if the substitution ever fails this falls
     back to today's 92% line instead of taking background-image down with it. */
  --line: color-mix(in oklab, var(--ink), transparent calc(92% - var(--cabin, 0) * 4%));
  background-image:
    repeating-linear-gradient(to right, var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  /* masked to a soft field, or the lines run into the frame edges and the whole
     thing reads as a table rather than as a sheet the object sits on */
  -webkit-mask-image: radial-gradient(74% 70% at 50% 52%, #000 0%, transparent 78%);
  mask-image: radial-gradient(74% 70% at 50% 52%, #000 0%, transparent 78%);
  opacity: calc(var(--t) * (0.5 + var(--asm) * 0.5));
  pointer-events: none;
}
/* The same pool of light that lifts the letterforms, on the sheet: a second
   copy of the grid at a stronger line, masked to a circle at the pointer. Two
   stacked layers rather than one composited mask — mask-composite is still
   uneven across engines and this needs no compositing at all. */
.rig__grid::after {
  content: "";
  position: absolute; inset: 0;
  --line: color-mix(in oklab, var(--ink), transparent 80%);
  background-image:
    repeating-linear-gradient(to right, var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  -webkit-mask-image: radial-gradient(circle clamp(200px, calc(200px + (100vw - 1440px) * 0.07143), 280px) at var(--px, -9999px) var(--py, -9999px),
                                      #000 0%, transparent 72%);
  mask-image: radial-gradient(circle clamp(200px, calc(200px + (100vw - 1440px) * 0.07143), 280px) at var(--px, -9999px) var(--py, -9999px),
                              #000 0%, transparent 72%);
  pointer-events: none;
}

/* the running count, in the same grammar as the panel's own kicker */
.rig__step {
  position: absolute; z-index: 3;
  left: 50%; transform: translateX(-50%);
  bottom: clamp(2rem, 6vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.0175), 75.6px));
  display: flex; align-items: center; gap: clamp(0.5rem, calc(0.5rem + (100vw - 1440px) * 0.002858), 0.7rem);
  font-family: var(--f-mono);
  font-size: clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.002215), 0.775rem); letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--ink-3);
  opacity: var(--asm);
  pointer-events: none;
  white-space: nowrap;
}
.rig__step b { font-weight: 400; color: var(--green); }
.rig__step i {
  display: block; width: clamp(40px, 9vw, clamp(110px, calc(110px + (100vw - 1440px) * 0.04911), 165px)); height: 1px;
  background: linear-gradient(to right,
    var(--green) 0 calc(var(--build) * 100%),
    var(--rule) calc(var(--build) * 100%) 100%);
}

/* the oversized name, drifting behind everything */
.rig__word {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: -0.04em;
  font-size: clamp(6rem, min(22vw, calc(316.8px + (100vw - 1440px) * 0.1636)), 500px);
  line-height: 1;
  /* The wash is a background layer, not a colour, so a second layer can be laid
     over it and both clipped to the letterforms: a spot that follows the
     pointer. INK, not the brand green — at this size the green read as a
     highlighter rather than as light falling on the letters. Parked far off the box until a pointer has actually been in the
     section, so nothing is lit on a touch device that never hovers. */
  --wash: rgba(14, 21, 18, 0.06);
  /* the word's own box; the grid gets the pin's, further down */
  color: transparent;
  -webkit-text-fill-color: transparent;   /* Safari wants this as well as color */
  background-image:
    radial-gradient(circle clamp(200px, calc(200px + (100vw - 1440px) * 0.07143), 280px) at var(--wx, -9999px) var(--wy, -9999px),
      color-mix(in oklab, var(--ink), transparent 82%) 0%,
      rgba(0, 0, 0, 0) 72%),
    linear-gradient(var(--wash), var(--wash));
  -webkit-background-clip: text;
  background-clip: text;
  white-space: nowrap;
  /* a background, not text: pointer-events alone only stops a selection STARTING
     on the word — a drag through the section, or select-all, still highlighted it */
  -webkit-user-select: none; user-select: none;
  pointer-events: none;
  will-change: transform;
  opacity: calc((1 - var(--dock) * 0.55) * (1 - var(--fly)));
}

.rig__stage {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.rig__canvas { width: 100%; height: 100%; display: block; }

/* ── the dock ──
   A hairline case that draws itself out of two corner brackets as the unit
   settles into it, rather than a filled card — the studio ground painted by
   .rig__pin::before runs straight through, so nothing is re-lit on arrival.
   It lives inside .rig__stage, which is pointer-events:none, so it opts back in. */
/* The UNIT's box. A frame in the lower left with the copy's field beside it —
   the object is a thing on a bench and the frame says so. The CABIN is a room
   and gets the whole page instead; see .rig.is-cabin below. */
.rig__box {
  position: absolute;
  left: var(--pad);
  bottom: clamp(2rem, 6vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.0175), 75.6px));
  /* 2.5x the area it started at — 1.58x on each side. A true 2.5x per side
     would be 1150x1035 and does not fit a 900px-tall viewport. Past 1440 both
     caps grow x1.5, so the box keeps its 1.11 aspect and the unit its framing. */
  width:  min(58vw, clamp(730px, calc(730px + (100vw - 1440px) * 0.3259), 1095px));
  height: min(73svh, clamp(660px, calc(660px + (100vw - 1440px) * 0.29465), 990px));
  pointer-events: auto;
  touch-action: pan-y;   /* the browser keeps vertical scroll; we take the rest */
  opacity: calc(clamp(0, var(--dock) * 3, 1) * (1 - var(--open)));
}
.rig__box::before, .rig__box::after {
  content: ""; position: absolute;
  width:  calc(clamp(14px, calc(14px + (100vw - 1440px) * 0.005), 19.6px) + (100% - clamp(14px, calc(14px + (100vw - 1440px) * 0.005), 19.6px)) * var(--dock) * var(--dock));
  height: calc(clamp(14px, calc(14px + (100vw - 1440px) * 0.005), 19.6px) + (100% - clamp(14px, calc(14px + (100vw - 1440px) * 0.005), 19.6px)) * var(--dock) * var(--dock));
  pointer-events: none;
}
.rig__box::before { top: 0; left: 0;
  border-top: 1px solid var(--ink-3); border-left: 1px solid var(--ink-3); }
.rig__box::after { bottom: 0; right: 0;
  border-bottom: 1px solid var(--ink-3); border-right: 1px solid var(--ink-3); }
/* The room is never boxed: it takes the frame, and the box element stays only
   as the surface the drag and the keyboard live on. Everything downstream reads
   BOX, so this one rule moves the view rect, the marker projection and the
   cabin's own distance fit with it. */
.rig.is-cabin .rig__box {
  inset: 0; width: auto; height: auto; opacity: 1;
}
.rig.is-cabin .rig__box::before,
.rig.is-cabin .rig__box::after { content: none; }
.rig__box:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }
.rig.is-cabin .rig__box { cursor: default; }
.rig.is-docked .rig__box { cursor: grab; }
.rig.is-docked .rig__box:active { cursor: grabbing; }

/* The cabin arrives behind this rather than by fading 27 materials: material
   .transparent is part of three's program cache key, so flipping it would force a
   shader recompile at each end of the fade, and 216 double-sided primitives would
   sort through themselves inside a closed room. */

/* Centred under the unit and sized to be READ, not found: at 0.58rem in the
   corner it was a caption for people who already knew. Two lines — the turn,
   then the points — with a turning glyph in front, in --ink-2 (the corner used
   --ink-3, which at that size fell under body contrast). It fades once the
   reader has turned the unit themselves (.is-turned, set by product.js): a
   prompt that has been followed is done. The unit's own idle sway, in
   product.js, is the other half of the same invitation. */
.rig__hint {
  position: absolute; left: 50%; bottom: clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.005143), 1.26rem); transform: translateX(-50%);
  display: grid; grid-auto-flow: row; justify-items: center; row-gap: clamp(0.28rem, calc(0.28rem + (100vw - 1440px) * 0.0016), 0.392rem);
  font-family: var(--f-mono); font-size: clamp(0.66rem, calc(0.66rem + (100vw - 1440px) * 0.002358), 0.825rem); line-height: 1;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-2); pointer-events: none; white-space: nowrap;
  opacity: calc(clamp(0, (var(--dock) - 0.85) / 0.15, 1) * (1 - var(--open)));
  transition: opacity 0.4s var(--ease);
}
/* the words are on the element (data-text-*), so each language's page carries its own */
.rig__hint::before { content: "\21BB\2002 " attr(data-text-drag); font-size: clamp(0.72rem, calc(0.72rem + (100vw - 1440px) * 0.002572), 0.9rem); color: var(--ink); }
.rig__hint::after  { content: attr(data-text-hover); color: var(--ink-3); }
@media (hover: none) {
  .rig__hint::before { content: "\21BB\2002 " attr(data-text-swipe); }
  .rig__hint::after  { content: attr(data-text-tap); }
}
.rig.is-turned .rig__hint { opacity: 0; }

/* the panel a hovered point fills — same measure, rule and type as the blocks
   it takes over from, so it reads as the copy re-attaching to the object */
/* The hovered point's own box, carried to it rather than parked in the margin.
   Positioned from JS in pin coordinates — the same frame the markers use — so
   it tracks its point while the unit turns underneath it. */
/* Standing copy for the docked unit. It lives and dies with the box — the same
   --dock * (1 - --open) the box itself uses — so it is gone the moment the frame
   opens for the flight, and it never shows during the four shots. */
.rig__note {
  position: absolute; z-index: 4;
  /* Beside the unit's box, in the field the box leaves: left edge is the box's
     own right edge, right edge is the page gutter. It needs no ground of its
     own — nothing is behind it. */
  left: calc(var(--pad) + min(58vw, clamp(730px, calc(730px + (100vw - 1440px) * 0.3259), 1095px)));
  right: var(--pad);
  margin-inline: auto;
  top: 50%; transform: translateY(-50%);
  width: min(clamp(36ch, calc(36ch + (100vw - 1440px) * 0.1730), 55.8ch), 27vw);
  opacity: calc(clamp(0, (var(--dock) - 0.6) / 0.4, 1) * (1 - var(--open)) * (1 - var(--say)));
  pointer-events: none;
}
/* live only once it is actually up, or it would swallow clicks over the box */
.rig.is-docked .rig__note { pointer-events: auto; }
/* The UNIT's panel only. The room takes the whole frame, so a standing panel
   would cover the thing it describes — there, each point carries its own box.
   display:none, not opacity: it is what tells setSel the panel is not available
   and the floating box has to do the work instead. */
.rig.is-cabin .rig__note { display: none; }
/* ── the docked panel ──
   Kicker, headline, two paragraphs, and a stepper under a rule. It reads the
   same <li>s the tour panel does; each shows the fields it needs and hides the
   rest, so there is one copy of the content and no second source to keep. */
.rig__nkick {
  display: flex; align-items: center; gap: clamp(0.7rem, calc(0.7rem + (100vw - 1440px) * 0.004), 0.98rem);
  font-family: var(--f-mono);
  font-size: clamp(0.7rem, calc(0.7rem + (100vw - 1440px) * 0.0025), 0.875rem); letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--green-deep);   /* small type: 4.8:1, where --green is 2.8:1 */
  margin-bottom: clamp(1rem, 2.4vh, clamp(1.6rem, calc(1.6rem + (100vw - 1440px) * 0.004143), 30.24px));
}
.rig__nkick i { display: block; width: clamp(34px, calc(34px + (100vw - 1440px) * 0.01518), 51px); height: 1px; flex: 0 0 auto;
                background: currentColor; opacity: 0.6; }
/* One element, not three: as flex items the "/" and the spaces between them
   became their own boxes and the line broke into five. */
.rig__nkick em { font-style: normal; white-space: nowrap; }

#rigNoteHead {
  font-size: clamp(1.7rem, 2.7vw, clamp(2.4rem, calc(2.4rem + (100vw - 1440px) * 0.01886), 59.52px));
  line-height: 1.03; letter-spacing: -0.028em;
  margin-bottom: clamp(1rem, 2.4vh, clamp(1.6rem, calc(1.6rem + (100vw - 1440px) * 0.004143), 30.24px));
}
/* The headline is hand-broken into one span per line, as the copy was written;
   those spans are what linify() measures before it rebuilds the element as its
   own lines, so they have to be blocks while it looks. */
#rigNoteHead span { display: block; }

/* THE COPY ARRIVES A LINE AT A TIME. Each line sits in a box that clips, and
   rises into it from below, one after the next — the plotter that drew the
   headline with a green pen and wiped the paragraphs in from the left is gone
   (Andreas asked for another reveal, and chose this one from four).
   The lines are MEASURED, not authored: product.js (linify) wraps the words
   where they stand, reads which ones share a line, and rebuilds the element as
   one <i class="rig__ln"> per line with the line inside it. So German, which
   wraps differently, and a phone, which wraps differently again, each get their
   own lines — and a resize re-reads them from the authored markup, which is
   kept on the node.
   The panel's reveal restarts by construction: product.js rebuilds these nodes
   on every swap. The tour's four parts run theirs off .on, the class their beat
   already carries.
   A clipping box would cut a descender — the y of "railway" at a heading's
   1.03 line height. It is not padded to make room: padding is layout, and the
   band's height is measured from these elements, so 0.14em a line came back as
   14px of window on a phone. `clip` with a margin costs the layout nothing. */
.rig__ln {
  display: block;
  overflow: clip; overflow-clip-margin: 0.16em;
  font-style: normal;
}
.rig__ln > i {
  display: block; font-style: normal;
  transform: translateY(112%);
}
@keyframes rigLine {
  from { transform: translateY(112%); }
  to   { transform: none; }
}
.rig__note:not(.is-swap) .rig__ln > i,
.rig__list li.on .rig__ln > i {
  animation: rigLine 0.72s cubic-bezier(0.22, 0.61, 0.36, 1) both;
  animation-delay: var(--st, 0s);
}
/* No JS, or reduced motion: the copy is simply there. A line left translated out
   of its own box with nothing to bring it back would be invisible, so the guard
   undoes the resting state as well as the animation. */
.no-motion .rig__ln > i, .rig--flat .rig__ln > i {
  animation: none;
  transform: none;
}

#rigNoteP1, #rigNoteP2 {
  font-size: clamp(0.98rem, calc(0.98rem + (100vw - 1440px) * 0.0049), 1.323rem); line-height: 1.62; color: var(--ink-2);
  max-width: 34ch; text-wrap: pretty;
}
#rigNoteP1 { margin-bottom: clamp(1rem, calc(1rem + (100vw - 1440px) * 0.005715), 1.4rem); }

.rig__nav {
  display: flex; align-items: center; gap: clamp(0.55rem, calc(0.55rem + (100vw - 1440px) * 0.003143), 0.77rem);
  margin-top: clamp(1.4rem, 3.4vh, clamp(2.2rem, calc(2.2rem + (100vw - 1440px) * 0.006822), 42.84px));
  padding-top: clamp(1rem, 2.4vh, clamp(1.5rem, calc(1.5rem + (100vw - 1440px) * 0.005572), 30.24px));
  border-top: 1px solid var(--rule);
}
.rig__nav button {
  width: clamp(52px, calc(52px + (100vw - 1440px) * 0.01858), 72.8px); height: clamp(52px, calc(52px + (100vw - 1440px) * 0.01858), 72.8px); flex: 0 0 auto;
  border: 1px solid var(--rule);
  display: grid; place-items: center;
  font-size: clamp(0.95rem, calc(0.95rem + (100vw - 1440px) * 0.005429), 1.33rem); color: var(--ink);
  transition: background 0.2s var(--ease), border-color 0.2s var(--ease);
}
.rig__nav button:hover { background: var(--card); border-color: var(--ink-3); }
.rig__nav button:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 2px; }
/* Semvac's labels are not all one word: "01 / 04 · Inlet and outlet valve" ran
   57px past the panel's rule at 1440 and was cut mid-word on a 375px phone.
   Two balanced lines fit beside the arrows at every size. */
.rig__ncount {
  margin-left: clamp(0.5rem, calc(0.5rem + (100vw - 1440px) * 0.002858), 0.7rem);
  font-family: var(--f-mono); font-size: clamp(0.7rem, calc(0.7rem + (100vw - 1440px) * 0.0025), 0.875rem); letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--ink-3);
  min-width: 0; line-height: 1.45; text-wrap: balance;
}
.rig__ncount b { font-weight: 400; color: var(--ink); }
.rig__ncount em { font-style: normal; }

/* The swap, on the way OUT only: the transition is declared on the .is-swap
   rule itself, so applying the class fades the old copy away and REMOVING it
   restores opacity in one frame. That is deliberate — the copy is brought back
   by the plot above, and a 0.14s fade-in laid over it would show the whole
   block arriving at once and make the drawn edge pointless. The count keeps
   both halves of its fade: nothing draws it. */
.rig__ncount {
  transition: opacity 0.14s cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 0.14s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.rig__note.is-swap .rig__nbody {
  opacity: 0;
  transition: opacity 0.14s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.rig__note.is-swap .rig__ncount { opacity: 0; transform: translateY(6px); }

/* the tour panel and the docked panel read the same items and show different
   fields of them — the headline and the second paragraph belong to the panel,
   the spec table to the tour */
.rig__list .rig__hl,
.rig__list .rig__p2 { display: none; }

/* the tour panel and the docked panel read the same items and show different
   fields of them — the headline and the second paragraph belong to the docked
   one, the spec table to the tour */
.rig__list .rig__hl,
.rig__list .rig__p2 { display: none; }

/* the flat page is the whole content, so it shows every field */
.rig--flat .rig__note, .rig--flat .rig__nav { display: none; }
.rig--flat .rig__list .rig__hl,
.rig--flat .rig__list .rig__p2 { display: block; }
.rig--flat .rig__list .rig__hl {
  font-size: clamp(1rem, 2vw, clamp(1.25rem, calc(1.25rem + (100vw - 1440px) * 0.00625), 1.6875rem)); line-height: 1.2; margin: clamp(0.1rem, calc(0.1rem + (100vw - 1440px) * 0.000572), 0.14rem) 0 clamp(0.2rem, calc(0.2rem + (100vw - 1440px) * 0.001143), 0.28rem);
}


/* The passage between the two objects: the box edges have dissolved, the unit is
   loose in the frame, and this sits alongside it while both drift. */

.rig__tip {
  position: absolute; left: 0; top: 0; z-index: 5;
  /* width, padding and type share one ratio (x1.35), so line breaks hold */
  width: min(clamp(36ch, calc(36ch + (100vw - 1440px) * 0.1101), 48.6ch), clamp(360px, calc(360px + (100vw - 1440px) * 0.1125), 486px));
  padding: clamp(1.15rem, calc(1.15rem + (100vw - 1440px) * 0.00575), 1.5525rem) clamp(1.3rem, calc(1.3rem + (100vw - 1440px) * 0.0065), 1.755rem) clamp(1.25rem, calc(1.25rem + (100vw - 1440px) * 0.00625), 1.6875rem);
  background: var(--card);
  border: 1px solid var(--ink-3);
  box-shadow: 0 clamp(12px, calc(12px + (100vw - 1440px) * 0.004286), 16.8px) clamp(32px, calc(32px + (100vw - 1440px) * 0.01143), 44.8px) rgba(14, 21, 18, 0.13);
  opacity: 0; pointer-events: none;
  transition: opacity 0.18s var(--ease);
}
.rig__tip.is-on { opacity: clamp(0, calc((var(--dock) - 0.6) / 0.25), 1); }
.rig__tip-n {
  display: block; margin-bottom: clamp(0.45rem, calc(0.45rem + (100vw - 1440px) * 0.002572), 0.63rem); font-style: normal;
  font-family: var(--f-mono); font-size: clamp(0.58rem, calc(0.58rem + (100vw - 1440px) * 0.002072), 0.725rem);
  letter-spacing: 0.16em; color: var(--green-deep);
}

/* the wedge, in two triangles so the 1px frame carries round it. --ay is the
   marker's height inside the box, so it still points at its point after the
   box has been clamped against the top or bottom of the frame. */
.rig__tip::before, .rig__tip::after {
  content: ""; position: absolute;
  top: var(--ay, 50%); margin-top: -7px;
  border: 7px solid transparent;
}
.rig__tip::before { left: -15px; border-right-color: var(--ink-3); }
.rig__tip::after  { left: -14px; border-right-color: var(--card); }
.rig__tip.is-left::before { left: auto; right: -15px;
  border-right-color: transparent; border-left-color: var(--ink-3); }
.rig__tip.is-left::after  { left: auto; right: -14px;
  border-right-color: transparent; border-left-color: var(--card); }

.rig__hotspots { position: absolute; inset: 0; }
.hot {
  appearance: none; padding: 0;
  position: absolute;
  width: clamp(26px, calc(26px + (100vw - 1440px) * 0.009286), 36.4px); height: clamp(26px, calc(26px + (100vw - 1440px) * 0.009286), 36.4px);
  /* always exactly half the size, so product.js still centres the dot on its point */
  margin: calc(clamp(26px, calc(26px + (100vw - 1440px) * 0.009286), 36.4px) / -2) 0 0 calc(clamp(26px, calc(26px + (100vw - 1440px) * 0.009286), 36.4px) / -2);
  border-radius: 50%;
  border: 1.5px solid var(--green-deep);
  display: grid; place-items: center;
  font-family: var(--f-mono);
  font-size: clamp(0.58rem, calc(0.58rem + (100vw - 1440px) * 0.002072), 0.725rem);
  color: var(--green-deep);
  background: rgba(220, 223, 221, 0.72);
  backdrop-filter: blur(3px);
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease),
              background 0.25s var(--ease), border-color 0.25s var(--ease),
              color 0.25s var(--ease);
}
.hot.on { opacity: 1; transform: scale(1); }
/* the cabin's fifth, while the unit (four) is up: display:grid above outranks
   the hidden attribute's UA display:none */
.hot[hidden] { display: none; }

/* under the dock, frame() writes marker opacity inline every frame for the
   facing fade — a transition on it would lag a drag by a third of a second */
.rig.is-docked .hot {
  transition: transform 0.35s var(--ease),
              background-color 0.2s linear, border-color 0.2s linear;
}
.rig.is-docked .hot.on { pointer-events: auto; }
.hot.is-sel {
  background: var(--green-wash); border-color: var(--green);
  color: var(--green-deep); transform: scale(1.18);
}
.hot:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }

@media (hover: none) {
  .hot {
    width: clamp(34px, calc(34px + (100vw - 1440px) * 0.01215), 47.6px); height: clamp(34px, calc(34px + (100vw - 1440px) * 0.01215), 47.6px);
    margin: calc(clamp(34px, calc(34px + (100vw - 1440px) * 0.01215), 47.6px) / -2) 0 0 calc(clamp(34px, calc(34px + (100vw - 1440px) * 0.01215), 47.6px) / -2);
  }
  .hot::after { content: ""; position: absolute; inset: -5px; }  /* 44px tap target */
}

/* No GSAP: nothing drives the scrub, so the section collapses to the one screen
   that needs no scrolling — the docked box, already interactive. */
.rig--static { height: 100svh; }

/* No WebGL, or reduced motion: the section stops being a scroll rig and becomes
   a plain readable list. Without this it was several screens of empty paper with
   every copy block stuck at opacity 0. */
.rig--flat { height: auto; padding: clamp(4rem, 10vh, clamp(7rem, calc(7rem + (100vw - 1440px) * 0.0125), 126px)) 0; }
.rig--flat .rig__stage,
.rig--flat .rig__box,
.rig--flat .rig__tip,
.rig--flat .rig__placeholder,
.rig--flat .rig__word,
.rig--flat .rig__grid,
.rig--flat .rig__step,
.rig--flat .rig__bar { display: none; }
.rig--flat .rig__pin { position: static; height: auto; overflow: visible; padding: 0 var(--pad); }
.rig--flat .rig__pin::before { display: none; }
.rig--flat .rig__eyebrow { display: none; }   /* the group headings label it now */
.rig--flat .rig__copy { position: static; }
.rig--flat .rig__kicker,
.rig--flat .rig__copy::after { display: none; }   /* a counter with no tour to step */
.rig--flat .rig__list,
.rig--flat .rig__list--src {
  display: grid; gap: clamp(2.6rem, calc(2.6rem + (100vw - 1440px) * 0.01486), 3.64rem); max-width: clamp(62ch, calc(62ch + (100vw - 1440px) * 0.2168), 86.8ch);
  position: static; right: auto; top: auto; width: auto;
}
.rig--flat .rig__slidebody { gap: clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.005143), 1.26rem); opacity: 1; transform: none; transition: none; }
.rig--flat .rig__list h3 { font-size: clamp(1.2rem, 2.4vw, clamp(1.7rem, calc(1.7rem + (100vw - 1440px) * 0.009472), 2.363rem)); }
.rig--flat .rig__list h3 span { display: inline; }
.rig--flat .rig__list p { font-size: clamp(0.95rem, calc(0.95rem + (100vw - 1440px) * 0.00475), 1.2825rem); }
.rig--flat .rig__spec { margin-top: clamp(0.4rem, calc(0.4rem + (100vw - 1440px) * 0.002286), 0.56rem); }
.rig--flat .rig__group {
  display: block; font-family: var(--f-mono); font-size: clamp(0.58rem, calc(0.58rem + (100vw - 1440px) * 0.002072), 0.725rem);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-3);
  margin: 0 0 clamp(1.4rem, calc(1.4rem + (100vw - 1440px) * 0.008), 1.96rem);
}
.rig--flat .rig__group + .rig__list + .rig__group { margin-top: clamp(3rem, calc(3rem + (100vw - 1440px) * 0.01715), 4.2rem); }
.rig--flat .rig__list li,
.rig--flat .rig__list li[data-hot] {
  position: static; width: auto;
  left: auto; right: auto; top: auto; bottom: auto;
  transform: none;
}

.rig__placeholder {
  position: absolute;
  left: 50%; bottom: clamp(4rem, 11vh, clamp(7rem, calc(7rem + (100vw - 1440px) * 0.02375), 138.6px));
  transform: translateX(-50%);
  font-family: var(--f-mono);
  font-size: clamp(0.58rem, calc(0.58rem + (100vw - 1440px) * 0.002072), 0.725rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
  white-space: nowrap;
}

.rig__eyebrow {
  position: absolute; z-index: 3;
  left: var(--pad); top: calc(var(--head-h) + clamp(2rem, calc(2rem + (100vw - 1440px) * 0.01143), 2.8rem));
}
/* On the span, never on the <p>: the paragraph's own opacity is var(--t), the
   scrubbed light ramp, and a transition there would lag the whole join. */
.rig__eyebrow span {
  display: inline-block;
  transition: opacity 0.16s var(--ease), transform 0.16s var(--ease);
}
.rig__eyebrow.is-swap span { opacity: 0; transform: translateY(4px); }

/* ── the component panel ──
   Built to the "Valve block — component text panel (1a)" handoff: a datasheet
   panel on the right of the stage, with a 01–04 index rail at the frame's edge.
   Every measurement below is the specced value; the colours are this site's
   tokens standing in for the sampled hex, as the handoff asks. */
.rig__copy { position: absolute; inset: 0; z-index: 2; pointer-events: none; }

/* the panel's own hairline and ghost, expressed against our ink token so they
   land on the specced rgba(17,17,17,.13) / .055 without a second hex */
.rig {
  --panel-hairline: color-mix(in oklab, var(--ink), transparent 87%);
}

/* A full-width band, not a fixed column: the first two components sit on the
   left and the last two on the right, so the tour changes sides once. */
.rig__list {
  list-style: none;
  position: absolute;
  left: 0; right: 0; top: 50%;
}
/* the cabin's copy is a data source for the docked panel and the flat page, not
   a second panel stacked on this one */
.rig__list--src { display: none; }
/* only ever seen on the flat path, where the two lists must not read as eight
   undifferentiated blocks half of which are about a different object */
.rig__group { display: none; }

/* Centred on the list's own line rather than sized to it, so a slide that grows
   a line still sits on the same optical centre as the other three. */
/* THE PANEL HOLDS ITS SHARE OF THE SCREEN, and every number below is why.
   It was 430px wide, 150px from the edge, with four fixed pixel type sizes —
   the same at every width, while the canvas behind it is full bleed. MEASURED
   on the real page: the panel is 30% of a 1440px window, 22% of 1920, 17% of
   2560 and 12.5% of 3440, with the headline at 52px throughout. The unit grows
   with the screen and the words do not, so on a large monitor the text is a
   small block a long way from the thing it describes.
   The floors are today's values, so nothing moves at 1440 and below; the
   ceilings stop it growing past the size the copy was written for.
   LINEAR FROM 1440 TO 2560, and not the vw terms it first had. Those started
   late and capped early, so between 1654 and 2560 the panel outgrew the rest of
   the page — at 1920 its body text was x1.30 where every other body on the page
   is x1.15, and its headline stood taller than the Services h2 from 1769 to
   2418px. Same value at 1440 and at 2560; an even climb between, like the rest. The OUTER
   inset stays 150px: the panel grows INWARD, toward the unit, which is the half
   of the problem that is about distance. */
/* KEPT BETWEEN THE EYEBROW AND THE BAR. With Semvac's full text in it a part runs
   to two paragraphs' worth and five spec rows, and centred on the stage it rose
   into "TV1 – Toilet module" on any screen under ~800px tall — the overlap
   Semvac sent a screenshot of. product.js (fitPanel) measures the tallest part
   against the room between the two and writes --fit, one scale for all four so
   the type does not change size from part to part, and --dy, the nudge down or
   up that keeps each one inside it. Scaled about its own left (right) middle, so
   the panel keeps its outer edge and its centre line. */
.rig__list li {
  position: absolute; top: 0;
  width: clamp(430px, calc(430px + (100vw - 1440px) * 0.210358), 665.6px);
  left: 150px;
  transform: translateY(calc(-50% + var(--dy, 0px))) scale(var(--fit, 1));
  transform-origin: 0 50%;
}
.rig__list li[data-hot="2"],
.rig__list li[data-hot="3"] { left: auto; right: 150px; transform-origin: 100% 50%; }
.rig__slidebody {
  position: relative;
  display: flex; flex-direction: column; gap: clamp(22px, calc(22px + (100vw - 1440px) * 0.0089286), 32px);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.24s cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.rig__list li.on .rig__slidebody { opacity: 1; transform: none; }

.rig__kicker {
  display: flex; align-items: center; gap: clamp(12px, calc(12px + (100vw - 1440px) * 0.0035714), 16px);
  font-family: var(--f-mono);
  font-size: clamp(12px, calc(12px + (100vw - 1440px) * 0.0026786), 15px); letter-spacing: 0.22em; text-transform: uppercase;
  /* --green-deep, not the handoff's brand green: 12px type needs 4.5:1, and
     --green is 2.8:1 on the paper */
  color: var(--green-deep);
}
.rig__kicker i { display: block; width: clamp(40px, calc(40px + (100vw - 1440px) * 0.017857), 60px); height: 1px; background: var(--green); opacity: 0.4; }
.rig__kicker em { font-style: normal; color: var(--ink-3); }

.rig__list h3 {
  font-weight: 700; font-size: clamp(52px, calc(52px + (100vw - 1440px) * 0.026786), 82px); line-height: 0.98; letter-spacing: -0.025em;
  color: var(--ink);
  /* Two lines, reserved. The handoff's content rule is that every name is
     hand-broken onto two lines, and the column is centred on its own middle —
     so a one-word name silently shortens the block by 51px and the whole panel
     jumps on that one swap. A rule the copy cannot break. */
  min-height: 1.96em;
}
/* hand-broken, one span per line — spans rather than <br> so h3.textContent
   still reads "Valve block" for the tab titles, the hover box and the labels */
.rig__list h3 span { display: block; }
.rig__list p {
  font-size: clamp(17px, calc(17px + (100vw - 1440px) * 0.0053571), 23px); line-height: 1.6; color: var(--ink-2);
  /* A MEASURE, NOT A WIDTH. The paragraph has to hold its line length as the
     type grows, so the cap is in characters. MEASURED in Schibsted Grotesk:
     1ch is 0.612em here, so 36.5ch renders at 380px at the floor size of 17px —
     today's number to the pixel — and at 520px once the type reaches 23px. */
  max-width: min(100%, 36.5ch); text-wrap: pretty;
}

.rig__spec { display: flex; flex-direction: column; margin-top: 8px; }
.rig__spec > div {
  display: flex; justify-content: space-between; gap: clamp(20px, calc(20px + (100vw - 1440px) * 0.0089286), 30px);
  padding: clamp(13px, calc(13px + (100vw - 1440px) * 0.0044643), 18px) 0;
  border-top: 1px solid var(--panel-hairline);
  font-family: var(--f-mono);
  font-size: clamp(12px, calc(12px + (100vw - 1440px) * 0.0026786), 15px); letter-spacing: 0.08em; text-transform: uppercase;
}
.rig__spec > div:last-child { border-bottom: 1px solid var(--panel-hairline); }
.rig__spec dt { color: var(--ink-3); }
.rig__spec dd { color: var(--ink); }

/* the floating box keeps the small sizes the panel has grown out of */
.rig__tip h3 {
  font-size: clamp(1.15rem, 1.8vw, clamp(1.55rem, calc(1.55rem + (100vw - 1440px) * 0.00775), 2.0925rem)); line-height: 1.12;
  letter-spacing: -0.02em; margin-bottom: clamp(0.7rem, calc(0.7rem + (100vw - 1440px) * 0.004), 0.98rem);
}
.rig__tip p { font-size: clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.0045), 1.215rem); line-height: 1.58; color: var(--ink-2); text-wrap: pretty; }
.rig__tip-p2 { margin-top: clamp(0.7rem, calc(0.7rem + (100vw - 1440px) * 0.004), 0.98rem); }

.rig__bar {
  opacity: calc(1 - var(--dock));
  position: absolute;
  left: 50%; transform: translateX(-50%);
  bottom: clamp(2rem, 6vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.0175), 75.6px));
  width: min(clamp(120px, calc(120px + (100vw - 1440px) * 0.05358), 180px), 22vw); height: 2px;
  background: var(--rule);
}
.rig__bar span { display: block; height: 100%; width: 0; background: var(--green); }

/* ══════════════ THE COST ══════════════
   A headline on the dark, then a rail of cards pushed sideways by the page's
   own vertical scroll — the same mechanic the intro's train strip uses, which
   is what makes the cards running past the right edge read as deliberate. */
.cost {
  /* THE CURTAIN. A negative top margin lifts the section over the rig's last
     screen and z-index puts it in front, so the dark runs up over the finished
     cabin — and the headline and the cards ride up inside it rather than
     waiting below for it to finish. One mechanism instead of a separate sheet
     plus a section that had to agree with it. */
  margin-top: -100svh;
  position: relative; z-index: 2;
  background: var(--ink-block);
  color: var(--ink-rev);
  /* 100svh of that is the rise; the rest is the rail's own travel. MEASURED: at
     260svh of travel the rail moved 0.43px sideways per pixel of scroll and the
     cards barely drifted. 190 puts it at 0.85. */
  height: 290svh;
  --c: 0;   /* 0 once the rise is done, 1 at the bottom */
}
.cost__pin {
  position: sticky; top: 0;
  height: 100svh;
  overflow: hidden;
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(2.5rem, 7vh, 5.5rem);
}
.cost__head {
  text-align: center;
  padding: 0 var(--pad);
  /* No fade. It rides up with the dark, so it is already legible by the time
     the section has arrived — fading it in on --c would have hidden it for
     exactly the stretch it is meant to be read on. */
}
.cost__head h2 {
  /* the measure, not a <br>: three lines at this width, and it re-breaks by
     itself when the copy is replaced */
  max-width: 19ch;
  margin-inline: auto;
  font-size: clamp(2rem, 4.4vw, clamp(3.6rem, calc(57.6px + (100vw - 1440px) * 0.029822), 91px));
  line-height: 1.02; letter-spacing: -0.03em;
  text-transform: uppercase;
}
.cost__lede {
  margin: clamp(1rem, 2.4vh, clamp(1.6rem, calc(25.6px + (100vw - 1440px) * 0.003929), 30px)) auto 0;
  max-width: 46ch;
  font-size: clamp(0.95rem, min(1.1vw, calc(15.84px + (100vw - 1440px) * 0.00495)), 21.38px);   /* character-identical to .proc__lede */
  line-height: 1.6;
  color: var(--ink-rev-2);
}

/* The rail. --span is the overflow it has to travel, measured in JS and written
   back as a pixel value: it depends on the cards' own widths and the gap, and
   guessing it either stalls short or runs the last card off the screen. */
.cost__rail {
  display: flex; gap: clamp(1rem, 2vw, clamp(1.75rem, calc(28px + (100vw - 1440px) * 0.01), 39.2px));
  padding-inline: var(--pad);
  width: max-content;
  transform: translateX(calc(var(--c) * var(--span, 0px) * -1));
  will-change: transform;
}
.cost__card {
  flex: 0 0 auto;
  /* x1.4, not x1.55: card 1's heading must keep breaking and card 2's must not */
  width: min(38vw, clamp(460px, calc(460px + (100vw - 1440px) * 0.164286), 644px));
  min-height: clamp(300px, 42svh, clamp(400px, calc(400px + (100vw - 1440px) * 0.115179), 529px));
  padding: clamp(1.8rem, 4vh, clamp(2.8rem, calc(44.8px + (100vw - 1440px) * 0.004643), 50px));
  border: 1px solid var(--rule-rev);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  gap: clamp(1.2rem, 3vh, clamp(2rem, calc(32px + (100vw - 1440px) * 0.005358), 38px));
}
.cost__ico {
  display: grid; place-items: center;
  width: clamp(44px, calc(44px + (100vw - 1440px) * 0.016072), 62px); height: clamp(44px, calc(44px + (100vw - 1440px) * 0.016072), 62px);
  color: var(--ink-rev);
}
.cost__ico svg { width: 100%; height: 100%; stroke-linecap: round; stroke-linejoin: round; }
.cost__card h3 {
  font-size: clamp(1.05rem, 1.5vw, clamp(1.3rem, calc(20.8px + (100vw - 1440px) * 0.006429), 28px));
  text-transform: uppercase; letter-spacing: 0.02em;
}
.cost__card p {
  font-size: clamp(0.88rem, min(1vw, calc(14.4px + (100vw - 1440px) * 0.0045)), 19.44px); line-height: 1.6;
  color: var(--ink-rev-2); max-width: 34ch; text-wrap: pretty;
}
.cost__bar {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: clamp(2rem, 6vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.0175), 75.6px));
  width: min(clamp(120px, calc(120px + (100vw - 1440px) * 0.05358), 180px), 22vw); height: 2px;
  background: var(--rule-rev);
}
.cost__bar span { display: block; height: 100%; width: 0; background: var(--green-lift); }

/* No scrub to push the rail: it becomes a plain readable stack. The negative
   margin has to go with it — there is no pin to run up over on this path, so
   it would simply swallow the end of the flat rig. */
.no-motion .cost, .cost--flat {
  height: auto; padding: clamp(4rem, 10vh, clamp(7rem, calc(112px + (100vw - 1440px) * 0.0125), 126px)) 0;
  margin-top: 0; z-index: auto;
}
.no-motion .cost__pin, .cost--flat .cost__pin {
  position: static; height: auto; overflow: visible;
}
.no-motion .cost__rail, .cost--flat .cost__rail {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  width: auto; transform: none; margin-top: clamp(3rem, calc(48px + (100vw - 1440px) * 0.016965), 67px);
}
/* The card's own width has to go with the rail. It is `min(38vw, 460px)` and
   `flex: 0 0 auto` for the sideways row; left standing inside a grid track it
   keeps 460px, bursts the track, and pushes the document 89px wider than the
   viewport — a horizontal scrollbar on the reduced-motion path only, which is
   exactly where nobody looks. MEASURED at 1440: scrollWidth 1529 before, 1440
   after. The min-height goes too — a grid row does not need the 400px that gave
   the moving row its presence. */
.no-motion .cost__card, .cost--flat .cost__card {
  width: auto; min-height: 0; flex: initial;
}
.no-motion .cost__head, .cost--flat .cost__head { opacity: 1; transform: none; }
.no-motion .cost__bar, .cost--flat .cost__bar { display: none; }

@media (max-width: 760px) {
  .cost__card { width: 78vw; min-height: 0; }
}

/* ══════════════ REFERENCES ══════════════
   Where the systems run: a world map in which only the reference countries
   answer, and a signal that leaves Odense to reach them. js/map.js owns the
   geometry, the signal and the names; everything here is the LOOK, keyed to
   data-look so the dark control room on the page and the line drawing on
   map-test.html are the same component with different paint.

   Dark, and placed straight after .cost for a reason beyond the story: the
   header knockout has to hold across both, and two adjacent dark sections keep
   the band dark for one continuous stretch. Put before the footer instead, the
   band would have flipped dark, light for the footer's paper margin, and dark
   again inside a single screen. */
.refs {
  position: relative;
  background: var(--ink-block);
  color: var(--ink-rev);
  /* EXACTLY one screen, and the map takes what the headline leaves. At
     min-height the section grew to 1012px in a 900px window — the heading plus
     a 1400px-wide map at its natural 2.1:1 — so the stop at the section's top
     framed it with Australia and half of South America below the fold. A fixed
     height with the map as the flexible child means the whole world is always
     inside the one screen the gesture lands on. */
  height: 100svh;
  min-height: 34rem;
  display: flex;
  flex-direction: column;
  padding: calc(var(--head-h) + clamp(2rem, 6vh, clamp(4rem, calc(4rem + (100vw - 1440px) * 0.01036), 4.725rem))) var(--pad) clamp(2.5rem, 7vh, clamp(5rem, calc(5rem + (100vw - 1440px) * 0.007322), 5.5125rem));
  overflow: hidden;
}
.refs--paper { background: var(--paper); color: var(--ink); }
.refs__head {
  flex: none;
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto clamp(1rem, 3vh, 2.5rem);
}
.refs__head h2 {
  margin-top: clamp(1.1rem, calc(1.1rem + (100vw - 1440px) * 0.006286), 1.54rem);
  max-width: 16ch;
  font-size: clamp(2rem, 4.4vw, clamp(3.6rem, calc(57.6px + (100vw - 1440px) * 0.029822), 91px));   /* character-identical to .cost__head h2 */
  line-height: 1.02; letter-spacing: -0.03em;
}

.map {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;             /* a flex child will not shrink below its content without it */
  display: flex;
  width: 100%;
  max-width: 1400px;
  margin-inline: auto;
}
.map__stage { position: relative; flex: 1; min-height: 0; }
/* Fills the box both ways and lets the viewBox do the fitting — the default
   preserveAspectRatio is xMidYMid meet, so the drawing is as large as the box
   allows in whichever dimension runs out first, centred in the other. */
.map__svg { display: block; width: 100%; height: 100%; }
.map--failed { display: none; }
/* .map is display:flex, which outranks the hidden attribute's UA display:none —
   the test page shows one of two components at a time with [hidden]. */
.map[hidden] { display: none; }
/* Hairlines stay hairlines. The SVG is a 1000-unit drawing shown at anything
   from 390 to 1400px, and a scaling stroke would be three times heavier on a
   wide screen than on a phone. */
.map__svg path, .map__svg circle { vector-effect: non-scaling-stroke; }

/* ── the dark control room (the default look) ── */
.map__graticule { fill: none; stroke: rgba(244, 247, 244, 0.05); stroke-width: 0.6px; }
.map__land      { fill: rgba(244, 247, 244, 0.075); }
.map__borders   { fill: none; stroke: var(--ink-block); stroke-width: 0.6px; }
/* A reference sits on the land in the land's own grey until the signal reaches
   it — so there is nothing to see before it lights, not a green country waiting
   under a grey one. Lit, the fill is a THIRD of the green and the edge is all of
   it: flat green at full strength turned Russia into a slab that outweighed
   every other country on the map. */
.map__ref {
  fill: rgba(244, 247, 244, 0.075);
  stroke: transparent;
  stroke-width: 0.8px;
  transition: fill 0.7s var(--ease), stroke 0.7s var(--ease);
}
.map__ref.is-lit { fill: color-mix(in oklab, var(--green-lift), transparent 64%); stroke: var(--green-lift); }
.map__ref.is-hover { fill: color-mix(in oklab, var(--green-lift), transparent 22%); }
.map__node {
  fill: #e9fff0;
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(0);
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), fill 0.2s var(--ease);
}
.map__node.is-lit { transform: scale(1); }
.map__node.is-hover { fill: var(--green-lift); transform: scale(1.6); }
.map__ring { fill: none; stroke: var(--green-lift); stroke-width: 1.2px; }
.map__hq-dot { fill: #fff; }
.map__hq-halo {
  fill: var(--green-lift);
  opacity: 0.35;
  transform-box: fill-box;
  transform-origin: center;
}
/* Odense keeps a slow pulse once the signal has gone out — the one thing on the
   map still transmitting. Never before, so it does not compete with the rings. */
.map.is-done .map__hq-halo { animation: hqPulse 2.6s ease-in-out infinite; }
@keyframes hqPulse {
  0%, 100% { transform: scale(1);   opacity: 0.35; }
  50%      { transform: scale(2.1); opacity: 0; }
}

.map__tip {
  position: absolute; left: 0; top: 0; z-index: 2;
  /* up and to the right of the pointer; the offset grows faster than the label
     (x1.40 against x1.25) so the taller label still clears the cursor */
  margin: clamp(-3.36rem, calc(-2.4rem - (100vw - 1440px) * 0.013715), -2.4rem) 0 0 clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.005143), 1.26rem);
  padding: clamp(0.42rem, calc(0.42rem + (100vw - 1440px) * 0.0024), 0.588rem) clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.003543), 0.868rem) clamp(0.4rem, calc(0.4rem + (100vw - 1440px) * 0.002286), 0.56rem);
  font-family: var(--f-mono); font-size: clamp(0.66rem, calc(0.66rem + (100vw - 1440px) * 0.002358), 0.825rem);
  letter-spacing: 0.14em; text-transform: uppercase; white-space: nowrap;
  background: var(--ink-rev); color: var(--ink-block);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s var(--ease);
}
.map__tip.is-on { opacity: 1; }

/* ── the line drawing (map-test.html) ── */
.map[data-look="line"] .map__graticule { stroke: color-mix(in oklab, var(--ink), transparent 94%); }
.map[data-look="line"] .map__land      { fill: none; stroke: color-mix(in oklab, var(--ink), transparent 45%); stroke-width: 0.7px; }
.map[data-look="line"] .map__borders   { stroke: color-mix(in oklab, var(--ink), transparent 70%); stroke-width: 0.5px; }
.map[data-look="line"] .map__ref       { fill: transparent; }
.map[data-look="line"] .map__ref.is-lit  { fill: color-mix(in oklab, var(--green), transparent 18%); stroke: var(--green-deep); }
.map[data-look="line"] .map__ref.is-hover { fill: var(--green-deep); }
.map[data-look="line"] .map__node       { fill: var(--ink); }
.map[data-look="line"] .map__node.is-hover { fill: var(--green-deep); }
.map[data-look="line"] .map__ring       { stroke: var(--green-deep); }
.map[data-look="line"] .map__hq-dot     { fill: var(--ink); }
.map[data-look="line"] .map__hq-halo    { fill: var(--green); }
.map[data-look="line"] .map__tip        { background: var(--ink); color: var(--paper); }

/* ── the globe (js/globe.js) ──
   Headline left, planet right, on anything wide enough to hold both: a globe is
   round, and stacked under a headline in a 16:9 screen it could only be as large
   as the height the headline left it — 560px at 1440x900. Beside it, the
   planet takes the section's full height. Stacked again below 900px, where a
   screen is taller than it is wide and the globe is width-bound either way. */
.refs--globe {
  display: grid;
  grid-template-columns: minmax(0, 0.78fr) minmax(0, 1.22fr);
  grid-template-rows: minmax(0, 1fr);
  align-items: center;
  column-gap: clamp(1rem, 4vw, clamp(3.6rem, calc(3.6rem + (100vw - 1440px) * 0.020572), 5.04rem));
}
.refs--globe .refs__head { margin: 0; max-width: none; }
.refs__hint {
  margin-top: clamp(1.2rem, 3vh, clamp(2rem, calc(2rem + (100vw - 1440px) * 0.005179), 2.3625rem));
  font-family: var(--f-mono); font-size: clamp(0.66rem, calc(0.66rem + (100vw - 1440px) * 0.002358), 0.825rem);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-rev-2);
}
/* the words are on the element (data-text-*), so each language's page carries its own */
.refs__hint::before { content: "\21BB\2002 " attr(data-text-drag); }
@media (hover: none) { .refs__hint::before { content: "\21BB\2002 " attr(data-text-swipe); } }
.refs--paper .refs__hint { color: var(--ink-2); }
/* When the globe cannot run, globe.js mounts the flat map in its place — which
   does not turn. A hint to drag it round would be telling the reader to do
   something that does nothing, so it goes; map-test.html hides it the same way. */
.refs:has(.globe--failed) .refs__hint { display: none; }
.globe {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 0;
}
.globe__stage { position: absolute; inset: 0; }
.globe__canvas {
  display: block;
  width: 100%; height: 100%;
  cursor: grab;
  /* The browser keeps vertical scrolling and hands the rest to the globe, so a
     reader swiping down the page past it can never be caught by it. The same
     trade the unit's box makes, for the same reason. */
  touch-action: pan-y;
}
.globe.is-dragging .globe__canvas { cursor: grabbing; }
.globe--failed .globe__stage { display: none; }
.globe[data-look="line"] .map__tip { background: var(--ink); color: var(--paper); }
@media (max-width: 899px) {
  .refs--globe {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    row-gap: clamp(0.5rem, 2vh, 1.5rem);
  }
}

@media (prefers-reduced-motion: reduce) {
  .map__ref, .map__node { transition: none; }
  .map.is-done .map__hq-halo { animation: none; }
}

/* ══════════════ HOW A UNIT IS MADE ══════════════
   The page's third scroll-driven section and deliberately its quietest: no
   scrub, no ScrollTrigger pin, no pin-spacer, no progress meter. The frame is a
   plain position:sticky box — the same choice .creed__pin, .rig__pin and
   .cost__pin already make — and the only thing JS does is say which of five
   plates is showing. */
.proc {
  /* BOTH declarations are load-bearing and neither works alone.
     .proc__sheet is a POSITIONED, z-auto element, so without a negative z-index
     it paints in step 8 of the painting order — ABOVE .wrap's non-positioned
     in-flow content, whatever the source order — and the lattice prints straight
     across the headline. .rig never meets this because nothing inside .rig__pin
     is unpositioned (.rig__copy z-index 2, .rig__step z-index 3, .rig__stage and
     .rig__word both absolute).
     But `position:relative; z-index:auto` is NOT a stacking context, so a -1
     child would escape to the root context and fall behind .proc's own opaque
     --paper — invisible. isolation makes .proc a stacking context WITHOUT giving
     it a z-index, which is what keeps it out of the argument with .cost's
     z-index 2 entirely. (The site's own -1 precedent, .rig__copy::after, works
     only because .rig__copy carries z-index 2; isolation is .trigger's idiom.) */
  position: relative;
  isolation: isolate;
  background: var(--paper);
  /* Deep opening air. The reader arrives MID-WIPE — .cost__pin is still sliding
     off the top of the screen for a full 100svh — so the first screen of this
     section is spent being wiped into and nothing legible may sit in it. At
     22svh the eyebrow does not enter until the boundary is four fifths off, and
     what arrives before it is the drawing sheet, not a blank. svh and not vh
     because the thing it is measured against is .cost's own 290svh. */
  padding: clamp(7rem, 22svh, clamp(15rem, calc(15rem + (100vw - 1440px) * 0.03322), 277.2px)) var(--pad) clamp(4rem, 10vh, clamp(7rem, calc(7rem + (100vw - 1440px) * 0.0125), 126px));
  /* The header is fixed at 74; without this an anchor lands the eyebrow under
     the band. Read by both routes an anchor can take: the browser's own jump
     honours it directly, and main.js's Lenis route reads it back as the offset. */
  scroll-margin-top: var(--head-h);
}

.proc__head { margin-bottom: clamp(3rem, 9vh, clamp(6rem, calc(6rem + (100vw - 1440px) * 0.01554), 113.4px)); }
/* The measure goes on the TYPE and never on a wrapper: `ch` resolves against
   the element's OWN font-size, so a max-width on .proc__head would compute at
   the body's 16px (~264px) and swallow both of these whole. Same rule as
   .cost__head h2 { max-width: 19ch }.
   Sentence case where .cost__head h2 is uppercase: that section is a mechanism
   announcing itself, this one is a chapter of a document. */
.proc__head h2 {
  margin: clamp(1.3rem, calc(1.3rem + (100vw - 1440px) * 0.007429), 29.12px) 0 clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.005143), 20.16px);
  max-width: 20ch;
  font-size: clamp(2rem, 4.4vw, clamp(3.4rem, calc(3.4rem + (100vw - 1440px) * 0.02822), 86px));
}
.proc__lede {
  max-width: 44ch;
  font-size: clamp(0.95rem, min(1.1vw, calc(15.84px + (100vw - 1440px) * 0.00495)), 21.38px);   /* character-identical to .cost__lede */
  color: var(--ink-2);
  text-wrap: pretty;
}

.proc__grid {
  position: relative;            /* the sheet's containing block */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(2rem, min(5vw, calc(72px + (100vw - 1440px) * 0.02572)), 100.8px);
  /* REQUIRED, and it does two jobs. A grid item's containing block is its GRID
     AREA, so the sticky column can only travel inside the row — and under the
     default `stretch` the item fills the row exactly and has nowhere to go.
     `start` also makes the frame LET GO at the bottom of the steps column,
     before the closing rule, so the page ends on a composition standing still
     where .cost today ends on a locked one. */
  align-items: start;
}

/* The rig's drawing sheet, carried across the join — same 56px pitch, so it
   reads as the same sheet, and it is what makes the arriving paper land as A
   SHEET rather than as a blank.
   At the rig's own RENDERED weight, not its alpha: .rig__grid is ink at 8%
   under opacity .5, which its comment at 1119-1127 measures as 8.2 levels below
   paper. Ink at 6% with no multiplier renders 12.4 — half again LOUDER than the
   sheet it claims to defer to. Same recipe, same result, no new number.
   STATIC. .rig__grid's opacity rides --t and --asm, which are .rig-scoped and
   written by product.js; copied verbatim it would compute to 0 and be invisible.
   It also must not become a fourth scrubbed thing on this page.
   RADIAL and not a left-to-right ramp, for the reason .rig__grid's own comment
   gives at 1140: a mask that dies on one axis leaves the lines running into the
   other three edges and the whole thing reads as a table.
   Anchored to .proc__grid and bled one page-margin left, which is what makes the
   mask honest: a percentage of the SECTION is a percentage of the viewport, and
   above 1450px the column stops growing while the viewport does not. Against
   this box the photo column runs 4.7%->49.7% and the text column starts at 55.0%
   at 1024, 1440, 1920 AND 2560. It will not line up with the rig's lattice —
   different section, different origin. That is fine; it is a sheet idiom, not
   one continuous drawing. */
.proc__sheet {
  position: absolute; z-index: -1;
  top: calc(clamp(2rem, 6vh, clamp(4rem, calc(4rem + (100vw - 1440px) * 0.01036), 75.6px)) * -1);
  bottom: calc(clamp(2rem, 6vh, clamp(4rem, calc(4rem + (100vw - 1440px) * 0.01036), 75.6px)) * -1);
  left: calc(var(--pad) * -1);
  right: 0;
  --line: color-mix(in oklab, var(--ink), transparent 92%);
  background-image:
    repeating-linear-gradient(to right,  var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  -webkit-mask-image: radial-gradient(40% 58% at 24% 48%, #000 0 34%, transparent 78%);
          mask-image: radial-gradient(40% 58% at 24% 48%, #000 0 34%, transparent 78%);
  opacity: 0.5;
  pointer-events: none;
}

.proc__media {
  position: sticky;
  /* NOT the .creed__pin / .cost__pin idiom of a full-height sticky box with its
     payload centred inside it. A grid item sticks only inside its own grid area,
     the area is the row the <ol> defines, and a 100svh box would eat most of
     that row and unstick a third of the way down the steps. The box is exactly
     the frame's own height, which is what holds the plate through step 05. The
     cost is that the frame cannot be optically centred in CSS — its height comes
     from the column width via aspect-ratio and is not knowable in a sheet — so
     the offset is set by hand and reads correctly against a step column whose
     activation line sits at 58%. */
  top: calc(var(--head-h) + clamp(1.25rem, 4vh, clamp(2.5rem, calc(2.5rem + (100vw - 1440px) * 0.009286), 50.4px)));
}
.proc__frame {
  position: relative;
  /* 4:3 is a compromise chosen with the numbers in front of it, not a default.
     Four of the five files are 3:2 (2000x1334) and hoses is 2:3 (2000x2999).
     With object-fit:cover in a frame of aspect A, a landscape (1.4993) keeps A/L
     of its WIDTH and a portrait (0.6669) keeps P/A of its HEIGHT:
       3:2   landscape 100.0%   hoses 44.5%
       4:3   landscape  88.9%   hoses 50.0%   <- this
       1:1   landscape  66.7%   hoses 66.7%
     1:1 is the symmetric answer and the wrong one: it spends a third of every
     photograph that has a person in it to save 17 points on the one that has
     nobody. 4:3 takes 5.6% off each side of the four, and nothing load-bearing
     sits inside that margin in any of them — checked frame by frame.
     Per-plate aspect-ratio was rejected outright: the frame would jump on every
     swap, which is the opposite of calm. */
  aspect-ratio: 4 / 3;
  /* A short viewport gets a wider band rather than a frame that overruns the
     screen. The width is definite from the grid, so this wins over the ratio;
     when it bites, the frame goes wider than 3:2 and the four landscapes start
     losing height instead of width, which is the only case their Y values below
     are live for. */
  max-block-size: calc(100svh - var(--head-h) - clamp(4rem, 12vh, clamp(8rem, calc(8rem + (100vw - 1440px) * 0.02072), 151.2px)));
  overflow: hidden;
  border-radius: 2px;              /* .intro__tile's radius */
  background: var(--paper-2);      /* the ground before a plate has decoded */
}

/* THE SWAP. Nothing ever fades OUT over the ground. Two plates crossing at 0.5
   leave (1-a)(1-b) = 25% of --paper-2 showing between them, which on greyscale
   photography is a milky flare rather than a dissolve, five times a pass, on the
   page's calmest section. So every plate already scrolled past is HELD fully
   opaque underneath, in DOM order, and only one plate is ever in motion:
     forward   the incoming plate fades in ON TOP of an opaque predecessor
     backward  the current plate fades out and REVEALS an opaque predecessor
   Either way the union of the stack is never below 1 and the ground is never
   seen again after the first decode. No z-index needed — later siblings already
   paint above earlier ones. */
.proc__plate {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity 0.45s var(--ease-fill);
}
.proc__plate.is-on   { opacity: 1; }
.proc__plate.is-seen { opacity: 1; transition: none; }

/* style.css:220 — picture needs an explicit box or the img falls back to auto
   height and object-fit/object-position never take effect. Paid for once. */
.proc__plate picture { display: block; width: 100%; height: 100%; }
.proc__plate img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  /* Colour, like the hero: the grey these carried put the work at a distance,
     and it is the work the section is about. The intro's strip keeps
     --photo-filter, which is what that variable is for. */
  filter: var(--proc-filter);
}
/* At exactly 4:3 a 3:2 image is HEIGHT-driven, so the vertical value is inert
   for the four landscapes; it becomes live only when max-block-size clamps the
   frame on a short viewport. Real values are given rather than 50% for that. */
.proc__plate[data-plate="0"] img { object-position: 50% 52%; }
/* THE PORTRAIT. At 4:3 the visible band is y 29%-79% of the original.
   KEPT: the brass union (which lands about a third down the frame), the clamps,
   the elbow bend and the top of the braided run — the whole "made off" subject.
   LOST: the galvanised frame and lagged panel above the union, and the bottom
   fifth where the lines carry on down past the clamps. The real casualty is
   CONTEXT, not subject — the sense that this is one wall of a tall assembly with
   the workshop receding behind it, which is precisely what made it the odd one
   out. Say that to the client rather than pretending the crop is free. Usable
   band is 50%-62%: below 50 the elbows start to go, above 62 the union does. */
.proc__plate[data-plate="2"] img { object-position: 50% 50%; }
.proc__plate[data-plate="4"] img { object-position: 50% 44%; }

/* the stage number, for the flat path only — see the note in index.html */
.proc__cap { display: none; }

.proc__steps { list-style: none; }
/* min-height in svh, and justify-content to centre the copy in it, because the
   step's height IS the dwell time of its photograph. Content alone gives a
   ~300px step, so on a 900px screen a plate would hold for a third of a screen
   and three steps would be legible at once — which destroys the one thing the
   mechanic is for, that this paragraph goes with this photograph.
   48svh holds each plate for about half a screen at any height. It used to carry
   a second argument as well — that it was also what made the last step reachable
   on a page with NO FOOTER, max scroll putting the document's bottom edge at the
   viewport's bottom so step 05's top landed at 100 - 48 - (the air below it) svh
   and the 58% activation line cleared it. THE FOOTER RETIRED THAT ARGUMENT AND
   STRENGTHENED THE GUARANTEE: it adds a screen and a half of scroll below step
   05, and the step triggers are onEnter/onEnterBack rather than onToggle, so 05
   lights and STAYS lit through the whole footer. MEASURED at max scroll with the
   footer in place: step 05 is the active step, with room to spare where there
   used to be almost none. No document-end guard is needed and none is written;
   the geometry is still the guarantee. */
.proc__step {
  display: flex; flex-direction: column; justify-content: center;
  min-height: 48svh;
  padding: clamp(2rem, 5vh, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.00625), 63px)) 0;
  border-top: 1px solid var(--rule);
}
/* border only. Zeroing the top padding as well would leave the first step's copy
   half a pad higher in its box than every other step's, because min-height plus
   justify-content:center centres within the content box. */
.proc__step:first-child { border-top: 0; }

.proc__n {
  display: flex; align-items: center; gap: clamp(0.75rem, calc(0.75rem + (100vw - 1440px) * 0.00375), 16.2px);
  font-family: var(--f-mono);
  font-size: 0.68rem; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase;
  /* --ink-2 (5.92:1) and NOT --ink-3 (3.34:1 when this was written; 4.75:1 now). The token's own comment says
     "above 3:1", which is the LARGE-text floor, not the 4.5:1 body one, and this
     line is 10.9px — four of the five kickers are on screen inactive at any
     moment and would fail AA the whole time. */
  color: var(--ink-2);
  transition: color 0.35s var(--ease);
}
/* fixed width, .rig__nkick's 34px. The house animates colour and opacity and
   leaves geometry alone; animating `width` inside a flex row relayouts the line
   every frame and is a kind of motion this sheet never uses. */
.proc__n i {
  display: block; flex: 0 0 auto;
  width: clamp(34px, calc(34px + (100vw - 1440px) * 0.01518), 51px); height: 1px;
  background: currentColor; opacity: 0.45;
  transition: background 0.35s var(--ease), opacity 0.35s var(--ease);
}

.proc__step h3 {
  margin: clamp(0.95rem, calc(0.95rem + (100vw - 1440px) * 0.005429), 21.28px) 0 clamp(0.7rem, calc(0.7rem + (100vw - 1440px) * 0.004), 15.68px);
  font-weight: 700;              /* .rig__list h3's weight, not the global 800 */
  font-size: clamp(1.35rem, 2.3vw, clamp(2rem, calc(2rem + (100vw - 1440px) * 0.01429), 48px));
  line-height: 1.08;
  color: var(--ink-2);
  transition: color 0.35s var(--ease);
}
.proc__step p {
  max-width: 46ch;
  /* the 1.05vw term only ever acted between 1448 and 1600; the floor wins below */
  font-size: clamp(0.95rem, calc(0.95rem + (100vw - 1440px) * 0.00475), 20.52px);
  color: var(--ink-2);
  text-wrap: pretty;
}
/* The creed's word-lighting, on the TITLES. MEASURED, because the first attempt
   put it on the body copy and it was invisible:
     creed  unlit 1.84:1 -> lit 17.15:1 on dark  = a 9.3x step
     body   unlit 5.92:1 -> lit 13.79:1 on paper = a 2.3x step
   Both ends of that body version are comfortably readable, which is precisely
   why nothing seemed to happen. And it cannot be widened: the note below rejects
   greying running body copy, and the lightest colour still clearing AA's 4.5:1
   for body text only reaches a 3.0x step.
   A title is LARGE text, where AA asks 3:1. --ink-3 is 3.34:1 and therefore
   legal here though it would not be on a paragraph — and it gives a 4.1x step.
   0.22s linear, matched to .creed__w so both sections light at the same rate. */
/* Two unlit ends, because the two are held to different contrast standards.
   MEASURED on --paper: --ink 13.79:1, old --ink-3 (#6d7a73) 3.34:1, --ink-2 5.92:1.
   A TITLE is large text, where AA asks 3:1 — so --ink-3 is legal there and gives
   a 4.1x step. A PARAGRAPH is body copy at 4.5:1, and --ink-3 would fail it. The
   lightest mix toward --ink-3 that still clears the bar is 42%, at 4.62:1, which
   gives 3.0x — weaker than the title, unavoidably, and still well above the 2.3x
   that made this invisible when it was first tried on the body copy. */
/* The unlit words keep the grey the highlight was tuned on, now that --ink-3 is
   darker for small text: the titles are large type, where 3:1 is the bar, and
   the new --ink-3 is almost exactly the old mix the paragraphs used. */
.proc__step h3 .proc__w { color: #6d7a73; }
.proc__step p .proc__w  { color: var(--ink-3); }
.proc__w { transition: color 0.22s linear; }
/* The lit rule has to match the UNLIT selectors' specificity or it loses. Those
   are (0,2,1) — two classes and an element — and a bare .proc__w.on is (0,2,0),
   so the unlit colour won and no word ever went to ink. The effect looked dead
   while the class was being applied perfectly, which is why counting classes is
   not a test: only the rendered colour is. */
.proc__step h3 .proc__w.on,
.proc__step p .proc__w.on { color: var(--ink); }
/* No JS, no ScrollTrigger, or reduced motion: every word simply reads at the lit
   colour. The unlit state is a scroll effect, not information. */
.no-motion .proc__w, .proc--flat .proc__w,
.no-motion .proc__step h3 .proc__w, .proc--flat .proc__step h3 .proc__w,
.no-motion .proc__step p .proc__w,  .proc--flat .proc__step p .proc__w { color: var(--ink); }

/* THE ACTIVE STATE, and the reason the BODY COPY never fades. Measured on
   --paper:  --ink 13.79:1 · --ink-2 5.92:1 · --ink-3 4.75:1 · --green-deep 4.8:1.
   The reference greys its inactive paragraphs outright. Doing that here, or
   fading them with opacity, puts running body copy at 3.34:1 for the four fifths
   of the section that are not active, which fails AA. So the NUMBER and the
   TITLE carry the state — 5.92 -> 13.79 on a title is a large, instant shift —
   and every paragraph on screen stays at 5.92 and legal. Green as the kicker's
   own colour, because every mono kicker on this site is green text: .eyebrow at
   130 and .rig__kicker at 1611. At most one is green at a time. */
.proc__step.is-on h3         { color: var(--ink); }
.proc__step.is-on .proc__n   { color: var(--green-deep); }
.proc__step.is-on .proc__n i { background: var(--green); opacity: 1; }

/* ── stacked, at the site's existing 1023 break ── */
/* BELOW 1024 EACH STEP CARRIES ITS OWN PHOTOGRAPH, and the frame goes. It was
   a band stuck under the header with the steps running up behind it: on a 390px
   phone it took 233px of an 844px screen, a link that scrolled under it printed
   over the photograph (its arrow made it a positioned box, painted above the
   band), and every step kept a 42svh dwell that on a phone reads as a hole
   between one paragraph and the next. Picture over words, one after another, is
   what a phone does well. The frame keeps its job at 1024 and up. */
.proc__thumb { display: none; }
@media (max-width: 1023px) {
  .proc__grid { display: block; }
  .proc__sheet { display: none; }          /* no left column left to be a board */
  .proc__media, .no-motion .proc__media, .proc--flat .proc__media { display: none; }
  .proc__head { margin-bottom: clamp(2rem, 5vh, 3rem); }
  .proc__step, .no-motion .proc__step, .proc--flat .proc__step {
    min-height: 0;
    padding: clamp(2rem, 5vh, 2.75rem) 0;
  }
  .proc__step:first-child { padding-top: 0; }
  .proc__thumb {
    display: block;
    margin-bottom: clamp(1.1rem, 3vh, 1.5rem);
    aspect-ratio: 3 / 2;                  /* the four landscapes' own shape */
    overflow: hidden; border-radius: 2px;
    background: var(--paper-2);
  }
  .proc__thumb picture { display: block; width: 100%; height: 100%; }
  .proc__thumb img { width: 100%; height: 100%; object-fit: cover; filter: var(--proc-filter); }
  .proc__step[data-plate="0"] .proc__thumb img { object-position: 50% 52%; }
  .proc__step[data-plate="4"] .proc__thumb img { object-position: 50% 44%; }
}

/* ── the two failure paths ──
   :root.no-motion is the GSAP-CDN-missing hook: main.js line 7 is
   gsap.registerPlugin at top level, so on that path the whole file throws,
   .proc--flat is never added, and CSS has to carry it alone. .proc--flat is
   added by JS on the reduced-motion path. Same shape as .no-motion .cost /
   .cost--flat, including its lesson: reset every fixed size that only made sense
   in the moving layout.
   .proc--flat ties .proc on specificity, so it must come after it in source.
   It does. */
.no-motion .proc, .proc--flat { padding-top: clamp(4rem, 10vh, clamp(7rem, calc(7rem + (100vw - 1440px) * 0.0125), 126px)); }
.no-motion .proc__grid, .proc--flat .proc__grid { display: block; }
.no-motion .proc__sheet, .proc--flat .proc__sheet { display: none; }
.no-motion .proc__media, .proc--flat .proc__media {
  position: static; background: none;
  padding-bottom: 0; margin-bottom: clamp(2rem, 6vh, clamp(3rem, calc(3rem + (100vw - 1440px) * 0.01715), 67.2px));
}
/* The frame becomes a plate strip: five thumbnails at the plates' own 4:3, each
   printing its stage number under it on paper. Photo and step are no longer side
   by side, so the shared number is what pairs them — a real <figcaption>, not a
   CSS counter burnt over the photograph in white with a drop shadow. There is
   not one counter and not one text-shadow anywhere in this sheet, and this is
   the accessibility path, which is the last place to trade house style or
   semantics for convenience.
   The interleaved alternative (display:contents on the wrappers plus order to
   weave figures between list items) needs it on the <ol> as well, which strips
   the list role in exactly the engines that already drop list semantics on
   list-style:none. Hence the strip, stated rather than hidden. */
.no-motion .proc__frame, .proc--flat .proc__frame {
  aspect-ratio: auto; max-block-size: none; overflow: visible; background: none;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(170px, calc(170px + (100vw - 1440px) * 0.08349), 263.5px), 1fr));
  gap: clamp(0.7rem, 1.8vw, clamp(1.1rem, calc(1.1rem + (100vw - 1440px) * 0.006286), 24.64px));
}
.no-motion .proc__plate, .proc--flat .proc__plate {
  position: relative; inset: auto; opacity: 1; transition: none;
}
.no-motion .proc__plate picture, .proc--flat .proc__plate picture {
  height: auto; aspect-ratio: 4 / 3;
  overflow: hidden; border-radius: 2px; background: var(--paper-2);
}
.no-motion .proc__cap, .proc--flat .proc__cap {
  display: block; margin-top: clamp(0.5rem, calc(0.5rem + (100vw - 1440px) * 0.002858), 11.2px);
  font-family: var(--f-mono); font-size: clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.002215), 12.4px);
  letter-spacing: 0.2em; color: var(--ink-3);
}
.no-motion .proc__step, .proc--flat .proc__step {
  min-height: 0; padding: clamp(2rem, 6vh, clamp(3rem, calc(3rem + (100vw - 1440px) * 0.01715), 67.2px)) 0;
}
/* nothing sticks, so nothing is "active": every step reads at full strength */
.no-motion .proc__step h3, .proc--flat .proc__step h3 { color: var(--ink); }
.no-motion .proc__n, .proc--flat .proc__n { color: var(--green-deep); }
.no-motion .proc__n i, .proc--flat .proc__n i { background: var(--green); opacity: 1; }

/* ═══════════════ FOOTER ═══════════════
   See the note in index.html. Two elements of decoration, one block of copy, and
   no new idiom in any of them. */

/* NO `overflow` here, and that is not an omission. The clip the wordmark needs
   would make .foot the sticky band's scroll container, and a sticky box in a
   container that never scrolls never sticks. The clip belongs ON the band —
   .rig__pin's exact arrangement, where sticky, the height and overflow:hidden
   are one element, which is legal because an element's overflow clips its
   DESCENDANTS and not itself. body's own overflow-x does not break it either:
   html's overflow is `visible`, so the value propagates to the VIEWPORT and body
   is never left as a scroll container — which is also why .proc__media sticks
   today. No `position` here either; nothing is positioned against .foot.
   NO top border. The paper->dark cut IS the terminal rule, which is why
   <hr class="proc__close"> is deleted in this same commit: the note it left
   behind in index.html is explicit that the page must not carry two hairlines a
   few rem apart, and a --rule line 90px above a hard tonal edge is exactly that. */
/* ══════════════ THE FOOTER YOU SCROLL INTO ══════════════
   Built to the "Scroll-into Footer Scene" handoff. Three layers:
     .foot        the TRACK — its height IS how far you scroll through the effect
     .foot__stage sticky, pins to one screen, clips
     .foot__scene the card: inset and rounded, its clip-path grown to full bleed
     .foot__in    the real content, on top of the card, fading up at the end
   Every default below is the EXPANDED state, so with no JS the footer simply
   looks finished rather than looking broken. */
.foot {
  position: relative;
  height: 210svh;
  /* PAPER, not ink. The track and the stage are the ground the card is cut out
     of, so they have to be the colour of the page the card is standing on —
     .proc's own --paper. Giving them the card's dark made the clip reveal
     nothing, because there was nothing behind it to see. */
  background: var(--paper);
  color: var(--ink-rev);
}
.foot__stage {
  position: sticky; top: 0;
  height: 100svh;
  background: var(--paper);
  /* clip and not hidden: hidden would make this a scroll container and the
     sticky would then resolve against itself. Learned here the hard way. */
  overflow: clip;
}
/* THE CARD. clip-path and not margin — margin changes the layout box, which
   changes the document height, which is what the scroll progress is measured
   against: a feedback loop between the animation and its own driver. Measured
   before this was understood, it stalled at 95.3% open and left a 3px hairline
   no scrolling could close. */
.foot__scene {
  position: absolute; inset: 0; z-index: 0;
  background: var(--ink-block);
  clip-path: inset(var(--fs-y, 0px) var(--fs-x, 0px) var(--fs-y, 0px)
             round var(--fs-r, 0px));
}
/* THE CONTENT, on the card and arriving after it. The handoff brings it up over
   the last third of the travel, once the card has finished opening — which is
   what makes the card read as the subject and the copy as its answer, rather
   than the two competing. */
.foot__in {
  position: absolute; inset: 0; z-index: 1;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: clamp(104px, 12vw, 160px) var(--pad) clamp(1.5rem, 4vh, 2.5rem);
  opacity: var(--fc, 1);
  transform: translateY(calc((1 - var(--fc, 1)) * 44px));
}
/* THE FALLBACK, which the handoff builds in: when pinning cannot work — the
   content taller than the viewport, as on most phones — or the reader prefers
   reduced motion, the whole thing is an ordinary footer with the scene behind
   it. Nothing is clipped, nothing is faded, nothing is pinned. */
.foot--static { height: auto; }
.foot--static .foot__stage { position: relative; height: auto; overflow: visible; }
/* AT LEAST A SCREEN TALL, the copy at its foot. Shorter than the screen — a phone's
   footer is ~770px of an 844px screen — the page ended on a strip of paper above
   the dark, under the header (Andreas: "a white edge at the top"). lvh and not
   svh: with the address bar slid away the screen is the LARGE viewport, and an
   svh footer left the strip back in exactly that state. */
.foot--static .foot__stage {
  min-height: 100vh; min-height: 100lvh;
  display: flex; flex-direction: column; justify-content: flex-end;
}
.foot--static .foot__scene { clip-path: none !important; }
/* the card is the whole footer when it does not open: full height, over every
   line of the copy — the phone's 62svh band (below) is the moving layout's */
.foot--static .foot__scene { height: auto; }
.foot--static .foot__in { position: relative; opacity: 1; transform: none; }

.foot__scene { transform: none; }
@media (prefers-reduced-motion: reduce) { .foot { --fopen: 1; } }

/* THE BAND, and the whole of the "wordmark alone first, content over it" read.
   A plain position:sticky box — .proc__media's choice, the quietest thing on
   this page — and zero JS. While the footer is entering, the band rides up with
   the page; on the frame .foot's top edge reaches y=0 it locks and stops dead,
   and from there it is the only thing on screen that does not move while the
   copy rises over it. Because the band is a full viewport tall, the content's
   leading edge is at the bottom of the screen on exactly that frame: one screen
   of word alone, then the content. That is not tuned, it falls out of 100svh.
   svh and not vh, matching .rig__pin, .creed__pin and .cost__pin — an iOS URL
   bar sliding away must not re-measure the last screen of the page.
   100svh is also .rig__pin's own box, which is what lets the sheet below use the
   rig's mask percentages instead of numbers tuned against a different frame. */


/* THE SHEET, at the rig's own geometry: 56px pitch, 1px line, two crossed
   repeating-linear-gradients, so it reads as the same drawing board. The mask is
   .rig__grid's verbatim — the band is that element's box, full width by 100svh,
   so the percentages resolve to the same ellipse — and RADIAL rather than a ramp
   for .rig__grid's own reason: a mask that dies on one axis leaves the lines
   running into the other three edges and the whole thing reads as a table rather
   than as a sheet the section is drawn on.
   THE COLOUR IS NOT A COPY AND COULD NOT BE. The rig's --line is --ink at 8%,
   and --ink and --ink-block are the SAME hex, #0e1512 — .cost's own end-note
   already records that — so ported verbatim it composites at 1.0000:1 and
   renders literally nothing. Inverted to --ink-rev, and taken at the rig's
   RENDERED weight rather than its raw alpha, which is .proc__sheet's discipline:
   .rig__grid is ink 8% under opacity 0.5, i.e. 4% effective. MEASURED, --ink-rev
   at 4% on --ink-block is #171e1b, 1.091:1, against the rig's own 4% on --paper
   at #d4d7d5, 1.080:1. The same step, inverted, and a notch quieter than the
   word below it exactly as it is on paper.
   Declared at its final alpha with NO opacity multiplier. .rig__grid needs the
   multiplier only because its ::after stacks a second lattice inside it that the
   opacity has to scale as well; that pointer pool is not ported here, and
   copying the multiplier without the second layer would be copying the scaffold.
   No var(--cabin) in the alpha and no opacity riding --t or --asm either: those
   are .rig-scoped and written by product.js, so out here they would compute this
   element to 0 — the mistake .proc__sheet's own note already caught once. */
.foot__grid {
  position: absolute; inset: 0;
  --line: color-mix(in oklab, var(--ink-rev), transparent 96%);
  background-image:
    repeating-linear-gradient(to right,  var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  -webkit-mask-image: radial-gradient(74% 70% at 50% 52%, #000 0%, transparent 78%);
          mask-image: radial-gradient(74% 70% at 50% 52%, #000 0%, transparent 78%);
  pointer-events: none;
}
/* The rig's pool of light, on the footer's sheet: a second copy of the lattice at
   a stronger line, masked to a circle at the pointer. Two stacked layers rather
   than one composited mask — mask-composite is still uneven across engines and
   this needs no compositing at all. --ink-rev and not --ink, because the ground
   here is --ink-block and --ink is the same hex as it: an ink pool on this
   surface composites at 1.0000:1 and is mathematically absent. Parked off the
   box until a pointer has actually been in the footer, so nothing is lit on a
   touch device that never hovers. */
.foot__grid::after {
  content: "";
  position: absolute; inset: 0;
  --line: color-mix(in oklab, var(--ink-rev), transparent 88%);
  background-image:
    repeating-linear-gradient(to right,  var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  -webkit-mask-image: radial-gradient(circle 200px at var(--px, -9999px) var(--py, -9999px),
                                      #000 0%, transparent 72%);
          mask-image: radial-gradient(circle 200px at var(--px, -9999px) var(--py, -9999px),
                                      #000 0%, transparent 72%);
  pointer-events: none;
}

/* THE NAME. .rig__word's setting — the face, the 800 weight, the -0.04em, the
   line-height 1, the nowrap, the absolute centring — with everything that was
   rig STATE taken out, and with the background-clip:text chassis taken out too.
   That last one is the only rig idiom that does not survive, and dropping it is
   a measurement rather than a preference:
     - The chassis exists so a SECOND layer can be clipped to the same
       letterforms: the pointer spot, color-mix(in oklab, var(--ink),
       transparent 82%). --ink and --ink-block are the same hex, so on this
       ground that spot composites at 1.0000:1 — mathematically absent, not
       merely subtle. (It is quietly absent in the rig's own dark stretch today
       for the same reason; there that stretch is one screen of a 1480svh
       section, and here it would be all of it.) It would also cost a second
       pointermove listener at the bottom of the page, for an effect nobody
       hovers at a footer and that does nothing at all on touch.
     - With the spot gone the chassis has nothing left to host, and plain `color`
       with alpha is the identical pixel in one declaration instead of five. If a
       lit spot is ever wanted here it comes back as the whole chassis with
       --ink-rev in place of --ink, not as a patch on this.
     - opacity: calc((1 - var(--dock) * 0.55) * (1 - var(--fly))) is dropped.
       --dock and --fly are .rig-scoped and written by product.js, so out here
       the calc is invalid at computed-value time and opacity falls back to its
       initial 1. Do not carry an expression that only works by failing.
     - will-change: transform is dropped. .rig__word declares it because
       product.js writes an inline transform to it every scroll frame; nothing
       writes to this one, and a standing compositor-layer request for a static
       element is a cost with no payer.
   THE TONE IS NOT A NEW VALUE. --ink-rev at 5% is exactly what
   .rig.is-armed .rig__word already resolves to at --t 0 — this device, on this
   ground, shipped for months. MEASURED, composited: #19201d, 1.1155:1 against
   the ground, a solid tonal shape a few shades lighter than it, which is what
   the reference has. The sheet then reads THROUGH the letters for free, and
   source order is all it takes: at 5% the word is a wash and not a fill, so a lattice line crossing
   it keeps almost all of its strength. No mask-composite, no z-index, no
   restacking.
   THE SIZE. MEASURED: "SEMVAC" in Schibsted Grotesk 800 at -0.04em advances
   4.0995 em, flat from 80px to 400px. So this expression sets the word edge to
   edge inside the page's own gutters at EVERY width, and there is no clamp()
   floor to burst below — the rig's clamp(6rem, 22vw, 20rem) is precisely what
   makes .rig__word 393.55px wide on a 390px phone. MEASURED, the word's ink
   spans exactly --pad to (viewport less --pad): 20 -> 370 at 390, 64.8 -> 1375.2
   at 1440, 80 -> 2480 at 2560, i.e. 89.7% / 91.0% / 93.8% of the viewport. On a
   machine whose scrollbar TAKES layout width it will sit ~5.5px proud of the
   gutter on each side, because 100vw includes the column :root reserves —
   reasoned from the mechanism, since headless uses overlay scrollbars and cannot
   show it. Invisible either way, and the band clips.
   The fallback faces are all NARROWER than Schibsted (Helvetica Neue 3.974 em,
   Arial 3.964 em), so a missing webfont fails safe; a fallback WIDER
   than it is what the band's clip is actually for. 4.0995 is a property of this
   string in this face — change either and re-measure.
   user-select: none, as on .rig__word and .intro__word: at 5% on dark, a
   drag-select through the address would highlight a screen-wide SEMVAC nobody
   meant to grab. */
.foot__word {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--f-display);
  font-weight: 800;
  letter-spacing: -0.04em;
  /* Smaller. It was sized to the page's gutters, which made it span very nearly
     the full viewport and left the card with no air of its own — the word WAS
     the rectangle rather than sitting inside one. Sized to the card's gutters
     now, and to 78% of that, so there is ground visible around it on every side
     at every width. */
  /* Sized against the CARD's own inset — the handoff's 6vw — rather than a token
     that no longer exists: --gap was declared on the .foot rule the restructure
     replaced, so this calc was resolving against nothing and the word collapsed
     to the browser's minimum. Measured at ~100px wide before this. 0.78 of the
     card's inner width leaves ground visible around it on every side. */
  font-size: calc((100vw - max(20px, 6vw) * 2) * 0.78 / 4.0995);
  line-height: 1;
  /* THE CHASSIS IS BACK, and exactly as the note above predicted it would have to
     come back: as the whole background-clip:text construction with --ink-rev in
     place of --ink, not as a patch on a plain colour. The wash is the same 5% it
     was as a flat colour, now as a layer, so a SECOND layer can be clipped to the
     same letterforms — the spot that follows the pointer.
     --ink-rev and not --ink: --ink and --ink-block are the same hex, so an ink
     spot on this ground composites at 1.0000:1 and is not subtle but absent.
     That measurement is why it was left out in the first place. */
  --wash: color-mix(in oklab, var(--ink-rev), transparent 95%);
  color: transparent;
  -webkit-text-fill-color: transparent;   /* Safari wants this as well as color */
  background-image:
    radial-gradient(circle 200px at var(--wx, -9999px) var(--wy, -9999px),
      color-mix(in oklab, var(--ink-rev), transparent 82%) 0%,
      rgba(0, 0, 0, 0) 72%),
    linear-gradient(var(--wash), var(--wash));
  -webkit-background-clip: text;
          background-clip: text;
  white-space: nowrap;
  -webkit-user-select: none; user-select: none;
  pointer-events: none;
}

/* THE CONTENT. position:relative and NO z-index, and that is the whole of the
   paint-order fix. .foot__scene is sticky and therefore POSITIONED, so it paints
   in step 8 above any non-positioned in-flow sibling whatever the source order —
   the bug .proc's note records for .proc__sheet, and the mobile .proc band
   settles it the same way in as many words: "no z-index needed, and none should
   be added." Making this positioned too puts both in step 8, where DOM order
   decides and the content, being second, wins.
   THE MEASURE IS THE SITE'S, IN TWO PARTS: the gutter here, the --maxw cap on
   the .wrap inside, exactly as .proc does it. Putting both on one box looks
   equivalent and is not — box-sizing is border-box globally, so max-width:--maxw
   WITH the padding gives a content column of --maxw less two gutters, and the
   footer would sit ~55px per side inside every other section on the page and
   ~620px inside its own wordmark on a wide monitor.
   The padding is here and not on .foot so it survives the band's collapse on the
   flat paths without a second rule. */
/* Deliberately NOT re-declaring position or padding: the block up in the footer
   scene owns both, and this rule sits AFTER it in the file. It used to set
   position:relative here, which silently beat the absolute inset:0 above — the
   content box then collapsed to its own 357px at the TOP of the stage, and the
   justify-content:flex-end that was meant to sit it on the bottom had no height
   to work in. MEASURED at five window sizes before this: relative at every one. */

/* .wrap caps and centres; this only sets the tracks. Blurb wide on the left, the
   three columns beside it — the reference's shape, with three columns rather
   than its three LINK columns because only three honest groups exist here.
   No background: the wordmark reading THROUGH the copy is the device. MEASURED
   against it — --ink-rev-2 is 7.93:1 on bare --ink-block, 7.09:1 over the
   wordmark's wash and 6.43:1 at the worst pixel in the footer, a lattice line
   under a letterform under the copy (#222825); --green-lift is 7.58 on the
   ground and 6.80 over the word. Nothing here goes below 6.4:1 against a 4.5:1
   floor. */
.foot__row {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) repeat(3, minmax(0, 1fr));
  gap: clamp(1.6rem, 4vw, 3.5rem);
  align-items: start;
}
.foot__blurb {
  max-width: 40ch;
  font-size: 0.95rem;
  color: var(--ink-rev-2);
  text-wrap: pretty;
}
/* The blurb and, under it, the film: a quiet line in the footer's own label
   type, not a button — it was a pill over the hero's film and drew the eye
   from the headline. 44px tall for a finger; the play mark in the brand's
   green, the running time dimmed. */
.foot__about { display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem; }
.foot__film {
  display: inline-flex; align-items: center; gap: 0.7rem;
  min-height: 44px;
  font-family: var(--f-mono); font-size: 0.68rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-rev);
  transition: color 0.25s var(--ease);
}
.foot__film i {
  width: 0; height: 0;
  border-style: solid; border-width: 5px 0 5px 8px;
  border-color: transparent transparent transparent var(--green-lift);
}
.foot__film b { font-weight: 400; color: var(--ink-rev-2); }
.foot__film:hover span { color: var(--green-lift); }

/* The page's last hairline, and now the only one down here, .proc__close having
   gone. --rule-rev is 1.4707:1 on --ink-block and 1.3185:1 where it crosses the
   wordmark, so it does weaken very slightly along its length as the bar rides up
   over the letterforms. That is the same wash every other mark in this footer
   sits on and it is left alone: an opaque strip behind the bar to protect one
   hairline would be a box the reference does not have, printed over the device
   the whole footer exists for. */
.foot__bar {
  margin-top: clamp(2.5rem, 7vh, 4.5rem);
  padding-top: clamp(1rem, 2.5vh, 1.5rem);
  border-top: 1px solid var(--rule-rev);
}
.foot__bar p {
  font-family: var(--f-mono);
  font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-rev-2);
}

/* ── stacked, at the site's own two breaks, not new ones ── */
@media (max-width: 1023px) {
  .foot__row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .foot__about { grid-column: 1 / -1; margin-bottom: clamp(1rem, 3vh, 2rem); }
  .foot__blurb { max-width: 56ch; }
}
@media (max-width: 760px) {
  .foot__row { grid-template-columns: 1fr; gap: 1.75rem; }
  /* The band is cut here, and it fixes a VERTICAL failure that a width proof
     cannot see. MEASURED at 390x844 with a full-viewport band: the caps are
     61.2px of an 844px frame — 7.2% of it, against 25.4% at 1440x900 — so "you
     are zoomed into it" inverts into a screen of near-empty dark with a small
     word floating in it, and a whole phone screen of scroll is spent getting
     past it. At 62svh the caps are 11.7% of the band and the footer drops from
     1.84 viewports to 1.46. What it costs is the screen of word ALONE: at the
     lock frame the copy's leading edge sits at 62% of the screen rather than at
     its foot, so a third of the first footer screen is already content. On a
     phone that is the better trade. The hold is unaffected — a sticky locks once
     the footer is at least a viewport tall, and 523px of band plus 707px of
     content clears 844 twice over. */
  .foot__scene { height: 62svh; }
}

/* ── the two failure paths ──
   :root.no-motion is the GSAP-CDN-missing hook: main.js line 7 is
   gsap.registerPlugin at top level, so on that path the whole file throws,
   .foot--flat is never added and CSS has to carry it alone. .foot--flat is added
   by JS on the reduced-motion path, which ?skip also takes. Same doubled shape
   as .no-motion .proc / .proc--flat, and .foot--flat ties .foot on specificity
   so it must come after it in source. It does.
   What is removed is the TRAVEL, not the wordmark. .rig--flat display:none's
   .rig__word because that word is a layer inside a 3D scene with no meaning
   without the scene; this one IS the footer's ground, and this is the
   accessibility path — the last place to trade house style for convenience. So
   the band goes static and KEEPS ITS CLIP: the word and the clip move together
   in both directions and never one without the other, which is .rig--flat's own
   rule. position:relative and not static, because .foot__word is absolutely
   positioned in the moving layout and this is its containing block — measured
   with the band static, its offsetParent becomes BODY and it lands in the middle
   of the DOCUMENT. The word goes static in the same breath, so the band sizes to
   it. The SHEET goes: a lattice masked to an ellipse 70% of a box now only as
   tall as one line of type has no field left to be a field of. */
.no-motion .foot__scene, .foot--flat .foot__scene {
  position: relative;
  height: auto;
  padding-block: clamp(2.5rem, 7vh, 4rem);
}
.no-motion .foot__grid, .foot--flat .foot__grid { display: none; }
.no-motion .foot__word, .foot--flat .foot__word {
  position: static; transform: none; text-align: center;
}

/* This sheet's FIRST forced-colors rule, and it earns it. In that mode the UA
   overrides `color` to the system foreground, so a 5% wash becomes a solid,
   full-contrast, screen-wide SEMVAC printed straight through the address and the
   links — and the sheet that would have held it down has gone as well, because
   background-image is dropped there too. Both are decoration and both are
   aria-hidden. .rig__word needs no such guard only because nothing readable sits
   in front of it; this one has the whole footer on top. */
@media (forced-colors: active) {
  .foot__scene { display: none; }
}

/* ─────────────── responsive ─────────────── */

/* the panel's own two breakpoints, as specced — they are not the 1080 the rest
   of the section uses, so they get their own blocks rather than being folded in */
@media (max-width: 1439px) {
  .rig__list li { left: 80px; width: 400px; }
  .rig__list li[data-hot="2"],
  .rig__list li[data-hot="3"] { left: auto; right: 80px; }
  .rig__list h3 { font-size: 40px; }
}

@media (max-width: 1023px) {
  /* No room beside the object: the panel drops under it and takes a foot band
     the full width of the page, 24px margins. */
  /* one foot band, so there is no side to change to */
  .rig__list {
    left: 0; right: 0;
    top: auto; bottom: clamp(1.5rem, 5vh, 3rem);
  }
  .rig__list li,
  .rig__list li[data-hot="2"],
  .rig__list li[data-hot="3"] {
    left: 24px; right: 24px; width: auto;
    top: auto; bottom: 0;
    transform: none;
  }
  /* The panel prints over the live render here, and a 12px mono row set across
     the object's dark castings is not readable. A foot scrim, painted behind the
     items but inside .rig__copy so it rides the same opacity ramp. */
  .rig__copy::after {
    content: "";
    position: absolute; left: 0; right: 0; bottom: 0; z-index: -1;
    /* Solid past the top of the panel, then a ramp. Measured: at 390x844 the
       kicker sits 426px off the foot, so 68svh x 80% = 459px clears it. */
    height: clamp(360px, 68svh, 560px);
    background: linear-gradient(to top, var(--paper) 0%, var(--paper) 80%, transparent 100%);
    pointer-events: none;
  }
}

@media (max-width: 1080px) {
  .rig__word { font-size: clamp(5rem, 32vw, 12rem); }
  /* no free lower-left corner at this width — the copy blocks already occupy the
     foot, so the box goes up top and the panel takes the band below it */
  /* Stacked, not overlaid: the box takes the top band and the panel the one under
     it. The panel grew a headline, a switch and a slide, so it no longer fits in
     whatever was left below a 52svh box. */
  /* Full-bleed is a desktop composition: on a phone the panel would cover the
     object it is describing, so the two stack — the render keeps a band at the
     top and the panel takes the one below it. */
  .rig__box {
    left: var(--pad); right: var(--pad); width: auto;
    top: calc(var(--head-h) + 2rem); bottom: auto;
    height: min(46svh, 82vw);
  }
  .rig.is-cabin .rig__box { inset: auto; left: var(--pad); right: var(--pad);
                            top: calc(var(--head-h) + 2rem); height: min(46svh, 82vw); }
  .rig__note {
    left: var(--pad); right: var(--pad); width: auto; margin-inline: 0;
    top: auto; bottom: clamp(1.5rem, 4vh, 2.5rem); transform: none;
  }
  /* no band deep enough for both — except where product.js finds one (.fits-p2) */
  .rig__note:not(.fits-p2) #rigNoteP2 { display: none; }
  .rig__nav { margin-top: 1rem; padding-top: 0.9rem; }
  .rig__nav button { width: 46px; height: 46px; }
  .rig__tip { width: min(30ch, 62vw); }
}

/* ══ THE RIG ON PHONES AND PORTRAIT TABLETS: A WINDOW OVER A BAND ══
   Andreas, looking at it on a phone: "you can't see the toilet now, it just fades
   out". MEASURED at 390x844: the tour's close-ups were framed for a wide screen and
   cropped off the right edge, and the paper scrim behind the copy (560px, solid for
   80% of it) covered two thirds of the unit. Only the docked beat — the box above,
   the panel below — read well. So every beat is now that beat.
   The pinned screen is two fields that never overlap. .rig__stage itself becomes
   the WINDOW, so the canvas, the markers and the box are one CSS rect and the
   render physically cannot reach the words; product.js (NARROW) frames the whole
   unit into it at every beat. The BAND of copy sits under it, on plain paper: the
   tour's part, the docked panel, "Delivery options" and the cabin's panel all take
   the same slot. No scrim. The corner marks of the box are there from the first
   frame and draw out into the case on the dock, as they already did.
   Every value is a layout of the existing elements — no new words anywhere, so
   the translated pages need nothing.
   THE SCREEN YOU CAN SEE, not the one with the browser's bars out. The pin was
   100svh, and on an iPhone the address bar slides away as you scroll down, so
   through this whole section the visible screen was ~85px taller than the pin
   and that strip under the meter was bare paper — Andreas, on his phone: a lot
   of air under the text. 100dvh follows the bars; the window takes the gain,
   and the canvas is re-fitted on the resize the bars fire anyway. */
@media (max-width: 1023px) {
  .rig__pin { height: 100dvh; }
  .rig {
    --win-top: calc(var(--head-h) + 3.1rem);
    --band-gap: 1.5rem;
    /* under the band: the section meter's line, and room above it */
    --band-foot: calc(max(2rem, 6vh) + 0.9rem);
    /* The band's height is MEASURED by product.js (measureBand): the tallest
       copy it will hold at this width, in this language. This is only the
       value until it has. */
    --band-h: 300px;
    /* The window takes the rest of the screen — never less than a frame the
       unit can be read in, never much taller than it is wide (the unit is
       fitted to a square-ish window; more height would only be air). */
    --win-h: clamp(150px,
      calc(100dvh - var(--win-top) - var(--band-gap) - var(--band-h) - var(--band-foot)),
      min(calc((100vw - 2 * var(--pad)) * 1.1), 64dvh));
    --band-top: calc(var(--win-top) + var(--win-h) + var(--band-gap));
  }
  .rig__stage {
    inset: auto;
    left: var(--pad); right: var(--pad);
    top: var(--win-top); height: var(--win-h);
  }
  .rig__box,
  .rig.is-cabin .rig__box {
    inset: 0; width: auto; height: auto;
    opacity: var(--t);
  }
  /* the case closes on the dock, opens for the flight and closes round the room */
  .rig.is-cabin .rig__box::before,
  .rig.is-cabin .rig__box::after { content: ""; }
  .rig__box::before, .rig__box::after {
    --k: max(calc(var(--dock) * var(--dock) * (1 - var(--open))), var(--cabin));
    width: calc(14px + (100% - 14px) * var(--k));
    height: calc(14px + (100% - 14px) * var(--k));
  }
  .rig.is-cabin .rig__hint { display: none; }
  /* The window is the hint's width, and a line longer than that wraps inside it:
     German's "für das zugehörige Bauteil auf einen Punkt tippen" is 393px set on
     one line, and ran off both edges of the phone. */
  .rig__hint { left: 0.5rem; right: 0.5rem; transform: none; white-space: normal; text-align: center; text-wrap: balance; }
  .rig__hint::after { line-height: 1.3; }
  /* No GSAP: the static path is the dock, and its list still switches part 04 on,
     which printed straight over the panel in the same band. The panel carries it. */
  .rig--static .rig__copy { opacity: 0; }
  .rig__eyebrow { top: calc(var(--win-top) - 1.75rem); }
  /* The ghost name is the arrival's, out of the dark. Once the tour is lit it
     would run straight behind the unit in a window this small — the busiest
     thing in it — so it goes as the lights come up. */
  .rig__word { opacity: calc((1 - var(--t)) * (1 - var(--fly))); }

  /* ── the band ── */
  .rig__copy::after { display: none; }
  .rig__list { top: var(--band-top); bottom: auto; }
  .rig__list li,
  .rig__list li[data-hot="2"],
  .rig__list li[data-hot="3"] {
    left: var(--pad); right: var(--pad); width: auto;
    top: 0; bottom: auto; transform: none;
  }
  /* THE BAND READS AT BODY SIZE. It was 15px copy and 10.5px specs, set to leave
     the window every pixel; the window has the screen's full height now, and the
     band is where the words are, so they get a reading size and room between
     the kicker, the name, the paragraph and the table. */
  .rig__slidebody { gap: 0.9rem; }
  .rig__kicker { font-size: 11px; gap: 10px; letter-spacing: 0.2em; }
  .rig__kicker i { width: 28px; }
  /* one line: the two-line reservation is the desktop panel's, and here a
     name-only part would stand on 80px of nothing */
  .rig__list h3 { font-size: clamp(30px, 8.7vw, 44px); line-height: 1.02; min-height: 0; }
  /* no last word alone on a line ("consumption.", "faible.") */
  .rig__list h3, #rigNoteHead { text-wrap: balance; }
  /* THE SECOND PARAGRAPH, WHERE THERE IS ROOM FOR IT. Every part has two, and a
     phone showed only the first: the band is as tall as the tallest part, so the
     shorter ones stood over an empty field (the ejector, one spec row, left 80-100px
     of it). product.js (measureBand, fitNoteP2) shows the second wherever it fits
     in the band as it is — the band never grows for it, so the window keeps its
     size — and the words are Semvac's own, already on the page. */
  .rig__list li.fits-p2 .rig__p2 { display: block; }
  .rig__list h3 span { display: inline; }
  .rig__list p { font-size: 16px; line-height: 1.6; max-width: 42ch; }
  /* THE SPECS ONE TO A LINE, label then value, between two hairlines. They were
     one row of three hairline cells, label over value — 57px for the three the
     first part had. Semvac's parts now have up to five, and as cells that is two
     rows with their labels broken over two lines: the flush unit's four made it
     the tallest thing in the band, and the band is what the window over it gives
     up — at 360x640 in German the window hit its 150px floor and the table still
     ran 6px into the meter. A line each is ~19px a spec and reads as a datasheet. */
  /* No rule under the table: the section meter sits 12px below the tallest
     part's, and the two read as a double line. */
  .rig__spec {
    display: flex; flex-direction: column; gap: 5px;
    margin-top: 0.35rem; padding: 10px 0 0;
    border-top: 1px solid var(--panel-hairline);
    max-width: 30rem;
  }
  .rig__spec > div,
  .rig__spec > div:last-child {
    display: flex; flex-direction: row; flex-wrap: wrap; align-items: baseline;
    justify-content: flex-start; gap: 0 7px;
    padding: 0; border: 0;
    font-size: 11px; line-height: 1.5; letter-spacing: 0.08em;
  }

  /* the docked panel takes the same band, its switch on the band's foot */
  .rig__note {
    /* its switch on the band's foot — above the meter's line, and clear of the
       next section's curtain, already ~26px into the frame at the cabin's beat */
    top: var(--band-top); bottom: var(--band-foot);
    display: flex; flex-direction: column; transform: none;
  }
  /* The cabin's points in the panel too, not in the floating card that covered
     the room: setSel() already falls back to the card only when the panel is
     not rendered. */
  .rig.is-cabin .rig__note {
    display: flex;
    opacity: calc(var(--cabin) * (1 - var(--say)));
  }
  .rig__nkick { font-size: 11px; margin-bottom: 1rem; }
  #rigNoteHead { font-size: clamp(26px, 7.4vw, 38px); margin-bottom: 0.9rem; }
  #rigNoteP1, #rigNoteP2 { font-size: 16px; line-height: 1.6; margin-bottom: 0; max-width: 42ch; }
  .rig__note.fits-p2 #rigNoteP1 { margin-bottom: 0.75rem; }
  /* the copy never runs onto the arrows' rule: MEASURED, the ejector's paragraph
     touched it (0px) in all three languages, being the panel the band is sized to */
  .rig__nbody { margin-bottom: 1.1rem; }
  .rig__nav { margin-top: auto; padding-top: 1rem; border-top-color: var(--panel-hairline); }
  /* the count over the part's name, not "01 / 04 ·" with the name wrapping under
     the dot — or the dot starting the second line, in German */
  .rig__ndot { display: none; }
  .rig__ncount em { display: block; }
  /* the switch is the only way to the parts whose markers overlap, so it has to be seen */
  .rig__nav button { border-color: var(--panel-hairline); }

}
/* A short screen — and that is most iPhones in Safari with its bars showing, about
   664px tall. The specs STAY: housing, weight and pressure are what a buyer came
   for. The window and the type give way instead. */
@media (max-width: 1023px) and (max-height: 700px) {
  .rig {
    --win-top: calc(var(--head-h) + 2.6rem);
    --band-gap: 1.1rem;
  }
  .rig__slidebody { gap: 0.7rem; }
  .rig__list h3 { font-size: 30px; }
  .rig__list p, #rigNoteP1, #rigNoteP2 { font-size: 15px; line-height: 1.5; }
  .rig__spec { gap: 4px; padding-top: 9px; }
  .rig__spec > div, .rig__spec > div:last-child { font-size: 10.5px; }
  #rigNoteHead { font-size: 25px; margin-bottom: 0.7rem; }
  .rig__nkick { margin-bottom: 0.8rem; }
  .rig__nav { padding-top: 0.8rem; }
  .rig__nav button { width: 42px; height: 42px; }
}
/* 640px and under — a small Android, a zoomed iPhone — keep the compact band
   they had: the reading sizes above cost these screens the window. MEASURED in
   German at 360x640 the band reached the meter with the window on its floor. */
@media (max-width: 1023px) and (max-height: 640px) {
  .rig { --band-gap: 0.9rem; }
  .rig__slidebody { gap: 0.5rem; }
  .rig__list h3 { font-size: 28px; }
  .rig__list p, #rigNoteP1, #rigNoteP2 { font-size: 14px; line-height: 1.45; }
  .rig__spec { gap: 3px; padding-top: 6px; }
  .rig__nbody { margin-bottom: 0.75rem; }
  .rig__spec > div, .rig__spec > div:last-child { font-size: 10px; }
  #rigNoteHead { font-size: 23px; margin-bottom: 0.5rem; }
  .rig__nkick { margin-bottom: 0.75rem; }
  .rig__nav { padding-top: 0.6rem; }
}
/* Shorter than 620px (an iPhone SE in Safari is 553): no room for the table under
   the paragraph as well — the rule the site had before this layout, put back after
   it, since the band's grid above would otherwise override it. */
@media (max-width: 1023px) and (max-height: 620px) {
  .rig__spec { display: none; }
}
/* and shorter still: the panel's kicker repeats what the counter by the arrows and
   the eyebrow above the window already say, and its line is the window's */
@media (max-width: 1023px) and (max-height: 600px) {
  .rig__nkick { display: none; }
}
/* A folded phone (280px): a value never breaks mid-word ("4.5–6 BAR" did, in the
   cells this list replaced); the pair wraps before it instead. */
@media (max-width: 339px) {
  .rig__spec dd { white-space: nowrap; }
}
/* A PHONE ON ITS SIDE has no height for a window over a band: at 844x390 the band
   began 330px down and the arrows were below the screen. So the two stand side by
   side — the window on the left, full height, the copy on the right. */
@media (max-width: 1023px) and (max-height: 500px) and (orientation: landscape) {
  .rig {
    --win-top: calc(var(--head-h) + 1.9rem);
    --win-h: calc(100svh - var(--win-top) - 2.75rem);
    --band-top: var(--win-top);
    --band-foot: 1.25rem;
  }
  .rig__stage { right: calc(50% + 0.75rem); }
  .rig__list { left: calc(50% + 0.75rem - var(--pad)); right: 0; }
  .rig__note { left: calc(50% + 0.75rem); right: var(--pad); }
  .rig__eyebrow { top: calc(var(--win-top) - 1.5rem); }
}

@media (max-width: 760px) {
  .menu__foot { grid-template-columns: 1fr; gap: 1.4rem; }
  .brand__name { display: none; }
}


/* set when the GSAP CDN did not arrive: the page has to stand up without it */
:root.no-motion .intro { display: none; }
:root.no-motion body.is-locked { overflow: auto; height: auto; }
:root.no-motion .head { opacity: 1; transform: none; }
:root.no-motion .hero__eyebrow,
:root.no-motion .hero__cta,
:root.no-motion .hero__veil,
:root.no-motion .hero__scroll { opacity: 1; }
:root.no-motion .hero__title .ln > span { transform: none; }
:root.no-motion .reveal { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  /* same reasoning as the flat path: a line that never rises cannot be read */
  .rig__ln > i { animation: none; transform: none; }
  .intro { display: none; }
  body.is-locked { overflow: auto; height: auto; }
  .head { opacity: 1; transform: none; }
  .hero__eyebrow, .hero__cta, .hero__veil, .hero__scroll { opacity: 1; }
  .hero__title .ln > span { transform: none; }
  .reveal { opacity: 1; transform: none; }
  .hero__scroll i::after, .creed__scroll i::after { animation: none; }
  /* the states stay, the travel between them goes. The sticky frame is untouched:
     sticky is LAYOUT, not animation, and this section has no scrub, no pin and no
     transform to disable. */
  .proc__plate, .proc__step h3, .proc__n, .proc__n i, .proc__w { transition: none; }
  /* States stay correct, the travel between them goes. !important because the
     delays these cancel are set at higher specificity
     (.trigger[aria-expanded="true"] .x, and the panel's item stagger). */
  .trigger, .trigger__layer, .trigger__fill, .trigger__icon, .route, .brand__ink,
  .lang__button, .lang__caret, .lang__rule, .lang__panel, .lang__item {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
  .trigger:hover .route--long, .trigger:hover .route--short { animation: none; }
}

/* ══════════════ CONTACT (contact.html) ══════════════
   The people, small: up to five cards to a row, so the team can grow without
   the page changing shape. The page is paper from top to bottom, on the same
   drawing board the rig and the workshop stand on, so the header never has to
   knock out until the footer's card arrives. */
.contact {
  position: relative;
  isolation: isolate;
  background: var(--paper);
  padding: calc(var(--head-h) + clamp(2rem, 7vh, 5rem)) var(--pad) clamp(5rem, 14vh, 9rem);
  overflow: hidden;
}
.contact__sheet {
  position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in oklab, var(--ink), transparent 93%) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklab, var(--ink), transparent 93%) 1px, transparent 1px);
  background-size: 56px 56px;
  -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 38%, #000 25%, transparent 78%);
          mask-image: radial-gradient(ellipse 70% 60% at 50% 38%, #000 25%, transparent 78%);
}
.contact__head {
  max-width: var(--maxw);
  margin: 0 auto clamp(2rem, 6vh, 4rem);
}
.contact__head h1 {
  margin-top: 1.1rem;
  max-width: 13ch;
  font-size: clamp(2.4rem, 5.2vw, 4.6rem);
  line-height: 0.97; letter-spacing: -0.036em;
}
.contact__lede {
  margin-top: 1.4rem;
  max-width: 46ch;
  font-size: clamp(1rem, 1.2vw, 1.12rem);
  line-height: 1.6;
  color: var(--ink-2);
}

.team { max-width: var(--maxw); margin: 0 auto; }
.team__title { line-height: inherit; }   /* .eyebrow's, not the h2's 1.02 */
.team__grid {
  --gap: clamp(0.75rem, 1.3vw, 1.25rem);
  display: grid;
  /* AT MOST FIVE TO A ROW, and never narrower than 9.5rem. The max() is the
     width five columns would have; auto-fill drops a column whenever that falls
     under the floor, so a laptop keeps five, a tablet four and a phone two. The
     half pixel stops rounding from tipping an exact five into four. auto-fill
     and not auto-fit: three people keep a card's width, not three fifths each. */
  grid-template-columns: repeat(auto-fill, minmax(max(9.5rem, calc((100% - 4 * var(--gap)) / 5 - 0.5px)), 1fr));
  gap: var(--gap);
  margin: clamp(1rem, 2.4vh, 1.6rem) 0 0; padding: 0;
  list-style: none;
}

/* The whole card is the button — see .person__reveal::after — so a card is
   clicked where it is, not aimed at. */
.person {
  position: relative;
  display: flex; flex-direction: column;
  background: var(--card);
  border: 1px solid var(--rule);
  transition: border-color 0.25s var(--ease);
}
.person:not(.is-open):hover { border-color: color-mix(in oklab, var(--ink), transparent 70%); }
.person__photo {
  position: relative;
  /* 4:5, a hair squarer than the photographs (368 × 482): a portrait gives up
     a sliver of jacket at the foot and none of the head. */
  aspect-ratio: 4 / 5;
  display: grid; place-items: center;
  overflow: hidden;
  background:
    linear-gradient(to right, color-mix(in oklab, var(--ink), transparent 94%) 1px, transparent 1px) 0 0 / 20px 20px,
    linear-gradient(to bottom, color-mix(in oklab, var(--ink), transparent 94%) 1px, transparent 1px) 0 0 / 20px 20px,
    var(--paper-2);
}
/* The <picture> absolute, out of the grid: as a grid item it made a second row
   and pushed the initials 55px off centre — and display: contents only handed
   that row to its <source>, which no UA sheet hides. */
.person__photo picture { position: absolute; inset: 0; }
.person__photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%; }
/* Under the photograph: the initials, faint on the board — what shows while it
   loads or if it fails, and all a new person has until theirs comes. */
.person__initials {
  font-family: var(--f-display); font-weight: 800;
  font-size: clamp(1.8rem, 2.6vw, 2.6rem);
  letter-spacing: -0.05em;
  color: color-mix(in oklab, var(--ink), transparent 84%);
}
.person__body {
  flex: 1;
  display: flex; flex-direction: column;
  padding: 0.8rem 0.85rem 0.85rem;
}
.person__name {
  font-size: clamp(0.95rem, 1.1vw, 1.06rem);
  line-height: 1.15; letter-spacing: -0.012em;
}
.person__role {
  margin-top: 0.35rem;
  font-family: var(--f-mono); font-size: 0.58rem;
  line-height: 1.5; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-3);
}
/* As tall as the two lines it opens into, so a card does not grow on click. */
.person__contact {
  margin-top: auto; padding-top: 0.85rem;
  min-height: 3.4rem;
  display: flex; flex-direction: column; justify-content: flex-end;
}
.person__reveal {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  width: 100%;
  padding: 0.5rem 0.65rem 0.48rem;
  border: 1px solid var(--ink);
  font-family: var(--f-mono); font-size: 0.6rem; line-height: 1.5;
  letter-spacing: 0.12em; text-transform: uppercase; text-align: left;
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}
.person__reveal i { font-style: normal; font-size: 0.8rem; line-height: 1; }
.person__reveal::after { content: ""; position: absolute; inset: -1px; }
.person:hover .person__reveal,
.person__reveal:focus-visible { background: var(--ink); color: var(--paper); }
.person__reveal:focus-visible { outline: none; }
.person__reveal:focus-visible::after { outline: 2px solid var(--green-deep); outline-offset: 2px; }
/* flex outranks the hidden attribute's UA display:none */
.person__reveal[hidden], .person__details[hidden] { display: none; }

/* THE REVEAL: each line slides up out of its own line box, the plain text
   reveal the home page's titles use. A keyframe and not a transition, because
   the lines arrive from display:none and a transition has nothing to start from. */
.person__details { display: grid; gap: 0.1rem; }
.person__line {
  display: block; width: fit-content; max-width: 100%;
  overflow: hidden;
  padding-block: 0.08em;        /* room for the descenders the clip would cut */
  font-size: 0.8rem; font-weight: 500; line-height: 1.35;
  overflow-wrap: anywhere;
  transition: color 0.2s var(--ease);
}
.person__line span { display: block; animation: personLine 0.75s var(--ease) both; }
.person__line + .person__line span { animation-delay: 0.09s; }
.person__line:hover { color: var(--green-deep); }
.person__line:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 2px; }
@keyframes personLine { from { transform: translateY(110%); } }

/* ARRIVING: a short fade up as the page opens, a beat apart. A keyframe with a
   backwards fill, and no script: MEASURED, a class added by js/contact.js came
   137ms after the cards had already been drawn, so they were seen, hidden and
   faded back — and the transition to hidden ran first and swallowed the fade. */
.person { animation: personIn 0.7s var(--ease) both; }
.person:nth-child(2) { animation-delay: 0.07s; }
.person:nth-child(3) { animation-delay: 0.14s; }
.person:nth-child(4) { animation-delay: 0.21s; }
.person:nth-child(n+5) { animation-delay: 0.28s; }
@keyframes personIn { from { opacity: 0; transform: translateY(14px); } }

@media (prefers-reduced-motion: reduce) {
  .person, .person__line span { animation: none; }
}

/* Semvac's line under the globe's headline; the section is dark, so it takes
   the reversed secondary ink rather than --ink-2. */
.refs__lede {
  margin-top: clamp(0.85rem, calc(0.85rem + (100vw - 1440px) * 0.004858), 1.19rem);
  max-width: 54ch;
  font-size: clamp(0.9rem, 1.05vw, clamp(0.945rem, calc(0.945rem + (100vw - 1440px) * 0.0045), 1.26rem));
  line-height: 1.55;
  color: var(--ink-rev-2);
}

/* ══════════════ SUBPAGES (products, capabilities, service) ══════════════
   One system for every page under the front page: a lede on the drawing board,
   then blocks that put the section's name on the left and its substance on the
   right, and one dark band per page where the page has something to say rather
   than something to list. Everything here is built from the tokens the front
   page already uses, so a subpage is recognisably the same site — the only new
   idea is the two-column block, which exists because these pages are lists of
   facts and a single column of them reads as a specification sheet.
   The copy on these pages is Semvac's, from their product presentation; where
   they have not written something, the page says nothing rather than guessing.
   Anything of ours is marked data-copy="draft". */
.lede {
  position: relative;
  isolation: isolate;
  background: var(--paper);
  padding: calc(var(--head-h) + clamp(2rem, 7vh, 4.5rem)) var(--pad) clamp(2.5rem, 7vh, 4.5rem);
  overflow: hidden;
}
.lede__sheet {
  position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in oklab, var(--ink), transparent 93%) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in oklab, var(--ink), transparent 93%) 1px, transparent 1px);
  background-size: 56px 56px;
  -webkit-mask-image: radial-gradient(ellipse 70% 62% at 40% 36%, #000 22%, transparent 78%);
          mask-image: radial-gradient(ellipse 70% 62% at 40% 36%, #000 22%, transparent 78%);
}
.lede__in { max-width: var(--maxw); margin: 0 auto; }
.lede h1 {
  margin-top: 1.1rem;
  max-width: 16ch;
  font-size: clamp(2.2rem, 4.6vw, 4rem);
  line-height: 0.98; letter-spacing: -0.036em;
}
.lede__p {
  margin-top: 1.3rem;
  max-width: 54ch;
  font-size: clamp(1rem, 1.2vw, 1.12rem);
  line-height: 1.6;
  color: var(--ink-2);
}

.blk { background: var(--paper); padding: clamp(2.6rem, 7vh, 4.5rem) var(--pad); }
.blk + .blk { padding-top: 0; }
.blk--dark {
  background: var(--ink-block);
  color: var(--ink-rev);
  padding-block: clamp(3.4rem, 9vh, 6rem);
}
.blk--dark + .blk { padding-top: clamp(2.6rem, 7vh, 4.5rem); }
.blk__in {
  max-width: var(--maxw); margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.1rem, 2.4vw, 2rem);
  padding-top: clamp(1.6rem, 4vh, 2.6rem);
  border-top: 1px solid var(--rule);
}
.blk--dark .blk__in { border-top-color: var(--rule-rev); }
@media (min-width: 900px) {
  .blk__in { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.5fr); gap: clamp(2rem, 4vw, 4rem); }
}
.blk__head h2 {
  margin-top: 0.9rem;
  font-size: clamp(1.45rem, 2.2vw, 2rem);
  line-height: 1.06; letter-spacing: -0.028em;
  max-width: 18ch;
}
.blk__body { display: flex; flex-direction: column; gap: 1.15rem; min-width: 0; }
.blk__body > p { max-width: 62ch; font-size: clamp(0.98rem, 1.1vw, 1.06rem); line-height: 1.62; color: var(--ink-2); }
.blk--dark .blk__body > p { color: var(--ink-rev-2); }
/* --green-deep is 2.9:1 on the dark ground and the eyebrow is 0.68rem in
   capitals; the reversed green is what the rest of the site uses there. */
.blk--dark .eyebrow { color: var(--green-lift); }
.blk--dark .eyebrow::before { background: var(--green-lift); }
.blk__body > p strong { color: var(--ink); font-weight: 600; }
.blk--dark .blk__body > p strong { color: var(--ink-rev); }

/* A list of what a product IS or DOES. The dash is the eyebrow's rule, at the
   size of a line of text — the same mark the site uses to open a section. */
.ticks { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.ticks li {
  position: relative;
  padding-left: 1.6rem;
  font-size: clamp(0.95rem, 1.05vw, 1.02rem);
  line-height: 1.5;
}
.ticks li::before {
  content: ""; position: absolute; left: 0; top: 0.72em;
  width: 0.9rem; height: 2px; background: var(--green);
}
.ticks--two { grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 0.6rem clamp(1rem, 2.4vw, 2rem); }

/* Figures with a number, like the rig's spec rows. */
.spec { display: flex; flex-direction: column; margin: 0.3rem 0 0; }
.spec > div {
  display: flex; justify-content: space-between; gap: 20px;
  padding: 0.75rem 0;
  border-top: 1px solid var(--rule);
  font-family: var(--f-mono);
  font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase;
}
.spec > div:last-child { border-bottom: 1px solid var(--rule); }
.spec dt { color: var(--ink-3); }
.spec dd { color: var(--ink); text-align: right; }
.blk--dark .spec > div { border-color: var(--rule-rev); }
.blk--dark .spec dt { color: var(--ink-rev-2); }
.blk--dark .spec dd { color: var(--ink-rev); }

/* Three or four things of the same kind, side by side. */
.cards {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  gap: clamp(0.8rem, 1.4vw, 1.2rem);
}
.card {
  display: flex; flex-direction: column; gap: 0.5rem;
  padding: clamp(0.95rem, 1.5vw, 1.25rem);
  background: var(--card); border: 1px solid var(--rule);
}
.card h3 { font-size: 1rem; line-height: 1.2; letter-spacing: -0.012em; }
.card p { font-size: 0.9rem; line-height: 1.5; color: var(--ink-2); }
.blk--dark .card { background: color-mix(in oklab, var(--ink-rev), transparent 94%); border-color: var(--rule-rev); }
.blk--dark .card p { color: var(--ink-rev-2); }

/* ══════════════ THE WAY ON ══════════════
   The pages this one stands next to, and the way to a person. It was four
   one-line links in a four-column grid, each with its arrow pushed to the far
   side of a 300px column by space-between — a thin strip of small type between
   the last section and the footer, which is what it looked like: an afterthought
   nobody laid out. It is an INDEX now, and it is set like one: the page titles
   at heading size, one to a line, a hairline between them, and the site's own
   kicker dash growing into the row on hover so the title steps aside for it.
   Same markup, same four links, same place in the page — the weight is what
   changed, because the weight is what was wrong. */
.next {
  background: var(--paper); padding: 0 var(--pad) clamp(3.5rem, 9vh, 6rem);
  position: relative; overflow: clip;
}
/* ── THE FOOTER'S SHEET, COMING UP THROUGH THE PAPER ──
   The lattice under the footer's wordmark, drawn on the last paper on the page
   so the ground has already changed before the dark does. It is the RIG's
   version of it and not the footer's: the footer draws --ink-rev lines on
   --ink-block, and out here the ground is --paper, where an --ink-rev line
   composites at 1.0000:1 and is mathematically absent. Same 56px pitch, same
   two stacked layers, same reason for stacking them — mask-composite is still
   uneven across engines and this needs no compositing at all.

   The element is the sheet and its ::after is the pool of light at the pointer,
   which is the shader the footer has. Both ride --grid, which js/reveal.js
   lifts from 0 when the block comes into the window, so the lattice arrives
   rather than being printed there, and the pool is parked off the box until a
   pointer has actually been in this block, so nothing is lit on a touch device
   that never hovers. With no script there is no --grid and no grid: it is
   decoration, and decoration is the one thing that may simply not turn up.

   Masked from the BOTTOM. The rig masks to a field at its centre because the
   object sits in the middle of it; here the thing the sheet belongs to is
   BELOW — the footer — so it is strongest along the bottom edge and gone by
   the top, and reads as the footer's own ground rising into the page. */
.next__grid {
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  opacity: var(--grid, 0);
  transition: opacity 1.2s var(--ease);
  /* MEASURED on the rig: --rule on paper is a ten-level difference and came out
     at 1.6 levels under its resting opacity — a grid nobody can see. Ink at 8%
     is sixteen levels before the opacity, and 8% under 0.55 is the 4.4%
     effective the rig's own sheet settles at. */
  --line: color-mix(in oklab, var(--ink), transparent 92%);
  background-image:
    repeating-linear-gradient(to right,  var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  -webkit-mask-image: radial-gradient(125% 105% at 50% 100%, #000 0%, transparent 74%);
          mask-image: radial-gradient(125% 105% at 50% 100%, #000 0%, transparent 74%);
}
/* the pool: the same lattice at a stronger line, masked to a circle at the
   pointer. 20% under the same 0.55 is 11% effective, which is where the rig's
   own pool lands. It is a CHILD of the sheet and not a second pseudo-element on
   .next, so the sheet's own mask clips it — a pool drawn near the top of the
   block would otherwise be cut flat by the block's edge, and compositing two
   masks on one box is the thing the footer's note refuses to do. */
.next__grid::after {
  content: "";
  position: absolute; inset: 0;
  --line: color-mix(in oklab, var(--ink), transparent 80%);
  background-image:
    repeating-linear-gradient(to right,  var(--line) 0 1px, transparent 1px 56px),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 56px);
  -webkit-mask-image: radial-gradient(circle 200px at var(--px, -9999px) var(--py, -9999px),
                                      #000 0%, transparent 72%);
          mask-image: radial-gradient(circle 200px at var(--px, -9999px) var(--py, -9999px),
                                      #000 0%, transparent 72%);
  pointer-events: none;
}
.next.is-lit .next__grid { --grid: 0.55; }
@media (prefers-reduced-motion: reduce) {
  .next__grid { transition: none; }
}
.next__in {
  position: relative; z-index: 1;     /* over the sheet, not under it */
  max-width: var(--maxw); margin: 0 auto;
  padding-top: clamp(2.2rem, 6vh, 3.6rem);
  border-top: 1px solid var(--rule);
}
.next__list { list-style: none; margin: clamp(1.1rem, 3vh, 1.8rem) 0 0; padding: 0; }
.next__list li { border-top: 1px solid var(--rule-soft); }
.next__list a {
  display: flex; align-items: center;
  padding: clamp(0.8rem, 2vh, 1.2rem) 0;
  font-size: clamp(1.1rem, 2.3vw, 1.8rem);
  line-height: 1.1; letter-spacing: -0.028em;
  /* 500, not the 700 the headings use: these are the page's exits, and at this
     size the heading weight would read as four more headlines. */
  font-weight: 500;
  transition: color 0.25s var(--ease);
}
/* the dash the whole site uses to open a kicker, drawn here on hover. It takes
   its width from nothing, so the row's type slides aside to let it in rather
   than the row growing a column it does not need when nobody is pointing. */
.next__list a::before {
  content: ""; flex: none;
  width: 0; height: 1px; margin-right: 0;
  background: var(--green-deep);
  transition: width 0.35s var(--ease), margin-right 0.35s var(--ease);
}
.next__list a i {
  margin-left: auto; padding-left: 1rem;
  font-style: normal; font-size: 0.78em; color: var(--green-deep);
  transition: transform 0.3s var(--ease);
}
.next__list a:hover { color: var(--green-deep); }
.next__list a:hover::before { width: clamp(0.9rem, 1.8vw, 1.5rem); margin-right: clamp(0.5rem, 1vw, 0.8rem); }
.next__list a:hover i { transform: translateX(6px); }
.next__list a:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }
/* THE PAGE'S LAST WORD, and it is bigger than the index above it on purpose:
   the index is where else you could go, this is the one thing to do. */
.ask-us {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1rem clamp(1rem, 3vw, 2.5rem);
  margin-top: clamp(2.2rem, 5vh, 3.2rem);
  padding-top: clamp(1.6rem, 3.5vh, 2.2rem);
  border-top: 1px solid var(--rule);
}
.ask-us p { font-size: clamp(1.25rem, 2.4vw, 1.95rem); line-height: 1.1; letter-spacing: -0.028em; font-weight: 700; max-width: 24ch; text-wrap: balance; }

/* The door out of a step, and the one thing in .proc that is not copy: it sits
   beside the h3 and the p rather than inside them, because main.js splits both
   of those into one span per word for the lighting. It carries the step's own
   dimming with it — the rule below is the same colour the unlit steps use. */
.proc__link {
  display: inline-flex; align-items: baseline; gap: 0.55em;
  margin-top: clamp(1rem, calc(1rem + (100vw - 1440px) * 0.005715), 22.4px);
  font-family: var(--f-mono); font-size: clamp(0.68rem, calc(0.68rem + (100vw - 1440px) * 0.002429), 13.6px);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--green-deep);
  transition: color 0.3s var(--ease), opacity 0.3s var(--ease);
}
.proc__link i { font-style: normal; transition: transform 0.25s var(--ease); }
.proc__link:hover i { transform: translateX(4px); }
.proc__link:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }
.proc__step:not(.is-on) .proc__link { opacity: 0.35; }
@media (prefers-reduced-motion: reduce) {
  .proc__link { transition: none; }
}

/* A card that is a door: the hub pages are lists of destinations, so the whole
   card takes the click and the arrow is the only ornament. Same idiom as the
   contact page's person cards — a stretched pseudo-element, not a wrapping
   <a>, so the heading keeps its own semantics. */
.card--link { position: relative; transition: border-color 0.25s var(--ease); }
.card--link h3 a::after { content: ""; position: absolute; inset: -1px; }
.card--link h3 i {
  font-style: normal; display: inline-block;
  margin-left: 0.4em; color: var(--green-deep);
  transition: transform 0.25s var(--ease);
}
.card--link:hover { border-color: color-mix(in oklab, var(--ink), transparent 70%); }
.card--link:hover h3 i { transform: translateX(4px); }
.card--link h3 a:focus-visible { outline: none; }
.card--link h3 a:focus-visible::after { outline: 2px solid var(--green-deep); outline-offset: 2px; }
.blk--dark .card--link h3 i { color: var(--green-lift); }
.blk--dark .card--link:hover { border-color: color-mix(in oklab, var(--ink-rev), transparent 60%); }
/* A section that is waiting for its material: the heading stands, and the line
   under it says so plainly. Marked draft in the markup as well. */
.awaiting { color: var(--ink-3); font-style: italic; }

/* ══════════════ SUBPAGE HERO: ONE SCREEN, ONE PHOTOGRAPH ══════════════
   A product page opens the way the front page does: a full screen of the thing
   itself, a veil over it so the words can be read, and the name in the middle.
   The photograph is Semvac's own and it is the page's subject, not decoration —
   the cabins page opens inside a finished cabin.
   The picture is taller than the screen and drifts a little as you leave it
   (js/page.js); the extra height is the -10% inset below, so the drift never
   pulls the frame off the bottom edge. The veil is heavier at the top and the
   bottom than in the middle: the header sits in the first, the scroll cue in
   the last, and the middle is where the photograph should still be a
   photograph. */
.fhero {
  position: relative;
  isolation: isolate;
  min-height: 100svh;
  display: grid; align-items: center; justify-items: center;
  padding: calc(var(--head-h) + clamp(2rem, 6vh, 4rem)) var(--pad) clamp(4rem, 11vh, 7rem);
  background: var(--ink-block);
  color: var(--ink-rev);
  overflow: hidden;
  text-align: center;
}
/* THE BOX IS TALLER THAN THE SCREEN AT BOTH ENDS, and that is what keeps the
   ground from showing. The picture drifts up by 6% of its own height as the
   reader leaves; with the box ending flush at the bottom (inset: … 0) those 6%
   came off the bottom edge and the section's own near-black showed through as a
   band. MEASURED: 66px of it in a 900px window. 12% of slack below and 8%
   above leaves the edge outside the frame at either end of the travel, and the
   picture is scaled a little as it goes, which covers the last hair of it. */
.fhero__bg { position: absolute; inset: -8% 0 -12%; z-index: -2; will-change: transform; }
/* <picture> is inline by default, so height:100% on the image resolved against
   an auto-height parent and the photograph filled a fifth of a phone's screen
   with flat dark under it. MEASURED on a 390px screen. */
.fhero__bg picture { display: block; width: 100%; height: 100%; }
.fhero__bg img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 52%; }
.fhero__veil {
  position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(ellipse 70% 55% at 50% 52%, rgba(8, 13, 11, 0.5) 0%, rgba(8, 13, 11, 0.12) 75%),
    linear-gradient(to bottom, rgba(8, 13, 11, 0.6) 0%, rgba(8, 13, 11, 0.3) 34%, rgba(8, 13, 11, 0.38) 62%, rgba(8, 13, 11, 0.66) 100%);
}
.fhero__in { display: grid; justify-items: center; gap: 1.15rem; max-width: 62ch; }
.fhero h1 {
  max-width: 15ch;
  font-size: clamp(2.5rem, 6vw, 5rem);
  line-height: 0.95; letter-spacing: -0.04em;
  text-shadow: 0 2px 30px rgba(8, 13, 11, 0.35);
}
.fhero__lede {
  max-width: 52ch;
  font-size: clamp(1rem, 1.25vw, 1.14rem);
  line-height: 1.6;
  /* not --ink-rev-2: this line sits on a photograph, not on a flat dark ground,
     and the grey that is comfortable on ink-block disappears over a lit wall */
  color: color-mix(in oklab, var(--ink-rev), transparent 12%);
  text-shadow: 0 1px 18px rgba(8, 13, 11, 0.45);
}
.fhero .btn { margin-top: 0.7rem; }
.fhero__scroll {
  position: absolute; left: 50%; bottom: clamp(1.2rem, 3.5vh, 2.2rem);
  transform: translateX(-50%);
  display: grid; justify-items: center; gap: 0.6rem;
  font-family: var(--f-mono); font-size: 0.62rem;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-rev-2);
}
.fhero__scroll i { display: block; width: 1px; height: clamp(24px, 4vh, 40px); background: var(--ink-rev-2); opacity: 0.6; }
.fhero__scroll:hover { color: var(--ink-rev); }
.fhero__scroll:focus-visible { outline: 2px solid var(--green-lift); outline-offset: 6px; }

/* ══════════════ ONE AT A TIME: THE STEPPED SECTION ══════════════
   THE ONE LOCKED SECTION ON A SUBPAGE. The section holds still and the reader
   scrolls through its steps — one at a time, the full width of the page, with
   room for the photograph AND for something to read about it. It replaced a
   sideways rail of three cards: at three abreast there was space for a line
   each, which is exactly what Andreas said was missing.
   The steps are stacked in one grid cell, so the section is as tall as its
   tallest step and nothing jumps as they change. Everything is a class on the
   section — js/page.js only counts the scroll and moves .is-on — so without the
   script, or under reduced motion, the same markup is the three steps in a
   column, one after another, and nothing is lost. */
.steps { background: var(--paper); }
.steps__pin {
  min-height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(1.2rem, 3.2vh, 2.2rem);
  padding: clamp(3rem, 7vh, 4.5rem) var(--pad);
  overflow: hidden;
}
.steps__head { max-width: var(--maxw); margin: 0 auto; width: 100%; }
.steps__head h2 {
  margin-top: 0.9rem;
  max-width: 20ch;
  font-size: clamp(1.7rem, 3vw, 2.6rem);
  line-height: 1.04; letter-spacing: -0.03em;
}
.steps__stage { display: grid; max-width: var(--maxw); margin: 0 auto; width: 100%; }
.steps__item {
  grid-area: 1 / 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.1rem, 2.6vw, 2.6rem);
  align-items: center;
}
@media (min-width: 900px) {
  .steps__item { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }
}
/* live: one step is shown, the others are held out of the way and out of the
   accessibility tree — visibility, not opacity alone, so nothing under the
   visible step can be tabbed into or read out */
.steps.is-live .steps__item {
  opacity: 0; visibility: hidden;
  transform: translateY(14px);
  transition: opacity 0.45s var(--ease), transform 0.6s var(--ease), visibility 0s linear 0.45s;
}
.steps.is-live .steps__item.is-on {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 0.5s var(--ease), transform 0.7s var(--ease), visibility 0s;
}
.steps__fig {
  margin: 0;
  /* tall enough that a step fills the screen it is pinned in: at 46svh the
     composition sat in the middle of a 900px window with a third of it empty */
  height: clamp(230px, 54svh, 560px);
  background: var(--card);
  border: 1px solid var(--rule);
  display: grid; place-items: center;
  overflow: hidden;
}
.steps__fig picture { display: block; width: 100%; height: 100%; }
.steps__fig img { width: 100%; height: 100%; object-fit: cover; }
/* a studio shot is cut out of its white ground and shown whole; a photograph
   taken in the workshop keeps its background and is cropped */
.steps__fig--cut { background: var(--paper); padding: clamp(0.8rem, 1.6vw, 1.4rem); }
.steps__fig--cut img { object-fit: contain; mix-blend-mode: multiply; }
.steps__fig--small img { width: auto; height: auto; max-width: 74%; max-height: 80%; object-fit: contain; }
.steps__copy { display: grid; gap: 0.85rem; align-content: center; min-width: 0; }
.steps__n {
  display: flex; align-items: center; gap: 0.7em;
  font-family: var(--f-mono); font-size: 0.68rem;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--green-deep);
}
.steps__n::before { content: ""; width: 1.6em; height: 2px; background: var(--green); flex: none; }
.steps__item h3 {
  font-size: clamp(1.4rem, 2.4vw, 2.1rem);
  line-height: 1.06; letter-spacing: -0.028em;
  max-width: 18ch;
}
.steps__item p { max-width: 54ch; font-size: clamp(0.98rem, 1.1vw, 1.06rem); line-height: 1.62; color: var(--ink-2); }
.steps__item p.steps__lead { color: var(--ink); font-size: clamp(1.05rem, 1.3vw, 1.18rem); line-height: 1.5; }
.steps__item .ticks { margin-top: 0.3rem; }
.steps__links { display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; margin-top: 0.3rem; }
.steps__links a {
  font-family: var(--f-mono); font-size: 0.66rem;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--green-deep);
}
.steps__links a:hover { text-decoration: underline; text-underline-offset: 0.3em; }
.steps__links a:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }
/* the meter: one segment per step, the live one filled */
.steps__meter {
  max-width: var(--maxw); margin: 0 auto; width: 100%;
  display: flex; gap: 6px;
}
.steps__meter span {
  flex: 1; height: 2px; background: var(--rule);
  position: relative; overflow: hidden;
}
.steps__meter span::after {
  content: ""; position: absolute; inset: 0;
  background: var(--green-deep);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 0.5s var(--ease);
}
.steps__meter span.is-on::after, .steps__meter span.is-done::after { transform: none; }
/* A SHORT SCREEN HOLDS LESS. The held step is the heading, a figure and the copy
   beside it, and the copy grew when the product pages got their fuller text:
   MEASURED at 1440x700, the tallest step ran 31-34px past the bottom of the held
   screen on the tanks, controllers and service pages, while every page fitted at
   1440x900. On a short live screen the pin's own air gives way first — its
   padding, the gaps between head, stage and meter, and the gaps inside the copy
   — never the words. js/page.js declines to pin a section that still does not
   fit, so this rule only decides how many short screens keep the effect. */
@media (min-width: 900px) and (max-height: 820px) {
  .steps.is-live .steps__pin { padding-block: clamp(1.4rem, 4.2vh, 3rem); gap: clamp(0.9rem, 2.2vh, 1.6rem); }
  .steps.is-live .steps__copy { gap: 0.6rem; }
  .steps.is-live .steps__head h2 { margin-top: 0.6rem; }
}
/* ══ THE CURTAIN ══
   The front page's own join, and its only real one: .cost is lifted a screen
   over the rig with `margin-top: -100svh; z-index: 2` (see that rule), so the
   dark runs UP over a screen that is standing still — the rig's sticky pin is
   still holding through the whole climb. Without something held still under it
   a curtain is just a cut: two boxes moving at the same speed have no edge that
   travels.
   A subpage has one held screen, and this is it: the stepped section's pin. So
   the dark panel after it is lifted by exactly the pin's own height — written
   into --curtain by js/page.js at every refresh, because the pin is a
   min-height and its content decides the rest — and the last delivery step
   stands still while the panel climbs over it.
   It is gated on .is-live, which only js/page.js sets, and only above 900px
   with GSAP present and motion allowed. Everywhere else this is a plain cut. */
.steps.is-live + .panel--curtain {
  margin-top: calc(var(--curtain, 100svh) * -1);
  /* AS TALL AS WHAT IT IS COVERING, and that is not tidiness. A curtain that is
     shorter than the screen it climbs over stops climbing with the bottom of
     that screen still showing under it: the panel's own min-height is 84svh,
     the pin is 100svh, and the 144px between them at 1440x900 held the stepped
     section's progress meter — three green segments drawn ON TOP of the band
     below, because a released ScrollTrigger pin is positioned and the band is
     not. The lift and the height are the same number for the same reason, so
     they read the same variable. min-height and not height: a panel whose copy
     needs more than the pin's height may have it, and covers more than it
     must, which is never the failure. */
  min-height: var(--curtain, 100svh);
  position: relative;
  z-index: 2;
}

/* not live: the three steps simply follow one another down the page */
.steps:not(.is-live) .steps__pin { min-height: 0; }
.steps:not(.is-live) .steps__stage { gap: clamp(2rem, 5vh, 3.4rem); }
.steps:not(.is-live) .steps__item { grid-area: auto; }
.steps:not(.is-live) .steps__meter { display: none; }

/* ══════════════ SUBPAGE: THE BAND AND THE CHAPTER PANELS ══════════════
   A product page is not a list of bullets with a picture at the top. It reads
   like the front page: a claim in large type with the reasoning beside it, then
   chapters, each one a full-width photograph with its number, its name and the
   facts under it. The reference Andreas sent (cryox.be) does the same thing,
   and it is the shape this site was already using on .cost and .proc — so the
   parts here are the site's own, at a subpage's scale.
   EVERY PANEL IS DARK OVER A PHOTOGRAPH, and dark sections on a subpage report
   themselves to the header through js/page.js, so the wordmark knocks out. */
/* The air is deliberately more than a section of text needs to breathe: a band
   sits between two slabs of --ink-block, and the paper either side of the words
   is what keeps the cut from reading as a lid. 9vh left the eyebrow 97px under
   the hero's edge with the header floating over it; 11.5vh is 124. */
.band { background: var(--paper); padding: clamp(4.6rem, 11.5vh, 8rem) var(--pad); }
.band + .band { padding-top: 0; }
.band__in {
  max-width: var(--maxw); margin: 0 auto;
  display: grid; gap: clamp(1.4rem, 3.5vw, 3.6rem);
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
@media (min-width: 900px) {
  .band__in { grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); }
}
.band__head h2 {
  margin-top: 0.9rem;
  max-width: 17ch;
  font-size: clamp(1.75rem, 3.2vw, 2.9rem);
  line-height: 1.02; letter-spacing: -0.032em;
}
.band__body { display: grid; gap: 1.05rem; min-width: 0; }
.band__body > p {
  max-width: 64ch;
  font-size: clamp(1rem, 1.12vw, 1.08rem);
  line-height: 1.68;
  color: var(--ink-2);
}
.band__body > p:first-child {
  font-size: clamp(1.1rem, 1.5vw, 1.35rem);
  line-height: 1.5;
  color: var(--ink);
  letter-spacing: -0.012em;
}
.band .ticks { margin-top: 0.4rem; }
/* A RULE BETWEEN THE TWO COLUMNS, and the figures stacked under the claim.
   The band carries two different things — what a cabin IS on the left, what
   that means on the right — and a hairline is the cheapest way to say so. It
   only appears where there are two columns to divide. */
@media (min-width: 900px) {
  .band__in--rule .band__body {
    border-left: 1px solid var(--rule);
    padding-left: clamp(1.6rem, 3vw, 3.4rem);
  }
}
.spec--stack { margin-top: clamp(1.4rem, 3.5vh, 2.4rem); }
.spec--stack > div {
  display: block;
  padding: 0.8rem 0;
  font-family: var(--f-body); font-size: inherit;
  letter-spacing: normal; text-transform: none;
}
.spec--stack dt {
  font-family: var(--f-mono); font-size: 0.64rem;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 0.35rem;
}
.spec--stack dd {
  font-size: clamp(0.98rem, 1.1vw, 1.05rem); font-weight: 600;
  letter-spacing: -0.012em; color: var(--ink); text-align: left;
}

/* The three ways a cabin is handed over, as a row of numbered columns under the
   prose: the summary the reader needs before the stepped section takes each one
   in turn. Divided by hairlines, which become horizontal when the row wraps. */
.forms { margin-top: clamp(1.5rem, 4vh, 2.6rem); }
/* Standalone, as the label over the mission on /people: inside .forms the block
   itself carries the space above, and a bare caption after a list has none. */
.band__body > .forms__cap { margin-top: clamp(1.6rem, 4vh, 2.4rem); }
.forms__cap {
  font-family: var(--f-mono); font-size: 0.64rem;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: clamp(0.9rem, 2vh, 1.3rem);
}
.forms__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
  gap: 0;
}
.forms__item { padding: 0 clamp(0.8rem, 1.6vw, 1.5rem); border-left: 1px solid var(--rule); }
.forms__item:first-child { padding-left: 0; border-left: 0; }
.forms__n { font-family: var(--f-mono); font-size: 0.66rem; letter-spacing: 0.14em; color: var(--green-deep); }
.forms__item h3 { margin: 0.55rem 0 0.4rem; font-size: clamp(0.98rem, 1.15vw, 1.08rem); line-height: 1.15; letter-spacing: -0.015em; }
.forms__item p { font-size: 0.9rem; line-height: 1.5; color: var(--ink-2); }
@media (max-width: 719px) {
  /* one column, or auto-fit leaves two narrow ones and the third alone under a
     rule that belongs to neither */
  .forms__grid { grid-template-columns: minmax(0, 1fr); }
  .forms__item { padding: 0.9rem 0; border-left: 0; border-top: 1px solid var(--rule); }
  .forms__item:first-child { padding-top: 0; border-top: 0; }
}
.band .spec { margin-top: 0.6rem; }

/* ══════════════ THE BAND AS A DRAWING SHEET ══════════════
   The Projects band was a heading beside a dump of bullets: kicker and headline
   alone in the left column, everything else in the right, and — MEASURED at
   1440x900 — 94px of empty paper under the left column with nothing to do in it.
   Six ticks in two sub-columns all looked alike and carried no weight, and the
   tail line read as a stray sentence rather than as the way on.

   It is a title block and a sheet now. The lede moves INTO the left column, so
   that column has work all the way down and the claim stands beside its own
   context; the six become one ruled row each, numbered by a CSS counter, at a
   size that says they are the subject; and the tail hangs off the sheet's
   closing rule with the site's own green dash in the gutter.

   Only .band--sheet is new — .band__in--rule is the modifier #scope already uses
   190 lines up this file, so the page's two paper bands share one spine. The
   ONE addition to it is align-items: stretch, scoped here: without it the
   existing hairline stops with the shorter column and reads as a fragment
   instead of as the sheet's division. */
.band.band--sheet {
  /* MORE AT THE TOP THAN AT THE BOTTOM, and the two edges are not the same kind
     of edge. Above is a hard tonal cut out of a full-screen dark panel with no
     line of its own, and the header floats over it besides — the paper has to
     establish itself as a new surface before the kicker's green dash. Below,
     .next opens with a drawn boundary of its own (border-top on .next__in) and
     adds ~54px before its kicker, so less empty paper is needed to feel the
     change. The sheet also now ENDS on a hairline; at equal padding that rule
     and .next's rule start to read as a pair of tramlines.
     1440x900: 135px over, 112.5px under, against 103.5 both ways before. */
  padding-top: clamp(6rem, 15vh, 10.5rem);
  padding-bottom: clamp(5rem, 12.5vh, 8.5rem);
}
.band--sheet .band__in { align-items: stretch; }

/* the lede, in the head column. Its own selector rather than .band__body > p's
   first-child register, because it is no longer in the body. */
.band__lede {
  margin-top: clamp(1.1rem, 2.6vh, 1.8rem);
  max-width: 46ch;
  font-size: clamp(1.1rem, 1.5vw, 1.35rem);
  line-height: 1.5; letter-spacing: -0.012em;
  color: var(--ink);
}

/* ── the sheet ── */
.band--sheet .band__body { --gut: clamp(2.4rem, 4.2vw, 3.6rem); gap: 0; }
.sheet { list-style: none; margin: 0; padding: 0; counter-reset: sheet; }
.sheet li {
  counter-increment: sheet;
  display: grid;
  grid-template-columns: var(--gut) minmax(0, 1fr);
  align-items: baseline;
  padding: clamp(0.8rem, 1.7vh, 1.15rem) 0;
  border-top: 1px solid var(--rule);
  /* the specification outranks the prose around it: bigger and darker than the
     .ticks it replaces, which is the point of moving it to one line each */
  font-size: clamp(1.02rem, 1.25vw, 1.2rem);
  font-weight: 500;
  line-height: 1.32; letter-spacing: -0.012em;
  color: var(--ink);
  text-wrap: pretty;
}
.sheet li:last-child { border-bottom: 1px solid var(--rule); }
/* A row that holds a link wraps its words in one <span>: an <a> child of the grid
   would be a grid item of its own and break the sentence onto the gutter, the
   trap .sheet__tail below describes. MEASURED on projects-engineering.html, where
   "toilet cabins" sat alone in the 38px gutter at 390px. */
/* NOT ONE DIGIT IN THE MARKUP. The figures are drawn, so the six lines in
   cabins.html are Semvac's own text and nothing else. */
.sheet li::before {
  content: counter(sheet, decimal-leading-zero);
  font-family: var(--f-mono);
  font-size: 0.68rem; font-weight: 500;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}

/* THE WAY ON, hanging off the sheet's closing rule. NOT a grid like the rows
   above it, and that is not inconsistency: an <a> child of a grid container is
   itself a grid item, so the sentence broke in two — "More under" in the text
   column and the link alone on a second row in the gutter. MEASURED, on the
   mockup. Padding and an absolute dash keep it one sentence. */
.band--sheet .sheet__tail {
  position: relative;
  margin-top: clamp(1.1rem, 2.6vh, 1.6rem);
  max-width: none;
  padding-left: var(--gut);
  font-size: 0.98rem; line-height: 1.6;
  color: var(--ink-2);
}
.band--sheet .sheet__tail::before {
  content: ""; position: absolute; left: 0; top: 0.7em;
  width: 0.9rem; height: 2px; background: var(--green);
}
/* a permanent underline, not the mono label the curtain above already spends on
   this same destination: the section's only exit should not be its smallest type */
.band--sheet .sheet__tail a {
  color: var(--green-deep);
  text-decoration: underline; text-underline-offset: 0.25em; text-decoration-thickness: 1px;
}
.band--sheet .sheet__tail a:hover { text-decoration-thickness: 2px; }
.band--sheet .sheet__tail a:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 3px; }

/* ══════════════ THE HUB INDEX ══════════════
   A hub page — Capabilities, Products & Services, People — exists to lead to
   the pages under it, so where a product page has its stepped section a hub has
   this: the branch's pages as a numbered index, one row each, the name at
   heading size and one line saying what is behind it. It is .next's index
   grown into a section of its own, and the sheet's counter: the same hairlines,
   the same mono figure in the gutter, the same green on hover.
   FULL WIDTH, not the band's two columns. A page name the length of "Service
   boxes, piping & accessories" at heading size beside a sentence does not fit
   in the band's right-hand 1.08fr, so the title block goes above and each row
   gets the whole measure: name on the left, its line on the right, from 900px.
   Under that the line drops under the name. */
.band--hub .band__in { grid-template-columns: minmax(0, 1fr); gap: clamp(1.6rem, 4vh, 2.6rem); }
.band--hub .band__head { max-width: 56rem; }
.hub { list-style: none; margin: 0; padding: 0; counter-reset: hub; border-bottom: 1px solid var(--rule); }
.hub li { counter-increment: hub; border-top: 1px solid var(--rule); }
.hub__row {
  display: grid;
  grid-template-columns: 2.6rem minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: clamp(0.9rem, 2.2vw, 2rem); row-gap: 0.4rem;
  padding: clamp(1rem, 2.4vh, 1.5rem) 0;
}
/* the figure is drawn, as on the sheet: not one digit in the markup */
.hub__row::before {
  content: counter(hub, decimal-leading-zero);
  grid-column: 1; grid-row: 1;
  font-family: var(--f-mono); font-size: 0.68rem; font-weight: 500;
  letter-spacing: 0.14em; font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}
.hub__t {
  grid-column: 2; grid-row: 1;
  font-size: clamp(1.2rem, 2.2vw, 1.8rem); font-weight: 500;
  line-height: 1.1; letter-spacing: -0.028em;
  color: var(--ink);
  transition: color 0.25s var(--ease);
}
.hub__d {
  grid-column: 2; grid-row: 2;
  max-width: 60ch;
  font-size: clamp(0.95rem, 1.05vw, 1.05rem); line-height: 1.55;
  color: var(--ink-2);
}
.hub__row i {
  grid-column: 3; grid-row: 1;
  font-style: normal; color: var(--green-deep);
  transition: transform 0.3s var(--ease);
}
.hub__row:hover .hub__t { color: var(--green-deep); }
.hub__row:hover i { transform: translateX(6px); }
.hub__row:focus-visible { outline: 2px solid var(--green-deep); outline-offset: 4px; }
@media (min-width: 900px) {
  .hub__row { grid-template-columns: 2.6rem minmax(0, 0.9fr) minmax(0, 1.1fr) auto; }
  .hub__d { grid-column: 3; grid-row: 1; }
  .hub__row i { grid-column: 4; }
}

.panel {
  position: relative;
  isolation: isolate;
  min-height: clamp(30rem, 84svh, 52rem);
  display: grid; place-items: center;
  padding: clamp(4rem, 10vh, 7rem) var(--pad);
  background: var(--ink-block);
  color: var(--ink-rev);
  overflow: hidden;
  text-align: center;
}
.panel__bg { position: absolute; inset: 0; z-index: -2; }
.panel__bg picture { display: block; width: 100%; height: 100%; }
.panel__bg img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 45%; }
.panel__veil {
  position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(ellipse 72% 60% at 50% 50%, rgba(8, 13, 11, 0.72) 0%, rgba(8, 13, 11, 0.42) 70%),
    linear-gradient(to bottom, rgba(8, 13, 11, 0.7) 0%, rgba(8, 13, 11, 0.45) 40%, rgba(8, 13, 11, 0.72) 100%);
}
.panel__in { display: grid; justify-items: center; gap: 1.1rem; max-width: 64ch; }
.panel__n {
  font-family: var(--f-mono); font-size: 0.68rem;
  letter-spacing: 0.3em; text-transform: uppercase;
  color: var(--green-lift);
}
.panel h2 {
  max-width: 19ch;
  font-size: clamp(1.9rem, 4.4vw, 3.6rem);
  line-height: 0.98; letter-spacing: -0.035em;
  text-shadow: 0 2px 26px rgba(8, 13, 11, 0.4);
}
.panel__p {
  max-width: 56ch;
  font-size: clamp(0.98rem, 1.2vw, 1.12rem);
  line-height: 1.62;
  color: color-mix(in oklab, var(--ink-rev), transparent 14%);
  text-shadow: 0 1px 16px rgba(8, 13, 11, 0.4);
}
/* the facts, set the way the site sets a part number: mono, wide, quiet */
.panel__list {
  list-style: none; margin: 0.4rem 0 0; padding: 0;
  display: grid; gap: 0.55rem;
  font-family: var(--f-mono); font-size: clamp(0.64rem, 0.8vw, 0.72rem);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: color-mix(in oklab, var(--ink-rev), transparent 22%);
}
.panel__list li { text-wrap: balance; }
.panel__links { margin-top: 0.8rem; display: flex; flex-wrap: wrap; gap: 0.6rem 1.6rem; justify-content: center; }
.panel__links a {
  font-family: var(--f-mono); font-size: 0.66rem;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--green-lift);
}
.panel__links a:hover { text-decoration: underline; text-underline-offset: 0.3em; }
.panel__links a:focus-visible { outline: 2px solid var(--green-lift); outline-offset: 4px; }

/* A panel whose heading is a whole sentence of Semvac's, not a title. */
.panel--long h2 { max-width: 24ch; font-size: clamp(1.6rem, 3.1vw, 2.5rem); line-height: 1.04; }

/* ══════════════ A PANEL YOU SCROLL INTO ══════════════
   The footer's scene, on a section in the middle of the page — the same three
   layers, and each of them is here for the same reason it is there:
     .panel--scene   the TRACK, whose height IS how far you scroll through it
     .panel__stage   sticky: holds one screen still, and clips
     .panel__card    the photograph and its veil, clip-path grown to full bleed
     .panel__in      the copy, on the open card and arriving after it
   The order is the point. The picture is the subject first — it opens alone,
   with the veil light enough to read as a photograph — and the words arrive on
   it once it has settled, instead of the two competing on the way in.

   EVERY DEFAULT BELOW IS THE FINISHED STATE, and the track only exists with
   .is-scene, which page.js adds after it has checked the copy fits in one held
   screen. With no JS, no GSAP, under reduced motion, or on a screen too short
   for the hold, this is the ordinary panel it was: a photograph, a veil and the
   words on top. */
/* place-items: normal IS LOAD-BEARING, and it is not tidying. .panel sets
   place-items: center for its own grid; this section is no longer a grid, and
   in a current Chrome justify-items now applies to BLOCK children too — so the
   inherited `center` made the stage shrink to its widest line and sit in the
   middle of the page. MEASURED: the stage came out 756px wide in a 1440px
   window, and an empty probe <div> in this section measured 9px against the
   same probe's 1310px in .band. Undo the alignment with the display. */
.panel--scene { display: block; place-items: normal; padding: 0; overflow: visible; }
.panel--scene .panel__stage {
  position: relative;
  min-height: clamp(30rem, 84svh, 52rem);
  display: grid; place-items: center;
  padding: clamp(4rem, 10vh, 7rem) var(--pad);
  /* clip and not hidden — hidden would make this a scroll container and the
     sticky below would then resolve against itself. The footer learned it. */
  overflow: clip;
  isolation: isolate;
  text-align: center;
}
/* THE CARD. clip-path and not margin, for the footer's reason: a margin changes
   the layout box, which changes the document height, which is what the progress
   driving the animation is measured against. */
.panel--scene .panel__card {
  position: absolute; inset: 0; z-index: 0;
  background: var(--ink-block);
  clip-path: inset(var(--ps-y, 0px) var(--ps-x, 0px) round var(--ps-r, 0px));
}
.panel--scene .panel__bg { z-index: 0; }
/* the photograph settles out of a small zoom as the card opens — the hero's
   move, at a quarter of the distance */
.panel--scene .panel__bg img { transform: scale(var(--ps-z, 1)); }
.panel--scene .panel__veil { z-index: 1; opacity: var(--ps-v, 1); }
.panel--scene .panel__in { position: relative; z-index: 2; }

/* LIVE: the track, the held screen, and the copy that comes up on the card. */
.panel--scene.is-scene { height: 200svh; background: var(--paper); }
.panel--scene.is-scene .panel__stage { position: sticky; top: 0; height: 100svh; min-height: 0; }
.panel--scene.is-scene .panel__in {
  opacity: var(--ps-c, 1);
  transform: translateY(calc((1 - var(--ps-c, 1)) * 34px));
}

/* ══════════════ THE SUBPAGE'S TEXT, ARRIVING ══════════════
   Two entrances, and both are the same 14-18px lift the contact cards already
   use. Nothing here lights words or masks lines — that was offered and turned
   down; what was asked for is rows that come in one after another, and a hero
   that assembles itself instead of being there all at once.

   BOTH ARE GATED ON MOTION BEING WELCOME. The hidden state lives inside
   `prefers-reduced-motion: no-preference`, so a reader who has asked for
   stillness never has anything to un-hide — the rule that would hide it is not
   in the sheet at all. That is the only correct way round: hiding first and
   restoring later leaves the restore as a thing that can fail. */
@media (prefers-reduced-motion: no-preference) {

  /* ── the hero, on load ──
     Pure CSS and no script: the opening screen is above the fold by definition,
     so there is nothing to wait for and nothing to observe.

     `backwards` AND NOT `both`, and the difference is not pedantry. Backwards
     is what covers the delay — the from-state applies before the animation
     starts, so a child is never painted at full strength and then yanked back
     (MEASURED once on the contact page: the cards were drawn 137ms before their
     class landed and the entrance was swallowed whole). Forwards would then
     hold the LAST frame for the life of the page, and an animation beats a
     normal declaration: the hero's call to action is .btn--solid, whose hover
     is transform: translateY(-2px), and a persisted `transform: none` kills it
     silently. Backwards hands the element back to the stylesheet when it is
     done, and the last frame is the stylesheet's own value anyway. */
  @keyframes fheroIn {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: none; }
  }
  .fhero__in > * { animation: fheroIn 0.85s var(--ease) backwards; }
  .fhero__in > :nth-child(1) { animation-delay: 0.10s; }
  .fhero__in > :nth-child(2) { animation-delay: 0.22s; }
  .fhero__in > :nth-child(3) { animation-delay: 0.36s; }
  .fhero__in > :nth-child(4) { animation-delay: 0.50s; }
  /* its own keyframe, because this one is centred with a transform of its own
     and a translateY would throw it half a width to the right */
  @keyframes fheroScrollIn {
    from { opacity: 0; transform: translate(-50%, 14px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
  }
  .fhero__scroll { animation: fheroScrollIn 0.8s var(--ease) 0.78s backwards; }

  /* ── rows, as they come into the window ──
     THE SELECTOR LIST IS THE CONTRACT WITH reveal.js, which adds .is-in to the
     same containers and numbers their children. It is written against the real
     components rather than a hook class the script would have to add, because a
     hook added by a deferred script arrives after the first paint — see the
     137ms above. The only thing the script adds is the GO. */
  .anim :is(.ticks, .spec, .cards, .forms__grid, .next__list, .sheet, .hub) > * {
    opacity: 0;
    transform: translateY(14px);
  }
  /* BOTH ENDS SPELLED OUT. A keyframe with only a `from` takes its `to` from
     the element's own value — and the element's own value here is the hidden
     state three lines above, so the rows animated from 0 to 0 and stayed
     invisible. MEASURED: every row still at opacity 0 after the whole page had
     been scrolled through and every block had is-in on it. */
  @keyframes rowIn {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
  }
  .anim :is(.ticks, .spec, .cards, .forms__grid, .next__list, .sheet, .hub).is-in > * {
    /* the arrived state is written as ordinary declarations, so the animation
       can be `backwards` here too and let go of the row when it lands */
    opacity: 1;
    transform: none;
    animation: rowIn 0.62s var(--ease) backwards;
    /* capped: six ticks at 65ms would still be arriving two-fifths of a second
       after the first one, which stops reading as one move */
    animation-delay: min(calc(var(--i, 0) * 65ms), 390ms);
  }
}

/* ═══════════════ THE LANDING PAGE ON A LARGE SCREEN ═══════════════
   The front page was drawn at 1440 and held its type and spacing there, so on a
   2560 monitor it read as a 1440 page floating in a field of paper. Everything
   in this change grows linearly from its value at 1440 to a ceiling at 2560 and
   holds there, by kind of value: display type x1.55-1.58, body x1.35, mono
   labels x1.25, spacing x1.40 of what renders at 1440x900, rule lengths x1.5,
   hairlines and strokes not at all. Every expression is written against
   (100vw - 1440px), which is zero or negative at 1440 and below, and each one
   was chosen so its result there is today's value at every height. Nothing at
   1440 or narrower renders differently.
   The rules below are the ones with no existing home: page-scoped copies of
   shared components, so the subpages keep today's sizes. body carries .home on
   index.html only. */

/* ONE column for the whole page. The hero's title, What we do's two columns and
   the footer's row all measure against it, so they share a left edge at every
   width, as they do today. 1320 at 1440 and below; 1980 (x1.5) at 2560. */
body.home { --maxw: clamp(1320px, calc(1320px + (100vw - 1440px) * 0.589286), 1980px); }

/* ONE eyebrow for the page, in place of five section copies. The dash and the
   gap get their own ramps so the kicker keeps the site's rule-length ratio (x1.5)
   rather than following its type at x1.25; at 1440 they are .eyebrow's own
   0.7em and 1.6em. */
body.home .eyebrow {
  font-size: clamp(0.68rem, calc(0.68rem + (100vw - 1440px) * 0.0024286), 0.85rem);
  gap: clamp(0.7em, calc(0.7em + (100vw - 1440px) * 0.0010201), 0.784em);
}
body.home .eyebrow::before { width: clamp(1.6em, calc(1.6em + (100vw - 1440px) * 0.0038858), 1.92em); }

/* The menu's own columns. .menu__col is also the footer's column on every page,
   so the base rules stay as they are and the overlay is scoped instead; the
   footer's copy below lands on the same 19.44 link size. */
.menu__foot .menu__col { gap: clamp(0.4rem, calc(0.4rem + (100vw - 1440px) * 0.0022857), 8.96px); }
.menu__foot .menu__col :is(h3, h4, .menu__kicker) {
  font-size: clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.0022143), 12.4px);
  margin-bottom: clamp(0.5rem, calc(0.5rem + (100vw - 1440px) * 0.0028571), 11.2px);
}
.menu__foot .menu__col :is(a:not(.menu__lang), span) { font-size: clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.0045), 19.44px); }

/* What we do's kicker takes its size from .proc__step p (0,1,1) today, which
   outranks .proc__n's own 0.68rem. This keeps it a label (x1.25) instead of
   letting it follow the paragraph's x1.35. 15.2px at 1440, as today. */
.proc__step .proc__n { font-size: clamp(0.95rem, calc(0.95rem + (100vw - 1440px) * 0.003393), 19px); }

/* THE FOOTER, landing page only. Inside a width query because .home .foot__row
   and .home .foot__blurb are (0,2,0) and would otherwise outrank the <=1023 and
   <=760 rules. Height-driven values keep their vh middle and grow only their
   ceiling, and only with width. */
@media (min-width: 1440px) {
  .home .foot__in {
    padding-top: clamp(104px, 12vw, clamp(160px, calc(160px + (100vw - 1440px) * 0.057143), 224px));
    padding-bottom: clamp(1.5rem, 4vh, clamp(2.5rem, calc(2.5rem + (100vw - 1440px) * 0.0092858), 50.4px));
  }
  .home .foot__row { gap: clamp(1.6rem, 4vw, clamp(3.5rem, calc(3.5rem + (100vw - 1440px) * 0.02), 4.9rem)); }
  .home .foot__blurb { font-size: clamp(0.95rem, calc(0.95rem + (100vw - 1440px) * 0.00475), 1.2825rem); }
  .home .foot .menu__col { gap: clamp(0.4rem, calc(0.4rem + (100vw - 1440px) * 0.0022858), 0.56rem); }
  .home .foot .menu__col h3 {
    font-size: clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.0022143), 0.775rem);
    margin-bottom: clamp(0.5rem, calc(0.5rem + (100vw - 1440px) * 0.0028572), 0.7rem);
  }
  .home .foot .menu__col :is(a, span) { font-size: clamp(0.9rem, calc(0.9rem + (100vw - 1440px) * 0.0045), 1.215rem); }
  .home .foot__bar {
    margin-top: clamp(2.5rem, 7vh, clamp(4.5rem, calc(4.5rem + (100vw - 1440px) * 0.014465), 88.2px));
    padding-top: clamp(1rem, 2.5vh, clamp(1.5rem, calc(1.5rem + (100vw - 1440px) * 0.0066965), 31.5px));
  }
  .home .foot__bar p { font-size: clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.0022143), 0.775rem); }
  .no-motion .home .foot__scene, .home .foot--flat .foot__scene {
    padding-block: clamp(2.5rem, 7vh, clamp(4rem, calc(4rem + (100vw - 1440px) * 0.0216072), 88.2px));
  }
  /* The word is the rig's SEMVAC returning, so it grows as the rig's does
     (x1.578, 0.761 of it at every width) instead of riding the card's width to
     x1.78. min() picks today's card-width term at 1440 (241.106 < 241.11). */
  .home .foot__word {
    font-size: min(calc((100vw - max(20px, 6vw) * 2) * 0.78 / 4.0995), calc(241.11px + (100vw - 1440px) * 0.124455), 380.5px);
    background-image:
    radial-gradient(circle clamp(200px, calc(200px + (100vw - 1440px) * 0.07143), 280px) at var(--wx, -9999px) var(--wy, -9999px),
      color-mix(in oklab, var(--ink-rev), transparent 82%) 0%,
      rgba(0, 0, 0, 0) 72%),
    linear-gradient(var(--wash), var(--wash));
  }
  /* the pointer pool, the same size as the rig's */
  .home .foot__grid::after {
    -webkit-mask-image: radial-gradient(circle clamp(200px, calc(200px + (100vw - 1440px) * 0.07143), 280px) at var(--px, -9999px) var(--py, -9999px),
                                        #000 0%, transparent 72%);
            mask-image: radial-gradient(circle clamp(200px, calc(200px + (100vw - 1440px) * 0.07143), 280px) at var(--px, -9999px) var(--py, -9999px),
                                        #000 0%, transparent 72%);
  }
}
/* .wrap is a flex item with margin-inline:auto, so it shrinks to its content and
   never reaches --maxw on its own; this holds the row at x1.5 (1924.7 at 2560).
   114ch + 168px is today's row (three 40ch blurbs at 0.95rem plus three 56px
   gaps) — change it with them. Past 1440 only, not from it: at 1440 it would
   have no effect on the box but would still turn min-width from auto into a
   length. */
@media (min-width: 1441px) {
  .home .foot .wrap { min-width: min(100%, var(--maxw), calc(114ch + 168px + clamp(0px, (100vw - 1440px) * 0.5728572, 641.6px))); }
}

/* ══════════════ SKIP TO CONTENT ══════════════
   The first thing a keyboard reaches: out of sight until it has focus, then a
   plain dark tab at the top left, over the header. */
.skip {
  position: fixed; left: var(--pad); top: 0; z-index: 1000;
  transform: translateY(-120%);
  padding: 0.7rem 1rem;
  background: var(--ink-block); color: var(--paper);
  font-family: var(--f-mono); font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
}
.skip:focus { transform: none; outline: 2px solid var(--green-lift); outline-offset: 2px; }

/* ══════════════ DOCUMENTS (legal, privacy) ══════════════
   Text to be read, not a composition: the contact page's paper and grid, one
   column at a reading measure, the site's eyebrow and heading type. */
.doc {
  position: relative; isolation: isolate; overflow: hidden;
  background: var(--paper);
  padding: calc(var(--head-h) + clamp(2rem, 7vh, 5rem)) var(--pad) clamp(5rem, 14vh, 9rem);
}
.doc__in { max-width: var(--maxw); margin: 0 auto; }
.doc__in > * { max-width: 44rem; }
.doc h1 {
  margin-top: 1.1rem;
  font-size: clamp(2.4rem, 5.2vw, 4.6rem); line-height: 0.97; letter-spacing: -0.036em;
}
.doc__lede { margin-top: 1.4rem; font-size: clamp(1rem, 1.2vw, 1.12rem); line-height: 1.6; color: var(--ink-2); }
.doc h2 {
  margin: clamp(2.2rem, 5vh, 3rem) 0 0.7rem;
  font-size: clamp(1.2rem, 1.7vw, 1.5rem); line-height: 1.15; letter-spacing: -0.015em;
}
.doc p, .doc li { color: var(--ink-2); line-height: 1.65; }
.doc p + p, .doc ul + p, .doc p + ul { margin-top: 0.8rem; }
.doc ul { padding-left: 1.2rem; }
.doc li + li { margin-top: 0.35rem; }
.doc strong { font-family: var(--f-mono); font-weight: 500; font-size: 0.92em; color: var(--ink); }
.doc a { color: var(--green-deep); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.doc a:hover { color: var(--ink); }
.doc__facts { margin-top: clamp(2rem, 5vh, 3rem); border-top: 1px solid var(--rule); }
.doc__facts > div {
  display: grid; grid-template-columns: minmax(8rem, 12rem) 1fr; gap: 1.5rem;
  padding: 0.85rem 0; border-bottom: 1px solid var(--rule);
}
.doc__facts dt {
  font-family: var(--f-mono); font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-3); padding-top: 0.2em;
}
.doc__facts dd { color: var(--ink); }
.doc .doc__date {
  margin-top: clamp(2.5rem, 6vh, 3.5rem);
  font-family: var(--f-mono); font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-3);
}
@media (max-width: 560px) {
  .doc__facts > div { grid-template-columns: 1fr; gap: 0.2rem; }
}

/* the legal links beside the copyright line, in its own type */
.foot__bar { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.6rem 1.5rem; }
/* the developer's credit, in the bar's own mono row (.foot__bar p sets the type),
   last in the row: the copyright takes the left, the legal links and the credit
   sit together at the right */
.foot__bar > p:first-child { margin-right: auto; }
.foot__by a { color: var(--ink-rev-2); text-decoration: none; border-bottom: 1px solid color-mix(in oklab, var(--ink-rev-2), transparent 65%); transition: color 0.2s var(--ease), border-color 0.2s var(--ease); }
.foot__by a:hover { color: var(--ink-rev); border-bottom-color: var(--ink-rev); }
.foot__legal { display: flex; flex-wrap: wrap; gap: 0.4rem 1.5rem; }
.foot__legal a {
  font-family: var(--f-mono);
  font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--ink-rev-2);
  transition: color 0.2s var(--ease);
}
.foot__legal a:hover { color: var(--ink-rev); }
.home .foot__legal a { font-size: clamp(0.62rem, calc(0.62rem + (100vw - 1440px) * 0.0022143), 0.775rem); }

/* ══════════════ 404 ══════════════
   Served by Vercel at any address that has no page, so it stands alone: no
   script, root-absolute paths, and a line in each of the site's four languages. */
.nf {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: 100vh; min-height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(6rem, 14vh, 9rem) var(--pad) clamp(4rem, 10vh, 7rem);
  background: var(--paper);
}
.nf__brand { position: absolute; left: var(--pad); top: clamp(1.2rem, 3vh, 1.8rem); display: flex; align-items: center; gap: 0.7rem; }
.nf__brand img { width: 64px; height: auto; }
.nf__brand span { font-family: var(--f-display); font-weight: 800; font-size: 1.45rem; letter-spacing: -0.03em; }
.nf__in { max-width: var(--maxw); width: 100%; margin: 0 auto; }
.nf__code {
  font-family: var(--f-display); font-weight: 900; font-size: clamp(6rem, 20vw, 15rem);
  line-height: 0.85; letter-spacing: -0.05em;
  color: color-mix(in oklab, var(--ink), transparent 88%);
}
.nf h1 { margin-top: 1rem; font-size: clamp(2rem, 4.4vw, 3.6rem); max-width: 16ch; }
.nf__list { margin-top: clamp(1.8rem, 5vh, 2.8rem); list-style: none; border-top: 1px solid var(--rule); max-width: 44rem; }
.nf__list li { display: grid; grid-template-columns: 3rem 1fr auto; gap: 1rem; align-items: baseline; padding: 0.8rem 0; border-bottom: 1px solid var(--rule); }
.nf__list b { font-family: var(--f-mono); font-weight: 500; font-size: 0.72rem; letter-spacing: 0.14em; color: var(--ink-3); }
.nf__list span { color: var(--ink-2); }
.nf__list a { font-family: var(--f-mono); font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--green-deep); white-space: nowrap; }
.nf__list a:hover { color: var(--ink); }
@media (max-width: 560px) { .nf__list li { grid-template-columns: 2.4rem 1fr; } .nf__list a { grid-column: 2; } }

/* ══════════════ COMING SOON ══════════════
   soon.html, where vercel.json sends the live domain's addresses of the
   pages not open yet (CLOSED in i18n/build.py). The 404's furniture: a line in
   each language with its own way home, and the contact page and the two direct
   lines under them. */
.soon {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: 100vh; min-height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(6rem, 14vh, 9rem) var(--pad) clamp(4rem, 10vh, 7rem);
  background: var(--paper);
}
.soon__brand { position: absolute; left: var(--pad); top: clamp(1.2rem, 3vh, 1.8rem); display: flex; align-items: center; gap: 0.7rem; }
.soon__brand img { width: 64px; height: auto; }
.soon__brand span { font-family: var(--f-display); font-weight: 800; font-size: 1.45rem; letter-spacing: -0.03em; }
.soon__in { max-width: var(--maxw); width: 100%; margin: 0 auto; }
.soon__eyebrow { color: var(--green-deep); }
.soon h1 { margin-top: 0.8rem; font-size: clamp(2rem, 4.4vw, 3.6rem); max-width: 18ch; }
.soon__list { margin-top: clamp(1.8rem, 5vh, 2.8rem); list-style: none; border-top: 1px solid var(--rule); max-width: 50rem; }
.soon__list li { display: grid; grid-template-columns: 3rem 1fr auto; gap: 1rem; align-items: baseline; padding: 0.8rem 0; border-bottom: 1px solid var(--rule); }
.soon__list b { font-family: var(--f-mono); font-weight: 500; font-size: 0.72rem; letter-spacing: 0.14em; color: var(--ink-3); }
.soon__list span { color: var(--ink-2); text-wrap: pretty; }
.soon__list a { font-family: var(--f-mono); font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--green-deep); white-space: nowrap; }
.soon__list a:hover { color: var(--ink); }
.soon__contact {
  margin-top: clamp(1.6rem, 4vh, 2.4rem);
  display: flex; flex-wrap: wrap; gap: 0.6rem 2.2rem;
  font-family: var(--f-mono); font-size: 0.82rem; letter-spacing: 0.1em;
}
.soon__contact a { color: var(--ink); border-bottom: 1px solid color-mix(in oklab, var(--ink), transparent 70%); padding-bottom: 2px; }
.soon__contact a:hover { color: var(--green-deep); border-bottom-color: var(--green-deep); }
@media (max-width: 560px) { .soon__list li { grid-template-columns: 2.4rem 1fr; } .soon__list a { grid-column: 2; } }

/* a section focused by an in-page link (main.js) or the skip link takes the
   reading position, not a ring round the whole page */
[tabindex="-1"]:focus { outline: none; }

/* ══════════════ THE MENU ON A PHONE, A TABLET, A NARROW WINDOW ══════════════
   Everywhere the menu is not the two-column hover layout (main.js `wide`): each
   item with pages under it gets a +/- button on its own row, the word itself
   is a plain link, and the language choice is four real buttons in a row
   instead of four 17x23px codes in a column. 44px is the smallest target a
   thumb should be asked to hit. */
.menu__toggle { display: none; }
@media not all and (hover: hover) and (pointer: fine) and (min-width: 1100px) {
  /* Top-aligned, not centred: centred, opening a fold re-centred the whole list
     and the +/- just tapped slid out from under the thumb (246px on a tablet). */
  .menu__nav { justify-content: flex-start; }
  /* every row at least the toggle's 44px, so the six keep one rhythm whether or
     not a row carries a button */
  .menu__item { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 0.75rem; min-height: 44px; }
  .menu__item > .menu__link { grid-column: 1; }
  .menu__item > .menu__sub { grid-column: 1 / -1; }
  .menu__toggle {
    display: grid; place-items: center;
    grid-column: 2; grid-row: 1;
    width: 44px; height: 44px;
    border: 1px solid var(--rule-rev); border-radius: 50%;
    color: var(--ink-rev);
    transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease), color 0.25s var(--ease);
  }
  /* a plus drawn in two bars; the upright one folds away into a minus when open */
  .menu__toggle::before, .menu__toggle::after {
    content: ""; grid-area: 1 / 1;
    width: 14px; height: 1.5px; background: currentColor;
    transition: transform 0.3s var(--ease);
  }
  .menu__toggle::after { transform: rotate(90deg); }
  .menu__toggle[aria-expanded="true"] { border-color: var(--green-lift); color: var(--green-lift); }
  .menu__toggle[aria-expanded="true"]::after { transform: rotate(0deg); }
  .menu__toggle:focus-visible { outline: 2px solid var(--green-lift); outline-offset: 2px; }

  .menu__col--lang {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem;
    max-width: 22rem;
  }
  .menu__col--lang .menu__kicker { grid-column: 1 / -1; }
  .menu__col .menu__lang {
    display: grid; place-items: center;
    min-height: 44px; padding: 0;
    border: 1px solid var(--rule-rev); border-radius: 2px;
    font-size: 0.8rem;
  }
  .menu__col .menu__lang[aria-current="true"] {
    background: var(--ink-rev); border-color: var(--ink-rev); color: var(--ink-block);
  }
}
/* the header's language dropdown under a finger: rows a thumb can hit, and the
   panel below the header band, clear of the menu square it used to overlap */
@media (hover: none), (pointer: coarse) {
  .lang__item { padding-block: 15px; min-width: 64px; }
  .lang__button { min-height: 44px; }
  .lang__panel { top: calc(100% + 8px); }
}
/* The narrowest phones (320): "Kompetenzen" / "Kompetencer" is one word too wide
   for the column beside the +/- and ran under it, taking its left edge. */
@media (max-width: 359px) {
  .menu__link { font-size: 1.7rem; }
}
/* A phone on its side: the words were sized by the WIDTH (~50px) and only four
   and a half of the six fitted the 290px of height; Contact was below the fold. */
@media (max-height: 500px) and (orientation: landscape) {
  .menu__link { font-size: 2rem; }
  .menu__nav { padding-block: 0.5rem; }
  .menu__list { gap: 0.25rem; }
}

/* ══════════════ PHONES: spacing found by the mobile audit (September 2026) ══════════════ */
@media (max-width: 760px) {
  /* The cost headline was centred up into the header band in German and French,
     whose headline, lede and cards run longer (FR h2 at 60px under a 74px header).
     The pin keeps the header band and the progress bar clear. */
  .cost__pin { padding-block: var(--head-h) 3rem; gap: clamp(1.25rem, 3.5vh, 5.5rem); }
  /* The footer on a phone is a plain dark block (main.js), and its watermark,
     centred in it, sat right behind "Get in touch" and the phone number. */
  .foot--static .foot__word { display: none; }
}
@media (max-width: 480px) {
  /* "La touche finale d'un voyage parfait" wrapped at 0.2em and left the rule
     stranded at the left edge; every language fits on one line at 0.12em */
  .hero__eyebrow { letter-spacing: 0.12em; }
}
@media (max-width: 1023px) {
  /* "What we do" opened with 185px of blank paper on a phone, twice the other
     sections: the 22svh was air for the cost section's wipe, and References now
     stands between the two. */
  .proc { padding-top: clamp(6rem, 15svh, 8rem); }
  /* the cards' icons and headings line up across the rail when the headings run
     to different lengths (German, French) */
  .cost__card { justify-content: flex-start; }
}
/* a link that wraps (French "Service, pièces de rechange et réparations") keeps
   its arrow after the last word, not stranded at the end of the first line */
.proc__link { display: inline-block; }
.proc__link i { display: inline-block; margin-left: 0.55em; }
/* "Swipe to turn the globe – tap a country" left COUNTRY alone on a 360px screen */
.refs__hint { text-wrap: balance; }
/* The hub list (products-services, capabilities, people) is followed by a sheet
   on the same paper, and the two paddings stacked into 205-233px of blank paper
   on a phone. Paper to paper takes the single band gap; the product pages, where
   the sheet follows a dark panel, keep their larger top padding. */
.band--hub + .band--sheet { padding-top: 0; }
/* The small green line over a hero photograph measured 1.8-2.9:1 on eight of the
   ten subpages; the text goes light and the dash keeps the green. */
.fhero .eyebrow--rev { color: var(--ink-rev); }

/* ─────────────── the film, over the page ─────────────── */
/* Opened from the footer's link (js/main.js): the whole screen black, the film
   entire in it (16:9 with its captions, never cropped), the browser's own
   controls, and one cross in the corner clear of them. The page under it does
   not scroll while it is open. */
.film {
  position: fixed; inset: 0;
  width: 100%; height: 100%; max-width: none; max-height: none;
  margin: 0; padding: 0; border: 0;
  background: #000; color: #fff;
  overflow: hidden;
}
.film[open] { display: grid; place-items: center; animation: film-in 0.35s var(--ease) both; }
.film::backdrop { background: #000; }
@keyframes film-in { from { opacity: 0; } }
.film__video { width: 100%; height: 100%; object-fit: contain; background: #000; }
.film__close {
  position: absolute; z-index: 1;
  top: max(16px, env(safe-area-inset-top)); right: max(16px, env(safe-area-inset-right));
  width: 48px; height: 48px;
  display: grid; place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 50%;
  background: rgba(10, 14, 12, 0.55);
  cursor: pointer;
  transition: background 0.25s var(--ease), border-color 0.25s var(--ease);
}
.film__close::before, .film__close::after {
  content: ''; grid-area: 1 / 1;
  width: 18px; height: 1.5px; background: #fff;
  transform: rotate(45deg);
}
.film__close::after { transform: rotate(-45deg); }
.film__close:hover { background: var(--green); border-color: var(--green); }
.film__close:focus-visible { outline: 2px solid var(--green-lift); outline-offset: 3px; }
:root:has(.film[open]) { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .film[open] { animation: none; } }
